- Added endpoints for listing and importing migration rosters. - Introduced audit export functionality for migration jobs in CSV and NDJSON formats. - Implemented tenant mismatch validation for Microsoft migration claims. - Enhanced error handling for email claiming and migration processes. - Added integration tests for roster import and claim workflows.
134 lines
4.3 KiB
Go
134 lines
4.3 KiB
Go
//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)
|
|
}
|
|
}
|