//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 TestClaimInviteFlexibleEmailMatch(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": "Flexible email match", "source_provider": "microsoft", }) 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) inviteEmail := "alice-" + uuid.NewString()[:8] + "@example.com" ssoEmail := "alice.sso-" + uuid.NewString()[:8] + "@example.com" inviteResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/invites", map[string]any{ "email": inviteEmail, "alternate_emails": []string{ssoEmail}, }) integrationtest.FailIf(err, t, "create invite") integrationtest.FailUnlessStatus(t, inviteResp, 201) var invite struct { Token string `json:"token"` } integrationtest.DecodeJSON(t, inviteResp, &invite) migrateeClaims := integrationtest.RegularUser(integrationtest.NewExternalID("flex-claim")) migrateeClaims.Email = ssoEmail migrateeClaims.PreferredUsername = inviteEmail 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": "test-password-123", }) integrationtest.FailIf(err, t, "claim invite") integrationtest.FailUnlessStatus(t, claimResp, 200) } func TestClaimInviteRejectsEmailMismatch(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": "Reject mismatch", "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) inviteEmail := "victim-" + uuid.NewString() + "@example.com" inviteResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/invites", map[string]string{ "email": inviteEmail, }) integrationtest.FailIf(err, t, "create invite") integrationtest.FailUnlessStatus(t, inviteResp, 201) var invite struct { Token string `json:"token"` } integrationtest.DecodeJSON(t, inviteResp, &invite) attackerClaims := integrationtest.RegularUser(integrationtest.NewExternalID("flex-claim-bad")) attackerClaims.Email = "attacker-" + uuid.NewString() + "@example.com" attackerClient, err := h.Client(attackerClaims) integrationtest.FailIf(err, t, "attacker client") if _, err := users.EnsureUser(ctx, h.Pool, attackerClaims); err != nil { t.Fatalf("ensure attacker: %v", err) } claimResp, err := attackerClient.Post("/api/v1/migration/claim", map[string]string{ "token": invite.Token, }) integrationtest.FailIf(err, t, "claim invite") integrationtest.AssertErrorCode(t, claimResp, 400, "email_mismatch") }