mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-04-29 02:49:55 -05:00
feat: add mage test:e2e for isolated end-to-end testing
This commit is contained in:
193
magefile.go
193
magefile.go
@@ -28,6 +28,8 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -69,6 +71,7 @@ var (
|
||||
"dev:make-notification": Dev.MakeNotification,
|
||||
"dev:prepare-worktree": Dev.PrepareWorktree,
|
||||
"dev:tag-release": Dev.TagRelease,
|
||||
"test:e2e": Test.E2E,
|
||||
"plugins:build": Plugins.Build,
|
||||
"lint": Check.Golangci,
|
||||
"lint:fix": Check.GolangciFix,
|
||||
@@ -316,6 +319,41 @@ func printSuccess(text string, args ...interface{}) {
|
||||
fmt.Printf(InfoColor+"\n", text)
|
||||
}
|
||||
|
||||
// getE2EPort returns the port from the given env var, or a random available port.
|
||||
func getE2EPort(envVar string) (int, error) {
|
||||
if v := os.Getenv(envVar); v != "" {
|
||||
return strconv.Atoi(v)
|
||||
}
|
||||
return getRandomPort()
|
||||
}
|
||||
|
||||
// getRandomPort finds a random available TCP port.
|
||||
func getRandomPort() (int, error) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer l.Close()
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
// waitForHTTP polls a URL until it returns a 200 status or the timeout expires.
|
||||
func waitForHTTP(url string, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
for time.Now().Before(deadline) {
|
||||
resp, err := client.Get(url)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("timed out waiting for %s after %s", url, timeout)
|
||||
}
|
||||
|
||||
// Fmt formats the code using go fmt
|
||||
func Fmt() error {
|
||||
mg.Deps(initVars)
|
||||
@@ -371,6 +409,161 @@ func (Test) All() {
|
||||
mg.Deps(Test.Feature, Test.Web)
|
||||
}
|
||||
|
||||
// E2E builds the API, starts it with an in-memory database and the frontend dev server,
|
||||
// runs the Playwright e2e tests against them, then tears everything down.
|
||||
// This does not touch your local database.
|
||||
//
|
||||
// Any arguments are passed through to Playwright. Examples:
|
||||
//
|
||||
// mage test:e2e "" # run all tests
|
||||
// mage test:e2e "tests/e2e/misc/menu.spec.ts" # run a specific test file
|
||||
// mage test:e2e "--grep menu" # filter by test name
|
||||
// mage test:e2e "--headed" # run in headed browser mode
|
||||
// mage test:e2e "--headed tests/e2e/misc/menu.spec.ts" # combine flags
|
||||
//
|
||||
// Environment variable overrides:
|
||||
// - VIKUNJA_E2E_API_PORT: API port (default: random)
|
||||
// - VIKUNJA_E2E_FRONTEND_PORT: Frontend port (default: random)
|
||||
// - VIKUNJA_E2E_TESTING_TOKEN: Testing token for seed endpoints (default: random)
|
||||
// - VIKUNJA_E2E_SKIP_BUILD: Set to "true" to skip rebuilding the API binary (default: false)
|
||||
func (Test) E2E(args string) error {
|
||||
mg.Deps(initVars)
|
||||
|
||||
// Determine ports
|
||||
apiPort, err := getE2EPort("VIKUNJA_E2E_API_PORT")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get API port: %w", err)
|
||||
}
|
||||
frontendPort, err := getE2EPort("VIKUNJA_E2E_FRONTEND_PORT")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get frontend port: %w", err)
|
||||
}
|
||||
|
||||
// Generate a random testing token
|
||||
testingToken := os.Getenv("VIKUNJA_E2E_TESTING_TOKEN")
|
||||
if testingToken == "" {
|
||||
testingToken = fmt.Sprintf("e2e-test-token-%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
fmt.Printf("E2E test configuration:\n")
|
||||
fmt.Printf(" API port: %d\n", apiPort)
|
||||
fmt.Printf(" Frontend port: %d\n", frontendPort)
|
||||
fmt.Printf(" Testing token: %s\n", testingToken)
|
||||
|
||||
// Build the API binary (unless skipped)
|
||||
if os.Getenv("VIKUNJA_E2E_SKIP_BUILD") != "true" {
|
||||
fmt.Println("\n--- Building API binary ---")
|
||||
if err := (Build{}).Build(); err != nil {
|
||||
return fmt.Errorf("failed to build API: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create temp directory for the SQLite DB and file uploads
|
||||
tmpDir, err := os.MkdirTemp("", "vikunja-e2e-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(tmpDir, "files"), 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create files dir: %w", err)
|
||||
}
|
||||
|
||||
// Start the API server — all config via env vars, no config file
|
||||
fmt.Println("\n--- Starting API server ---")
|
||||
apiCmd := exec.Command("./vikunja", "web")
|
||||
apiCmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("VIKUNJA_SERVICE_INTERFACE=:%d", apiPort),
|
||||
fmt.Sprintf("VIKUNJA_SERVICE_PUBLICURL=http://127.0.0.1:%d/", apiPort),
|
||||
fmt.Sprintf("VIKUNJA_SERVICE_TESTINGTOKEN=%s", testingToken),
|
||||
fmt.Sprintf("VIKUNJA_SERVICE_ROOTPATH=%s", tmpDir),
|
||||
"VIKUNJA_SERVICE_JWTSECRET=e2e-test-jwt-secret-do-not-use-in-production",
|
||||
"VIKUNJA_DATABASE_TYPE=sqlite",
|
||||
fmt.Sprintf("VIKUNJA_DATABASE_PATH=%s", filepath.Join(tmpDir, "vikunja-e2e.db")),
|
||||
fmt.Sprintf("VIKUNJA_FILES_BASEPATH=%s", filepath.Join(tmpDir, "files")),
|
||||
"VIKUNJA_LOG_LEVEL=WARNING",
|
||||
"VIKUNJA_MAILER_ENABLED=false",
|
||||
"VIKUNJA_REDIS_ENABLED=false",
|
||||
"VIKUNJA_CORS_ENABLE=true",
|
||||
"VIKUNJA_CORS_ORIGINS=http://127.0.0.1:*,http://localhost:*",
|
||||
)
|
||||
apiCmd.Stdout = os.Stdout
|
||||
apiCmd.Stderr = os.Stderr
|
||||
if err := apiCmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start API: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
fmt.Println("\n--- Stopping API server ---")
|
||||
if apiCmd.Process != nil {
|
||||
apiCmd.Process.Signal(os.Interrupt)
|
||||
apiCmd.Wait()
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for API to be ready
|
||||
apiBase := fmt.Sprintf("http://127.0.0.1:%d/api/v1", apiPort)
|
||||
fmt.Printf("Waiting for API at %s ...\n", apiBase)
|
||||
if err := waitForHTTP(apiBase+"/info", 30*time.Second); err != nil {
|
||||
return fmt.Errorf("API failed to start: %w", err)
|
||||
}
|
||||
printSuccess("API is ready!")
|
||||
|
||||
// Start the frontend dev server
|
||||
fmt.Println("\n--- Starting frontend dev server ---")
|
||||
frontendCmd := exec.Command("pnpm", "dev", "--port", strconv.Itoa(frontendPort))
|
||||
frontendCmd.Dir = "frontend"
|
||||
frontendCmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("VIKUNJA_FRONTEND_PORT=%d", frontendPort),
|
||||
fmt.Sprintf("DEV_PROXY=http://127.0.0.1:%d", apiPort),
|
||||
)
|
||||
frontendCmd.Stdout = os.Stdout
|
||||
frontendCmd.Stderr = os.Stderr
|
||||
if err := frontendCmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start frontend: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
fmt.Println("\n--- Stopping frontend dev server ---")
|
||||
if frontendCmd.Process != nil {
|
||||
frontendCmd.Process.Signal(os.Interrupt)
|
||||
frontendCmd.Wait()
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for frontend to be ready
|
||||
frontendBase := fmt.Sprintf("http://127.0.0.1:%d", frontendPort)
|
||||
fmt.Printf("Waiting for frontend at %s ...\n", frontendBase)
|
||||
if err := waitForHTTP(frontendBase, 60*time.Second); err != nil {
|
||||
return fmt.Errorf("frontend failed to start: %w", err)
|
||||
}
|
||||
printSuccess("Frontend is ready!")
|
||||
|
||||
// Run Playwright tests
|
||||
fmt.Println("\n--- Running Playwright e2e tests ---")
|
||||
playwrightArgs := []string{"test:e2e"}
|
||||
if strings.TrimSpace(args) != "" {
|
||||
playwrightArgs = append(playwrightArgs, strings.Fields(args)...)
|
||||
}
|
||||
playwrightCmd := exec.Command("pnpm", playwrightArgs...)
|
||||
playwrightCmd.Dir = "frontend"
|
||||
playwrightCmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("API_URL=%s", apiBase),
|
||||
fmt.Sprintf("BASE_URL=%s", frontendBase),
|
||||
fmt.Sprintf("VIKUNJA_SERVICE_TESTINGTOKEN=%s", testingToken),
|
||||
fmt.Sprintf("TEST_SECRET=%s", testingToken),
|
||||
)
|
||||
playwrightCmd.Stdout = os.Stdout
|
||||
playwrightCmd.Stderr = os.Stderr
|
||||
|
||||
testErr := playwrightCmd.Run()
|
||||
|
||||
if testErr != nil {
|
||||
return fmt.Errorf("e2e tests failed: %w", testErr)
|
||||
}
|
||||
|
||||
printSuccess("All e2e tests passed!")
|
||||
return nil
|
||||
}
|
||||
|
||||
type Check mg.Namespace
|
||||
|
||||
// GotSwag checks if the swagger docs need to be re-generated from the code annotations
|
||||
|
||||
Reference in New Issue
Block a user