session.service.spec.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import { TestBed } from '@angular/core/testing';
  2. import { SessionService } from './session.service';
  3. import { Me } from './api/models';
  4. /**
  5. * 会话状态(登录态)—— 全项目唯一持有者。
  6. *
  7. * 这里出错影响最大:登录态判断错了,整个 App 都会显示异常。
  8. * 重点回归:
  9. * ① 持久化与恢复(刷新后仍登录)
  10. * ② logout 必须清干净(含旧版兼容键)
  11. * ③ localStorage 不可用时降级为内存态,不崩
  12. */
  13. describe('SessionService', () => {
  14. const ME: Me = { id: 'u1', name: '用户9836', mobile: '19170119836' };
  15. beforeEach(() => localStorage.clear());
  16. afterEach(() => localStorage.clear());
  17. function make(): SessionService {
  18. TestBed.resetTestingModule();
  19. TestBed.configureTestingModule({ providers: [SessionService] });
  20. return TestBed.inject(SessionService);
  21. }
  22. it('初始态未登录', () => {
  23. const s = make();
  24. expect(s.isLoggedIn()).toBeFalse();
  25. expect(s.token()).toBeNull();
  26. });
  27. it('setSession 后 isLoggedIn 为真,且写入 localStorage', () => {
  28. const s = make();
  29. s.setSession('r:abc', ME);
  30. expect(s.isLoggedIn()).toBeTrue();
  31. expect(localStorage.getItem('fmode_session_token')).toBe('r:abc');
  32. expect(JSON.parse(localStorage.getItem('fmode_me')!)).toEqual(ME);
  33. });
  34. it('🔴 刷新后能恢复登录态(新实例读同一份 localStorage)', () => {
  35. make().setSession('r:abc', ME);
  36. const s2 = make(); // 模拟刷新后重新创建
  37. expect(s2.isLoggedIn()).toBeTrue();
  38. expect(s2.token()).toBe('r:abc');
  39. expect(s2.me()?.name).toBe('用户9836');
  40. });
  41. it('🔴 logout 必须清干净(token / me / localStorage)', () => {
  42. const s = make();
  43. s.setSession('r:abc', ME);
  44. s.logout();
  45. expect(s.token()).toBeNull();
  46. expect(s.me()).toBeNull();
  47. expect(s.isLoggedIn()).toBeFalse();
  48. expect(localStorage.getItem('fmode_session_token')).toBeNull();
  49. expect(localStorage.getItem('fmode_me')).toBeNull();
  50. });
  51. it('只有 token 没有 me → 仍视为未登录', () => {
  52. localStorage.setItem('fmode_session_token', 'r:abc');
  53. const s = make();
  54. expect(s.token()).toBe('r:abc');
  55. expect(s.isLoggedIn()).toBeFalse(); // 缺 me 不算登录
  56. });
  57. it('me 的 JSON 损坏 → 不崩,视为未登录', () => {
  58. localStorage.setItem('fmode_session_token', 'r:abc');
  59. localStorage.setItem('fmode_me', '{坏掉的json');
  60. const s = make();
  61. expect(s.me()).toBeNull();
  62. expect(s.isLoggedIn()).toBeFalse();
  63. });
  64. it('setMe 单独更新身份(不换 token)', () => {
  65. const s = make();
  66. s.setSession('r:abc', ME);
  67. s.setMe({ ...ME, name: '改名了' });
  68. expect(s.token()).toBe('r:abc');
  69. expect(s.me()?.name).toBe('改名了');
  70. });
  71. /*
  72. * session 过期(2026-11-12)。
  73. *
  74. * 密码登录的账号带 `expiresAt`(实测有效期 1 年)。
  75. * 过期后必须能本地判出来,否则用户会看到一堆 401 却不知道原因。
  76. */
  77. describe('过期时间', () => {
  78. it('未过期 → 仍算已登录', () => {
  79. const s = make();
  80. const future = new Date(Date.now() + 86_400_000).toISOString();
  81. s.setSession('r:abc', ME, future);
  82. expect(s.isExpired()).toBeFalse();
  83. expect(s.isLoggedIn()).toBeTrue();
  84. });
  85. it('已过期 → isExpired=true 且 isLoggedIn=false', () => {
  86. const s = make();
  87. const past = new Date(Date.now() - 1000).toISOString();
  88. s.setSession('r:abc', ME, past);
  89. expect(s.isExpired()).toBeTrue();
  90. expect(s.isLoggedIn()).toBeFalse();
  91. });
  92. it('🔴 不传 expiresAt → 视为不过期(老登录态不能被踢下线)', () => {
  93. const s = make();
  94. s.setSession('r:abc', ME);
  95. expect(s.expiresAt()).toBeNull();
  96. expect(s.isExpired()).toBeFalse();
  97. expect(s.isLoggedIn()).toBeTrue();
  98. });
  99. it('过期时间跨实例持久化(刷新后仍生效)', () => {
  100. const past = new Date(Date.now() - 1000).toISOString();
  101. make().setSession('r:abc', ME, past);
  102. expect(make().isExpired()).toBeTrue();
  103. });
  104. it('logout 一并清除过期时间', () => {
  105. const s = make();
  106. s.setSession('r:abc', ME, new Date(Date.now() + 86_400_000).toISOString());
  107. s.logout();
  108. expect(s.expiresAt()).toBeNull();
  109. expect(make().isExpired()).toBeFalse();
  110. });
  111. it('格式非法的过期时间 → 不误判为过期', () => {
  112. const s = make();
  113. s.setSession('r:abc', ME, 'not-a-date');
  114. expect(s.isExpired()).toBeFalse();
  115. });
  116. });
  117. });