// OAuth sessions record the device (User-Agent) that opened them, so two // sessions of the same app can be told apart in the Apps list. formatUserAgent // renders a short "Browser · OS" label, and listConnectedApps surfaces the // stored value. import { describe, expect, it } from 'vitest'; import { formatUserAgent } from '../packages/account-ui/src/lib/userAgent.js'; import { createAccountHandlers } from '../packages/core/src/handlers/account.js'; describe('formatUserAgent', () => { it('parses common browser + OS pairs', () => { expect( formatUserAgent( 'Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Mobile Safari/537.36', ), ).toBe('Chrome · Android'); expect( formatUserAgent( 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15', ), ).toBe('Safari · macOS'); expect( formatUserAgent( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0', ), ).toBe('Firefox · Windows'); expect( formatUserAgent( 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', ), ).toBe('Safari · iPhone'); }); it('names common non-browser HTTP clients', () => { expect(formatUserAgent('node')).toBe('Node.js'); expect(formatUserAgent('curl/8.7.1')).toBe('curl'); expect(formatUserAgent('okhttp/4.9.0')).toBe('OkHttp'); expect(formatUserAgent('Go-http-client/1.1')).toBe('Go'); expect(formatUserAgent('python-requests/2.32.3')).toBe('Python'); }); it('falls back and handles missing values', () => { expect(formatUserAgent(null)).toBeNull(); expect(formatUserAgent('')).toBeNull(); // A genuinely unrecognized agent keeps its trimmed raw value. expect(formatUserAgent('SomeUnknownBot/1.0')).toBe('SomeUnknownBot/1.0'); }); }); describe('listConnectedApps surfaces the device', () => { it('returns the stored userAgent for a session', async () => { const account = createAccountHandlers( /** @type {any} */ ({ jwtSecret: 'test-secret', sharedStorage: { listOAuthTokensByDid: async () => [ { did: 'did:plc:x', clientId: 'https://client.example/metadata.json', scope: 'atproto', dpopJkt: 'abcdef123456', createdAt: Date.now(), userAgent: 'Mozilla/5.0 … Chrome/150 Mobile', tokenId: 'ref-1', }, ], }, }), ); const apps = await account.listConnectedApps('did:plc:x'); expect(apps).toHaveLength(1); expect(apps[0].userAgent).toBe('Mozilla/5.0 … Chrome/150 Mobile'); }); it('orders sessions by last access and falls back to createdAt', async () => { const account = createAccountHandlers( /** @type {any} */ ({ jwtSecret: 'test-secret', sharedStorage: { listOAuthTokensByDid: async () => [ // Authorized first, but refreshed most recently -> sorts to the top. { did: 'did:plc:x', clientId: 'https://old.example/metadata.json', createdAt: Date.parse('2026-01-01T00:00:00Z'), updatedAt: Date.parse('2026-08-01T00:00:00Z'), tokenId: 'ref-old', }, // Authorized later but never refreshed since. { did: 'did:plc:x', clientId: 'https://new.example/metadata.json', createdAt: Date.parse('2026-07-01T00:00:00Z'), updatedAt: Date.parse('2026-07-01T00:00:00Z'), tokenId: 'ref-new', }, // Legacy token with no updatedAt: createdAt stands in. { did: 'did:plc:x', clientId: 'https://legacy.example/metadata.json', createdAt: Date.parse('2026-06-01T00:00:00Z'), tokenId: 'ref-legacy', }, ], }, }), ); const apps = await account.listConnectedApps('did:plc:x'); expect(apps.map((a) => a.clientId)).toEqual([ 'https://old.example/metadata.json', 'https://new.example/metadata.json', 'https://legacy.example/metadata.json', ]); // authorizedAt tracks first authorization; lastAccessedAt tracks the refresh. expect(apps[0].authorizedAt).toBe('2026-01-01'); expect(apps[0].lastAccessedAt).toBe('2026-08-01T00:00:00.000Z'); // Legacy token with no updatedAt reports createdAt as its last access. expect(apps[2].lastAccessedAt).toBe('2026-06-01T00:00:00.000Z'); }); });