snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:19:12 +05:00
commit c60cc7ca75
8092 changed files with 394160 additions and 0 deletions

View File

@@ -0,0 +1,898 @@
// Copyright © 2025 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package identity
import (
"bytes"
"context"
"encoding/json"
"errors"
"testing"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/herodot"
"github.com/ory/x/snapshotx"
)
func TestImportCredentialsOIDCSAML(t *testing.T) {
t.Parallel()
ctx := context.Background()
// Setup handler with minimal mock requirements
h := &Handler{}
testCases := []struct {
name string
setupIdentity func() *Identity
credentials interface{}
credType CredentialsType
}{
{
name: "OIDC new credential without organization",
setupIdentity: func() *Identity {
return &Identity{}
},
credentials: &AdminIdentityImportCredentialsOIDC{
Config: AdminIdentityImportCredentialsOIDCConfig{
Providers: []AdminCreateIdentityImportCredentialsOIDCProvider{
{
Provider: "github",
Subject: "12345",
},
},
},
},
credType: CredentialsTypeOIDC,
},
{
name: "OIDC new credential with organization",
setupIdentity: func() *Identity {
return &Identity{}
},
credentials: &AdminIdentityImportCredentialsOIDC{
Config: AdminIdentityImportCredentialsOIDCConfig{
Providers: []AdminCreateIdentityImportCredentialsOIDCProvider{
{
Provider: "github",
Subject: "12345",
Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true},
},
},
},
},
credType: CredentialsTypeOIDC,
},
{
name: "OIDC update credential without organization",
setupIdentity: func() *Identity {
i := &Identity{}
_ = i.SetCredentialsWithConfig(
CredentialsTypeOIDC,
Credentials{
Identifiers: []string{OIDCUniqueID("google", "67890")},
},
CredentialsOIDC{
Providers: []CredentialsOIDCProvider{
{
Provider: "google",
Subject: "67890",
},
},
},
)
return i
},
credentials: &AdminIdentityImportCredentialsOIDC{
Config: AdminIdentityImportCredentialsOIDCConfig{
Providers: []AdminCreateIdentityImportCredentialsOIDCProvider{
{
Provider: "github",
Subject: "12345",
},
},
},
},
credType: CredentialsTypeOIDC,
},
{
name: "OIDC update credential with organization",
setupIdentity: func() *Identity {
i := &Identity{}
_ = i.SetCredentialsWithConfig(
CredentialsTypeOIDC,
Credentials{
Identifiers: []string{OIDCUniqueID("google", "67890")},
},
CredentialsOIDC{
Providers: []CredentialsOIDCProvider{
{
Provider: "google",
Subject: "67890",
},
},
},
)
return i
},
credentials: &AdminIdentityImportCredentialsOIDC{
Config: AdminIdentityImportCredentialsOIDCConfig{
Providers: []AdminCreateIdentityImportCredentialsOIDCProvider{
{
Provider: "github",
Subject: "12345",
Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true},
},
},
},
},
credType: CredentialsTypeOIDC,
},
{
name: "OIDC update with multiple providers",
setupIdentity: func() *Identity {
i := &Identity{}
_ = i.SetCredentialsWithConfig(
CredentialsTypeOIDC,
Credentials{
Identifiers: []string{OIDCUniqueID("google", "67890")},
},
CredentialsOIDC{
Providers: []CredentialsOIDCProvider{
{
Provider: "google",
Subject: "67890",
},
},
},
)
return i
},
credentials: &AdminIdentityImportCredentialsOIDC{
Config: AdminIdentityImportCredentialsOIDCConfig{
Providers: []AdminCreateIdentityImportCredentialsOIDCProvider{
{
Provider: "github",
Subject: "12345",
Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true},
},
{
Provider: "gitlab",
Subject: "abcdef",
},
},
},
},
credType: CredentialsTypeOIDC,
},
{
name: "SAML new credential without organization",
setupIdentity: func() *Identity {
return &Identity{}
},
credentials: &AdminIdentityImportCredentialsSAML{
Config: AdminIdentityImportCredentialsSAMLConfig{
Providers: []AdminCreateIdentityImportCredentialsSAMLProvider{
{
Provider: "okta",
Subject: "user123",
},
},
},
},
credType: CredentialsTypeSAML,
},
{
name: "SAML new credential with organization",
setupIdentity: func() *Identity {
return &Identity{}
},
credentials: &AdminIdentityImportCredentialsSAML{
Config: AdminIdentityImportCredentialsSAMLConfig{
Providers: []AdminCreateIdentityImportCredentialsSAMLProvider{
{
Provider: "okta",
Subject: "user123",
Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true},
},
},
},
},
credType: CredentialsTypeSAML,
},
{
name: "SAML update credential without organization",
setupIdentity: func() *Identity {
i := &Identity{}
_ = i.SetCredentialsWithConfig(
CredentialsTypeSAML,
Credentials{
Identifiers: []string{OIDCUniqueID("onelogin", "user456")},
},
CredentialsOIDC{
Providers: []CredentialsOIDCProvider{
{
Provider: "onelogin",
Subject: "user456",
},
},
},
)
return i
},
credentials: &AdminIdentityImportCredentialsSAML{
Config: AdminIdentityImportCredentialsSAMLConfig{
Providers: []AdminCreateIdentityImportCredentialsSAMLProvider{
{
Provider: "okta",
Subject: "user123",
},
},
},
},
credType: CredentialsTypeSAML,
},
{
name: "SAML update credential with organization",
setupIdentity: func() *Identity {
i := &Identity{}
_ = i.SetCredentialsWithConfig(
CredentialsTypeSAML,
Credentials{
Identifiers: []string{OIDCUniqueID("onelogin", "user456")},
},
CredentialsOIDC{
Providers: []CredentialsOIDCProvider{
{
Provider: "onelogin",
Subject: "user456",
},
},
},
)
return i
},
credentials: &AdminIdentityImportCredentialsSAML{
Config: AdminIdentityImportCredentialsSAMLConfig{
Providers: []AdminCreateIdentityImportCredentialsSAMLProvider{
{
Provider: "okta",
Subject: "user123",
Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true},
},
},
},
},
credType: CredentialsTypeSAML,
},
{
name: "SAML update with multiple providers",
setupIdentity: func() *Identity {
i := &Identity{}
_ = i.SetCredentialsWithConfig(
CredentialsTypeSAML,
Credentials{
Identifiers: []string{OIDCUniqueID("onelogin", "user456")},
},
CredentialsOIDC{
Providers: []CredentialsOIDCProvider{
{
Provider: "onelogin",
Subject: "user456",
},
},
},
)
return i
},
credentials: &AdminIdentityImportCredentialsSAML{
Config: AdminIdentityImportCredentialsSAMLConfig{
Providers: []AdminCreateIdentityImportCredentialsSAMLProvider{
{
Provider: "okta",
Subject: "user123",
Organization: uuid.NullUUID{UUID: uuid.FromStringOrNil("e7e3cbae-04cc-45f3-ae52-ea749a2ffaff"), Valid: true},
},
{
Provider: "auth0",
Subject: "user789",
},
},
},
},
credType: CredentialsTypeSAML,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Set up a fresh identity for each test
i := tc.setupIdentity()
var err error
// Perform the import based on a credential type
switch tc.credType {
case CredentialsTypeOIDC:
err = h.importOIDCCredentials(ctx, i, tc.credentials.(*AdminIdentityImportCredentialsOIDC))
case CredentialsTypeSAML:
err = h.importSAMLCredentials(ctx, i, tc.credentials.(*AdminIdentityImportCredentialsSAML))
}
require.NoError(t, err)
// Verify credential was set correctly
creds, ok := i.GetCredentials(tc.credType)
require.True(t, ok, "credentials should be set")
// Verify the credentials contain proper identifiers and config
assert.NotEmpty(t, creds.Identifiers)
assert.NotEmpty(t, creds.Config)
// Take a snapshot of the credentials
snapshotx.SnapshotT(t, creds)
// Additional checks based on credential type
switch tc.credType {
case CredentialsTypeOIDC:
oidcCreds := tc.credentials.(*AdminIdentityImportCredentialsOIDC)
for _, p := range oidcCreds.Config.Providers {
id := OIDCUniqueID(p.Provider, p.Subject)
assert.Contains(t, creds.Identifiers, id)
}
case CredentialsTypeSAML:
samlCreds := tc.credentials.(*AdminIdentityImportCredentialsSAML)
for _, p := range samlCreds.Config.Providers {
id := OIDCUniqueID(p.Provider, p.Subject)
assert.Contains(t, creds.Identifiers, id)
}
}
})
}
}
func TestImportLookupSecretCredentials(t *testing.T) {
t.Parallel()
ctx := context.Background()
// Setup handler with minimal mock requirements
h := &Handler{}
testCases := []struct {
name string
setupIdentity func() *Identity
credentials *AdminIdentityImportCredentialsLookupSecret
expectedCodes int
}{
{
name: "new lookup secret credential",
setupIdentity: func() *Identity {
return &Identity{}
},
credentials: &AdminIdentityImportCredentialsLookupSecret{
Config: AdminIdentityImportCredentialsLookupSecretConfig{
Codes: []RecoveryCode{
{Code: "code1"},
{Code: "code2"},
},
},
},
expectedCodes: 2,
},
{
name: "update existing lookup secret credential",
setupIdentity: func() *Identity {
i := &Identity{}
_ = i.SetCredentialsWithConfig(
CredentialsTypeLookup,
Credentials{},
CredentialsLookupConfig{
RecoveryCodes: []RecoveryCode{
{Code: "existing-code"},
},
},
)
return i
},
credentials: &AdminIdentityImportCredentialsLookupSecret{
Config: AdminIdentityImportCredentialsLookupSecretConfig{
Codes: []RecoveryCode{
{Code: "new-code1"},
{Code: "new-code2"},
},
},
},
expectedCodes: 3, // 1 existing + 2 new
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Set up a fresh identity for each test
i := tc.setupIdentity()
// Perform the import
err := h.importLookupSecretCredentials(ctx, i, tc.credentials)
require.NoError(t, err)
// Verify credential was set correctly
creds, ok := i.GetCredentials(CredentialsTypeLookup)
require.True(t, ok, "credentials should be set")
// Parse the config to check the recovery codes
var config CredentialsLookupConfig
require.NoError(t, json.Unmarshal(creds.Config, &config))
// Verify the expected number of codes
assert.Len(t, config.RecoveryCodes, tc.expectedCodes)
// Take a snapshot of the credentials
snapshotx.SnapshotT(t, creds)
// Verify specific codes based on a test case
if tc.name == "new lookup secret credential" {
assert.Contains(t, getCodeValues(config.RecoveryCodes), "code1")
assert.Contains(t, getCodeValues(config.RecoveryCodes), "code2")
} else {
assert.Contains(t, getCodeValues(config.RecoveryCodes), "existing-code")
assert.Contains(t, getCodeValues(config.RecoveryCodes), "new-code1")
assert.Contains(t, getCodeValues(config.RecoveryCodes), "new-code2")
}
})
}
}
func TestImportPasskeyCredentials(t *testing.T) {
t.Parallel()
ctx := context.Background()
// Setup handler
h := &Handler{}
// Create test credentials
cred1 := CredentialWebAuthn{ID: []byte("cred1"), PublicKey: []byte("pk1")}
cred2 := CredentialWebAuthn{ID: []byte("cred2"), PublicKey: []byte("pk2")}
cred1Updated := CredentialWebAuthn{ID: []byte("cred1"), PublicKey: []byte("pk1-updated")}
cred3 := CredentialWebAuthn{ID: []byte("cred3"), PublicKey: []byte("pk3")}
testCases := []struct {
name string
setupIdentity func() *Identity
credentials CredentialsWebAuthn
userHandle []byte
verify func(t *testing.T, i *Identity)
}{
{
name: "new passkey credentials",
setupIdentity: func() *Identity {
return &Identity{}
},
credentials: CredentialsWebAuthn{cred1, cred2},
userHandle: []byte("user1"),
verify: func(t *testing.T, i *Identity) {
creds, ok := i.GetCredentials(CredentialsTypePasskey)
require.True(t, ok)
var config CredentialsWebAuthnConfig
require.NoError(t, json.Unmarshal(creds.Config, &config))
assert.Len(t, config.Credentials, 2)
assert.Equal(t, []byte("user1"), config.UserHandle)
assert.Contains(t, creds.Identifiers, "user1")
},
},
{
name: "update existing credentials - add new, update existing",
setupIdentity: func() *Identity {
i := &Identity{}
err := i.SetCredentialsWithConfig(
CredentialsTypePasskey,
Credentials{
Identifiers: []string{"existingUser"},
},
CredentialsWebAuthnConfig{
Credentials: CredentialsWebAuthn{cred1, cred2},
UserHandle: []byte("existingUser"),
},
)
require.NoError(t, err)
return i
},
credentials: CredentialsWebAuthn{cred1Updated, cred3},
userHandle: []byte{}, // Empty to test reusing existing
verify: func(t *testing.T, i *Identity) {
creds, ok := i.GetCredentials(CredentialsTypePasskey)
require.True(t, ok)
var config CredentialsWebAuthnConfig
require.NoError(t, json.Unmarshal(creds.Config, &config))
// Should have updated cred1, kept cred2, and added cred3
assert.Len(t, config.Credentials, 3)
assert.Equal(t, []byte("existingUser"), config.UserHandle)
// Find the updated credential
var found bool
for _, c := range config.Credentials {
if bytes.Equal(c.ID, []byte("cred1")) {
assert.Equal(t, []byte("pk1-updated"), c.PublicKey)
found = true
break
}
}
assert.True(t, found, "Updated credential should be present")
// Verify cred3 was added
var foundNew bool
for _, c := range config.Credentials {
if bytes.Equal(c.ID, []byte("cred3")) {
foundNew = true
break
}
}
assert.True(t, foundNew, "New credential should be added")
},
},
{
name: "new user handle added to identifiers",
setupIdentity: func() *Identity {
i := &Identity{}
err := i.SetCredentialsWithConfig(
CredentialsTypePasskey,
Credentials{
Identifiers: []string{"existingUser"},
},
CredentialsWebAuthnConfig{
Credentials: CredentialsWebAuthn{cred1},
UserHandle: []byte("existingUser"),
},
)
require.NoError(t, err)
return i
},
credentials: CredentialsWebAuthn{cred2},
userHandle: []byte("newUser"),
verify: func(t *testing.T, i *Identity) {
creds, ok := i.GetCredentials(CredentialsTypePasskey)
require.True(t, ok)
var config CredentialsWebAuthnConfig
require.NoError(t, json.Unmarshal(creds.Config, &config))
assert.Equal(t, []byte("newUser"), config.UserHandle)
assert.Contains(t, creds.Identifiers, "existingUser")
assert.Contains(t, creds.Identifiers, "newUser")
assert.Len(t, creds.Identifiers, 2)
assert.Len(t, config.Credentials, 2)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
i := tc.setupIdentity()
err := h.importPasskeyCredentials(ctx, i, tc.credentials, tc.userHandle)
require.NoError(t, err)
tc.verify(t, i)
snapshotx.SnapshotT(t, i.Credentials)
})
}
// Negative cases — input that would store a non-functional credential
// must be rejected up front. See
// https://github.com/ory/kratos/issues/4561.
//
// herodot wraps the user-facing detail in Reason; err.Error() returns
// the canonical status message ("The request was malformed or
// contained invalid parameters"), so unwrap to a *herodot.DefaultError
// and assert against Reason() instead.
requireBadRequestReason := func(t *testing.T, err error, mustContain string) {
t.Helper()
require.Error(t, err)
var herr *herodot.DefaultError
require.True(t, errors.As(err, &herr), "expected a *herodot.DefaultError, got %T: %v", err, err)
assert.Equal(t, herodot.ErrBadRequest().StatusCode(), herr.StatusCode())
assert.Contains(t, herr.Reason(), mustContain)
}
t.Run("case=rejects import without user_handle on a new credential", func(t *testing.T) {
i := &Identity{}
err := h.importPasskeyCredentials(ctx, i, CredentialsWebAuthn{cred1}, nil)
requireBadRequestReason(t, err, "user_handle")
_, ok := i.GetCredentials(CredentialsTypePasskey)
assert.False(t, ok, "no passkey credential should be persisted on the identity")
})
t.Run("case=rejects import without user_handle when previous credential also lacks one", func(t *testing.T) {
i := &Identity{}
require.NoError(t, i.SetCredentialsWithConfig(
CredentialsTypePasskey,
Credentials{},
CredentialsWebAuthnConfig{Credentials: CredentialsWebAuthn{cred1}},
))
err := h.importPasskeyCredentials(ctx, i, CredentialsWebAuthn{cred2}, nil)
requireBadRequestReason(t, err, "user_handle")
})
t.Run("case=rejects import without any credentials", func(t *testing.T) {
i := &Identity{}
err := h.importPasskeyCredentials(ctx, i, CredentialsWebAuthn{}, []byte("user1"))
requireBadRequestReason(t, err, "credentials")
})
}
func TestImportWebAuthnCredentials(t *testing.T) {
t.Parallel()
ctx := context.Background()
// Setup handler
h := &Handler{}
// Create test credentials
cred1 := CredentialWebAuthn{ID: []byte("cred1"), PublicKey: []byte("pk1")}
cred2 := CredentialWebAuthn{ID: []byte("cred2"), PublicKey: []byte("pk2")}
cred1Updated := CredentialWebAuthn{ID: []byte("cred1"), PublicKey: []byte("pk1-updated")}
cred3 := CredentialWebAuthn{ID: []byte("cred3"), PublicKey: []byte("pk3")}
testCases := []struct {
name string
setupIdentity func() *Identity
credentials CredentialsWebAuthn
userHandle []byte
verify func(t *testing.T, i *Identity)
}{
{
name: "new webauthn credentials",
setupIdentity: func() *Identity {
return &Identity{}
},
credentials: CredentialsWebAuthn{cred1, cred2},
userHandle: []byte("user1"),
verify: func(t *testing.T, i *Identity) {
creds, ok := i.GetCredentials(CredentialsTypeWebAuthn)
require.True(t, ok)
var config CredentialsWebAuthnConfig
require.NoError(t, json.Unmarshal(creds.Config, &config))
assert.Len(t, config.Credentials, 2)
assert.Equal(t, []byte("user1"), config.UserHandle)
},
},
{
name: "update existing credentials - add new, update existing",
setupIdentity: func() *Identity {
i := &Identity{}
err := i.SetCredentialsWithConfig(
CredentialsTypeWebAuthn,
Credentials{},
CredentialsWebAuthnConfig{
Credentials: CredentialsWebAuthn{cred1, cred2},
UserHandle: []byte("existingUser"),
},
)
require.NoError(t, err)
return i
},
credentials: CredentialsWebAuthn{cred1Updated, cred3},
userHandle: []byte{}, // Empty to test reusing existing
verify: func(t *testing.T, i *Identity) {
creds, ok := i.GetCredentials(CredentialsTypeWebAuthn)
require.True(t, ok)
var config CredentialsWebAuthnConfig
require.NoError(t, json.Unmarshal(creds.Config, &config))
// Should have updated cred1, kept cred2, and added cred3
assert.Len(t, config.Credentials, 3)
assert.Equal(t, []byte("existingUser"), config.UserHandle)
// Find the updated credential
var found bool
for _, c := range config.Credentials {
if bytes.Equal(c.ID, []byte("cred1")) {
assert.Equal(t, []byte("pk1-updated"), c.PublicKey)
found = true
break
}
}
assert.True(t, found, "Updated credential should be present")
// Verify cred3 was added
var foundNew bool
for _, c := range config.Credentials {
if bytes.Equal(c.ID, []byte("cred3")) {
foundNew = true
break
}
}
assert.True(t, foundNew, "New credential should be added")
},
},
{
name: "override existing user handle",
setupIdentity: func() *Identity {
i := &Identity{}
err := i.SetCredentialsWithConfig(
CredentialsTypeWebAuthn,
Credentials{},
CredentialsWebAuthnConfig{
Credentials: CredentialsWebAuthn{cred1},
UserHandle: []byte("existingUser"),
},
)
require.NoError(t, err)
return i
},
credentials: CredentialsWebAuthn{cred2},
userHandle: []byte("newUser"),
verify: func(t *testing.T, i *Identity) {
creds, ok := i.GetCredentials(CredentialsTypeWebAuthn)
require.True(t, ok)
var config CredentialsWebAuthnConfig
require.NoError(t, json.Unmarshal(creds.Config, &config))
assert.Equal(t, []byte("newUser"), config.UserHandle)
assert.Len(t, config.Credentials, 2)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
i := tc.setupIdentity()
err := h.importWebAuthnCredentials(ctx, i, tc.credentials, tc.userHandle)
require.NoError(t, err)
tc.verify(t, i)
snapshotx.SnapshotT(t, i.Credentials)
})
}
}
func TestImportTOTPCredentials(t *testing.T) {
t.Parallel()
ctx := context.Background()
// Setup handler
h := &Handler{}
testCases := []struct {
name string
setupIdentity func() *Identity
credentials *AdminIdentityImportCredentialsTOTP
verify func(t *testing.T, i *Identity)
}{
{
name: "new totp credential",
setupIdentity: func() *Identity {
return &Identity{}
},
credentials: &AdminIdentityImportCredentialsTOTP{
Config: AdminIdentityImportCredentialsTOTPConfig{
TOTPURL: "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
},
},
verify: func(t *testing.T, i *Identity) {
creds, ok := i.GetCredentials(CredentialsTypeTOTP)
require.True(t, ok)
var config CredentialsTOTPConfig
require.NoError(t, json.Unmarshal(creds.Config, &config))
assert.Equal(t, "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example", config.TOTPURL)
// The importer leaves Identifiers empty because the identity ID
// is not yet known. The persister defaults it to the persisted
// identity ID at credential-create time. See
// https://github.com/ory/kratos/issues/4561.
assert.Empty(t, creds.Identifiers)
},
},
{
name: "update existing totp credential",
setupIdentity: func() *Identity {
i := &Identity{}
err := i.SetCredentialsWithConfig(
CredentialsTypeTOTP,
Credentials{},
CredentialsTOTPConfig{
TOTPURL: "otpauth://totp/Example:alice@example.com?secret=OLDSECRET&issuer=Example",
},
)
require.NoError(t, err)
return i
},
credentials: &AdminIdentityImportCredentialsTOTP{
Config: AdminIdentityImportCredentialsTOTPConfig{
TOTPURL: "otpauth://totp/Example:alice@example.com?secret=NEWSECRET&issuer=Example",
},
},
verify: func(t *testing.T, i *Identity) {
creds, ok := i.GetCredentials(CredentialsTypeTOTP)
require.True(t, ok)
var config CredentialsTOTPConfig
require.NoError(t, json.Unmarshal(creds.Config, &config))
// Should have the new TOTP URL
assert.Equal(t, "otpauth://totp/Example:alice@example.com?secret=NEWSECRET&issuer=Example", config.TOTPURL)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
i := tc.setupIdentity()
err := h.importTOTPCredentials(ctx, i, tc.credentials)
require.NoError(t, err)
tc.verify(t, i)
snapshotx.SnapshotT(t, i.Credentials)
})
}
}
// Helper function to extract code values for easier assertions
func getCodeValues(codes []RecoveryCode) []string {
var values []string
for _, code := range codes {
values = append(values, code.Code)
}
return values
}
// TestBatchImport_RegionWireThrough verifies that identityFromCreateIdentityBody
// correctly propagates the Region field from CreateIdentityBody to the resulting
// Identity for each entry in a batch import. This ensures the multi-region
// persister receives identities with the correct region so it can route INSERTs
// to the right CockroachDB region.
func TestBatchImport_RegionWireThrough(t *testing.T) {
t.Parallel()
ctx := context.Background()
// A minimal handler is sufficient: importCredentials is a no-op when
// Credentials is nil, so no registry wiring is needed.
h := &Handler{}
testCases := []struct {
name string
body *CreateIdentityBody
expectedRegion string
}{
{
name: "region eu-central is propagated",
body: &CreateIdentityBody{
SchemaID: "default",
Traits: json.RawMessage(`{"bar":"baz"}`),
Region: "eu-central",
},
expectedRegion: "eu-central",
},
{
name: "region us-west is propagated",
body: &CreateIdentityBody{
SchemaID: "default",
Traits: json.RawMessage(`{"bar":"baz"}`),
Region: "us-west",
},
expectedRegion: "us-west",
},
{
name: "empty region is preserved as empty",
body: &CreateIdentityBody{
SchemaID: "default",
Traits: json.RawMessage(`{"bar":"baz"}`),
},
expectedRegion: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ident, err := h.identityFromCreateIdentityBody(ctx, tc.body)
require.NoError(t, err)
assert.Equal(t, tc.expectedRegion, string(ident.Region))
})
}
}