Analyze news revisions with independent model settings, evidence-backed risk ratings, event grouping, and rolling market outlooks. Add a default homepage, archival history, pending-result refresh, and isolated regression coverage. Keep analysis disabled until explicitly enabled. Require a separately authorized schema migration before deployment and preserve compatibility with the previous six-service instance.
209 lines
9.1 KiB
TypeScript
209 lines
9.1 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import test from "node:test"
|
|
import { setImmediate } from "node:timers/promises"
|
|
import { moduleLoader, require } from "./module-loader.ts"
|
|
|
|
const axios = require("axios").default
|
|
const pinia = require("pinia")
|
|
const vue = require("vue")
|
|
const { createRouter, createMemoryHistory } = require("vue-router")
|
|
|
|
const ok = (data: unknown) => ({ status: 200, body: { code: 0, data, message: "" } })
|
|
const failure = (status: number) => ({ status, body: { code: status, data: null, message: `failure ${status}` } })
|
|
|
|
function environment(respond: (config: any) => any) {
|
|
const messages: string[] = []
|
|
const values = new Map<string, string>()
|
|
const storage = { getItem: (key: string) => values.get(key) ?? null, setItem: (key: string, value: string) => values.set(key, value), removeItem: (key: string) => values.delete(key) }
|
|
pinia.setActivePinia(pinia.createPinia())
|
|
const routes = [
|
|
{ path: "/login", name: "login" },
|
|
{ path: "/news", name: "news", meta: { admin: true } },
|
|
{ path: "/home", name: "home", meta: { admin: true } },
|
|
{ path: "/calendar", name: "calendar", meta: { admin: true } },
|
|
{ path: "/403", name: "forbidden" },
|
|
{ path: "/connection-error", name: "connection-error", meta: { public: true } }
|
|
].map(route => ({ ...route, component: { render: () => null } }))
|
|
const router = createRouter({ history: createMemoryHistory(), routes })
|
|
const load = moduleLoader({
|
|
"@/router": { router },
|
|
"element-plus": { ElMessage: { error: (message: string) => messages.push(message) } },
|
|
"axios": { __esModule: true, ...axios, default: { ...axios, create: (config: any) => axios.create({
|
|
...config,
|
|
adapter: async (config: any) => {
|
|
const { status, body } = await respond(config)
|
|
const response = { status, data: body, statusText: "", headers: {}, config }
|
|
if (status >= 400) throw new axios.AxiosError(`HTTP ${status}`, "ERR_BAD_RESPONSE", config, undefined, response)
|
|
return response
|
|
}
|
|
}) } }
|
|
}, { localStorage: storage, ...vue })
|
|
const { request, ApiError } = load("@/http/axios")
|
|
const user = load("@/pinia/stores/user").useUserStore()
|
|
const guards = load("@/router/guards")
|
|
return { request, ApiError, user, guards, router, messages, load, storage }
|
|
}
|
|
|
|
test("JSON 解包、明确拒绝错误、403 保留身份且只提示一次", async () => {
|
|
let result = ok([1])
|
|
const env = environment(() => result)
|
|
env.user.setToken("current")
|
|
assert.deepEqual(await env.request({ url: "/news", method: "get" }), [1])
|
|
result = failure(403)
|
|
await assert.rejects(env.request({ url: "/news", method: "get" }), (error: any) => error.code === 403)
|
|
assert.equal(env.user.token, "current")
|
|
assert.equal(env.messages.length, 1)
|
|
result = { status: 200, body: { code: 400, data: null, message: "business failure" } }
|
|
await assert.rejects(env.request({ url: "/news", method: "get" }), /business failure/)
|
|
result = { status: 200, body: { items: [] } } as any
|
|
await assert.rejects(env.request({ url: "/news", method: "get" }), /响应格式无效/)
|
|
})
|
|
|
|
test("旧身份恢复响应不能写入新会话;同 Token 重新设置也隔离", async () => {
|
|
const pending = Promise.withResolvers<any>()
|
|
const env = environment(() => pending.promise)
|
|
env.user.setToken("same-token")
|
|
const restore = env.user.fetchMe()
|
|
const rejected = assert.rejects(restore, (error: any) => axios.isCancel(error))
|
|
await setImmediate()
|
|
env.user.setToken("same-token")
|
|
pending.resolve(ok({ username: "old", is_admin: true }))
|
|
await rejected
|
|
assert.equal(env.user.identity, undefined)
|
|
assert.equal(env.user.token, "same-token")
|
|
assert.equal(env.messages.length, 0)
|
|
})
|
|
|
|
test("旧 401 与旧注销结果都不能注销新登录", async () => {
|
|
for (const request of ["read", "logout"]) {
|
|
const pending = Promise.withResolvers<any>()
|
|
const env = environment(() => pending.promise)
|
|
env.user.setToken("old")
|
|
const operation = request === "logout" ? env.user.logout() : env.request({ url: "/news", method: "get" })
|
|
const rejected = assert.rejects(operation, (error: any) => axios.isCancel(error))
|
|
await setImmediate()
|
|
env.user.setToken("new")
|
|
pending.resolve(request === "logout" ? ok(null) : failure(401))
|
|
await rejected
|
|
assert.equal(env.user.token, "new")
|
|
assert.equal(env.messages.length, 0)
|
|
}
|
|
})
|
|
|
|
test("注销失败保留会话;成功才清除持久 Token", async () => {
|
|
let result: ReturnType<typeof ok> = failure(503)
|
|
const env = environment(() => result)
|
|
env.user.setToken("current")
|
|
await assert.rejects(env.user.logout())
|
|
assert.equal(env.user.token, "current")
|
|
result = ok(null)
|
|
await env.user.logout()
|
|
assert.equal(env.user.token, "")
|
|
assert.equal(env.storage.getItem("alphapulse.auth.token"), null)
|
|
})
|
|
|
|
test("匿名深链、登录回跳与已登录刷新使用真实身份", async () => {
|
|
let count = 0
|
|
const env = environment(() => {
|
|
count++
|
|
return ok({ username: "admin", is_admin: true })
|
|
})
|
|
env.guards.installGuards(env.router)
|
|
await env.router.push("/calendar?date=2026-10-25")
|
|
assert.equal(env.router.currentRoute.value.name, "login")
|
|
assert.equal(env.router.currentRoute.value.query.redirect, "/calendar?date=2026-10-25")
|
|
env.user.setToken("current")
|
|
await env.router.push("/news")
|
|
await env.router.push("/login?redirect=/calendar%3Fdate%3D2026-10-25")
|
|
assert.equal(env.router.currentRoute.value.fullPath, "/calendar?date=2026-10-25")
|
|
assert.equal(count, 1)
|
|
await env.router.push("/news")
|
|
assert.equal(count, 1)
|
|
for (const target of ["//evil.test", "https://evil.test", "/\\evil.test", "/login?redirect=/login"])
|
|
assert.equal(env.guards.safeRedirect(target), "/home")
|
|
})
|
|
|
|
test("身份恢复 401 保留深链回跳;403 不清身份,网络错误允许重试", async () => {
|
|
for (const mode of ["expired", "forbidden", "network"]) {
|
|
const env = environment(() => mode === "expired" ? failure(401) : mode === "network" ? failure(503) : ok({ username: "former-admin", is_admin: false }))
|
|
env.user.setToken("current")
|
|
env.guards.installGuards(env.router)
|
|
await env.router.push("/calendar?date=today")
|
|
const route = env.router.currentRoute.value
|
|
assert.equal(route.name, mode === "expired" ? "login" : mode === "forbidden" ? "forbidden" : "connection-error")
|
|
assert.equal(env.user.token, mode === "expired" ? "" : "current")
|
|
if (mode === "expired") assert.equal(route.query.redirect, "/calendar?date=today")
|
|
if (mode === "forbidden") assert.equal(env.user.identity.username, "former-admin")
|
|
}
|
|
})
|
|
|
|
test("共享来源缓存只有一个在途请求,切换会话清空且丢弃旧响应", async () => {
|
|
const pending = Promise.withResolvers<any>()
|
|
let count = 0
|
|
const env = environment(() => {
|
|
count++
|
|
return pending.promise
|
|
})
|
|
env.user.setToken("old")
|
|
const sources = env.load("@/pinia/stores/sources").useSourcesStore()
|
|
const first = sources.load()
|
|
assert.equal(first, sources.load())
|
|
const rejected = assert.rejects(first, (error: any) => axios.isCancel(error))
|
|
await setImmediate()
|
|
env.user.setToken("new")
|
|
pending.resolve(ok({ sources: [{ code: "private", name: "old" }] }))
|
|
await rejected
|
|
assert.equal(count, 1)
|
|
assert.deepEqual(Object.keys(sources.names), [])
|
|
})
|
|
|
|
test("确认框结束后不能以新会话发布旧页面的 Skill", async () => {
|
|
const confirm = Promise.withResolvers<void>()
|
|
const user = vue.reactive({ token: "old", sessionVersion: 1 })
|
|
let published = 0
|
|
const node = { id: 1, kind: "skill", name: "test" }
|
|
const load = moduleLoader({
|
|
"vue": { ...vue, onBeforeUnmount() {} },
|
|
"@/pinia/stores/user": { useUserStore: () => user },
|
|
"@@/apis/skills": {
|
|
listSkillTreeApi: async () => [node],
|
|
listSkillVersionsApi: async () => [{ id: 1, version_no: 1, content: "text" }],
|
|
publishSkillVersionApi: async () => {
|
|
published++
|
|
}
|
|
},
|
|
"@@/components/SkillTree/index.vue": {},
|
|
"@@/components/SkillVersionDrawer/index.vue": {},
|
|
"@@/utils/markdown": { renderMarkdown: (text: string) => text }
|
|
}, {
|
|
...vue,
|
|
onMounted() {},
|
|
onBeforeUnmount() {},
|
|
onBeforeRouteLeave() {},
|
|
ElMessage: { success() {} },
|
|
ElMessageBox: { confirm: () => confirm.promise }
|
|
})
|
|
const page = load("@/pages/skills/index.vue").default.setup({}, { expose() {} })
|
|
await page.loadTree()
|
|
await page.select(node)
|
|
const operation = page.publishLatest()
|
|
await setImmediate()
|
|
user.sessionVersion++
|
|
confirm.resolve()
|
|
await assert.rejects(operation, (error: any) => axios.isCancel(error))
|
|
assert.equal(published, 0)
|
|
})
|
|
|
|
test("OpenAPI 仅显式原样返回;201/202 保留业务数据与 null", async () => {
|
|
let result: { status: number, body: unknown } = { status: 200, body: { openapi: "3.1.0", info: { title: "test", version: "1" }, paths: {} } }
|
|
const env = environment(() => result)
|
|
env.user.setToken("current")
|
|
const spec = await env.load("@@/apis/docs").getOpenApiApi()
|
|
assert.equal(spec.openapi, "3.1.0")
|
|
await assert.rejects(env.request({ url: "/news", method: "get" }), /响应格式无效/)
|
|
for (const status of [201, 202]) {
|
|
result = { status, body: { code: 0, data: null, message: "" } }
|
|
assert.equal(await env.request({ url: "/write", method: "post" }), null)
|
|
}
|
|
})
|