package mysql import ( "context" "encoding/json" "os" "strings" "sync" "testing" "time" "git.yuntuo.hk/k2safe/orbit/internal/consoleauth" "git.yuntuo.hk/k2safe/orbit/internal/fault" "git.yuntuo.hk/k2safe/orbit/platform/auth" ) func TestIntegrationConsoleApplications(t *testing.T) { dsn := os.Getenv("ORBIT_TEST_MYSQL_DSN") if dsn == "" { t.Skip("requires disposable MySQL") } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() db, err := Open(ctx, dsn, 10, 3) if err != nil { t.Fatal(err) } defer db.Close() if err := Migrate(ctx, db); err != nil { t.Fatal(err) } username := "access-" + string(testID(t)) password := "local-test-password-12345" var initial BootstrapResult if err := Bootstrap(ctx, db, BootstrapInput{Application: username, Namespace: "default", Environment: "test", Queue: "initial", ConsoleUsername: username, ConsolePassword: password}, func(v BootstrapResult) error { initial = v; return nil }); err != nil { t.Fatal(err) } login, err := NewConsoleAuthStore(db).Login(ctx, username, password, "test", "127.0.0.1") if err != nil { t.Fatal(err) } admin, _ := auth.Parse(login.Token) sdk, _ := auth.Parse(initial.Token) store := NewConsoleApplicationStore(db, username) if _, err := store.ListApplications(ctx, sdk, ""); fault.Code(err) != "PERMISSION_DENIED" { t.Fatalf("SDK reached admin: %v", err) } if _, err := NewConsoleApplicationStore(db, "").ListApplications(ctx, admin, ""); fault.Code(err) != "PERMISSION_DENIED" { t.Fatal("disabled admin accepted") } if _, err := NewConsoleApplicationStore(db, "different-admin").ListApplications(ctx, admin, ""); fault.Code(err) != "PERMISSION_DENIED" { t.Fatal("other user accepted") } in := consoleauth.ApplicationInput{Kind: "CREATE", Application: "created-" + string(testID(t)), Namespace: "default", Environment: "test", Queue: "work", Capabilities: []string{"worker", "events", "schedules", "workflows"}, ValidDays: 30, Reason: "integration onboarding"} op, err := store.PrepareApplication(ctx, admin, in) if err != nil { t.Fatal(err) } // 准备只生成秘密,尚未允许业务访问。 candidate, _ := auth.Parse(op.Token) if err := NewTaskStore(db).AuthorizeSubmit(ctx, candidate); err == nil { t.Fatal("prepared token accepted before commit") } var wg sync.WaitGroup receipts := make([]consoleauth.ApplicationReceipt, 2) errs := make([]error, 2) for i := range receipts { wg.Add(1) go func(i int) { defer wg.Done(); receipts[i], errs[i] = store.ApplyApplication(ctx, admin, op) }(i) } wg.Wait() for _, err := range errs { if err != nil { t.Fatal(err) } } if receipts[0].Deduplicated == receipts[1].Deduplicated { t.Fatal("concurrent original operation did not deduplicate") } if err := NewTaskStore(db).AuthorizeSubmit(ctx, candidate); err != nil { t.Fatal(err) } // 同一操作身份不可改变请求,重名不能留下半套资源。 changed := op changed.Input.Reason = "modified" if _, err := store.ApplyApplication(ctx, admin, changed); fault.Code(err) != "IDEMPOTENCY_CONFLICT" { t.Fatalf("changed operation accepted: %v", err) } duplicate, err := store.PrepareApplication(ctx, admin, in) if err != nil { t.Fatal(err) } if _, err := store.ApplyApplication(ctx, admin, duplicate); fault.Code(err) != "STATE_CONFLICT" { t.Fatalf("duplicate name accepted: %v", err) } var n int if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM orbit_namespaces WHERE id=?`, duplicate.NamespaceID).Scan(&n); err != nil || n != 0 { t.Fatal("duplicate left namespace") } detail, err := store.GetApplication(ctx, admin, op.NamespaceID) if err != nil { t.Fatal(err) } if detail.Entry.ApplicationID != op.ApplicationID || len(detail.Queues) != 1 || len(detail.Credentials) != 1 || !detail.Credentials[0].Permissions.Allows("events:consume") { t.Fatal("incomplete onboarding") } raw, _ := json.Marshal(detail) if strings.Contains(string(raw), op.Token) || strings.Contains(string(raw), "secret_digest") { t.Fatal("detail leaked credential") } rotation := consoleauth.ApplicationInput{Kind: "ROTATE", NamespaceID: op.NamespaceID, CredentialID: op.CredentialID, ExpectedRevision: 1, ValidDays: 90, Reason: "scheduled rotation"} rotated, err := store.PrepareApplication(ctx, admin, rotation) if err != nil { t.Fatal(err) } receipt, err := store.ApplyApplication(ctx, admin, rotated) if err != nil || receipt.Revision != 2 { t.Fatalf("rotate: %v", err) } newToken, _ := auth.Parse(rotated.Token) if NewTaskStore(db).AuthorizeSubmit(ctx, candidate) == nil || NewTaskStore(db).AuthorizeSubmit(ctx, newToken) != nil { t.Fatal("rotation auth boundary failed") } replay, err := store.ApplyApplication(ctx, admin, rotated) if err != nil || !replay.Deduplicated { t.Fatalf("rotation replay: %v", err) } if _, err := store.PrepareApplication(ctx, admin, rotation); fault.Code(err) != "STATE_CONFLICT" { t.Fatalf("stale revision accepted: %v", err) } detail, err = store.GetApplication(ctx, admin, op.NamespaceID) if err != nil || len(detail.Credentials) != 1 || detail.Credentials[0].ID != op.CredentialID || detail.Queues[0].ID != op.QueueID { t.Fatal("rotation changed identity") } rows, err := db.QueryContext(ctx, `SELECT actor_id,CAST(`+"`after`"+` AS CHAR) FROM orbit_audit_events WHERE namespace_id=? AND action IN ('console.application.create','credential.rotate')`, op.NamespaceID) if err != nil { t.Fatal(err) } count := 0 for rows.Next() { var actor, body string if err := rows.Scan(&actor, &body); err != nil { t.Fatal(err) } if actor != "console:"+login.User.ID || strings.Contains(body, op.Token) || strings.Contains(body, rotated.Token) { t.Fatal("audit principal or secrecy") } count++ } err = rows.Err() rows.Close() if err != nil || count != 2 { t.Fatalf("audit count=%d err=%v", count, err) } // 登出在数据库生效后,原会话不能再次应用操作,即使持有原回执内容。 if err := NewConsoleAuthStore(db).RevokeSession(ctx, admin); err != nil { t.Fatal(err) } if _, err := store.ApplyApplication(ctx, admin, rotated); fault.Code(err) != "PERMISSION_DENIED" { t.Fatalf("logged out replay accepted: %v", err) } if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.referential_constraints WHERE constraint_schema=DATABASE()`).Scan(&n); err != nil || n != 0 { t.Fatalf("foreign keys=%d err=%v", n, err) } }