b491b6a146
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
111 lines
5.1 KiB
TypeScript
111 lines
5.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
const readLoginView = () => readFileSync(resolve(__dirname, "./Login.vue"), "utf8");
|
|
|
|
describe("Login protocol agreement", () => {
|
|
it("requires protocol agreement before calling login", () => {
|
|
const source = readLoginView();
|
|
const protocolGuardIndex = source.indexOf("!form.agreeProtocol");
|
|
const loginCallIndex = source.indexOf("auth.login(form.email, form.password)");
|
|
|
|
expect(source).toContain('v-model="form.agreeProtocol"');
|
|
expect(source).toContain("我已阅读并同意");
|
|
expect(source).toContain("请先阅读并同意用户协议");
|
|
expect(protocolGuardIndex).toBeGreaterThan(-1);
|
|
expect(loginCallIndex).toBeGreaterThan(-1);
|
|
expect(protocolGuardIndex).toBeLessThan(loginCallIndex);
|
|
});
|
|
|
|
it("does not access browser password credentials on the login page", () => {
|
|
const source = readLoginView();
|
|
|
|
expect(source).toContain('autocomplete="username"');
|
|
expect(source).toContain('name="username"');
|
|
expect(source).toContain('name="ctms-login-password"');
|
|
expect(source).toContain('autocomplete="new-password"');
|
|
expect(source).not.toContain('v-model="form.rememberPassword"');
|
|
expect(source).not.toContain("记住密码");
|
|
expect(source).not.toContain("tryLoadBrowserCredential");
|
|
expect(source).not.toContain("tryStoreBrowserCredential");
|
|
expect(source).not.toContain("navigator.credentials");
|
|
expect(source).not.toContain("PasswordCredential");
|
|
expect(source).not.toContain('localStorage.setItem("ctms_saved_password"');
|
|
expect(source).not.toContain("localStorage.setItem('ctms_saved_password'");
|
|
});
|
|
|
|
it("opens a CTMS-specific protocol dialog from the protocol text", () => {
|
|
const source = readLoginView();
|
|
|
|
expect(source).toContain("authProtocolSections");
|
|
expect(source).toContain('v-model="protocolDialogVisible"');
|
|
expect(source).toContain('@click.stop.prevent="openProtocolDialog"');
|
|
expect(source).toContain('import { authProtocolSections } from "../content/authProtocol"');
|
|
expect(source).toContain("const protocolSections = authProtocolSections");
|
|
expect(source).toContain("confirmProtocol");
|
|
});
|
|
|
|
it("persists the protocol agreement checkbox in localStorage", () => {
|
|
const source = readLoginView();
|
|
|
|
expect(source).toContain("const AGREE_PROTOCOL_KEY = \"ctms_agree_protocol\"");
|
|
expect(source).toContain('localStorage.getItem(AGREE_PROTOCOL_KEY) === "true"');
|
|
expect(source).toContain('localStorage.setItem(AGREE_PROTOCOL_KEY, String(checked))');
|
|
expect(source).toContain("confirmProtocol");
|
|
});
|
|
|
|
it("avoids browser password caching", () => {
|
|
const source = readLoginView();
|
|
|
|
expect(source).toContain('name="ctms-login-password"');
|
|
expect(source).toContain('autocomplete="new-password"');
|
|
expect(source).not.toContain('autocomplete="current-password"');
|
|
});
|
|
|
|
it("restores the current user's last project before routing after login", () => {
|
|
const source = readLoginView();
|
|
const loginCallIndex = source.indexOf("auth.login(form.email, form.password)");
|
|
const userKeyIndex = source.indexOf("const userKey = auth.user?.email || form.email");
|
|
const restoreIndex = source.indexOf("studyStore.restoreStudyForUser(userKey");
|
|
const projectOverviewRouteIndex = source.indexOf('router.push("/project/overview")');
|
|
|
|
expect(userKeyIndex).toBeGreaterThan(loginCallIndex);
|
|
expect(restoreIndex).toBeGreaterThan(loginCallIndex);
|
|
expect(restoreIndex).toBeLessThan(projectOverviewRouteIndex);
|
|
expect(source).toContain("preferActive: !!auth.user?.is_admin");
|
|
});
|
|
|
|
it("shows persistent logout reason notices on the login card", () => {
|
|
const source = readLoginView();
|
|
|
|
expect(source).toContain('v-if="logoutNotice"');
|
|
expect(source).toContain("LOGOUT_REASON_TIMEOUT");
|
|
expect(source).toContain("LOGOUT_REASON_AUTH_EXPIRED");
|
|
expect(source).toContain("LOGOUT_REASON_MANUAL");
|
|
expect(source).toContain("30 分钟未检测到任何操作。为保护项目数据,请重新登录后继续。");
|
|
expect(source).toContain("登录状态已失效");
|
|
expect(source).toContain("已安全退出");
|
|
expect(source).toContain(".logout-notice");
|
|
});
|
|
|
|
it("keeps the login card wide enough on desktop while remaining responsive", () => {
|
|
const source = readLoginView();
|
|
|
|
expect(source).toContain("width: clamp(480px, 34vw, 560px);");
|
|
expect(source).toContain("max-width: calc(100vw - 40px);");
|
|
});
|
|
|
|
it("uses only server-configured email domains on web and desktop", () => {
|
|
const source = readLoginView();
|
|
|
|
expect(source).toContain('fetchEmailDomains()');
|
|
expect(source).toContain(':disabled="availableEmailDomains.length === 0"');
|
|
expect(source).toContain("const availableEmailDomains = computed(() => configuredEmailDomains.value)");
|
|
expect(source).toContain("configuredEmailDomains.value.includes(domain)");
|
|
expect(source).not.toContain("preservedEmailDomain");
|
|
expect(source).not.toContain("showDomainSelect");
|
|
expect(source).not.toContain("email-domain-input");
|
|
});
|
|
});
|