Skip to content

Commit 066ecb5

Browse files
committed
improvement(search): unify source setup and member connection actions
1 parent 9a1821a commit 066ecb5

24 files changed

Lines changed: 2787 additions & 1285 deletions
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/** @vitest-environment node */
2+
import { authMockFns, createMockRequest } from '@sim/testing'
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
4+
5+
const mocks = vi.hoisted(() => ({ execute: vi.fn() }))
6+
vi.mock('@/lib/knowledge/application/search-sources', () => ({
7+
listSearchSources: { operation: { id: 'knowledge.search.sources.list' }, execute: mocks.execute },
8+
}))
9+
vi.mock('@/lib/knowledge/application/search', () => ({
10+
KnowledgeSearchProvenanceUnavailableError: class extends Error {},
11+
}))
12+
vi.mock('@/lib/knowledge/application/upload-sessions', () => ({
13+
KnowledgeDocumentUnsupportedMediaTypeError: class extends Error {},
14+
}))
15+
16+
import { NoWorkspaceAccessError } from '@/lib/core/application/workspace-authorization'
17+
import { GET } from '@/app/api/knowledge/sim-search/sources/route'
18+
19+
const WORKSPACE_ID = '7d28e5e2-fb03-4118-9c52-4ab77ccff369'
20+
const source = {
21+
knowledgeBaseId: 'search-index',
22+
connectorId: 'source',
23+
connectorType: 'google_drive',
24+
sourceDescription: 'Handbook',
25+
accessMode: 'admin',
26+
availability: 'available',
27+
enabled: true,
28+
isSyncing: false,
29+
lastSyncAt: null,
30+
hasSyncError: false,
31+
viewerDocumentCount: 0,
32+
viewerEmailVerified: true,
33+
connectionRequired: false,
34+
viewerMembership: null,
35+
}
36+
37+
beforeEach(() => {
38+
vi.clearAllMocks()
39+
authMockFns.mockGetSession.mockResolvedValue({
40+
user: { id: 'reader' },
41+
session: { id: 'session' },
42+
})
43+
mocks.execute.mockResolvedValue({ sources: [source] })
44+
})
45+
46+
describe('GET Search sources', () => {
47+
it('authenticates before parsing the workspace query', async () => {
48+
authMockFns.mockGetSession.mockResolvedValue(null)
49+
const response = await GET(createMockRequest('GET'))
50+
expect(response.status).toBe(401)
51+
expect(mocks.execute).not.toHaveBeenCalled()
52+
})
53+
54+
it('refuses a missing workspace before entering the use case', async () => {
55+
const response = await GET(createMockRequest('GET'))
56+
expect(response.status).toBe(400)
57+
expect(mocks.execute).not.toHaveBeenCalled()
58+
})
59+
60+
it('passes the authenticated subject into the registered operation and projects only the contract fields', async () => {
61+
mocks.execute.mockResolvedValue({
62+
sources: [
63+
{
64+
...source,
65+
credentialId: 'secret',
66+
sourceConfig: { token: 'secret' },
67+
lastSyncError: 'private failure',
68+
},
69+
],
70+
})
71+
const response = await GET(
72+
createMockRequest(
73+
'GET',
74+
undefined,
75+
{},
76+
`http://localhost/api/knowledge/sim-search/sources?workspaceId=${WORKSPACE_ID}`
77+
)
78+
)
79+
expect(response.status).toBe(200)
80+
expect(response.headers.get('Cache-Control')).toBe('private, no-store')
81+
const body = await response.json()
82+
expect(body).toMatchObject({ success: true, data: [source] })
83+
expect(body.data[0]).toEqual(source)
84+
expect(mocks.execute).toHaveBeenCalledWith(
85+
expect.objectContaining({
86+
principal: { kind: 'session', userId: 'reader', sessionId: 'session' },
87+
input: { workspaceId: WORKSPACE_ID },
88+
})
89+
)
90+
})
91+
92+
it('preserves authorization rejection and conceals source data', async () => {
93+
mocks.execute.mockRejectedValue(new NoWorkspaceAccessError())
94+
const response = await GET(
95+
createMockRequest(
96+
'GET',
97+
undefined,
98+
{},
99+
`http://localhost/api/knowledge/sim-search/sources?workspaceId=${WORKSPACE_ID}`
100+
)
101+
)
102+
expect(response.status).toBe(404)
103+
expect(await response.json()).not.toHaveProperty('data')
104+
})
105+
106+
it('does not publish infrastructure errors or mistake failures for an empty list', async () => {
107+
mocks.execute.mockRejectedValue(new Error('database private connection string'))
108+
const response = await GET(
109+
createMockRequest(
110+
'GET',
111+
undefined,
112+
{},
113+
`http://localhost/api/knowledge/sim-search/sources?workspaceId=${WORKSPACE_ID}`
114+
)
115+
)
116+
expect(response.status).toBe(500)
117+
const body = await response.json()
118+
expect(body.error).toBe('Internal server error')
119+
expect(body).not.toHaveProperty('data')
120+
})
121+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { listSearchSourcesContract } from '@/lib/api/contracts/knowledge'
2+
import {
3+
defineInternalJsonRoute,
4+
internalRateLimits,
5+
internalSessionAuth,
6+
} from '@/lib/api/server/routes'
7+
import { internalKnowledgeErrorPolicies } from '@/lib/knowledge/api/route-policies'
8+
import { knowledgeOperations } from '@/lib/knowledge/application/operations'
9+
import { listSearchSources } from '@/lib/knowledge/application/search-sources'
10+
11+
export const GET = defineInternalJsonRoute({
12+
contract: listSearchSourcesContract,
13+
auth: internalSessionAuth,
14+
operation: knowledgeOperations.listSearchSources,
15+
rateLimit: internalRateLimits.none({
16+
reason: 'Workspace source summaries for the Search page and indexing status polling',
17+
}),
18+
errorPolicy: internalKnowledgeErrorPolicies.connectors,
19+
mapInput: ({ query }) => ({ workspaceId: query.workspaceId }),
20+
useCase: listSearchSources,
21+
present: ({ sources }) => ({ success: true as const, data: sources }),
22+
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
23+
})

0 commit comments

Comments
 (0)