Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/modules/auth/stores/auth.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,28 @@ export const useAuthStore = defineStore('auth', {
},

/**
* @desc Sign out the current user: clear auth state, abilities, and localStorage.
* @desc Sign out the current user: call backend to clear the httpOnly TOKEN cookie,
* then reset auth state, abilities, and localStorage. Backend failures never trap the
* user as logged-in on the client — local reset always runs.
* @returns {Promise<void>}
*/
async signout() {
const api = `${config.api.protocol}://${config.api.host}:${config.api.port}/${config.api.base}`;
Comment thread
PierreBrisorgueil marked this conversation as resolved.
const coreStore = useCoreStore();

// Call backend first so the server can clear the httpOnly TOKEN cookie.
// Swallow any error (older backends may not expose this endpoint, or the
// server may be unreachable) — the local reset below must still run.
// `__isRetryRequest: true` flags this request so the 401 interceptor does
// not re-enter signout() and recurse (see src/lib/services/axios.js).
try {
await axios.post(`${api}/${config.api.endPoints.auth}/signout`, null, {
__isRetryRequest: true,
});
} catch {
// Never trap the user logged-in on backend failure.
}
Comment thread
PierreBrisorgueil marked this conversation as resolved.

this.auth = false;
this.cookieExpire = 0;
this.user = null;
Expand Down
70 changes: 69 additions & 1 deletion src/modules/auth/tests/auth.store.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ describe('Auth Store', () => {
beforeEach(() => {
setActivePinia(createPinia());
localStorage.clear();
// Reset mocks
// Reset mocks — clear axios call history so per-test `toHaveBeenCalledWith`
// assertions don't see calls from previous tests.
axios.post.mockReset();
axios.get.mockReset();
mockUpdateAbilities.mockClear();
// Reset posthog mocks
posthog.__loaded = false;
Expand Down Expand Up @@ -101,6 +104,7 @@ describe('Auth Store', () => {
localStorage.setItem(`${config.cookie.prefix}CookieExpire`, '12345');
localStorage.setItem(`${config.cookie.prefix}LastLoginAt`, '2026-01-01T00:00:00Z');

axios.post.mockResolvedValueOnce({ data: {} });
await authStore.signout();

expect(authStore.auth).toBe(false);
Expand All @@ -111,6 +115,70 @@ describe('Auth Store', () => {
expect(localStorage.getItem(`${config.cookie.prefix}LastLoginAt`)).toBe(null);
});

describe('signout backend call', () => {
it('should call backend signout endpoint with the correct URL', async () => {
const authStore = useAuthStore();
authStore.auth = true;
authStore.user = { id: 'u1' };

axios.post.mockResolvedValueOnce({ data: {} });
await authStore.signout();

expect(axios.post).toHaveBeenCalledWith(
'http://localhost:3000/api/auth/signout',
null,
{ __isRetryRequest: true },
);
});
Comment thread
PierreBrisorgueil marked this conversation as resolved.

it('should clear local state even when backend signout rejects', async () => {
const authStore = useAuthStore();
authStore.auth = true;
authStore.cookieExpire = Date.now() + 1000;
authStore.user = { id: 'u1' };
authStore.pendingRequests = [{ id: 'r1' }];
localStorage.setItem(`${config.cookie.prefix}UserRoles`, 'user');
localStorage.setItem(`${config.cookie.prefix}CookieExpire`, '12345');
localStorage.setItem(`${config.cookie.prefix}LastLoginAt`, '2026-01-01T00:00:00Z');

const backendError = new Error('Backend unreachable');
backendError.response = { status: 500 };
axios.post.mockRejectedValueOnce(backendError);

// Must not throw — signout always resolves so the user is never trapped as logged-in.
await expect(authStore.signout()).resolves.toBeUndefined();

expect(axios.post).toHaveBeenCalledWith(
'http://localhost:3000/api/auth/signout',
null,
{ __isRetryRequest: true },
);
expect(authStore.auth).toBe(false);
expect(authStore.cookieExpire).toBe(0);
expect(authStore.user).toBe(null);
expect(authStore.pendingRequests).toEqual([]);
Comment thread
PierreBrisorgueil marked this conversation as resolved.
expect(localStorage.getItem(`${config.cookie.prefix}UserRoles`)).toBe(null);
expect(localStorage.getItem(`${config.cookie.prefix}CookieExpire`)).toBe(null);
expect(localStorage.getItem(`${config.cookie.prefix}LastLoginAt`)).toBe(null);
expect(mockUpdateAbilities).toHaveBeenCalledWith([]);
});

it('should clear local state when backend signout returns 404 (older backend)', async () => {
const authStore = useAuthStore();
authStore.auth = true;
authStore.user = { id: 'u1' };

const notFoundError = new Error('Not found');
notFoundError.response = { status: 404 };
axios.post.mockRejectedValueOnce(notFoundError);

await authStore.signout();

expect(authStore.auth).toBe(false);
expect(authStore.user).toBe(null);
});
});

it('should have mail state initialized', () => {
const authStore = useAuthStore();
expect(authStore.mail).toEqual({ status: false, message: '' });
Expand Down
Loading