//go:build e2e package e2e import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "testing" "github.com/stretchr/testify/require" ) const successCode = 102000 // Fixture holds seed output from cmd/e2e-seed. type Fixture struct { BaseURL string `json:"base_url"` TenantID string `json:"tenant_id"` TenantSlug string `json:"tenant_slug"` UID string `json:"uid"` Email string `json:"email"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` RoleKey string `json:"role_key"` NoRoleUID string `json:"no_role_uid"` NoRoleEmail string `json:"no_role_email"` NoRoleAccessToken string `json:"no_role_access_token"` NoRoleRefreshToken string `json:"no_role_refresh_token"` } // Client is a thin HTTP helper for Gateway E2E tests. type Client struct { BaseURL string HTTP *http.Client Fixture Fixture } // Envelope matches gateway/types.Status JSON. type Envelope struct { Code int64 `json:"code"` Message string `json:"message"` Data json.RawMessage `json:"data"` Error json.RawMessage `json:"error,omitempty"` } // LoadFixture returns the shared bootstrap fixture (refreshed once in TestMain). func LoadFixture(t *testing.T) Fixture { t.Helper() fx := loadFixture() require.NotEmpty(t, fx.BaseURL) require.NotEmpty(t, fx.AccessToken) return fx } // NewClient builds a client from the shared fixture. func NewClient(t *testing.T) *Client { t.Helper() return freshClient(t) } // NewNoRoleClient builds a client for the seeded member that intentionally // has no role assignment. It is used by Casbin-enabled authorization tests. func NewNoRoleClient(t *testing.T) *Client { t.Helper() c := freshClient(t) require.NotEmpty(t, c.Fixture.NoRoleUID) require.NotEmpty(t, c.Fixture.NoRoleAccessToken) c.Fixture.UID = c.Fixture.NoRoleUID c.Fixture.Email = c.Fixture.NoRoleEmail c.Fixture.AccessToken = c.Fixture.NoRoleAccessToken c.Fixture.RefreshToken = c.Fixture.NoRoleRefreshToken return c } func (c *Client) URL(path string) string { return c.BaseURL + path } func (c *Client) Do(t *testing.T, method, path string, body any, auth bool) (*http.Response, Envelope) { t.Helper() var r io.Reader if body != nil { raw, err := json.Marshal(body) require.NoError(t, err) r = bytes.NewReader(raw) } req, err := http.NewRequest(method, c.URL(path), r) require.NoError(t, err) if body != nil { req.Header.Set("Content-Type", "application/json") } if auth { req.Header.Set("Authorization", "Bearer "+c.Fixture.AccessToken) } resp, err := c.HTTP.Do(req) require.NoError(t, err) defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) require.NoError(t, err) var env Envelope require.NoError(t, json.Unmarshal(respBody, &env), "body=%s", string(respBody)) return resp, env } func (c *Client) DoExpectOK(t *testing.T, method, path string, body any, auth bool) Envelope { t.Helper() resp, env := c.Do(t, method, path, body, auth) require.Equal(t, http.StatusOK, resp.StatusCode, "path=%s code=%d body=%s", path, env.Code, string(mustRaw(env))) require.Equal(t, int64(successCode), env.Code, "path=%s message=%s", path, env.Message) return env } func (c *Client) DoExpectHTTP(t *testing.T, method, path string, body any, auth bool, httpStatus int) Envelope { t.Helper() resp, env := c.Do(t, method, path, body, auth) require.Equal(t, httpStatus, resp.StatusCode, "path=%s env=%+v", path, env) return env } func mustRaw(env Envelope) []byte { if len(env.Data) == 0 { return []byte(env.Message) } return env.Data } // FetchE2EOTP reads OTP plain code stashed by verify_helper when GATEWAY_E2E=1. func FetchE2EOTP(t *testing.T, challengeID string) string { t.Helper() if cli := os.Getenv("REDISCLI"); cli != "" { out, err := runCmd(cli, "-c", fmt.Sprintf("GET e2e:otp:%s", challengeID)) require.NoError(t, err) require.NotEmpty(t, out, "missing e2e otp for challenge %s", challengeID) return out } out, err := runCmd("docker", "exec", "gateway-redis", "redis-cli", "GET", "e2e:otp:"+challengeID) require.NoError(t, err, "fetch otp via docker exec (is gateway-redis running?)") require.NotEmpty(t, out, "missing e2e otp for challenge %s", challengeID) return out }