@startsimpli/auth 0.4.25 → 0.4.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -123,3 +123,55 @@ describe('AuthClient.login', () => {
|
|
|
123
123
|
expect(session.accessToken).toBe(VALID_TOKEN);
|
|
124
124
|
});
|
|
125
125
|
});
|
|
126
|
+
|
|
127
|
+
// Regression coverage for startsim-768w.2.3: a dead / wrong-audience refresh
|
|
128
|
+
// cookie must end the session ONCE (clear + signal expiry, so the app bounces
|
|
129
|
+
// to login) rather than spin in a refresh loop — while a transient backend
|
|
130
|
+
// flake (5xx / network) must NOT log the user out. This session-dead-vs-transient
|
|
131
|
+
// split in performTokenRefresh is the loop-prevention; pin it so a refactor
|
|
132
|
+
// can't reintroduce either the auth loop or premature logout.
|
|
133
|
+
describe('AuthClient.refreshToken — session-dead vs transient (768w.2.3)', () => {
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
vi.restoreAllMocks();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
function clientWithSession(onSessionExpired: () => void) {
|
|
139
|
+
const client = new AuthClient({ apiBaseUrl: 'http://localhost:8001', onSessionExpired });
|
|
140
|
+
(client as any).session = {
|
|
141
|
+
user: { id: 'u', email: 'e@x.com', firstName: '', lastName: '', isEmailVerified: false, createdAt: '', updatedAt: '' },
|
|
142
|
+
accessToken: VALID_TOKEN,
|
|
143
|
+
expiresAt: Date.now() + 3600000,
|
|
144
|
+
};
|
|
145
|
+
return client;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
it('clears the session + signals expiry on a 401 refresh (dead/wrong-aud cookie → no loop)', async () => {
|
|
149
|
+
const onSessionExpired = vi.fn();
|
|
150
|
+
const client = clientWithSession(onSessionExpired);
|
|
151
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce({ status: 401, ok: false }));
|
|
152
|
+
|
|
153
|
+
await expect(client.refreshToken()).rejects.toThrow(/session expired/i);
|
|
154
|
+
expect(onSessionExpired).toHaveBeenCalledTimes(1);
|
|
155
|
+
expect((client as any).session).toBeFalsy();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('does NOT clear the session on a 5xx refresh (transient backend flake → retry, no logout)', async () => {
|
|
159
|
+
const onSessionExpired = vi.fn();
|
|
160
|
+
const client = clientWithSession(onSessionExpired);
|
|
161
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce({ status: 503, ok: false }));
|
|
162
|
+
|
|
163
|
+
await expect(client.refreshToken()).rejects.toThrow(/transient/i);
|
|
164
|
+
expect(onSessionExpired).not.toHaveBeenCalled();
|
|
165
|
+
expect((client as any).session).toBeTruthy();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('does NOT clear the session on a network error (unreachable backend)', async () => {
|
|
169
|
+
const onSessionExpired = vi.fn();
|
|
170
|
+
const client = clientWithSession(onSessionExpired);
|
|
171
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValueOnce(new Error('ECONNREFUSED')));
|
|
172
|
+
|
|
173
|
+
await expect(client.refreshToken()).rejects.toThrow(/transient/i);
|
|
174
|
+
expect(onSessionExpired).not.toHaveBeenCalled();
|
|
175
|
+
expect((client as any).session).toBeTruthy();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -45,7 +45,7 @@ function hasLoggedOutFlag(): boolean {
|
|
|
45
45
|
export class AuthClient implements AuthBackend {
|
|
46
46
|
private config: Required<AuthConfig>;
|
|
47
47
|
private session: Session | null = null;
|
|
48
|
-
private refreshTimer:
|
|
48
|
+
private refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
49
49
|
private isRefreshing = false;
|
|
50
50
|
private refreshPromise: Promise<string> | null = null;
|
|
51
51
|
|