testing/cross_backend/identity_parity.ts

Cross-backend identity-primitive parity for fuz_app's own spine over real HTTP — the primitive_schemas twins (Username, UsernameProvided, Email) and the login/signup input handling that enforces them, pinned facet-by-facet so a TS↔Rust divergence in any one surfaces as a failure: how usernames are canonicalized on the login lookup, the ASCII-only creation invariant that bounds what can ever be stored, and the email format rule applied at signup.

UsernameProvided (login/lookup) canonicalizes the submitted username via .trim().toLowerCase(), and the DB matches case-insensitively (LOWER(username) = LOWER($1)); Username (creation) lowercases at store and restricts to ASCII via ^[a-zA-Z][0-9a-zA-Z_-]*[0-9a-zA-Z]$. The Rust spine must produce the same canonical form, the same case-insensitive lookup, and the same ASCII-only rejection — or a username that logs in on TS silently 401s on Rust, or a non-ASCII username storable on one backend reopens the homograph-collision surface. The spec-derived round-trip + conformance suites never vary username casing or charset, so this corner was unpinned.

Canonicalization (login lookup):

  • case-insensitive login — an account created Mixed_Case logs in via an all-uppercase submission (proves the *lookup* folds case, not merely that the stored form was lowercased).
  • whitespace-trim login — the same shape logs in with surrounding whitespace (proves .trim() on the lookup path).
  • no Unicode case-fold collision (negative) — a Turkish-İ (U+0130) variant of an existing ASCII username must NOT match. Both JS .toLowerCase() and Rust str::to_lowercase() map İi + U+0307 (combining dot above), never plain i, so the cased homograph stays a distinct, non-existent username → 401 on every backend.

Username-or-email login lookup: the login identifier resolves against username or email — TS query_account_by_username_or_email, Rust the converged query_account_with_password_hash OR-lookup (this was the divergence the suite closed: the Rust spine previously matched username only). An account created with an email logs in via that email, case-insensitively (Email stores the original case; folding rides the LOWER(email) = LOWER($1) lookup), and username login keeps working when an email is present. A non-existent email → 401.

Login input validation: malformed login input → 400 invalid_request_body on every spine — whitespace-only username (empty after trim), over-long username (> 255), empty password, and an unknown body key (strict object). TS runs the full LoginInput Zod schema; the Rust spine enforces the same shape in account_login (the convergence the suite closed: the Rust spine previously let three of these fall through to a lookup-miss 401, and TS let a whitespace-only identifier through to a 401). Asserted via the error reason, not just the status, so a same-status-wrong-body backend still fails.

ASCII-only creation invariant: a non-ASCII username is rejected at signup input validation → 400 on every backend, so no Unicode username is ever stored — the precondition the login no-collision case relies on. (ASCII-only is the intended invariant for both spines, not an accident of the TS regex.)

Length + format creation parity: the full Username shape — [USERNAME_LENGTH_MIN, USERNAME_LENGTH_MAX] = [3, 39] and the regex ^[a-zA-Z][0-9a-zA-Z_-]*[0-9a-zA-Z]$ — pinned across both backends. The TS Zod .min()/.max()/.regex() and the Rust hand-rolled byte-scan (is_valid_username_for_creation) are independent reimplementations of the same rule, so each length boundary is tested just-outside (→ 400) and just-inside (→ 403 no-matching-invite, the settle for a valid username on a spine with open_signup at its false default), and each format violation (leading non-letter, trailing punctuation, embedded disallowed char) → 400, with mid-string _/- accepted siblings as the don't-over-reject control. Signup is also strict-object on both spines (TS z.strictObject, Rust #[serde(deny_unknown_fields)]), so an unknown signup body key → 400.

Email format (creation): the optional signup email is validated to a loose local@domain.tld shape on both spines — TS Email (^[^\s@]+@[^\s@]+\.[^\s@]+$ plus a 254-byte (RFC 5321 octet) length bound; whitespace White_Space ∪ {U+FEFF}), Rust the hand-rolled is_valid_email. A malformed email → 400, a well-formed one → 403 no-matching-invite (the same accept/reject settle the username tables use). The accepted siblings include the single-char-TLD a@b.c that Zod's z.email() would reject — the deliberately-looser rule both spines now share. This closed a real divergence: the Rust spine previously length-checked the email only, accepting any non-empty string TS rejected.

Both surfaces are flat REST on every spine, so this is an imperative suite (not a conformance_table row) and ungated. Runs both legs via the shared {setup_test} protocol: the in-process leg (cross_backend/identity_parity.db.test.ts, plain gro test) and the cross-process leg (cross_backend/identity_parity.cross.test.ts, the TS spine binaries + Rust testing_spine_stub over real HTTP).

$lib-free by contract (relative specifiers only).

view source

Declarations
#

2 declarations

describe_identity_parity_cross_tests
#

IdentityParityCrossTestOptions
#

testing/cross_backend/identity_parity.ts view source

IdentityParityCrossTestOptions import type {IdentityParityCrossTestOptions} from '@fuzdev/fuz_app/testing/cross_backend/identity_parity.js';

Options for the identity-primitive parity suite.

setup_test

Per-test fixture producer (in-process or cross-process).

type SetupTest

readonly

login_path?

REST login route path. Default /api/account/login (the spine convention).

type string

readonly

signup_path?

REST signup route path. Default /api/account/signup (the spine convention).

type string

readonly

Depends on
#