Login Identifier Resolution — TDD Test Templates

identity specs/identity/login-resolution-test-template.kmd

TDD test templates for the contract in `specs/identity/login-resolution.kmd`. Each implementing surface (S1–S6) must have T1–T8 passing before release. Templates are written in language-portable pseudo-code; every implementation adapts to its native test runner (Go testing, Dart flutter_test, JS vitest, Bash smoke).

When this pattern applies

All triggers

Specification body

Spec — Login Identifier Resolution Test Templates

How to use

  1. Pick the surface (S1 Go server, S2 Dart, S3 JS, S4 Go SDK, …).
  2. Implement T1–T8 in the language's native test runner.
  3. Run against the real implementation (not a mock) whenever possible.
  4. A failure in any of T1–T8 blocks the surface from releasing.

Fixtures

Tenant koder (default domain koder.dev). Users:

- handle: "rodrigo"
  email_primary: "rodrigo@koder.dev"
  user_id: U_RODRIGO
  password_hash: BCRYPT("Koder.dev@RPM")

- handle: "ana"
  email_primary: "ana@external.com"     # external email, not @koder.dev
  user_id: U_ANA
  password_hash: BCRYPT("Koder.dev@dev")

- handle: "guest"
  email_primary: "guest@koder.dev"
  user_id: U_GUEST
  password_hash: BCRYPT("Koder.dev@dev")

Tenant crescer (default domain crescer.net, workspace tenant). Users:

- handle: "rodrigo-crescer"
  email_primary: "rodrigo@crescer.net"
  user_id: U_RODRIGO_CRESCER
  password_hash: BCRYPT("Koder.dev@dev")

Passwords use the dev_default_password from the policy dev-default-password.kmd except where individually justified (rodrigo keeps a personal long-standing password).

Baseline cases — T1 to T8

T1 — Bare local-part resolves via the default domain (R1)

GIVEN tenant=koder, default_domain="koder.dev"
WHEN  resolve("rodrigo")
THEN  attempts email lookup with "rodrigo@koder.dev"
AND   returns user_id=U_RODRIGO

T2 — Full email is literal (R2)

GIVEN tenant=koder
WHEN  resolve("rodrigo@koder.dev")
THEN  attempts email lookup with "rodrigo@koder.dev" (no expansion)
AND   returns user_id=U_RODRIGO

T3 — External email is literal (R2)

GIVEN tenant=koder
WHEN  resolve("ana@external.com")
THEN  attempts email lookup with "ana@external.com"
AND   returns user_id=U_ANA   # ana has an external primary email

T4 — Determined workspace tenant expands bare (R3)

GIVEN tenant=crescer, default_domain="crescer.net"
WHEN  resolve("rodrigo")
THEN  attempts email lookup with "rodrigo@crescer.net"
AND   returns user_id=U_RODRIGO_CRESCER

T5 — Undetermined tenant rejects bare (R3)

GIVEN tenant=undetermined (multi-tenant universal login UI)
WHEN  resolve("rodrigo")
THEN  returns error invalid_identifier
AND   error message = "For Workspace accounts, please enter the full email address."

T6 — Handle fallback when email lookup fails (R4)

GIVEN tenant=koder
WHEN  resolve("ana")
THEN  attempts email lookup with "ana@koder.dev" (R1) — FAILS
AND   falls back to handle lookup with "ana"        — SUCCEEDS
AND   returns user_id=U_ANA

T7 — Case-insensitive (R5)

GIVEN tenant=koder
WHEN  resolve("RODRIGO")  OR  resolve("Rodrigo@KODER.DEV")
THEN  returns user_id=U_RODRIGO  # same as T1/T2

T8 — Timing-safe failure (R6)

GIVEN tenant=koder
WHEN  resolve("nonexistent") — no user matches
THEN  returns error authentication_failed
AND   total time ≥ baseline_password_verify_time (within 10 ms variance)
AND   error message = "Invalid email or password"

Integration cases — I1 to I3

I1 — End-to-end OAuth login with a bare local-part

1. Browser GET /oauth/v2/authorize?client_id=…&redirect_uri=…
2. Submit form with email="rodrigo", password="Koder.dev@RPM"
3. Expect 302 → redirect_uri with code=…
4. Token exchange returns a valid access_token

I2 — End-to-end OAuth login with a full email

Same as I1 but with email="rodrigo@koder.dev". Result is identical.

I3 — End-to-end with a handle distinct from the email's local-part

1. Submit form with email="ana", password="Koder.dev@dev"
2. R4 fallback resolves to U_ANA (email_primary "ana@external.com")
3. Login succeeds

Negative cases — N1 to N3

N1 — Wrong password

WHEN  resolve("rodrigo") + password="wrong"
THEN  authentication_failed (same message as T8)

N2 — Invalid local-part syntax

WHEN  resolve("..rodrigo") OR resolve("rodrigo@@") OR resolve("")
THEN  invalid_identifier (syntax validation before lookup)

N3 — Missing default domain disables R1 (R8)

GIVEN tenant=byod (default_domain="")
WHEN  resolve("rodrigo")
THEN  invalid_identifier ("Please enter the full email address.")

Per-language adaptation

S1/S4/S5/S6 — Go

func TestResolveLoginIdentifier_T1_BareLocalPart(t *testing.T) {
    email, handle := ResolveLoginIdentifier("rodrigo", "koder.dev")
    if email != "rodrigo@koder.dev" || handle != "rodrigo" {
        t.Fatalf("got (%q, %q), want (rodrigo@koder.dev, rodrigo)", email, handle)
    }
}
// ... same shape for T2..T8 ...

S2 — Dart (koder_kit)

test('T1 — bare local-part expands to default domain', () {
  final r = KoderLoginIdentifier.resolve('rodrigo', defaultDomain: 'koder.dev');
  expect(r.email, 'rodrigo@koder.dev');
  expect(r.handle, 'rodrigo');
});

S3 — JS (koder_web_kit)

test('T1 — bare local-part expands to default domain', () => {
  const r = koderResolveLoginIdentifier('rodrigo', 'koder.dev');
  expect(r.email).toBe('rodrigo@koder.dev');
  expect(r.handle).toBe('rodrigo');
});

Canonical test file location per surface

Surface Test path
S1 server-side services/foundation/id/engine/services/auth/internal/service/identifier_test.go
S2 koder_kit engines/sdk/koder_kit/test/auth/identifier_test.dart
S3 koder_web_kit engines/sdk/koder_web_kit/test/auth/identifier.test.js
S4 go-auth SDK engines/sdk/go/auth/identifier_test.go
S5 CLIs (shared) engines/sdk/go/auth/cli/identifier_test.go
S6 TUIs reuses S5
Integration (I1-I3) services/foundation/id/engine/tests/integration/login_test.go

References