Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: E2E Tests

on:
workflow_dispatch: # Allow manual trigger
issue_comment:
types: [created]

permissions:
contents: read
pull-requests: write

jobs:
# Check if comment is /e2e and commenter has write permission
check-trigger:
runs-on: ubuntu-22.04
if: github.event_name == 'issue_comment' && github.event.issue.pull_request
outputs:
should-run: ${{ steps.check.outputs.should-run }}
pr-ref: ${{ steps.pr.outputs.ref }}
steps:
- name: Check comment and author
id: check
env:
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
REPO: ${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Check if comment contains /e2e
if [[ "$COMMENT_BODY" != *"/e2e"* ]]; then
echo "should-run=false" >> $GITHUB_OUTPUT
exit 0
fi

# Check if author has write permission (owner or maintainer)
PERMISSION=$(gh api repos/$REPO/collaborators/$COMMENT_AUTHOR/permission --jq '.permission')

if [[ "$PERMISSION" == "admin" ]] || [[ "$PERMISSION" == "write" ]]; then
echo "should-run=true" >> $GITHUB_OUTPUT
echo "✅ User $COMMENT_AUTHOR has permission: $PERMISSION"
else
echo "should-run=false" >> $GITHUB_OUTPUT
echo "❌ Only repo owners and maintainers can trigger E2E tests (current permission: $PERMISSION)"
fi

- name: Get PR ref
if: steps.check.outputs.should-run == 'true'
id: pr
env:
PR_NUMBER: ${{ github.event.issue.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER)
REF=$(echo "$PR_DATA" | jq -r .head.ref)
echo "ref=$REF" >> $GITHUB_OUTPUT

# Run E2E tests
e2e-test:
runs-on: ubuntu-22.04
needs: [check-trigger]
environment: Main
if: |
always() &&
(github.event_name == 'workflow_dispatch' ||
needs.check-trigger.outputs.should-run == 'true')
steps:
- name: Add reaction
if: github.event_name == 'issue_comment'
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ github.event.comment.id }}
reactions: rocket

- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ needs.check-trigger.outputs.pr-ref || github.ref }}

- name: Setup Golang env
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true

- name: Install Ginkgo
run: go install github.com/onsi/ginkgo/v2/ginkgo

- name: Build
run: go build

- name: Restore credentials from secrets
env:
TG_CREDENTIALS: ${{ secrets.TG_CREDENTIALS }}
run: |
echo "$TG_CREDENTIALS" > test/credentials.json
chmod 600 test/credentials.json

- name: E2E Test
env:
TDL_TEST_CREDENTIALS_FILE: ${{ github.workspace }}/test/credentials.json
run: ginkgo -v -r --timeout=3m ./test

- name: Comment result on PR
if: github.event_name == 'issue_comment' && always()
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.issue.number }}
body: |
## E2E Test Results

