// Command e2e-seed prepares a fresh E2E tenant, member, permission roles, and JWT tokens. // // Usage (usually invoked by scripts/e2e-run.sh): // // go run ./cmd/e2e-seed -f test/e2e/fixtures/e2e.yaml -out test/e2e/fixtures/state.json package main import ( "context" "encoding/json" "flag" "fmt" "os" "time" "gateway/internal/config" redislib "gateway/internal/library/redis" domauth "gateway/internal/model/auth/domain/usecase" authrepo "gateway/internal/model/auth/repository" authusecase "gateway/internal/model/auth/usecase" dommember "gateway/internal/model/member/domain/usecase" memberusecase "gateway/internal/model/member/usecase" "gateway/internal/model/permission/domain/enum" domperm "gateway/internal/model/permission/domain/usecase" permrepo "gateway/internal/model/permission/repository" permseed "gateway/internal/model/permission/seed" permusecase "gateway/internal/model/permission/usecase" "github.com/zeromicro/go-zero/core/conf" ) const ( defaultTenantID = "e2e-tenant" defaultSlug = "e2e" defaultPrefix = "E2E" defaultEmail = "e2e-owner@example.com" defaultNoRoleEmail = "e2e-no-role@example.com" defaultRoleKey = "tenant_owner" ) var ( configFile = flag.String("f", "test/e2e/fixtures/e2e.yaml", "config file") outFile = flag.String("out", "test/e2e/fixtures/state.json", "output fixture JSON") tenantID = flag.String("tenant", defaultTenantID, "tenant_id") slug = flag.String("slug", defaultSlug, "tenant slug") uidPrefix = flag.String("prefix", defaultPrefix, "uid prefix") email = flag.String("email", defaultEmail, "member email") roleKey = flag.String("role", defaultRoleKey, "system role key to assign") ) // State is consumed by test/e2e HTTP tests. type State 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"` } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run() error { flag.Parse() var c config.Config conf.MustLoad(*configFile, &c) if c.Mongo.Host == "" || c.Redis.Host == "" { return fmt.Errorf("e2e-seed: Mongo and Redis are required") } if !c.Auth.Defaults().Enabled() { return fmt.Errorf("e2e-seed: Auth JWT secrets are required") } ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) defer cancel() rds, err := redislib.NewClient(c.Redis) if err != nil { return fmt.Errorf("e2e-seed: redis: %w", err) } memberMod, err := memberusecase.NewModuleFromParam(memberusecase.ModuleParam{ Redis: rds, MongoConf: &c.Mongo, Config: c.Member, }) if err != nil { return fmt.Errorf("e2e-seed: member module: %w", err) } if memberMod.Tenant == nil || memberMod.Lifecycle == nil || memberMod.Profile == nil { return fmt.Errorf("e2e-seed: member module incomplete (need Mongo)") } if _, err := memberMod.Tenant.Create(ctx, &dommember.CreateTenantRequest{ TenantID: *tenantID, Slug: *slug, Name: "E2E Tenant", UIDPrefix: *uidPrefix, }); err != nil { fmt.Printf("e2e-seed: tenant create skipped (may exist): %v\n", err) } uid, err := ensureMember(ctx, memberMod, *tenantID, *email) if err != nil { return err } if err := seedPermissionAndAssignRole(ctx, c, *tenantID, uid, *roleKey); err != nil { return err } noRoleUID, err := ensureMemberWithZitadel(ctx, memberMod, *tenantID, defaultNoRoleEmail, "e2e-no-role-sub", "E2E No Role") if err != nil { return err } tokens := authusecase.MustTokenUseCase(authusecase.TokenUseCaseParam{ Config: c.Auth, Revoke: authrepo.NewRedisTokenRevokeStore(rds), }) pair, err := issueTokenPair(ctx, tokens, *tenantID, uid) if err != nil { return fmt.Errorf("e2e-seed: issue token: %w", err) } noRolePair, err := issueTokenPair(ctx, tokens, *tenantID, noRoleUID) if err != nil { return fmt.Errorf("e2e-seed: issue no-role token: %w", err) } state := State{ BaseURL: fmt.Sprintf("http://127.0.0.1:%d", c.Port), TenantID: *tenantID, TenantSlug: *slug, UID: uid, Email: *email, AccessToken: pair.AccessToken, RefreshToken: pair.RefreshToken, RoleKey: *roleKey, NoRoleUID: noRoleUID, NoRoleEmail: defaultNoRoleEmail, NoRoleAccessToken: noRolePair.AccessToken, NoRoleRefreshToken: noRolePair.RefreshToken, } raw, err := json.MarshalIndent(state, "", " ") if err != nil { return fmt.Errorf("e2e-seed: marshal state: %w", err) } if err := os.WriteFile(*outFile, raw, 0o600); err != nil { return fmt.Errorf("e2e-seed: write %s: %w", *outFile, err) } fmt.Printf("e2e-seed: tenant=%s uid=%s role=%s\n", state.TenantID, state.UID, state.RoleKey) fmt.Printf("e2e-seed: wrote %s (base_url=%s)\n", *outFile, state.BaseURL) return nil } func issueTokenPair(ctx context.Context, tokens domauth.TokenUseCase, tenantID, uid string) (*domauth.TokenPair, error) { return tokens.IssuePair(ctx, &domauth.IssuePairRequest{ TenantID: tenantID, UID: uid, }) } func ensureMember(ctx context.Context, mod *memberusecase.Module, tenantID, email string) (string, error) { return ensureMemberWithZitadel(ctx, mod, tenantID, email, "", "E2E Owner") } func ensureMemberWithZitadel(ctx context.Context, mod *memberusecase.Module, tenantID, email, zitadelUserID, displayName string) (string, error) { m, err := mod.Lifecycle.CreateUnverified(ctx, &dommember.CreatePlatformMemberRequest{ TenantID: tenantID, Email: email, ZitadelUserID: zitadelUserID, DisplayName: displayName, Language: "zh-tw", }) if err == nil { if actErr := mod.Lifecycle.Activate(ctx, tenantID, m.UID); actErr != nil { return "", fmt.Errorf("e2e-seed: activate member: %w", actErr) } return m.UID, nil } // Idempotent re-run: find existing member by listing (dev tenant has one owner). list, listErr := mod.Profile.List(ctx, &dommember.ListMembersRequest{ TenantID: tenantID, Limit: 50, }) if listErr != nil { return "", fmt.Errorf("e2e-seed: create member: %w (list fallback: %v)", err, listErr) } for _, item := range list.Items { if item.ZitadelEmail == email || item.BusinessEmail == email { return item.UID, nil } } return "", fmt.Errorf("e2e-seed: create member: %w", err) } func seedPermissionAndAssignRole(ctx context.Context, c config.Config, tenantID, uid, roleKey string) error { perms := permrepo.NewPermissionRepository(permrepo.PermissionRepositoryParam{Conf: &c.Mongo}) roles := permrepo.NewRoleRepository(permrepo.RoleRepositoryParam{Conf: &c.Mongo}) rolePerms := permrepo.NewRolePermissionRepository(permrepo.RolePermissionRepositoryParam{Conf: &c.Mongo}) if _, err := permseed.Apply(ctx, perms, roles, rolePerms, permseed.ApplyOptions{ TenantIDs: []string{tenantID}, }); err != nil { return fmt.Errorf("e2e-seed: permission seed: %w", err) } permMod, err := permusecase.NewModuleFromParam(permusecase.FactoryParam{ MongoConf: &c.Mongo, Redis: nil, Config: c.Permission, }) if err != nil { return fmt.Errorf("e2e-seed: permission module: %w", err) } role, err := permMod.Role.GetByKey(ctx, tenantID, roleKey) if err != nil { return fmt.Errorf("e2e-seed: get role %q: %w", roleKey, err) } if _, err := permMod.UserRole.Assign(ctx, &domperm.AssignParam{ TenantID: tenantID, UID: uid, RoleID: role.ID.Hex(), Source: enum.RoleSourceManual, }); err != nil { // Idempotent re-run when role already assigned. fmt.Printf("e2e-seed: assign role skipped: %v\n", err) } return nil }