| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- import { provideHttpClient } from '@angular/common/http';
- import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
- import { TestBed } from '@angular/core/testing';
- import { Router } from '@angular/router';
- import { ADMIN_PARSE_URL, ADMIN_SESSION_KEY, CLOUD_FUNCTION_BASE_URL } from './admin.constants';
- import { AdminSessionService } from './admin-session.service';
- describe('AdminSessionService', () => {
- let http: HttpTestingController;
- let router: jasmine.SpyObj<Router>;
- beforeEach(() => {
- sessionStorage.clear();
- localStorage.removeItem(ADMIN_SESSION_KEY);
- router = jasmine.createSpyObj<Router>('Router', ['navigate']);
- TestBed.configureTestingModule({
- providers: [provideHttpClient(), provideHttpClientTesting(), AdminSessionService, { provide: Router, useValue: router }],
- });
- http = TestBed.inject(HttpTestingController);
- });
- afterEach(() => {
- http.verify();
- sessionStorage.clear();
- localStorage.removeItem(ADMIN_SESSION_KEY);
- });
- it('logs administrators in through the app cloud gateway', async () => {
- const service = TestBed.inject(AdminSessionService);
- const result = service.login(' admin ', 'secret-pass');
- const request = http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/app/gateway`);
- expect(request.request.body).toEqual({ params: { action: 'user_login_passwd', name: 'admin', passwd: 'secret-pass' } });
- request.flush({ retcode: 0, result: { objectId: 'admin-1', userName: 'admin', honeyName: '管理员', sessionToken: 'r:admin-token' } });
- await expectAsync(result).toBeResolvedTo(jasmine.objectContaining({ objectId: 'admin-1', username: 'admin', displayName: '管理员', sessionToken: 'r:admin-token' }));
- expect(JSON.parse(sessionStorage.getItem(ADMIN_SESSION_KEY) || '{}').sessionToken).toBe('r:admin-token');
- expect(JSON.parse(localStorage.getItem(ADMIN_SESSION_KEY) || '{}').sessionToken).toBe('r:admin-token');
- });
- it('revokes the Parse session through the admin cloud function on logout', () => {
- sessionStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-1', username: 'root', sessionToken: 'r:admin-token' }));
- const service = TestBed.inject(AdminSessionService);
- service.logout(false);
- const request = http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/admin/gateway`);
- expect(request.request.body).toEqual({ token: 'r:admin-token', params: { operation: 'logout', sessionToken: 'r:admin-token' } });
- expect(service.session()).toBeNull();
- expect(sessionStorage.getItem(ADMIN_SESSION_KEY)).toBeNull();
- request.flush({ success: true, data: { revoked: true } });
- });
- it('restores a login after the previous tab was closed and mirrors it for route caches', () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({objectId:'admin-1',sessionToken:'saved'}));
- const service=TestBed.inject(AdminSessionService);
- expect(service.session()?.sessionToken).toBe('saved');
- expect(JSON.parse(sessionStorage.getItem(ADMIN_SESSION_KEY)!).sessionToken).toBe('saved');
- });
- it('migrates an existing tab login without requiring another password', () => {
- sessionStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({objectId:'admin-1',sessionToken:'legacy'}));
- expect(TestBed.inject(AdminSessionService).session()?.sessionToken).toBe('legacy');
- expect(JSON.parse(localStorage.getItem(ADMIN_SESSION_KEY)!).sessionToken).toBe('legacy');
- });
- it('ignores failure from an older session and never revokes the new one', () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({objectId:'admin-1',sessionToken:'new'}));
- const service=TestBed.inject(AdminSessionService);
- service.invalidateSession('old');
- expect(service.session()?.sessionToken).toBe('new');
- expect(router.navigate).not.toHaveBeenCalled();
- http.expectNone(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/admin/gateway`);
- });
- it('clears a confirmed expired session once, without sending a logout request', () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({objectId:'admin-1',sessionToken:'expired'}));
- const service=TestBed.inject(AdminSessionService);
- service.invalidateSession('expired');service.invalidateSession('expired');
- expect(service.session()).toBeNull();expect(localStorage.getItem(ADMIN_SESSION_KEY)).toBeNull();
- expect(sessionStorage.getItem(ADMIN_SESSION_KEY)).toBeNull();expect(router.navigate).toHaveBeenCalledTimes(1);
- http.expectNone(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/admin/gateway`);
- });
- it('shares logout across tabs without another server logout call', () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({objectId:'admin-1',sessionToken:'shared'}));
- const service=TestBed.inject(AdminSessionService);
- localStorage.removeItem(ADMIN_SESSION_KEY);
- window.dispatchEvent(new StorageEvent('storage',{key:ADMIN_SESSION_KEY,newValue:null,storageArea:localStorage}));
- expect(service.session()).toBeNull();expect(sessionStorage.getItem(ADMIN_SESSION_KEY)).toBeNull();
- expect(router.navigate).toHaveBeenCalled();
- });
- it('deduplicates gateway-failure checks and retains a session that Parse confirms is valid', async () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({objectId:'admin-1',sessionToken:'valid'}));
- const service=TestBed.inject(AdminSessionService);
- const a=service.verifySessionAfterFailure('valid'),b=service.verifySessionAfterFailure('valid');
- expect(a).toBe(b);
- const req=http.expectOne(`${ADMIN_PARSE_URL}/users/me`);
- expect(req.request.headers.get('X-Parse-Session-Token')).toBe('valid');req.flush({objectId:'admin-1'});
- await a;expect(service.isAuthenticated()).toBeTrue();expect(router.navigate).not.toHaveBeenCalled();
- });
- it('requires re-login after Parse confirms an invalid token', async () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({objectId:'admin-1',sessionToken:'expired'}));
- const service=TestBed.inject(AdminSessionService),pending=service.verifySessionAfterFailure('expired');
- http.expectOne(`${ADMIN_PARSE_URL}/users/me`).flush({code:209,error:'invalid session token'},{status:400,statusText:'Bad Request'});
- await pending;expect(service.isAuthenticated()).toBeFalse();expect(router.navigate).toHaveBeenCalled();
- });
- it('keeps the login when session verification has a network failure', async () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({objectId:'admin-1',sessionToken:'valid'}));
- const service=TestBed.inject(AdminSessionService),pending=service.verifySessionAfterFailure('valid');
- http.expectOne(`${ADMIN_PARSE_URL}/users/me`).error(new ProgressEvent('network'));
- await pending;expect(service.isAuthenticated()).toBeTrue();
- });
- it('does not apply stale identity metadata to a different account',()=>{
- localStorage.setItem(ADMIN_SESSION_KEY,JSON.stringify({objectId:'new-admin',sessionToken:'new'}));
- const service=TestBed.inject(AdminSessionService);
- service.applyIdentity({objectId:'old-admin',username:'old',displayName:'old',roles:['super-admin'],isSuperAdmin:true,operationsRole:'ops-manager'});
- expect(service.session()?.objectId).toBe('new-admin');expect(service.session()?.isSuperAdmin).toBeUndefined();
- });
- it('keeps the login when the verification endpoint returns an unclassified 401', async () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-1', sessionToken: 'valid' }));
- const service = TestBed.inject(AdminSessionService);
- const pending = service.verifySessionAfterFailure('valid');
- http.expectOne(`${ADMIN_PARSE_URL}/users/me`).flush({ message: 'Unauthorized' }, { status: 401, statusText: 'Unauthorized' });
- await pending;
- expect(service.isAuthenticated()).toBeTrue();
- expect(localStorage.getItem(ADMIN_SESSION_KEY)).not.toBeNull();
- expect(router.navigate).not.toHaveBeenCalled();
- });
- it('keeps the login when verification returns an unexpected success body', async () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-1', sessionToken: 'valid' }));
- const service = TestBed.inject(AdminSessionService);
- const pending = service.verifySessionAfterFailure('valid');
- http.expectOne(`${ADMIN_PARSE_URL}/users/me`).flush({ message: 'upstream unavailable' });
- await pending;
- expect(service.isAuthenticated()).toBeTrue();
- expect(router.navigate).not.toHaveBeenCalled();
- });
- it('does not clear a newer shared login before its storage event arrives', async () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-1', sessionToken: 'old' }));
- const service = TestBed.inject(AdminSessionService);
- const pending = service.verifySessionAfterFailure('old');
- // Another tab logs in while this request is in flight, before storage events are delivered.
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-1', sessionToken: 'new' }));
- http.expectOne(`${ADMIN_PARSE_URL}/users/me`).flush({ code: 209, error: 'invalid session token' }, { status: 400, statusText: 'Bad Request' });
- await pending;
- expect(service.session()?.sessionToken).toBe('new');
- expect(JSON.parse(localStorage.getItem(ADMIN_SESSION_KEY)!).sessionToken).toBe('new');
- expect(JSON.parse(sessionStorage.getItem(ADMIN_SESSION_KEY)!).sessionToken).toBe('new');
- expect(router.navigate).not.toHaveBeenCalled();
- });
- it('reads the latest shared login instead of replaying a queued logout event', () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-1', sessionToken: 'old' }));
- const service = TestBed.inject(AdminSessionService);
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-1', sessionToken: 'new' }));
- window.dispatchEvent(new StorageEvent('storage', { key: ADMIN_SESSION_KEY, newValue: null, storageArea: localStorage }));
- expect(service.session()?.sessionToken).toBe('new');
- expect(router.navigate).not.toHaveBeenCalled();
- });
- it('does not overwrite another tabs login with a late identity response', () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-1', sessionToken: 'old' }));
- const service = TestBed.inject(AdminSessionService);
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-2', sessionToken: 'new' }));
- service.applyIdentity({ objectId: 'admin-1', username: 'old', displayName: 'old', roles: ['super-admin'], isSuperAdmin: true, operationsRole: 'ops-manager' });
- expect(service.session()?.objectId).toBe('admin-2');
- expect(JSON.parse(localStorage.getItem(ADMIN_SESSION_KEY)!).sessionToken).toBe('new');
- });
- it('still rejects a verified account mismatch', async () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-1', sessionToken: 'invalid' }));
- const service = TestBed.inject(AdminSessionService);
- const pending = service.verifySessionAfterFailure('invalid');
- http.expectOne(`${ADMIN_PARSE_URL}/users/me`).flush({ objectId: 'different-admin' });
- await pending;
- expect(service.isAuthenticated()).toBeFalse();
- expect(router.navigate).toHaveBeenCalled();
- });
- it('preserves a tab login when persistent writes are unavailable', async () => {
- const originalSetItem = Storage.prototype.setItem;
- spyOn(Storage.prototype, 'setItem').and.callFake(function(this: Storage, key: string, value: string) {
- if (this === localStorage) throw new DOMException('Storage full', 'QuotaExceededError');
- originalSetItem.call(this, key, value);
- });
- sessionStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-1', sessionToken: 'valid' }));
- const service = TestBed.inject(AdminSessionService);
- const pending = service.verifySessionAfterFailure('valid');
- http.expectOne(`${ADMIN_PARSE_URL}/users/me`).flush({ objectId: 'admin-1' });
- await pending;
- expect(service.session()?.sessionToken).toBe('valid');
- expect(router.navigate).not.toHaveBeenCalled();
- });
- it('still rejects a verified disabled account', async () => {
- localStorage.setItem(ADMIN_SESSION_KEY, JSON.stringify({ objectId: 'admin-1', sessionToken: 'disabled' }));
- const service = TestBed.inject(AdminSessionService);
- const pending = service.verifySessionAfterFailure('disabled');
- http.expectOne(`${ADMIN_PARSE_URL}/users/me`).flush({ objectId: 'admin-1', isDisabled: true });
- await pending;
- expect(service.isAuthenticated()).toBeFalse();
- expect(router.navigate).toHaveBeenCalledTimes(1);
- });
- });
|