**Status:** ${{ job.status == 'success' && '✅ Passed' || '❌ Failed' }}
**Triggered by:** @${{ github.event.comment.user.login }}
**Run:** [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
23 changes: 0 additions & 23 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,3 @@ jobs:
run: go build
- name: Unit Test
run: go test -v $(go list ./... | grep -v /test) # skip e2e test
e2e-test:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Golang env
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Install Ginkgo
run: go install github.com/onsi/ginkgo/v2/ginkgo
- name: Setup Teamgram Env
run: |
git clone https://github.com/iyear/teamgram-server.git
cd teamgram-server
git checkout 10151bb92555aa1bedcba9f8f24b0e7deac22dee
sudo docker compose -f ./docker-compose-env.yaml up -d --quiet-pull
sudo docker compose up -d --quiet-pull
- name: Build
run: go build
- name: E2E Test
run: ginkgo -v -r ./test
1 change: 1 addition & 0 deletions test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/*.json
33 changes: 33 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# E2E Testing

End-to-end tests for tdl using exported credentials.

## Usage

```bash
# 1. Export credentials
cd tools
go run export_credentials.go -namespace default -output ../test/credentials.json

# 2. Run tests
cd ..
TDL_TEST_CREDENTIALS_FILE=$(pwd)/test/credentials.json go test ./test/... -v
```

## GitHub Actions

E2E tests run via `.github/workflows/e2e.yml`:

- **Trigger**: Comment `/e2e` on PR (requires write permission) or manual run
- **Credentials**: Stored in `secrets.TG_CREDENTIALS`

## GitHub Actions

E2E tests run via `.github/workflows/e2e.yml`:

- **Trigger**: Comment `/e2e` on PR (requires write permission) or manual run
- **Credentials**: Stored in `secrets.TG_CREDENTIALS`

## Security

- Never commit credentials to version control
122 changes: 122 additions & 0 deletions test/import_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package test

import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"

"github.com/go-faster/errors"

"github.com/iyear/tdl/pkg/kv"
)

// CredentialImport represents imported credentials
type CredentialImport struct {
Namespace string `json:"namespace"`
Data map[string][]byte `json:"data"`
}

// SetupWithImportedCredentials sets up e2e tests with imported credentials from file
func SetupWithImportedCredentials(ctx context.Context) (account string, sessionFile string, _ error) {
credFile := os.Getenv("TDL_TEST_CREDENTIALS_FILE")
if credFile == "" {
return "", "", errors.New("TDL_TEST_CREDENTIALS_FILE is required")
}

log.Printf("Loading credentials from file: %s", credFile)
return setupFromFile(ctx, credFile)
}

func setupFromFile(ctx context.Context, credFile string) (string, string, error) {
data, err := os.ReadFile(credFile)
if err != nil {
return "", "", errors.Wrap(err, "read credentials file")
}

return setupFromJSON(ctx, string(data))
}

func setupFromJSON(ctx context.Context, credJSON string) (string, string, error) {
var cred CredentialImport
if err := json.Unmarshal([]byte(credJSON), &cred); err != nil {
return "", "", errors.Wrap(err, "unmarshal credentials")
}

if cred.Namespace == "" {
return "", "", errors.New("namespace is empty in credentials")
}

if len(cred.Data) == 0 {
return "", "", errors.New("no data in credentials")
}

// Verify session data exists
if _, ok := cred.Data["session"]; !ok {
return "", "", errors.New("session data not found in credentials")
}

// Create temporary session file
account := fmt.Sprintf("e2e-imported-%s", cred.Namespace)
sessionFile := filepath.Join(os.TempDir(), "tdl-e2e", account)

// Create session directory
if err := os.MkdirAll(filepath.Dir(sessionFile), 0o755); err != nil {
return "", "", errors.Wrap(err, "create session directory")
}

log.Printf("Creating session file: %s", sessionFile)

// Create KV storage
kvd, err := kv.New(kv.DriverFile, map[string]any{
"path": sessionFile,
})
if err != nil {
return "", "", errors.Wrap(err, "create kv storage")
}
defer kvd.Close()

// Open namespace
stg, err := kvd.Open(account)
if err != nil {
return "", "", errors.Wrap(err, "open namespace")
}

// Import all data
for key, value := range cred.Data {
if err := stg.Set(ctx, key, value); err != nil {
return "", "", errors.Wrapf(err, "set key: %s", key)
}
log.Printf("Imported key: %s (%d bytes)", key, len(value))
}

log.Printf("Successfully imported credentials for namespace: %s", cred.Namespace)
log.Printf("Account: %s, Session file: %s", account, sessionFile)

return account, sessionFile, nil
}

// ValidateCredentials validates that the credentials contain necessary data
func ValidateCredentials(credJSON string) error {
var cred CredentialImport
if err := json.Unmarshal([]byte(credJSON), &cred); err != nil {
return errors.Wrap(err, "unmarshal credentials")
}

if cred.Namespace == "" {
return errors.New("namespace is empty")
}

if _, ok := cred.Data["session"]; !ok {
return errors.New("session data not found")
}

// Optional but recommended
if _, ok := cred.Data["app"]; !ok {
log.Println("Warning: app type not found in credentials")
}

return nil
}
17 changes: 14 additions & 3 deletions test/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import (
"fmt"
"io"
"log"
"math/rand"
"os"
"testing"
"time"

"github.com/fatih/color"
"github.com/spf13/cobra"

tcmd "github.com/iyear/tdl/cmd"
"github.com/iyear/tdl/test/testserver"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand All @@ -35,7 +34,14 @@ var (

var _ = BeforeSuite(func(ctx context.Context) {
var err error
testAccount, sessionFile, err = testserver.Setup(ctx, rand.NewSource(GinkgoRandomSeed()))

// Use imported credentials from file
if os.Getenv("TDL_TEST_CREDENTIALS_FILE") == "" {
log.Fatal("TDL_TEST_CREDENTIALS_FILE is required")
}

log.Println("Using imported credentials for e2e tests")
testAccount, sessionFile, err = SetupWithImportedCredentials(ctx)
Expect(err).To(Succeed())

log.SetOutput(GinkgoWriter)
Expand All @@ -45,6 +51,11 @@ var _ = BeforeEach(func() {
cmd = tcmd.New()
})

var _ = AfterEach(func() {
// Add 1 second delay between tests to avoid rate limiting
time.Sleep(1 * time.Second)
})

func exec(cmd *cobra.Command, args []string, success bool) {
r, w, err := os.Pipe()
Expect(err).To(Succeed())
Expand Down
Empty file added test/test.json
Empty file.
8 changes: 0 additions & 8 deletions test/testserver/public_key.pem

This file was deleted.

Loading