2026-08-12 14:02:25 +03:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { createTestClient } from './helpers/createClient.js';
|
|
|
|
|
import { RudderNetworkError, RudderAuthError } from '../src/client/RudderError.js';
|
|
|
|
|
|
|
|
|
|
function jsonResponse(body: unknown): Response {
|
|
|
|
|
return new Response(JSON.stringify(body), { status: 200 });
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 12:08:03 +03:00
|
|
|
function stubKernelFetch(
|
|
|
|
|
extra?: (url: string) => Response | undefined,
|
|
|
|
|
): ReturnType<typeof vi.fn> {
|
|
|
|
|
return vi.fn().mockImplementation((url: string) => {
|
|
|
|
|
const custom = extra?.(url);
|
|
|
|
|
if (custom) return Promise.resolve(custom);
|
|
|
|
|
if (String(url).includes('/sdk/v1/authorization/device')) {
|
|
|
|
|
return Promise.resolve(jsonResponse({
|
|
|
|
|
accessToken: 'access-token-123',
|
|
|
|
|
refreshToken: 'refresh-token-456',
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
if (String(url).includes('/sdk/v1/catalog')) {
|
|
|
|
|
return Promise.resolve(jsonResponse({ currencies: [] }));
|
|
|
|
|
}
|
|
|
|
|
if (String(url).includes('/sdk/v1/sync')) {
|
|
|
|
|
return Promise.resolve(jsonResponse({}));
|
|
|
|
|
}
|
|
|
|
|
return Promise.resolve(jsonResponse({ player: null, wallets: [] }));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 14:02:25 +03:00
|
|
|
describe('AuthService', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.stubGlobal('crypto', {
|
|
|
|
|
randomUUID: () => '00000000-0000-4000-a000-000000000001',
|
|
|
|
|
});
|
|
|
|
|
localStorage.clear();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('loginWithDevice sends correct request body', async () => {
|
|
|
|
|
const client = createTestClient();
|
2026-09-17 12:08:03 +03:00
|
|
|
const fetchMock = stubKernelFetch();
|
2026-08-12 14:02:25 +03:00
|
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
|
|
|
|
|
|
const response = await client.auth.loginWithDevice({ region: 'us', language: 'en' });
|
|
|
|
|
|
|
|
|
|
const [url, init] = fetchMock.mock.calls[0];
|
|
|
|
|
expect(url).toContain('/sdk/v1/authorization/device');
|
|
|
|
|
const body = JSON.parse(init.body);
|
|
|
|
|
expect(body.key).toBe('test-project-key');
|
|
|
|
|
expect(body.deviceId).toBe('00000000-0000-4000-a000-000000000001');
|
|
|
|
|
expect(body.region).toBe('us');
|
|
|
|
|
expect(body.language).toBe('en');
|
|
|
|
|
expect(response.accessToken).toBe('access-token-123');
|
|
|
|
|
expect(response.refreshToken).toBe('refresh-token-456');
|
2026-09-17 12:08:03 +03:00
|
|
|
const paths = fetchMock.mock.calls.map(([callUrl]) => new URL(String(callUrl)).pathname);
|
|
|
|
|
expect(paths[0]).toBe('/sdk/v1/authorization/device');
|
|
|
|
|
expect(paths).toContain('/sdk/v1/player/information');
|
|
|
|
|
expect(paths).toContain('/sdk/v1/catalog');
|
|
|
|
|
expect(paths).toContain('/sdk/v1/sync');
|
|
|
|
|
expect(paths).not.toContain('/sdk/v1/remote-configs');
|
|
|
|
|
expect(paths).not.toContain('/sdk/v1/stores');
|
|
|
|
|
expect(paths).not.toContain('/sdk/v1/scenarios/pending');
|
|
|
|
|
expect(paths).not.toContain('/sdk/v1/scenarios/trigger');
|
2026-08-12 14:02:25 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('loginWithDevice saves tokens on success', async () => {
|
|
|
|
|
const client = createTestClient();
|
2026-09-17 12:08:03 +03:00
|
|
|
vi.stubGlobal('fetch', stubKernelFetch((url) => {
|
|
|
|
|
if (url.includes('/sdk/v1/authorization/device')) {
|
|
|
|
|
return jsonResponse({ accessToken: 'at', refreshToken: 'rt' });
|
2026-08-12 14:02:25 +03:00
|
|
|
}
|
2026-09-17 12:08:03 +03:00
|
|
|
return undefined;
|
2026-08-12 14:02:25 +03:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
await client.auth.loginWithDevice();
|
|
|
|
|
expect(client.options.tokenStore.getAccessToken()).toBe('at');
|
|
|
|
|
expect(client.options.tokenStore.getRefreshToken()).toBe('rt');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('loginWithDevice throws RudderNetworkError on network failure', async () => {
|
|
|
|
|
const client = createTestClient();
|
|
|
|
|
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Offline')));
|
|
|
|
|
await expect(client.auth.loginWithDevice()).rejects.toThrow(RudderNetworkError);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('loginWithDevice throws RudderAuthError on 401', async () => {
|
|
|
|
|
const client = createTestClient();
|
|
|
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(
|
|
|
|
|
new Response('unauthorized', { status: 401, statusText: 'Unauthorized' }),
|
|
|
|
|
));
|
|
|
|
|
await expect(client.auth.loginWithDevice()).rejects.toThrow(RudderAuthError);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('logout clears tokens', () => {
|
|
|
|
|
const client = createTestClient();
|
|
|
|
|
client.options.tokenStore.saveTokens('at', 'rt');
|
|
|
|
|
client.auth.logout();
|
|
|
|
|
expect(client.options.tokenStore.getAccessToken()).toBeNull();
|
|
|
|
|
expect(client.options.tokenStore.getRefreshToken()).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('loginWithDevice sends token in Authorization header after login', async () => {
|
|
|
|
|
const client = createTestClient();
|
2026-09-17 12:08:03 +03:00
|
|
|
const fetchMock = stubKernelFetch((url) => {
|
2026-08-12 14:02:25 +03:00
|
|
|
if (url.includes('/sdk/v1/authorization/device')) {
|
2026-09-17 12:08:03 +03:00
|
|
|
return jsonResponse({ accessToken: 'at2', refreshToken: 'rt2' });
|
2026-08-12 14:02:25 +03:00
|
|
|
}
|
2026-09-17 12:08:03 +03:00
|
|
|
return undefined;
|
2026-08-12 14:02:25 +03:00
|
|
|
});
|
|
|
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
|
|
|
|
|
|
await client.auth.loginWithDevice();
|
|
|
|
|
|
|
|
|
|
const response = await client.player.reload();
|
|
|
|
|
expect(response).toEqual({ player: null, wallets: [] });
|
|
|
|
|
const playerCall = fetchMock.mock.calls.find(([url]) =>
|
|
|
|
|
String(url).includes('/sdk/v1/player/information'),
|
|
|
|
|
);
|
|
|
|
|
const headers = playerCall?.[1].headers;
|
|
|
|
|
expect(headers['Authorization']).toBe('Bearer at2');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('loginWithDevice defaults to region global and language en', async () => {
|
|
|
|
|
const client = createTestClient();
|
2026-09-17 12:08:03 +03:00
|
|
|
const fetchMock = stubKernelFetch();
|
2026-08-12 14:02:25 +03:00
|
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
|
|
|
|
|
|
await client.auth.loginWithDevice();
|
|
|
|
|
const [, init] = fetchMock.mock.calls[0];
|
|
|
|
|
const body = JSON.parse(init.body);
|
|
|
|
|
expect(body.region).toBe('global');
|
|
|
|
|
expect(body.language).toBe('en');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('auth state', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.stubGlobal('crypto', {
|
|
|
|
|
randomUUID: () => '00000000-0000-4000-a000-000000000001',
|
|
|
|
|
});
|
|
|
|
|
localStorage.clear();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('onAuthStateChange fires immediately, then on login and logout', async () => {
|
|
|
|
|
const client = createTestClient();
|
2026-09-17 12:08:03 +03:00
|
|
|
vi.stubGlobal('fetch', stubKernelFetch());
|
2026-08-12 14:02:25 +03:00
|
|
|
|
|
|
|
|
const states: string[] = [];
|
|
|
|
|
const unsubscribe = client.auth.onAuthStateChange((state) => states.push(state));
|
|
|
|
|
expect(states).toEqual(['signed-out']);
|
|
|
|
|
expect(client.auth.isAuthenticated).toBe(false);
|
|
|
|
|
|
|
|
|
|
await client.auth.loginWithDevice();
|
|
|
|
|
expect(states).toEqual(['signed-out', 'signed-in']);
|
|
|
|
|
expect(client.auth.isAuthenticated).toBe(true);
|
|
|
|
|
|
|
|
|
|
client.auth.logout();
|
|
|
|
|
expect(states).toEqual(['signed-out', 'signed-in', 'signed-out']);
|
|
|
|
|
expect(client.auth.isAuthenticated).toBe(false);
|
|
|
|
|
|
|
|
|
|
unsubscribe();
|
|
|
|
|
client.auth.logout();
|
|
|
|
|
expect(states).toHaveLength(3);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('starts signed-in when the token store already holds a token', () => {
|
|
|
|
|
const client = createTestClient();
|
|
|
|
|
client.options.tokenStore.saveTokens('at', 'rt');
|
|
|
|
|
const states: string[] = [];
|
|
|
|
|
client.auth.onAuthStateChange((state) => states.push(state));
|
|
|
|
|
expect(states).toEqual(['signed-in']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('emits signed-out when the transport session expires (refresh fails)', async () => {
|
|
|
|
|
const client = createTestClient();
|
|
|
|
|
client.options.tokenStore.saveTokens('expired-at', 'expired-rt');
|
|
|
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(
|
|
|
|
|
new Response('unauthorized', { status: 401, statusText: 'Unauthorized' }),
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
const states: string[] = [];
|
|
|
|
|
client.auth.onAuthStateChange((state) => states.push(state));
|
|
|
|
|
expect(states).toEqual(['signed-in']);
|
|
|
|
|
|
|
|
|
|
await expect(client.player.reload()).rejects.toThrow(RudderAuthError);
|
|
|
|
|
expect(states).toEqual(['signed-in', 'signed-out']);
|
|
|
|
|
expect(client.auth.isAuthenticated).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|