- 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.
258 lines
8.9 KiB
Go
258 lines
8.9 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 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]any{
|
|
"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")
|
|
}
|
|
|
|
func TestClaimInviteRejectsMicrosoftTenantMismatch(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": "Tenant mismatch",
|
|
"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)
|
|
|
|
expectedTenant := "11111111-2222-3333-4444-555555555555"
|
|
if _, err := h.Pool.Exec(ctx, `
|
|
UPDATE migration_projects SET microsoft_tenant_id = $1 WHERE id = $2::uuid
|
|
`, expectedTenant, created.ID); err != nil {
|
|
t.Fatalf("set tenant: %v", err)
|
|
}
|
|
|
|
inviteEmail := "tenant-user-" + uuid.NewString()[:8] + "@example.com"
|
|
inviteResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/invites", map[string]any{
|
|
"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)
|
|
|
|
wrongTenantClaims := integrationtest.RegularUser(integrationtest.NewExternalID("tenant-mismatch"))
|
|
wrongTenantClaims.Email = inviteEmail
|
|
wrongTenantClaims.TID = "99999999-aaaa-bbbb-cccc-dddddddddddd"
|
|
wrongTenantClient, err := h.Client(wrongTenantClaims)
|
|
integrationtest.FailIf(err, t, "wrong tenant client")
|
|
|
|
if _, err := users.EnsureUser(ctx, h.Pool, wrongTenantClaims); err != nil {
|
|
t.Fatalf("ensure user: %v", err)
|
|
}
|
|
|
|
claimResp, err := wrongTenantClient.Post("/api/v1/migration/claim", map[string]string{
|
|
"token": invite.Token,
|
|
})
|
|
integrationtest.FailIf(err, t, "claim invite")
|
|
integrationtest.AssertErrorCode(t, claimResp, 400, "tenant_mismatch")
|
|
}
|
|
|
|
func TestClaimInviteGoogleProjectIgnoresTenant(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": "Google ignores tenant",
|
|
"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)
|
|
|
|
if _, err := h.Pool.Exec(ctx, `
|
|
UPDATE migration_projects SET microsoft_tenant_id = $1 WHERE id = $2::uuid
|
|
`, "11111111-2222-3333-4444-555555555555", created.ID); err != nil {
|
|
t.Fatalf("set tenant: %v", err)
|
|
}
|
|
|
|
inviteEmail := "google-user-" + uuid.NewString()[:8] + "@example.com"
|
|
inviteResp, err := adminClient.Post("/api/v1/admin/migration/projects/"+created.ID+"/invites", map[string]any{
|
|
"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)
|
|
|
|
userClaims := integrationtest.RegularUser(integrationtest.NewExternalID("google-tenant-ignore"))
|
|
userClaims.Email = inviteEmail
|
|
userClaims.TID = "wrong-tenant-id"
|
|
userClient, err := h.Client(userClaims)
|
|
integrationtest.FailIf(err, t, "user client")
|
|
|
|
if _, err := users.EnsureUser(ctx, h.Pool, userClaims); err != nil {
|
|
t.Fatalf("ensure user: %v", err)
|
|
}
|
|
|
|
claimResp, err := userClient.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)
|
|
}
|