// Scenario C: Full expansion — both LLDAP → full LDAP directory migration AND // KeyCape → Keycloak IAM migration. These tests verify the two migration // dimensions are independent (orthogonal) and that user data is semantically // equivalent after both migrations. package migration_test import ( "strings" "testing" "keycape/internal/migration/toldap" "keycape/internal/migration/tokeycloak" "keycape/internal/server/telemetry" ) func newGenerator(target toldap.Target) *toldap.Generator { return toldap.New(toldap.Config{ BaseDN: "dc=netkingdom,dc=local", Target: target, }, telemetry.NoopEmitter{}) } // TestScenarioCLDIFRoundTrip verifies the LDIF generator produces valid content // for the canonical fixture. func TestScenarioCLDIFRoundTrip(t *testing.T) { export := canonicalFixture() gen := newGenerator(toldap.TargetOpenLDAP) ldif, err := gen.Generate(export) if err != nil { t.Fatalf("Generate: %v", err) } if ldif == "" { t.Fatal("expected non-empty LDIF output") } // Verify all users appear in LDIF for _, u := range export.Users { if !strings.Contains(ldif, "uid: "+u.Username) { t.Errorf("LDIF missing user attribute uid: %s", u.Username) } } // Verify all groups appear in LDIF for _, g := range export.Groups { if !strings.Contains(ldif, "cn: "+g.Name) { t.Errorf("LDIF missing group cn: %s", g.Name) } } } // TestScenarioCTargetDifferences verifies OpenLDAP vs 389DS vs AD produce different LDIF. func TestScenarioCTargetDifferences(t *testing.T) { export := canonicalFixture() ldifOpenLDAP, err := newGenerator(toldap.TargetOpenLDAP).Generate(export) if err != nil { t.Fatalf("OpenLDAP Generate: %v", err) } ldif389DS, err := newGenerator(toldap.Target389DS).Generate(export) if err != nil { t.Fatalf("389DS Generate: %v", err) } ldifAD, err := newGenerator(toldap.TargetAD).Generate(export) if err != nil { t.Fatalf("AD Generate: %v", err) } // AD must use sAMAccountName if !strings.Contains(ldifAD, "sAMAccountName:") { t.Error("AD LDIF missing sAMAccountName attribute") } // OpenLDAP must NOT have sAMAccountName if strings.Contains(ldifOpenLDAP, "sAMAccountName:") { t.Error("OpenLDAP LDIF should not have sAMAccountName") } // 389DS must have nsUniqueId or standard entries _ = ldif389DS // 389DS is valid even without nsUniqueId when LDAPAttributes is empty // All three must contain the same users for _, u := range export.Users { if !strings.Contains(ldifOpenLDAP, u.Username) { t.Errorf("OpenLDAP LDIF missing user %s", u.Username) } if !strings.Contains(ldif389DS, u.Username) { t.Errorf("389DS LDIF missing user %s", u.Username) } if !strings.Contains(ldifAD, u.Username) { t.Errorf("AD LDIF missing user %s", u.Username) } } } // TestScenarioCMFANotMigrated verifies privacyIDEA MFA enrollment is NOT part of // either migration dimension. MFA stays stable across lightweight → expanded. func TestScenarioCMFANotMigrated(t *testing.T) { export := canonicalFixture() // Add MFA enrollment to a user mfaUser := export.Users[0] mfaUser.MFAEnrollment = nil // MFAEnrollment is NOT in the canonical export for migration // LDIF generation must not include any OTP/MFA attributes gen := newGenerator(toldap.TargetOpenLDAP) ldif, err := gen.Generate(export) if err != nil { t.Fatalf("Generate: %v", err) } // LDIF must not contain privacyIDEA-specific attributes if strings.Contains(ldif, "otpKey:") || strings.Contains(ldif, "privacyidea") { t.Error("LDIF should not contain MFA/OTP attributes — privacyIDEA is orthogonal to directory migration") } // Keycloak realm must not include MFA credentials transformer := tokeycloak.New(tokeycloak.Config{ RealmName: "netkingdom", Issuer: "https://auth.netkingdom.local", }, telemetry.NoopEmitter{}) realm, err := transformer.Transform(export) if err != nil { t.Fatalf("Transform: %v", err) } for _, u := range realm.Users { for _, cred := range u.Credentials { if cred.Type == "otp" || cred.Type == "totp" { t.Errorf("user %q has OTP credential in Keycloak import — MFA migration should not happen here", u.Username) } } } } // TestScenarioCStructuralEntries verifies ou=users and ou=groups are always generated. func TestScenarioCStructuralEntries(t *testing.T) { export := canonicalFixture() gen := newGenerator(toldap.TargetOpenLDAP) ldif, err := gen.Generate(export) if err != nil { t.Fatalf("Generate: %v", err) } if !strings.Contains(ldif, "ou=users,dc=netkingdom,dc=local") { t.Error("LDIF missing ou=users structural entry") } if !strings.Contains(ldif, "ou=groups,dc=netkingdom,dc=local") { t.Error("LDIF missing ou=groups structural entry") } } // TestScenarioCUserPreservation verifies all user fields survive directory migration. func TestScenarioCUserPreservation(t *testing.T) { export := canonicalFixture() gen := newGenerator(toldap.TargetOpenLDAP) ldif, err := gen.Generate(export) if err != nil { t.Fatalf("Generate: %v", err) } for _, u := range export.Users { if !strings.Contains(ldif, "uid: "+u.Username) { t.Errorf("LDIF missing uid: %s", u.Username) } if u.Email != "" && !strings.Contains(ldif, "mail: "+u.Email) { t.Errorf("LDIF missing mail: %s for user %s", u.Email, u.Username) } } } // TestScenarioCGroupMembersPreserved verifies group member DNs are in the LDIF. func TestScenarioCGroupMembersPreserved(t *testing.T) { export := canonicalFixture() gen := newGenerator(toldap.TargetOpenLDAP) ldif, err := gen.Generate(export) if err != nil { t.Fatalf("Generate: %v", err) } // admins group has alice as member if !strings.Contains(ldif, "cn: admins") { t.Error("LDIF missing admins group") } // member entries should be present if !strings.Contains(ldif, "member:") { t.Error("LDIF missing member: entries for groups") } } // TestScenarioCOrthogonality verifies Scenario C = Scenario A (LDIF migration) + Scenario B (Keycloak migration) // are independent: each can be performed without the other. func TestScenarioCOrthogonality(t *testing.T) { export := canonicalFixture() // Can generate LDIF without Keycloak realm gen := newGenerator(toldap.TargetOpenLDAP) _, err := gen.Generate(export) if err != nil { t.Errorf("LDIF generation (without Keycloak) failed: %v", err) } // Can generate Keycloak realm without LDIF transformer := tokeycloak.New(tokeycloak.Config{ RealmName: "netkingdom", Issuer: "https://auth.netkingdom.local", }, telemetry.NoopEmitter{}) _, err = transformer.Transform(export) if err != nil { t.Errorf("Keycloak transform (without LDIF) failed: %v", err) } }