//go:build integration package migration_test import ( "bytes" "context" "encoding/json" "io" "net/http" "testing" "github.com/google/uuid" "github.com/ultisuite/ulti-backend/internal/integrationtest" "github.com/ultisuite/ulti-backend/internal/migration" "github.com/ultisuite/ulti-backend/internal/users" ) const testProvisionSecret = "test-provision-secret" func postProvision(t *testing.T, h *integrationtest.Harness, body map[string]any) *integrationtest.Response { t.Helper() data, err := json.Marshal(body) integrationtest.FailIf(err, t, "marshal provision body") req, err := http.NewRequest(http.MethodPost, h.Server.URL+"/internal/provision/user", bytes.NewReader(data)) integrationtest.FailIf(err, t, "provision request") req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Provision-Secret", testProvisionSecret) resp, err := http.DefaultClient.Do(req) integrationtest.FailIf(err, t, "provision call") defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) integrationtest.FailIf(err, t, "read provision response") return &integrationtest.Response{Status: resp.StatusCode, Body: bodyBytes, Header: resp.Header} } func TestProvisionEnrollThenClaim(t *testing.T) { h := integrationtest.RequireHarness(t) ctx := context.Background() adminClient, adminClaims := integrationtest.RequireAdminClient(t, h) if _, err := users.EnsureUser(ctx, h.Pool, adminClaims); err != nil { t.Fatalf("ensure admin: %v", err) } if err := users.GrantPlatformAdmin(ctx, h.Pool, adminClaims.Sub); err != nil { t.Fatalf("grant admin: %v", err) } domainName := "enroll-claim-" + uuid.NewString()[:8] + ".test" var domainID string err := h.Pool.QueryRow(ctx, ` INSERT INTO mail_domains (name, status, is_platform_domain) VALUES ($1, 'active', false) RETURNING id::text `, domainName).Scan(&domainID) integrationtest.FailIf(err, t, "insert domain") createResp, err := adminClient.Post("/api/v1/admin/migration/projects", map[string]any{ "name": "Enroll then claim", "source_provider": "google", "domain_id": domainID, }) integrationtest.FailIf(err, t, "create project") integrationtest.FailUnlessStatus(t, createResp, 201) var created struct { ID string `json:"id"` } integrationtest.DecodeJSON(t, createResp, &created) actResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/activate", nil) integrationtest.FailIf(err, t, "activate project") integrationtest.FailUnlessStatus(t, actResp, 200) migrateeEmail := "user@" + domainName inviteResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/invites", map[string]string{ "email": migrateeEmail, }) integrationtest.FailIf(err, t, "create invite") integrationtest.FailUnlessStatus(t, inviteResp, 201) var invite struct { Token string `json:"token"` } integrationtest.DecodeJSON(t, inviteResp, &invite) externalID := integrationtest.NewExternalID("enroll-claim") provisionResp := postProvision(t, h, map[string]any{ "email": migrateeEmail, "name": "Migratee", "password": "enroll-password-123", "external_id": externalID, }) if provisionResp.Status != 200 { t.Fatalf("provision status = %d, want 200; body=%s", provisionResp.Status, string(provisionResp.Body)) } migrateeClaims := integrationtest.RegularUser(externalID) migrateeClaims.Email = migrateeEmail migrateeClient, err := h.Client(migrateeClaims) integrationtest.FailIf(err, t, "migratee client") if _, err := users.EnsureUser(ctx, h.Pool, migrateeClaims); err != nil { t.Fatalf("ensure migratee: %v", err) } claimResp, err := migrateeClient.Post("/api/v1/migration/claim", map[string]string{ "token": invite.Token, "password": "claim-password-123", }) integrationtest.FailIf(err, t, "claim invite") integrationtest.FailUnlessStatus(t, claimResp, 200) audit, err := migration.AuditProvisionByEmail(ctx, h.Pool, migrateeEmail) integrationtest.FailIf(err, t, "audit provision") if audit.Users != 1 { t.Fatalf("users = %d, want 1", audit.Users) } if audit.Mailboxes != 1 { t.Fatalf("mailboxes = %d, want 1", audit.Mailboxes) } if audit.MailAccounts != 1 { t.Fatalf("mail_accounts = %d, want 1", audit.MailAccounts) } } func TestProvisionClaimThenEnroll(t *testing.T) { h := integrationtest.RequireHarness(t) ctx := context.Background() adminClient, adminClaims := integrationtest.RequireAdminClient(t, h) if _, err := users.EnsureUser(ctx, h.Pool, adminClaims); err != nil { t.Fatalf("ensure admin: %v", err) } if err := users.GrantPlatformAdmin(ctx, h.Pool, adminClaims.Sub); err != nil { t.Fatalf("grant admin: %v", err) } domainName := "claim-enroll-" + uuid.NewString()[:8] + ".test" var domainID string err := h.Pool.QueryRow(ctx, ` INSERT INTO mail_domains (name, status, is_platform_domain) VALUES ($1, 'active', false) RETURNING id::text `, domainName).Scan(&domainID) integrationtest.FailIf(err, t, "insert domain") createResp, err := adminClient.Post("/api/v1/admin/migration/projects", map[string]any{ "name": "Claim then enroll", "source_provider": "google", "domain_id": domainID, }) integrationtest.FailIf(err, t, "create project") integrationtest.FailUnlessStatus(t, createResp, 201) var created struct { ID string `json:"id"` } integrationtest.DecodeJSON(t, createResp, &created) actResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/activate", nil) integrationtest.FailIf(err, t, "activate project") integrationtest.FailUnlessStatus(t, actResp, 200) migrateeEmail := "user@" + domainName inviteResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/invites", map[string]string{ "email": migrateeEmail, }) integrationtest.FailIf(err, t, "create invite") integrationtest.FailUnlessStatus(t, inviteResp, 201) var invite struct { Token string `json:"token"` } integrationtest.DecodeJSON(t, inviteResp, &invite) externalID := integrationtest.NewExternalID("claim-enroll") migrateeClaims := integrationtest.RegularUser(externalID) migrateeClaims.Email = migrateeEmail migrateeClient, err := h.Client(migrateeClaims) integrationtest.FailIf(err, t, "migratee client") userID, err := users.EnsureUser(ctx, h.Pool, migrateeClaims) integrationtest.FailIf(err, t, "ensure migratee") claimResp, err := migrateeClient.Post("/api/v1/migration/claim", map[string]string{ "token": invite.Token, "password": "claim-password-123", }) integrationtest.FailIf(err, t, "claim invite") integrationtest.FailUnlessStatus(t, claimResp, 200) provisionResp := postProvision(t, h, map[string]any{ "email": migrateeEmail, "name": "Migratee", "password": "enroll-password-123", "external_id": externalID, }) if provisionResp.Status != 200 { t.Fatalf("provision status = %d, want 200; body=%s", provisionResp.Status, string(provisionResp.Body)) } var provisionBody struct { UserID string `json:"user_id"` } integrationtest.DecodeJSON(t, provisionResp, &provisionBody) if provisionBody.UserID != userID { t.Fatalf("provision user_id = %q, want %q", provisionBody.UserID, userID) } audit, err := migration.AuditProvisionByEmail(ctx, h.Pool, migrateeEmail) integrationtest.FailIf(err, t, "audit provision") if audit.Users != 1 { t.Fatalf("users = %d, want 1", audit.Users) } if audit.Mailboxes != 1 { t.Fatalf("mailboxes = %d, want 1", audit.Mailboxes) } if audit.MailAccounts != 1 { t.Fatalf("mail_accounts = %d, want 1", audit.MailAccounts) } } func TestProvisionEnrollmentDefersMailboxForPendingInvite(t *testing.T) { h := integrationtest.RequireHarness(t) ctx := context.Background() adminClient, adminClaims := integrationtest.RequireAdminClient(t, h) if _, err := users.EnsureUser(ctx, h.Pool, adminClaims); err != nil { t.Fatalf("ensure admin: %v", err) } if err := users.GrantPlatformAdmin(ctx, h.Pool, adminClaims.Sub); err != nil { t.Fatalf("grant admin: %v", err) } platformEmail := "pending-" + uuid.NewString()[:8] + "@ultisuite.local" createResp, err := adminClient.Post("/api/v1/admin/migration/projects", map[string]any{ "name": "Pending invite defer", "source_provider": "google", }) integrationtest.FailIf(err, t, "create project") integrationtest.FailUnlessStatus(t, createResp, 201) var created struct { ID string `json:"id"` } integrationtest.DecodeJSON(t, createResp, &created) actResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/activate", nil) integrationtest.FailIf(err, t, "activate project") integrationtest.FailUnlessStatus(t, actResp, 200) inviteResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/invites", map[string]string{ "email": platformEmail, }) integrationtest.FailIf(err, t, "create invite") integrationtest.FailUnlessStatus(t, inviteResp, 201) externalID := integrationtest.NewExternalID("pending-invite") provisionResp := postProvision(t, h, map[string]any{ "email": platformEmail, "name": "Pending User", "password": "enroll-password-123", "external_id": externalID, }) if provisionResp.Status != 200 { t.Fatalf("provision status = %d, want 200; body=%s", provisionResp.Status, string(provisionResp.Body)) } var provisionBody struct { MailboxDeferred bool `json:"mailbox_deferred"` } integrationtest.DecodeJSON(t, provisionResp, &provisionBody) if !provisionBody.MailboxDeferred { t.Fatal("expected mailbox_deferred=true for pending invite enrollment") } audit, err := migration.AuditProvisionByEmail(ctx, h.Pool, platformEmail) integrationtest.FailIf(err, t, "audit after deferred enroll") if audit.Users != 1 { t.Fatalf("users = %d, want 1", audit.Users) } if audit.Mailboxes != 0 { t.Fatalf("mailboxes = %d, want 0 before claim", audit.Mailboxes) } }