Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 80 additions & 3 deletions frontend/src/App.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,31 @@ import { createMemoryHistory, createRouter } from "vue-router";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import App from "./App.vue";

vi.mock("./authSession", () => ({
const mocks = vi.hoisted(() => ({
clearSession: vi.fn(),
onSessionChange: vi.fn(() => () => undefined),
readAuth: vi.fn(() => null),
readAuth: vi.fn(() => null as unknown),
saveAuth: vi.fn(),
refreshAuthSession: vi.fn(),
logoutCurrentSession: vi.fn(),
}));

vi.mock("./authSession", () => ({
clearSession: mocks.clearSession,
onSessionChange: mocks.onSessionChange,
readAuth: mocks.readAuth,
saveAuth: mocks.saveAuth,
}));

vi.mock("./composables/useTheme", () => ({
initializeTheme: vi.fn(),
}));

vi.mock("./apiClient", () => ({
apiClient: { logoutCurrentSession: vi.fn() },
apiClient: {
logoutCurrentSession: mocks.logoutCurrentSession,
refreshAuthSession: mocks.refreshAuthSession,
},
}));

vi.mock("./components/shell/AppShell.vue", () => ({
Expand All @@ -25,6 +38,12 @@ vi.mock("./components/shell/AppShell.vue", () => ({
let mountedApp: VueApp<Element> | null = null;

beforeEach(() => {
mocks.readAuth.mockReset();
mocks.readAuth.mockReturnValue(null);
mocks.saveAuth.mockReset();
mocks.refreshAuthSession.mockReset();
mocks.onSessionChange.mockReset();
mocks.onSessionChange.mockReturnValue(() => undefined);
vi.stubGlobal(
"fetch",
vi.fn(async () => ({ ok: true })),
Expand Down Expand Up @@ -77,4 +96,62 @@ describe("应用会话恢复", () => {
});
expect(protectedMounted).not.toHaveBeenCalled();
});

it("启动时从服务端刷新缓存角色,避免展示过期社团身份", async () => {
const staleAuth = {
token: "stale-token",
user: { id: 8, username: "club_admin", realName: "王夫人", accountStatus: "normal" },
roles: [
{
id: 6,
code: "ADVISOR",
name: "指导老师",
displayName: "图灵社指导老师",
scope: "club",
clubId: 1000003,
clubIds: [1000003],
permissions: ["club:internal:view"],
},
],
permissions: ["club:internal:view"],
};
const currentAuth = {
...staleAuth,
token: "current-token",
roles: [
{
...staleAuth.roles[0],
code: "CLUB_ADMIN",
name: "社团管理员",
displayName: "社团管理员",
scope: "system",
clubId: null,
clubIds: [],
permissions: ["club:review"],
},
],
permissions: ["club:review"],
};
mocks.readAuth.mockReturnValue(staleAuth);
mocks.refreshAuthSession.mockResolvedValue(currentAuth);

const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: "/dashboard", component: { template: "<div />" } }],
});
await router.push("/dashboard");
await router.isReady();

const host = document.createElement("div");
document.body.appendChild(host);
mountedApp = createApp(App);
mountedApp.use(ElementPlus);
mountedApp.use(router);
mountedApp.mount(host);

await vi.waitFor(() => {
expect(mocks.refreshAuthSession).toHaveBeenCalledOnce();
expect(mocks.saveAuth).toHaveBeenCalledWith(currentAuth);
});
});
});
29 changes: 26 additions & 3 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { computed, onMounted, onUnmounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import { ElMessage } from "element-plus";
import { type AuthResponse, clearSession, onSessionChange, readAuth } from "./authSession";
import {
type AuthResponse,
clearSession,
onSessionChange,
readAuth,
saveAuth,
} from "./authSession";
import { apiClient } from "./apiClient";
import { buildNavigationGroups, resolveActiveNavigation } from "./navigation";
import { initializeTheme } from "./composables/useTheme";
Expand Down Expand Up @@ -49,6 +55,23 @@ function refreshSession() {
sessionResolved.value = true;
}

async function refreshStoredSession() {
if (!readAuth()) {
refreshSession();
return;
}

try {
const refreshedAuth = await apiClient.refreshAuthSession();
saveAuth(refreshedAuth);
} catch {
// Keep a cached session during a transient network failure. A 401 response
// is handled by apiClient and clears the session before this fallback runs.
}

refreshSession();
}

async function checkHealth() {
healthChecking.value = true;
try {
Expand All @@ -73,9 +96,9 @@ async function logout() {
}

onMounted(() => {
refreshSession();
checkHealth();
stopSessionListener = onSessionChange(refreshSession);
void refreshStoredSession();
checkHealth();
});

onUnmounted(() => {
Expand Down
Loading