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
- Implementing or porting the login resolver in any Koder SDK
- Auditing test coverage of the login flow
- Adding a new login surface to the Koder Stack
Specification body
Spec — Login Identifier Resolution Test Templates
How to use
- Pick the surface (S1 Go server, S2 Dart, S3 JS, S4 Go SDK, …).
- Implement T1–T8 in the language's native test runner.
- Run against the real implementation (not a mock) whenever possible.
- 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_passwordfrom the policydev-default-password.kmdexcept where individually justified (rodrigokeeps 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
meta/docs/stack/specs/identity/login-resolution.kmd