-
Notifications
You must be signed in to change notification settings - Fork 3.8k
fix(search): read Confluence pages through the v2 API the Search grant allows #8321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ba8c7d5
fix(search): read Confluence pages through the v2 API the Search gran…
waleedlatif1 727875a
fix(search): use search-response spaces for verification and fall bac…
waleedlatif1 77593c6
fix(search): fall back to blog posts only when the legacy page is mis…
waleedlatif1 a9480f3
test(search): check that provider failures keep their HTTP status
waleedlatif1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| import { describe, expect, it, vi } from 'vitest' | ||
| import { readAtlassian, searchAtlassian } from '@/lib/sim-search/live/atlassian' | ||
| import { NativeSearchError } from '@/lib/sim-search/live/http' | ||
| import type { NativeClient } from '@/lib/sim-search/live/types' | ||
|
|
||
| const SITE = { id: 'cloud', url: 'https://acme.atlassian.net' } | ||
|
|
||
| /** Answers only the paths a test names, so a read through the v1 content API fails loudly. */ | ||
| function client(rows: Record<string, unknown>): NativeClient & { json: ReturnType<typeof vi.fn> } { | ||
| return { | ||
| json: vi.fn(async (path: string) => { | ||
| if (path === '/oauth/token/accessible-resources') return [SITE] | ||
| if (!(path in rows)) throw new Error(`Unexpected request: ${path}`) | ||
| return rows[path] | ||
| }), | ||
| text: vi.fn(), | ||
| } | ||
| } | ||
|
|
||
| const v2 = '/ex/confluence/cloud/wiki/api/v2' | ||
|
|
||
| describe('Confluence live documents', () => { | ||
| it('reads pages and blog posts through v2, which needs only the granular read scopes', async () => { | ||
| const api = client({ | ||
| [`${v2}/pages/123`]: { | ||
| id: '123', | ||
| title: 'Runbook', | ||
| body: { view: { value: '<p>Restart the <b>ingest</b> worker.</p>' } }, | ||
| version: { createdAt: '2026-09-18T04:50:29.778Z' }, | ||
| _links: { webui: '/spaces/ENG/pages/123/Runbook' }, | ||
| }, | ||
| [`${v2}/blogposts/9`]: { | ||
| id: '9', | ||
| title: 'Release notes', | ||
| body: { view: { value: '<p>Shipped search.</p>' } }, | ||
| version: { createdAt: '2026-09-20T00:00:00.000Z' }, | ||
| _links: { webui: '/spaces/ENG/blog/9' }, | ||
| }, | ||
| }) | ||
| await expect(readAtlassian(api, 'confluence', '123', 'cloud', 'page')).resolves.toMatchObject({ | ||
| id: '123', | ||
| kind: 'page', | ||
| title: 'Runbook', | ||
| content: expect.stringContaining('Restart the ingest worker.'), | ||
| url: 'https://acme.atlassian.net/wiki/spaces/ENG/pages/123/Runbook', | ||
| modifiedAt: '2026-09-18T04:50:29.778Z', | ||
| }) | ||
| await expect(readAtlassian(api, 'confluence', '9', 'cloud', 'blogpost')).resolves.toMatchObject( | ||
| { | ||
| kind: 'blogpost', | ||
| content: expect.stringContaining('Shipped search.'), | ||
| } | ||
| ) | ||
| }) | ||
|
|
||
| it('reads a legacy reference without a kind as a blog post when no page has that id', async () => { | ||
| const api: NativeClient = { | ||
| json: vi.fn(async (path: string) => { | ||
| if (path === '/oauth/token/accessible-resources') return [SITE] | ||
| if (path === `${v2}/blogposts/9`) | ||
| return { id: '9', title: 'Release notes', body: { view: { value: '<p>Shipped.</p>' } } } | ||
| throw new NativeSearchError('unavailable', 'Provider request failed (404).', undefined, 404) | ||
| }), | ||
| text: vi.fn(), | ||
| } | ||
| await expect(readAtlassian(api, 'confluence', '9', 'cloud')).resolves.toMatchObject({ | ||
| kind: 'blogpost', | ||
| content: expect.stringContaining('Shipped.'), | ||
| }) | ||
| }) | ||
|
|
||
| it('keeps a legacy reference page failure that is not a missing page', async () => { | ||
| const failure = new NativeSearchError( | ||
| 'unavailable', | ||
| 'Provider request failed (500).', | ||
| undefined, | ||
| 500 | ||
| ) | ||
| const api: NativeClient = { | ||
| json: vi.fn(async (path: string) => { | ||
| if (path === '/oauth/token/accessible-resources') return [SITE] | ||
| if (path === `${v2}/pages/9`) throw failure | ||
| throw new Error(`Unexpected request: ${path}`) | ||
| }), | ||
| text: vi.fn(), | ||
| } | ||
| await expect(readAtlassian(api, 'confluence', '9', 'cloud')).rejects.toBe(failure) | ||
| }) | ||
|
|
||
| it('reads a space result as its homepage, keeping the space as the document', async () => { | ||
| const api = client({ | ||
| [`${v2}/spaces`]: { | ||
| results: [{ id: '7', key: 'ENG', name: 'Engineering', homepageId: '55' }], | ||
| }, | ||
| [`${v2}/pages/55`]: { | ||
| id: '55', | ||
| title: 'Engineering Home', | ||
| body: { view: { value: '<p>Team charter.</p>' } }, | ||
| _links: { webui: '/spaces/ENG/overview' }, | ||
| }, | ||
| }) | ||
| await expect(readAtlassian(api, 'confluence', 'ENG', 'cloud', 'space')).resolves.toMatchObject({ | ||
| id: 'ENG', | ||
| kind: 'space', | ||
| title: 'Engineering', | ||
| content: expect.stringContaining('Team charter.'), | ||
| }) | ||
| }) | ||
|
|
||
| it('records whether a search result is a page, blog post, or space so its read picks the endpoint', async () => { | ||
| const api = client({ | ||
| '/ex/confluence/cloud/wiki/rest/api/search': { | ||
| results: [ | ||
| { | ||
| content: { | ||
| id: '123', | ||
| type: 'page', | ||
| space: { key: 'ENG' }, | ||
| title: 'Runbook', | ||
| _links: { webui: '/spaces/ENG/pages/123' }, | ||
| }, | ||
| }, | ||
| { | ||
| content: { | ||
| id: '9', | ||
| type: 'blogpost', | ||
| title: 'Release notes', | ||
| _links: { webui: '/spaces/ENG/blog/9' }, | ||
| }, | ||
| }, | ||
| { | ||
| entityType: 'space', | ||
| title: 'Engineering', | ||
| url: '/spaces/ENG', | ||
| space: { key: 'ENG', name: 'Engineering' }, | ||
| }, | ||
| ], | ||
| _links: {}, | ||
| }, | ||
| }) | ||
| const page = await searchAtlassian(api, 'confluence', { | ||
| query: 'runbook', | ||
| limit: 10, | ||
| scopes: [], | ||
| }) | ||
| expect(page.documents.map(({ id, kind }) => ({ id, kind }))).toEqual([ | ||
| { id: '123', kind: 'page' }, | ||
| { id: '9', kind: 'blogpost' }, | ||
| { id: 'ENG', kind: 'space' }, | ||
| ]) | ||
| expect(page.documents[2]?.url).toBe('https://acme.atlassian.net/wiki/spaces/ENG') | ||
| expect(page.documents[0]?.accessMetadata).toEqual({ spaceKey: 'ENG' }) | ||
| expect(page.documents[2]?.accessMetadata).toEqual({ spaceKey: 'ENG' }) | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.