feat: First-Start Create Admin Modal 1.01
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
STORAGE_KEY,
|
||||
readStoredSession,
|
||||
writeStoredSession,
|
||||
clearStoredSession,
|
||||
StoredSession,
|
||||
} from '../../frontend/src/app/core/services/auth.session-storage';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
function makeStorage(initial: Record<string, string> = {}): {
|
||||
store: Record<string, string>;
|
||||
getItem: jest.Mock;
|
||||
setItem: jest.Mock;
|
||||
removeItem: jest.Mock;
|
||||
} {
|
||||
const store = { ...initial };
|
||||
return {
|
||||
store,
|
||||
getItem: jest.fn((k: string) => (k in store ? store[k] : null)),
|
||||
setItem: jest.fn((k: string, v: string) => {
|
||||
store[k] = v;
|
||||
}),
|
||||
removeItem: jest.fn((k: string) => {
|
||||
delete store[k];
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const adminSession: StoredSession = {
|
||||
token: 'tok-1',
|
||||
user: { id: 'u1', username: 'alice', role: 'admin' },
|
||||
};
|
||||
|
||||
describe('auth.session-storage helpers', () => {
|
||||
it('readStoredSession returns null when empty', () => {
|
||||
const s = makeStorage();
|
||||
expect(readStoredSession(s as any)).toBeNull();
|
||||
});
|
||||
|
||||
it('writeStoredSession + readStoredSession round-trip', () => {
|
||||
const s = makeStorage();
|
||||
writeStoredSession(s as any, adminSession);
|
||||
expect(s.setItem).toHaveBeenCalledWith(STORAGE_KEY, JSON.stringify(adminSession));
|
||||
expect(readStoredSession(s as any)).toEqual(adminSession);
|
||||
});
|
||||
|
||||
it('clearStoredSession removes the key', () => {
|
||||
const s = makeStorage({ [STORAGE_KEY]: JSON.stringify(adminSession) });
|
||||
clearStoredSession(s as any);
|
||||
expect(s.removeItem).toHaveBeenCalledWith(STORAGE_KEY);
|
||||
expect(readStoredSession(s as any)).toBeNull();
|
||||
});
|
||||
|
||||
it('readStoredSession returns null for malformed JSON', () => {
|
||||
const s = makeStorage({ [STORAGE_KEY]: 'not-json' });
|
||||
expect(readStoredSession(s as any)).toBeNull();
|
||||
});
|
||||
|
||||
it('readStoredSession returns null for shape mismatch', () => {
|
||||
const s = makeStorage({ [STORAGE_KEY]: JSON.stringify({ foo: 'bar' }) });
|
||||
expect(readStoredSession(s as any)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreSession refresh flow (pure)', () => {
|
||||
it('successful refresh updates state and persists new token', async () => {
|
||||
const stored = makeStorage({ [STORAGE_KEY]: JSON.stringify(adminSession) });
|
||||
const restored = readStoredSession(stored as any);
|
||||
expect(restored).toEqual(adminSession);
|
||||
|
||||
const httpPost = jest.fn();
|
||||
|
||||
const response = await new Promise<{ accessToken: string; user: any }>((resolve, reject) => {
|
||||
const obs = of({ accessToken: 'fresh-tok', expiresIn: 900, user: adminSession.user });
|
||||
httpPost.mockReturnValueOnce(obs);
|
||||
(httpPost as any)('/api/v1/auth/refresh', {}, { withCredentials: true }).subscribe({
|
||||
next: (r: unknown) => resolve(r as any),
|
||||
error: (e: unknown) => reject(e),
|
||||
});
|
||||
});
|
||||
|
||||
expect(httpPost).toHaveBeenCalledWith(
|
||||
'/api/v1/auth/refresh',
|
||||
{},
|
||||
{ withCredentials: true },
|
||||
);
|
||||
expect(response.accessToken).toBe('fresh-tok');
|
||||
|
||||
writeStoredSession(stored as any, {
|
||||
token: response.accessToken,
|
||||
user: response.user,
|
||||
});
|
||||
expect(stored.store[STORAGE_KEY]).toContain('fresh-tok');
|
||||
});
|
||||
|
||||
it('failed refresh triggers clearStoredSession', async () => {
|
||||
const stored = makeStorage({ [STORAGE_KEY]: JSON.stringify(adminSession) });
|
||||
const httpPost = jest.fn();
|
||||
|
||||
let errored = false;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
httpPost.mockReturnValueOnce(throwError(() => ({ status: 401 })));
|
||||
(httpPost as any)('/api/v1/auth/refresh', {}, { withCredentials: true }).subscribe({
|
||||
next: () => resolve(undefined),
|
||||
error: (e: unknown) => reject(e),
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
errored = true;
|
||||
}
|
||||
expect(errored).toBe(true);
|
||||
|
||||
clearStoredSession(stored as any);
|
||||
expect(stored.removeItem).toHaveBeenCalledWith(STORAGE_KEY);
|
||||
expect(readStoredSession(stored as any)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { decideAuthRedirect } from '../../frontend/src/app/core/guards/auth.guard.decision';
|
||||
|
||||
function makeRouter() {
|
||||
return {
|
||||
createUrlTree: jest.fn((cmds: string[]) => ({ __urlTree: true, cmds })),
|
||||
};
|
||||
}
|
||||
|
||||
describe('decideAuthRedirect (race-fix core)', () => {
|
||||
it('redirects to /bootstrap while bootstrap is uninitialized', () => {
|
||||
const router = makeRouter();
|
||||
const result = decideAuthRedirect(
|
||||
{ initialized: false, isAuthenticated: false },
|
||||
router as any,
|
||||
);
|
||||
expect(router.createUrlTree).toHaveBeenCalledWith(['/bootstrap']);
|
||||
expect((result as any).__urlTree).toBe(true);
|
||||
});
|
||||
|
||||
it('redirects to /login when initialized but not authenticated', () => {
|
||||
const router = makeRouter();
|
||||
const result = decideAuthRedirect(
|
||||
{ initialized: true, isAuthenticated: false },
|
||||
router as any,
|
||||
);
|
||||
expect(router.createUrlTree).toHaveBeenCalledWith(['/login']);
|
||||
expect((result as any).__urlTree).toBe(true);
|
||||
});
|
||||
|
||||
it('allows navigation when initialized AND authenticated (no /bootstrap redirect)', () => {
|
||||
const router = makeRouter();
|
||||
const result = decideAuthRedirect(
|
||||
{ initialized: true, isAuthenticated: true },
|
||||
router as any,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
expect(router.createUrlTree).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user