import { TestBed } from '@angular/core/testing'; import { SessionService } from './session.service'; import { Me } from './api/models'; /** * 会话状态(登录态)—— 全项目唯一持有者。 * * 这里出错影响最大:登录态判断错了,整个 App 都会显示异常。 * 重点回归: * ① 持久化与恢复(刷新后仍登录) * ② logout 必须清干净(含旧版兼容键) * ③ localStorage 不可用时降级为内存态,不崩 */ describe('SessionService', () => { const ME: Me = { id: 'u1', name: '用户9836', mobile: '19170119836' }; beforeEach(() => localStorage.clear()); afterEach(() => localStorage.clear()); function make(): SessionService { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [SessionService] }); return TestBed.inject(SessionService); } it('初始态未登录', () => { const s = make(); expect(s.isLoggedIn()).toBeFalse(); expect(s.token()).toBeNull(); }); it('setSession 后 isLoggedIn 为真,且写入 localStorage', () => { const s = make(); s.setSession('r:abc', ME); expect(s.isLoggedIn()).toBeTrue(); expect(localStorage.getItem('fmode_session_token')).toBe('r:abc'); expect(JSON.parse(localStorage.getItem('fmode_me')!)).toEqual(ME); }); it('🔴 刷新后能恢复登录态(新实例读同一份 localStorage)', () => { make().setSession('r:abc', ME); const s2 = make(); // 模拟刷新后重新创建 expect(s2.isLoggedIn()).toBeTrue(); expect(s2.token()).toBe('r:abc'); expect(s2.me()?.name).toBe('用户9836'); }); it('🔴 logout 必须清干净(token / me / localStorage)', () => { const s = make(); s.setSession('r:abc', ME); s.logout(); expect(s.token()).toBeNull(); expect(s.me()).toBeNull(); expect(s.isLoggedIn()).toBeFalse(); expect(localStorage.getItem('fmode_session_token')).toBeNull(); expect(localStorage.getItem('fmode_me')).toBeNull(); }); it('只有 token 没有 me → 仍视为未登录', () => { localStorage.setItem('fmode_session_token', 'r:abc'); const s = make(); expect(s.token()).toBe('r:abc'); expect(s.isLoggedIn()).toBeFalse(); // 缺 me 不算登录 }); it('me 的 JSON 损坏 → 不崩,视为未登录', () => { localStorage.setItem('fmode_session_token', 'r:abc'); localStorage.setItem('fmode_me', '{坏掉的json'); const s = make(); expect(s.me()).toBeNull(); expect(s.isLoggedIn()).toBeFalse(); }); it('setMe 单独更新身份(不换 token)', () => { const s = make(); s.setSession('r:abc', ME); s.setMe({ ...ME, name: '改名了' }); expect(s.token()).toBe('r:abc'); expect(s.me()?.name).toBe('改名了'); }); /* * session 过期(2026-11-12)。 * * 密码登录的账号带 `expiresAt`(实测有效期 1 年)。 * 过期后必须能本地判出来,否则用户会看到一堆 401 却不知道原因。 */ describe('过期时间', () => { it('未过期 → 仍算已登录', () => { const s = make(); const future = new Date(Date.now() + 86_400_000).toISOString(); s.setSession('r:abc', ME, future); expect(s.isExpired()).toBeFalse(); expect(s.isLoggedIn()).toBeTrue(); }); it('已过期 → isExpired=true 且 isLoggedIn=false', () => { const s = make(); const past = new Date(Date.now() - 1000).toISOString(); s.setSession('r:abc', ME, past); expect(s.isExpired()).toBeTrue(); expect(s.isLoggedIn()).toBeFalse(); }); it('🔴 不传 expiresAt → 视为不过期(老登录态不能被踢下线)', () => { const s = make(); s.setSession('r:abc', ME); expect(s.expiresAt()).toBeNull(); expect(s.isExpired()).toBeFalse(); expect(s.isLoggedIn()).toBeTrue(); }); it('过期时间跨实例持久化(刷新后仍生效)', () => { const past = new Date(Date.now() - 1000).toISOString(); make().setSession('r:abc', ME, past); expect(make().isExpired()).toBeTrue(); }); it('logout 一并清除过期时间', () => { const s = make(); s.setSession('r:abc', ME, new Date(Date.now() + 86_400_000).toISOString()); s.logout(); expect(s.expiresAt()).toBeNull(); expect(make().isExpired()).toBeFalse(); }); it('格式非法的过期时间 → 不误判为过期', () => { const s = make(); s.setSession('r:abc', ME, 'not-a-date'); expect(s.isExpired()).toBeFalse(); }); }); });