feat(POSTV1-03): 增加本机测试直达入口
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
This commit is contained in:
@@ -263,6 +263,7 @@ export interface CreateAppOptions {
|
|||||||
eventHub?: EventHub;
|
eventHub?: EventHub;
|
||||||
generations?: GenerationSubmissionService;
|
generations?: GenerationSubmissionService;
|
||||||
latestExports?: LatestExportService;
|
latestExports?: LatestExportService;
|
||||||
|
localTestAuth?: boolean;
|
||||||
models?: ModelConfigurationService;
|
models?: ModelConfigurationService;
|
||||||
networkBoundary?: NetworkBoundaryOptions;
|
networkBoundary?: NetworkBoundaryOptions;
|
||||||
publicAssets?: PublicAssetResolver;
|
publicAssets?: PublicAssetResolver;
|
||||||
@@ -2075,6 +2076,45 @@ export async function createApp(options: CreateAppOptions = {}) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (options.localTestAuth && options.registration) {
|
||||||
|
app.get(
|
||||||
|
"/api/v1/auth/local-test",
|
||||||
|
{ schema: { hide: true } },
|
||||||
|
async () => ({ available: true }),
|
||||||
|
);
|
||||||
|
app.post(
|
||||||
|
"/api/v1/auth/local-test",
|
||||||
|
{ schema: { hide: true } },
|
||||||
|
async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const result = options.registration!.createLocalTestSession();
|
||||||
|
reply.header(
|
||||||
|
"Set-Cookie",
|
||||||
|
`${userSessionCookieName}=${result.sessionToken}; Max-Age=${30 * 24 * 60 * 60}; Path=/; HttpOnly; SameSite=Strict`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
audience: result.audience,
|
||||||
|
credits: {
|
||||||
|
available_balance: result.credits.availableBalance,
|
||||||
|
reserved_balance: result.credits.reservedBalance,
|
||||||
|
},
|
||||||
|
session_expires_at: new Date(result.sessionExpiresAt).toISOString(),
|
||||||
|
status: result.status,
|
||||||
|
user: {
|
||||||
|
creator_name: result.user.creatorName,
|
||||||
|
role: result.user.role,
|
||||||
|
social_id: result.user.socialId,
|
||||||
|
status: result.user.status,
|
||||||
|
user_id: result.user.userId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return registrationFailure(reply, request.id, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
app.post(
|
app.post(
|
||||||
"/api/v1/auth/login/complete",
|
"/api/v1/auth/login/complete",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ let models: ModelConfigurationService | undefined;
|
|||||||
let recentAssets: RecentAssetService | undefined;
|
let recentAssets: RecentAssetService | undefined;
|
||||||
let stickers: StickerReleaseService | undefined;
|
let stickers: StickerReleaseService | undefined;
|
||||||
let amap: AmapAdapter = new MockAmapAdapter();
|
let amap: AmapAdapter = new MockAmapAdapter();
|
||||||
|
let localTestAuth = false;
|
||||||
const instanceConfigPath = process.env.DADA_INSTANCE_CONFIG_PATH ?? defaultInstanceConfigPath();
|
const instanceConfigPath = process.env.DADA_INSTANCE_CONFIG_PATH ?? defaultInstanceConfigPath();
|
||||||
if (credentialChannelEnabled) {
|
if (credentialChannelEnabled) {
|
||||||
const clients = initializeApiCredentialClients(await receiveApiCredentials());
|
const clients = initializeApiCredentialClients(await receiveApiCredentials());
|
||||||
try {
|
try {
|
||||||
amap = clients.amap;
|
amap = clients.amap;
|
||||||
|
localTestAuth = !clients.resendConfigured;
|
||||||
const derivePepper = (purpose: string) => createHmac("sha256", clients.adminAllowlistPepper)
|
const derivePepper = (purpose: string) => createHmac("sha256", clients.adminAllowlistPepper)
|
||||||
.update(`Dada/P0A/${purpose}/v1`, "utf8")
|
.update(`Dada/P0A/${purpose}/v1`, "utf8")
|
||||||
.digest();
|
.digest();
|
||||||
@@ -101,6 +103,7 @@ const app = await createApp({
|
|||||||
...(browserSupportRelease ? { browserSupportRelease } : {}),
|
...(browserSupportRelease ? { browserSupportRelease } : {}),
|
||||||
...(credits ? { credits } : {}),
|
...(credits ? { credits } : {}),
|
||||||
...(latestExports ? { latestExports } : {}),
|
...(latestExports ? { latestExports } : {}),
|
||||||
|
...(registration && localTestAuth ? { localTestAuth: true } : {}),
|
||||||
...(models ? { models } : {}),
|
...(models ? { models } : {}),
|
||||||
...(projects ? { projects } : {}),
|
...(projects ? { projects } : {}),
|
||||||
...(registration ? { registration } : {}),
|
...(registration ? { registration } : {}),
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface RegistrationTransactionEvent {
|
|||||||
| "registration_send"
|
| "registration_send"
|
||||||
| "registration_complete"
|
| "registration_complete"
|
||||||
| "registration_send_compensation"
|
| "registration_send_compensation"
|
||||||
|
| "local_test_session"
|
||||||
| "login_send"
|
| "login_send"
|
||||||
| "login_complete"
|
| "login_complete"
|
||||||
| "admin_login_send"
|
| "admin_login_send"
|
||||||
@@ -654,6 +655,60 @@ export class RegistrationService {
|
|||||||
return outcome;
|
return outcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createLocalTestSession(): LoginCompleteResult {
|
||||||
|
const now = this.options.clock();
|
||||||
|
return this.runImmediate("local_test_session", () => {
|
||||||
|
const registrationId = "local-test-user-v1";
|
||||||
|
const existing = this.database.prepare(`
|
||||||
|
SELECT user_id, role, status FROM users WHERE registration_id = ?
|
||||||
|
`).get(registrationId) as {
|
||||||
|
role: "user" | "super_admin";
|
||||||
|
status: "active" | "suspended" | "deleted";
|
||||||
|
user_id: string;
|
||||||
|
} | undefined;
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.role !== "user" || existing.status !== "active") {
|
||||||
|
throw new RegistrationError("AUTH_ENTRY_REJECTED", "account_suspended");
|
||||||
|
}
|
||||||
|
const session = this.insertSession(existing.user_id, "user", now);
|
||||||
|
return {
|
||||||
|
outcome: "committed",
|
||||||
|
value: this.loginResult(this.readCompletedRegistration(existing.user_id, session.sessionId)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = randomUUID();
|
||||||
|
this.database.prepare(`
|
||||||
|
INSERT INTO users (
|
||||||
|
user_id, normalized_email, role, status, counts_toward_stage_limit,
|
||||||
|
registration_id, created_at
|
||||||
|
) VALUES (?, 'local-test-user@dada.invalid', 'user', 'active', 0, ?, ?)
|
||||||
|
`).run(userId, registrationId, now);
|
||||||
|
this.database.prepare(`
|
||||||
|
INSERT INTO user_profiles (
|
||||||
|
user_id, creator_name, social_id, private_content_notice_version,
|
||||||
|
private_content_notice_acknowledged_at
|
||||||
|
) VALUES (?, '本机测试用户', '@dada_local_test', NULL, NULL)
|
||||||
|
`).run(userId);
|
||||||
|
this.database.prepare(`
|
||||||
|
INSERT INTO credit_accounts (user_id, available_balance, reserved_balance, updated_at)
|
||||||
|
VALUES (?, 10, 0, ?)
|
||||||
|
`).run(userId, now);
|
||||||
|
this.database.prepare(`
|
||||||
|
INSERT INTO credit_ledger (
|
||||||
|
ledger_id, user_id, operation_key, entry_type, amount,
|
||||||
|
available_before, available_after, reserved_before, reserved_after, created_at
|
||||||
|
) VALUES (?, ?, 'local-test-registration:v1', 'registration_grant', 10, 0, 10, 0, 0, ?)
|
||||||
|
`).run(randomUUID(), userId, now);
|
||||||
|
const session = this.insertSession(userId, "user", now);
|
||||||
|
return {
|
||||||
|
outcome: "committed",
|
||||||
|
value: this.loginResult(this.readCompletedRegistration(userId, session.sessionId)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
applySecureConfig(candidate: SecureConfigCandidate) {
|
applySecureConfig(candidate: SecureConfigCandidate) {
|
||||||
const now = this.options.clock();
|
const now = this.options.clock();
|
||||||
const fail = (reason: string): never => {
|
const fail = (reason: string): never => {
|
||||||
|
|||||||
@@ -27,12 +27,13 @@ export async function receiveApiCredentials(input: NodeJS.ReadableStream = proce
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initializeApiCredentialClients(credentials: Record<(typeof API_CREDENTIALS)[number], string>) {
|
export function initializeApiCredentialClients(credentials: Record<(typeof API_CREDENTIALS)[number], string>) {
|
||||||
try {
|
try {
|
||||||
const adminPepper = credentials["Dada/P0A/admin/pepper"];
|
const adminPepper = credentials["Dada/P0A/admin/pepper"];
|
||||||
if (!adminPepper) throw new Error("admin_pepper_not_configured");
|
if (!adminPepper) throw new Error("admin_pepper_not_configured");
|
||||||
return {
|
return {
|
||||||
adminAllowlistPepper: Buffer.from(adminPepper, "utf8"),
|
adminAllowlistPepper: Buffer.from(adminPepper, "utf8"),
|
||||||
amap: credentials["Dada/P0A/api/amap"] ? new RealAmapAdapter(credentials["Dada/P0A/api/amap"]) : new MockAmapAdapter(),
|
amap: credentials["Dada/P0A/api/amap"] ? new RealAmapAdapter(credentials["Dada/P0A/api/amap"]) : new MockAmapAdapter(),
|
||||||
|
resendConfigured: Boolean(credentials["Dada/P0A/api/resend"]),
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
for (const name of API_CREDENTIALS) credentials[name] = "";
|
for (const name of API_CREDENTIALS) credentials[name] = "";
|
||||||
|
|||||||
@@ -124,6 +124,25 @@ button {
|
|||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-test-entry {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
border-bottom: 1px solid #b4b4af;
|
||||||
|
padding-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-test-entry .auth-primary {
|
||||||
|
margin-top: 0;
|
||||||
|
border-color: #111111;
|
||||||
|
background: #111111;
|
||||||
|
color: #f2f500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-test-entry .auth-primary:disabled {
|
||||||
|
border-color: #777773;
|
||||||
|
background: #deded9;
|
||||||
|
color: #777773;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-tabs {
|
.auth-tabs {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ export function UserAuthPage() {
|
|||||||
const [sendState, setSendState] = useState<SendState>("idle");
|
const [sendState, setSendState] = useState<SendState>("idle");
|
||||||
const [countdown, setCountdown] = useState(0);
|
const [countdown, setCountdown] = useState(0);
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
|
const [localTestAvailable, setLocalTestAvailable] = useState(false);
|
||||||
|
const [localTestError, setLocalTestError] = useState<string>();
|
||||||
|
const [localTestSubmitting, setLocalTestSubmitting] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const emailValid = /^[^@\s]+@[^@\s]+$/.test(email);
|
const emailValid = /^[^@\s]+@[^@\s]+$/.test(email);
|
||||||
const registrationReady = Boolean(
|
const registrationReady = Boolean(
|
||||||
@@ -93,6 +96,18 @@ export function UserAuthPage() {
|
|||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, [countdown]);
|
}, [countdown]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
void fetch("/api/v1/auth/local-test", { credentials: "same-origin", signal: controller.signal })
|
||||||
|
.then(async (response) => {
|
||||||
|
if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) return;
|
||||||
|
const body = await response.json() as { available?: boolean };
|
||||||
|
if (body.available === true) setLocalTestAvailable(true);
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return () => controller.abort();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!noticeOpen) return;
|
if (!noticeOpen) return;
|
||||||
const previousOverflow = document.body.style.overflow;
|
const previousOverflow = document.body.style.overflow;
|
||||||
@@ -255,6 +270,27 @@ export function UserAuthPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function enterLocalTest() {
|
||||||
|
if (localTestSubmitting) return;
|
||||||
|
setLocalTestSubmitting(true);
|
||||||
|
setLocalTestError(undefined);
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/v1/auth/local-test", {
|
||||||
|
credentials: "same-origin",
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
setLocalTestError("本机测试会话未能建立,请重试。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.assign("/app");
|
||||||
|
} catch {
|
||||||
|
setLocalTestError("本机测试会话未能建立,请重试。");
|
||||||
|
} finally {
|
||||||
|
setLocalTestSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main className="auth-page">
|
<main className="auth-page">
|
||||||
@@ -271,6 +307,14 @@ export function UserAuthPage() {
|
|||||||
<section className="auth-content">
|
<section className="auth-content">
|
||||||
<a className="auth-admin-link" href="/admin/login">管理员登录</a>
|
<a className="auth-admin-link" href="/admin/login">管理员登录</a>
|
||||||
<div className="auth-panel">
|
<div className="auth-panel">
|
||||||
|
{localTestAvailable ? (
|
||||||
|
<div className="auth-test-entry">
|
||||||
|
<button className="auth-primary" disabled={localTestSubmitting} onClick={enterLocalTest} type="button">
|
||||||
|
{localTestSubmitting ? "正在进入" : "直接进入本机测试"}
|
||||||
|
</button>
|
||||||
|
{localTestError ? <p className="auth-error" role="alert">{localTestError}</p> : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="auth-tabs" role="tablist" aria-label="认证方式">
|
<div className="auth-tabs" role="tablist" aria-label="认证方式">
|
||||||
<button
|
<button
|
||||||
aria-selected={mode === "login"}
|
aria-selected={mode === "login"}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { createApp } from "../../apps/api/src/app.js";
|
||||||
|
import { RegistrationService } from "../../apps/api/src/registration.js";
|
||||||
|
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
|
||||||
|
|
||||||
|
const fixedNow = Date.parse("2026-08-05T06:00:00.000Z");
|
||||||
|
const writeHeaders = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
|
||||||
|
const roots: string[] = [];
|
||||||
|
const services: RegistrationService[] = [];
|
||||||
|
|
||||||
|
function createRegistrationService() {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "dada-local-test-session-"));
|
||||||
|
roots.push(root);
|
||||||
|
const registration = new RegistrationService({
|
||||||
|
challengePepper: Buffer.alloc(32, 0x51),
|
||||||
|
clock: () => fixedNow,
|
||||||
|
currentPrivacyNoticeVersion: "p0a-notice-v1",
|
||||||
|
databasePath: join(root, "dada.sqlite3"),
|
||||||
|
invitePepper: Buffer.alloc(32, 0x52),
|
||||||
|
resend: new MockResendAdapter(),
|
||||||
|
sessionPepper: Buffer.alloc(32, 0x53),
|
||||||
|
});
|
||||||
|
services.push(registration);
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const service of services.splice(0)) {
|
||||||
|
try { service.close(); } catch { /* already closed by the test */ }
|
||||||
|
}
|
||||||
|
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("POSTV1-03 local test session", () => {
|
||||||
|
it("does not expose the local test route unless explicitly enabled", async () => {
|
||||||
|
const registration = createRegistrationService();
|
||||||
|
const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration });
|
||||||
|
|
||||||
|
const status = await app.inject({ headers: writeHeaders, method: "GET", url: "/api/v1/auth/local-test" });
|
||||||
|
const created = await app.inject({ headers: writeHeaders, method: "POST", url: "/api/v1/auth/local-test" });
|
||||||
|
|
||||||
|
expect(status.statusCode).toBe(404);
|
||||||
|
expect(created.statusCode).toBe(404);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates one isolated fixture account and restores it without duplicate credits", async () => {
|
||||||
|
const registration = createRegistrationService();
|
||||||
|
const app = await createApp({
|
||||||
|
browserGate: false,
|
||||||
|
localTestAuth: true,
|
||||||
|
networkBoundary: { allowTestPort: true },
|
||||||
|
registration,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await app.inject({ headers: writeHeaders, method: "GET", url: "/api/v1/auth/local-test" });
|
||||||
|
expect(status.statusCode).toBe(200);
|
||||||
|
expect(status.json()).toEqual({ available: true });
|
||||||
|
|
||||||
|
const first = await app.inject({ headers: writeHeaders, method: "POST", url: "/api/v1/auth/local-test" });
|
||||||
|
expect(first.statusCode).toBe(200);
|
||||||
|
expect(first.json()).toMatchObject({
|
||||||
|
audience: "user",
|
||||||
|
credits: { available_balance: 10, reserved_balance: 0 },
|
||||||
|
status: "authenticated",
|
||||||
|
user: { creator_name: "本机测试用户", role: "user", social_id: "@dada_local_test", status: "active" },
|
||||||
|
});
|
||||||
|
expect(first.headers["set-cookie"]).toContain("dada_session=");
|
||||||
|
|
||||||
|
const session = await app.inject({
|
||||||
|
headers: { cookie: first.headers["set-cookie"], host: "127.0.0.1:43121" },
|
||||||
|
method: "GET",
|
||||||
|
url: "/api/v1/auth/session",
|
||||||
|
});
|
||||||
|
expect(session.statusCode).toBe(200);
|
||||||
|
expect(session.json()).toMatchObject({ authenticated: true, credits: { available_balance: 10 } });
|
||||||
|
|
||||||
|
const second = await app.inject({ headers: writeHeaders, method: "POST", url: "/api/v1/auth/local-test" });
|
||||||
|
expect(second.statusCode).toBe(200);
|
||||||
|
expect(second.json().user.user_id).toBe(first.json().user.user_id);
|
||||||
|
expect(registration.database.prepare("SELECT COUNT(*) AS count FROM users").get()).toEqual({ count: 1 });
|
||||||
|
expect(registration.database.prepare("SELECT COUNT(*) AS count FROM credit_ledger").get()).toEqual({ count: 1 });
|
||||||
|
expect(registration.database.prepare("SELECT counts_toward_stage_limit FROM users").get()).toEqual({ counts_toward_stage_limit: 0 });
|
||||||
|
|
||||||
|
const openapi = JSON.stringify(app.swagger());
|
||||||
|
expect(openapi).not.toContain("/api/v1/auth/local-test");
|
||||||
|
expect(openapi).not.toContain("local-test-user");
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -40,8 +40,23 @@ describe("TDD-WP7-EXT-003 production Amap adapter", () => {
|
|||||||
|
|
||||||
const clients = initializeApiCredentialClients(credentials);
|
const clients = initializeApiCredentialClients(credentials);
|
||||||
expect(clients.amap).toBeInstanceOf(RealAmapAdapter);
|
expect(clients.amap).toBeInstanceOf(RealAmapAdapter);
|
||||||
|
expect(clients.resendConfigured).toBe(true);
|
||||||
expect(Object.values(credentials)).toEqual(["", "", ""]);
|
expect(Object.values(credentials)).toEqual(["", "", ""]);
|
||||||
clients.amap.dispose();
|
clients.amap.dispose?.();
|
||||||
|
clients.adminAllowlistPepper.fill(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports an empty Resend credential without retaining its value", () => {
|
||||||
|
const credentials = {
|
||||||
|
"Dada/P0A/admin/pepper": "fixture-admin-value",
|
||||||
|
"Dada/P0A/api/amap": "",
|
||||||
|
"Dada/P0A/api/resend": "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const clients = initializeApiCredentialClients(credentials);
|
||||||
|
expect(clients.resendConfigured).toBe(false);
|
||||||
|
expect(Object.values(credentials)).toEqual(["", "", ""]);
|
||||||
|
clients.amap.dispose?.();
|
||||||
clients.adminAllowlistPepper.fill(0);
|
clients.adminAllowlistPepper.fill(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,29 @@ test.beforeAll(async () => {
|
|||||||
|
|
||||||
test.afterAll(async () => vite.close());
|
test.afterAll(async () => vite.close());
|
||||||
|
|
||||||
|
test("POSTV1-03 enters the workspace through the local test session", async ({ page }) => {
|
||||||
|
await page.route("**/api/v1/auth/local-test", (route) => {
|
||||||
|
if (route.request().method() === "GET") {
|
||||||
|
return route.fulfill({ contentType: "application/json", status: 200, body: JSON.stringify({ available: true }) });
|
||||||
|
}
|
||||||
|
expect(route.request().postData()).toBeNull();
|
||||||
|
return route.fulfill({ contentType: "application/json", status: 200, body: JSON.stringify({ status: "authenticated" }) });
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(webUrl);
|
||||||
|
const button = page.getByRole("button", { name: "直接进入本机测试" });
|
||||||
|
await expect(button).toBeVisible();
|
||||||
|
await button.click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(`${webUrl}/app`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("POSTV1-03 hides the local test entry when the API does not enable it", async ({ page }) => {
|
||||||
|
await page.route("**/api/v1/auth/local-test", (route) => route.fulfill({ status: 404, body: "" }));
|
||||||
|
await page.goto(webUrl);
|
||||||
|
await expect(page.getByRole("button", { name: "直接进入本机测试" })).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
test("TDD-WP1-NOTICE-001 expands DVPM8 only after successful code delivery", async ({ page }) => {
|
test("TDD-WP1-NOTICE-001 expands DVPM8 only after successful code delivery", async ({ page }) => {
|
||||||
await page.route("**/api/v1/auth/register/send", (route) => route.fulfill({
|
await page.route("**/api/v1/auth/register/send", (route) => route.fulfill({
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
|
|||||||
Reference in New Issue
Block a user