|
|
@@ -121,4 +121,92 @@ describe('AdminSessionService', () => {
|
|
|
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);
|
|
|
+ });
|
|
|
+
|
|
|
});
|