//go:build e2e package e2e import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "testing" "time" ) var sharedFixture Fixture func TestMain(m *testing.M) { if err := loadSharedFixture(); err != nil { fmt.Fprintf(os.Stderr, "e2e bootstrap: %v\n", err) os.Exit(1) } os.Exit(m.Run()) } // e2eStep prints a one-line banner at the start of every E2E test so `go test -v` // shows the user-facing test ID, HTTP method/path, and a Chinese summary instead // of just the Go function name. Format: // // ▶ [M-01] GET /api/v1/members/me — 讀 profile // // Keep IDs in sync with docs/e2e-testing.md (測試覆蓋矩陣). func e2eStep(t *testing.T, id, method, path, desc string) { t.Helper() switch { case method == "" && path == "": t.Logf("▶ [%s] %s", id, desc) case method == "": t.Logf("▶ [%s] %s — %s", id, path, desc) default: t.Logf("▶ [%s] %s %s — %s", id, method, path, desc) } } func loadSharedFixture() error { path := os.Getenv("E2E_STATE_FILE") if path == "" { path = filepath.Join("fixtures", "state.json") } raw, err := os.ReadFile(path) if err != nil { return fmt.Errorf("read %s: %w (run make e2e-full)", path, err) } if err := json.Unmarshal(raw, &sharedFixture); err != nil { return err } if sharedFixture.BaseURL == "" || sharedFixture.AccessToken == "" { return fmt.Errorf("invalid fixture in %s", path) } if override := os.Getenv("E2E_BASE_URL"); override != "" { sharedFixture.BaseURL = override } return nil } func refreshTokenPair(baseURL, refreshToken string) (Fixture, error) { body, _ := json.Marshal(map[string]string{"refresh_token": refreshToken}) req, err := http.NewRequest(http.MethodPost, baseURL+"/api/v1/auth/token/refresh", bytes.NewReader(body)) if err != nil { return Fixture{}, err } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(req) if err != nil { return Fixture{}, err } defer resp.Body.Close() raw, err := io.ReadAll(resp.Body) if err != nil { return Fixture{}, err } var env Envelope if err := json.Unmarshal(raw, &env); err != nil { return Fixture{}, err } if resp.StatusCode != http.StatusOK || env.Code != successCode { return Fixture{}, fmt.Errorf("refresh failed: status=%d code=%d", resp.StatusCode, env.Code) } var data struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` } if err := json.Unmarshal(env.Data, &data); err != nil { return Fixture{}, err } out := Fixture{AccessToken: data.AccessToken, RefreshToken: data.RefreshToken} if out.AccessToken == "" || out.RefreshToken == "" { return Fixture{}, fmt.Errorf("refresh returned empty tokens") } return out, nil } func loadFixture() Fixture { return sharedFixture } func freshClient(t *testing.T) *Client { t.Helper() fx := loadFixture() base := fx.BaseURL if override := os.Getenv("E2E_BASE_URL"); override != "" { base = override } return &Client{ BaseURL: base, HTTP: &http.Client{Timeout: 15 * time.Second}, Fixture: fx, } } func isolatedAuthClient(t *testing.T) *Client { t.Helper() path := os.Getenv("E2E_STATE_FILE") if path == "" { path = filepath.Join("fixtures", "state.json") } raw, err := os.ReadFile(path) if err != nil { t.Fatalf("read fixture: %v", err) } var fx Fixture if err := json.Unmarshal(raw, &fx); err != nil { t.Fatalf("parse fixture: %v", err) } if override := os.Getenv("E2E_BASE_URL"); override != "" { fx.BaseURL = override } pair, err := refreshTokenPair(fx.BaseURL, fx.RefreshToken) if err != nil { t.Fatalf("isolated refresh: %v", err) } fx.AccessToken = pair.AccessToken fx.RefreshToken = pair.RefreshToken return &Client{ BaseURL: fx.BaseURL, HTTP: &http.Client{Timeout: 15 * time.Second}, Fixture: fx, } }