246 lines
7.9 KiB
Go
246 lines
7.9 KiB
Go
|
|
// 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
|
||
|
|
}
|