From cf74c868c0f94beed3a7f4f45145921aa5374499 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Sat, 14 Feb 2026 18:39:47 -0500 Subject: [PATCH] test: add focused tests for session sharing utilities and API Co-Authored-By: Claude Opus 4.6 --- frontend/src/api/sessions.test.ts | 72 +++++++++++++++++++++++ frontend/src/lib/sessionShare.test.ts | 85 +++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 frontend/src/api/sessions.test.ts create mode 100644 frontend/src/lib/sessionShare.test.ts diff --git a/frontend/src/api/sessions.test.ts b/frontend/src/api/sessions.test.ts new file mode 100644 index 00000000..b1cb0e12 --- /dev/null +++ b/frontend/src/api/sessions.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { sessionsApi } from './sessions' +import apiClient from './client' + +vi.mock('./client', () => ({ + default: { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + patch: vi.fn(), + }, +})) + +const mockClient = apiClient as unknown as { + get: ReturnType + post: ReturnType + delete: ReturnType +} + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('sessionsApi sharing methods', () => { + it('createShare hits POST /sessions/{id}/shares with correct payload', async () => { + const mockShare = { id: 'share-1', share_token: 'tok-123', visibility: 'public' } + mockClient.post.mockResolvedValue({ data: mockShare }) + + const payload = { visibility: 'public' as const, share_name: 'My Share' } + const result = await sessionsApi.createShare('session-42', payload) + + expect(mockClient.post).toHaveBeenCalledWith('/sessions/session-42/shares', payload) + expect(result).toEqual(mockShare) + }) + + it('listMyShares hits GET /shares/my-shares', async () => { + const mockShares = [ + { id: 'share-1', share_token: 'tok-1' }, + { id: 'share-2', share_token: 'tok-2' }, + ] + mockClient.get.mockResolvedValue({ data: mockShares }) + + const result = await sessionsApi.listMyShares() + + expect(mockClient.get).toHaveBeenCalledWith('/shares/my-shares') + expect(result).toEqual(mockShares) + }) + + it('revokeShare hits DELETE /shares/{id}', async () => { + mockClient.delete.mockResolvedValue({}) + + await sessionsApi.revokeShare('share-99') + + expect(mockClient.delete).toHaveBeenCalledWith('/shares/share-99') + }) + + it('getSharedSession hits GET /share/{token}', async () => { + const mockView = { + session_id: 'sess-1', + tree_name: 'DNS Troubleshooting', + path_taken: ['root', 'node-1'], + decisions: [], + } + mockClient.get.mockResolvedValue({ data: mockView }) + + const result = await sessionsApi.getSharedSession('tok-abc') + + expect(mockClient.get).toHaveBeenCalledWith('/share/tok-abc') + expect(result).toEqual(mockView) + }) +}) diff --git a/frontend/src/lib/sessionShare.test.ts b/frontend/src/lib/sessionShare.test.ts new file mode 100644 index 00000000..c4f2cbc8 --- /dev/null +++ b/frontend/src/lib/sessionShare.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, vi, afterEach } from 'vitest' +import type { SessionShare } from '@/types' +import { buildSessionShareUrl, filterSharesForSession, getLatestActiveShareForSession } from './sessionShare' + +function makeMockShare(overrides: Partial = {}): SessionShare { + return { + id: 'share-1', + session_id: 'session-1', + account_id: 'account-1', + share_token: 'abc123', + share_name: null, + visibility: 'public', + created_by: 'user-1', + created_at: '2026-02-14T10:00:00Z', + updated_at: '2026-02-14T10:00:00Z', + expires_at: null, + view_count: 0, + last_viewed_at: null, + is_active: true, + share_url: null, + ...overrides, + } +} + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe('buildSessionShareUrl', () => { + it('returns share_url when present', () => { + const share = makeMockShare({ share_url: 'https://resolutionflow.com/share/abc123' }) + expect(buildSessionShareUrl(share)).toBe('https://resolutionflow.com/share/abc123') + }) + + it('constructs URL from token when share_url is null', () => { + vi.stubGlobal('location', { origin: 'http://localhost:5173' }) + const share = makeMockShare({ share_token: 'tok-xyz', share_url: null }) + expect(buildSessionShareUrl(share)).toBe('http://localhost:5173/share/tok-xyz') + }) +}) + +describe('filterSharesForSession', () => { + it('filters to matching session_id and active shares', () => { + const shares = [ + makeMockShare({ id: 's1', session_id: 'sess-A', is_active: true }), + makeMockShare({ id: 's2', session_id: 'sess-B', is_active: true }), + makeMockShare({ id: 's3', session_id: 'sess-A', is_active: true }), + ] + const result = filterSharesForSession(shares, 'sess-A') + expect(result).toHaveLength(2) + expect(result.map(s => s.id)).toEqual(['s1', 's3']) + }) + + it('excludes inactive shares', () => { + const shares = [ + makeMockShare({ id: 's1', session_id: 'sess-A', is_active: true }), + makeMockShare({ id: 's2', session_id: 'sess-A', is_active: false }), + ] + const result = filterSharesForSession(shares, 'sess-A') + expect(result).toHaveLength(1) + expect(result[0].id).toBe('s1') + }) +}) + +describe('getLatestActiveShareForSession', () => { + it('returns the most recently created share', () => { + const shares = [ + makeMockShare({ id: 'old', session_id: 'sess-A', created_at: '2026-02-10T10:00:00Z', is_active: true }), + makeMockShare({ id: 'newest', session_id: 'sess-A', created_at: '2026-02-14T12:00:00Z', is_active: true }), + makeMockShare({ id: 'mid', session_id: 'sess-A', created_at: '2026-02-12T10:00:00Z', is_active: true }), + ] + const result = getLatestActiveShareForSession(shares, 'sess-A') + expect(result).not.toBeNull() + expect(result!.id).toBe('newest') + }) + + it('returns null when no shares match', () => { + const shares = [ + makeMockShare({ session_id: 'sess-B', is_active: true }), + makeMockShare({ session_id: 'sess-A', is_active: false }), + ] + const result = getLatestActiveShareForSession(shares, 'sess-A') + expect(result).toBeNull() + }) +})