feat(identity): test-only fixed OTP channel and rectification candidate cards
- IDENTITY_TEST_OTP env: when set to a 6-digit code, no real email is delivered; login page surfaces the fixed code so testers can register and sign in without a mailbox. Opt-in, never set in production. - email-otp-login: test-channel notice with the pinned code. - Rectification candidate list restyled with the project design system (warm canvas, action color, display serif, soft shadow, hover lift). - Regression tests for config parsing, pinned OTP generation, login UI notice, and candidate card styles.
This commit is contained in:
@@ -6,7 +6,7 @@ import { createDatabaseAdminAuthorizer, createIdentityPool } from "../src/module
|
||||
import { FakeEmailOtpSender } from "../src/modules/identity/email/fake-email-otp-sender.ts";
|
||||
import type { SelfHostedIdentityConfig } from "../src/modules/identity/config.ts";
|
||||
|
||||
const config: SelfHostedIdentityConfig = { provider: "self-hosted", databaseUrl: "postgresql://identity_runtime:test-password@postgres:5432/jyotisha", userOrigin: "https://staging.jyotisha.chat", adminOrigin: "https://admin.staging.jyotisha.chat", userSecret: "user-secret-that-is-at-least-32-bytes-long", resendApiKey: "re_test", resendFrom: "Jyotisha <login@staging.jyotisha.chat>" };
|
||||
const config: SelfHostedIdentityConfig = { provider: "self-hosted", databaseUrl: "postgresql://identity_runtime:test-password@postgres:5432/jyotisha", userOrigin: "https://staging.jyotisha.chat", adminOrigin: "https://admin.staging.jyotisha.chat", userSecret: "user-secret-that-is-at-least-32-bytes-long", resendApiKey: "re_test", resendFrom: "Jyotisha <login@staging.jyotisha.chat>", testOtp: null };
|
||||
const database = { kind: "pool" } as unknown as Pool;
|
||||
|
||||
test("Better Auth trusts only the two exact origins and keeps host-only cookies", () => {
|
||||
@@ -41,6 +41,17 @@ test("OTP policy remains hashed and bounded", async () => {
|
||||
assert.match(sender.messages[0].idempotencyKey, /^otp-[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
test("test OTP channel pins a fixed code and never sends a real email", async () => {
|
||||
const sender = new FakeEmailOtpSender();
|
||||
const options = createEmailOtpOptions(sender, config.userSecret, false, "123456");
|
||||
assert.equal(options.generateOTP?.({ email: "tester@example.com", type: "sign-in" }), "123456");
|
||||
const message = sender.messages[0];
|
||||
await options.sendVerificationOTP({ email: "tester@example.com", otp: "123456", type: "sign-in" });
|
||||
assert.equal(sender.messages.length, 1);
|
||||
assert.equal(sender.messages[0].otp, "123456");
|
||||
assert.match(sender.messages[0].idempotencyKey, /^otp-[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
test("identity pool forces the identity search path", async () => {
|
||||
const pool = createIdentityPool(config.databaseUrl);
|
||||
try { assert.equal(pool.options.options, "-c search_path=identity,pg_catalog"); } finally { await pool.end(); }
|
||||
|
||||
@@ -133,6 +133,7 @@ test("Better Auth supports shared user OTP/password sessions for admins", async
|
||||
userSecret: "user-secret-that-is-at-least-32-bytes-long",
|
||||
resendApiKey: "re_test",
|
||||
resendFrom: "Jyotisha <login@staging.jyotisha.chat>",
|
||||
testOtp: null,
|
||||
};
|
||||
const previousEnv = new Map(
|
||||
envKeys.map((key) => [key, process.env[key]] as const),
|
||||
|
||||
@@ -60,3 +60,20 @@ test("localhost may use two distinct HTTP origins", () => {
|
||||
});
|
||||
assert.equal(config.provider, "self-hosted");
|
||||
});
|
||||
|
||||
test("identity test OTP channel is opt-in and strictly six digits", () => {
|
||||
const without = readIdentityConfig(selfHostedEnvironment);
|
||||
assert.equal(without.provider === "self-hosted" ? without.testOtp : "unexpected", null);
|
||||
|
||||
const withOtp = readIdentityConfig({ ...selfHostedEnvironment, IDENTITY_TEST_OTP: "123456" });
|
||||
assert.equal(withOtp.provider === "self-hosted" ? withOtp.testOtp : null, "123456");
|
||||
|
||||
assert.throws(
|
||||
() => readIdentityConfig({ ...selfHostedEnvironment, IDENTITY_TEST_OTP: "12345" }),
|
||||
/must be a 6-digit/,
|
||||
);
|
||||
assert.throws(
|
||||
() => readIdentityConfig({ ...selfHostedEnvironment, IDENTITY_TEST_OTP: "abcdef" }),
|
||||
/must be a 6-digit/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ const config: SelfHostedIdentityConfig = {
|
||||
userSecret: "user-secret-that-is-at-least-32-bytes-long",
|
||||
resendApiKey: "re_test",
|
||||
resendFrom: "Jyotisha <login@staging.jyotisha.chat>",
|
||||
testOtp: null,
|
||||
};
|
||||
|
||||
test("identity host accepts only the two configured origins", () => {
|
||||
|
||||
@@ -245,6 +245,21 @@ test("login UI preserves accessible OTP, password, registration, and reset input
|
||||
assert.doesNotMatch(component, /hostname\.startsWith|admin\.staging/);
|
||||
assert.match(component, /动态验证码/);
|
||||
assert.match(component, /恢复码/);
|
||||
assert.match(component, /testOtp\?: string \| null/);
|
||||
assert.match(component, /测试环境:验证码固定为/);
|
||||
assert.match(component, /不会发送真实邮件/);
|
||||
|
||||
const loginPage = readFileSync(
|
||||
new URL("../src/app/login/page.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(loginPage, /testOtp=\{config\.provider === "self-hosted" \? config\.testOtp : null\}/);
|
||||
|
||||
const styles = readFileSync(
|
||||
new URL("../src/app/globals.css", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(styles, /\.auth-test-channel \{[\s\S]*border: 1px dashed/);
|
||||
|
||||
const route = readFileSync(
|
||||
new URL("../src/app/api/account/password/route.ts", import.meta.url),
|
||||
|
||||
Reference in New Issue
Block a user