//go:build integration package migration_test import ( "context" "testing" "github.com/google/uuid" "github.com/ultisuite/ulti-backend/internal/integrationtest" "github.com/ultisuite/ulti-backend/internal/users" ) func TestMigrationRosterImportAndClaim(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) } createResp, err := adminClient.Post("/api/v1/admin/migration/projects", map[string]any{ "name": "Roster migration", "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) csv := "email,display_name,alternate_emails\n" + "migratee-" + uuid.NewString() + "@example.com,Test User,alt-" + uuid.NewString() + "@example.com\n" importResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/roster", map[string]string{ "csv": csv, }) integrationtest.FailIf(err, t, "import roster") integrationtest.FailUnlessStatus(t, importResp, 200) var importResult struct { Created int `json:"created"` SkippedDuplicates int `json:"skipped_duplicates"` } integrationtest.DecodeJSON(t, importResp, &importResult) if importResult.Created != 1 { t.Fatalf("expected 1 created, got %#v", importResult) } listResp, err := adminClient.Get("/api/v1/admin/migration/projects/" + created.ID + "/roster") integrationtest.FailIf(err, t, "list roster") integrationtest.FailUnlessStatus(t, listResp, 200) var rosterList struct { Roster []struct { Email string `json:"email"` DisplayName string `json:"display_name"` Status string `json:"status"` } `json:"roster"` } integrationtest.DecodeJSON(t, listResp, &rosterList) if len(rosterList.Roster) != 1 { t.Fatalf("expected 1 roster entry, got %d", len(rosterList.Roster)) } if rosterList.Roster[0].Status != "invited" { t.Fatalf("expected invited status, got %q", rosterList.Roster[0].Status) } migrateeEmail := rosterList.Roster[0].Email dupResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/roster", map[string]string{ "csv": csv, }) integrationtest.FailIf(err, t, "duplicate import") integrationtest.FailUnlessStatus(t, dupResp, 200) var dupResult struct { SkippedDuplicates int `json:"skipped_duplicates"` } integrationtest.DecodeJSON(t, dupResp, &dupResult) if dupResult.SkippedDuplicates != 1 { t.Fatalf("expected 1 skipped duplicate, got %d", dupResult.SkippedDuplicates) } var inviteToken string err = h.Pool.QueryRow(ctx, ` SELECT token FROM migration_invites WHERE project_id = $1::uuid AND email = $2 `, created.ID, migrateeEmail).Scan(&inviteToken) if err != nil { t.Fatalf("lookup invite token: %v", err) } if inviteToken == "" { t.Fatal("missing invite token for roster entry") } migrateeClaims := integrationtest.RegularUser(integrationtest.NewExternalID("roster-migratee")) 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": inviteToken, "password": "test-password-123", "display_name": "Test User", }) integrationtest.FailIf(err, t, "claim invite") integrationtest.FailUnlessStatus(t, claimResp, 200) var rosterStatus string err = h.Pool.QueryRow(ctx, ` SELECT status FROM migration_roster WHERE project_id = $1::uuid AND email = $2 `, created.ID, migrateeEmail).Scan(&rosterStatus) if err != nil { t.Fatalf("roster status: %v", err) } if rosterStatus != "claimed" { t.Fatalf("expected claimed roster, got %q", rosterStatus) } }