template-monorepo/cmd/e2e-seed/main.go

246 lines
7.9 KiB
Go
Raw Normal View History

2026-05-21 23:52:39 +00:00
// 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
}