Skip to content

Commit 732b1bf

Browse files
os-zhuangclaude
andauthored
test(attachments): pin the both-dialects signed-URL readers (objectstack#3689) (#2913)
`GET /api/v1/storage/files/:fileId/url` answered a bare `{ url }` and now answers the declared `{ success: true, data: { url } }` envelope (objectstack#3689 — the success-path half of the #3675 error-path fix). Both console readers of that route — `RecordAttachmentsPanel.handleDownload` and `ApprovalsInboxPage.openAttachment` — already took either shape, which is what lets the two repos land in any order. Nothing to fix, then; what was missing is any statement that the tolerance is deliberate. So the panel's download suite now drives the enveloped shape in its main case and the legacy bare one in a dedicated case, mirroring how the same file already covers both error dialects, and both readers say in a comment why they read two. No behaviour change. Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3c4d935 commit 732b1bf

3 files changed

Lines changed: 40 additions & 4 deletions

File tree

apps/console/src/pages/system/ApprovalsInboxPage.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,9 @@ export function ApprovalsInboxPage() {
449449
const res = await authFetch(`${base}/api/v1/storage/files/${encodeURIComponent(att.id)}/url`);
450450
if (!res.ok) throw new Error(`HTTP_${res.status}`);
451451
const body = await res.json().catch(() => null);
452+
// Both dialects: the declared `{ success: true, data: { url } }` envelope
453+
// this route answers as of objectstack#3689, and the bare `{ url }` an
454+
// older server still sends — the console deploys independently of it.
452455
const raw = body?.data?.url ?? body?.url;
453456
if (!raw) throw new Error('NO_URL');
454457
// Signed URLs from the local adapter are relative; S3/GCS are absolute.

packages/app-shell/src/views/RecordAttachmentsPanel.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ export const RecordAttachmentsPanel: React.FC<RecordAttachmentsPanelProps> = ({
228228
throw Object.assign(new Error(code ?? `Download failed (${res.status})`), { code });
229229
}
230230
const body = await res.json();
231+
// Both URL dialects, for the same independent-deploy reason as the
232+
// error branch above: the route answered a bare `{ url }` until
233+
// objectstack#3689 moved it into the declared
234+
// `{ success: true, data: { url } }` envelope.
231235
const url: string | undefined = body?.url ?? body?.data?.url;
232236
if (!url) throw new Error('Download URL missing from response');
233237
const target = /^https?:/i.test(url) ? url : `${baseUrl}${url}`;

packages/app-shell/src/views/__tests__/RecordAttachmentsPanel.test.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,13 @@ describe('RecordAttachmentsPanel — server-denial error mapping (#2755)', () =>
9797
describe('RecordAttachmentsPanel — authenticated signed-URL download (#2970)', () => {
9898
it('fetches /files/:id/url with auth and opens the signed URL', async () => {
9999
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
100+
// The declared envelope the route answers as of objectstack#3689 — the URL
101+
// moved from the top level down under `data`.
100102
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
101-
new Response(JSON.stringify({ url: '/api/v1/storage/_local/raw/tok123' }), {
102-
status: 200,
103-
headers: { 'Content-Type': 'application/json' },
104-
}),
103+
new Response(
104+
JSON.stringify({ success: true, data: { url: '/api/v1/storage/_local/raw/tok123' } }),
105+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
106+
),
105107
);
106108
setup(makeDataSource());
107109
await waitFor(() => expect(screen.getByText('report.pdf')).toBeInTheDocument());
@@ -190,4 +192,31 @@ describe('RecordAttachmentsPanel — authenticated signed-URL download (#2970)',
190192
);
191193
expect(openSpy).not.toHaveBeenCalled();
192194
});
195+
196+
// Same reasoning on the SUCCESS path (objectstack#3689): the route used to
197+
// answer a bare `{ url }` with no envelope at all. The reader takes both, so
198+
// whichever repo lands first, downloads keep working — and this pins that
199+
// tolerance as deliberate rather than incidental.
200+
it('still opens the legacy bare `{ url }` shape (older server)', async () => {
201+
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
202+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
203+
new Response(JSON.stringify({ url: '/api/v1/storage/_local/raw/legacy-tok' }), {
204+
status: 200,
205+
headers: { 'Content-Type': 'application/json' },
206+
}),
207+
);
208+
setup(makeDataSource());
209+
await waitFor(() => expect(screen.getByText('report.pdf')).toBeInTheDocument());
210+
211+
await userEvent.setup().click(screen.getByRole('button', { name: 'Download' }));
212+
213+
await waitFor(() =>
214+
expect(openSpy).toHaveBeenCalledWith(
215+
expect.stringContaining('/api/v1/storage/_local/raw/legacy-tok'),
216+
'_blank',
217+
'noopener,noreferrer',
218+
),
219+
);
220+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
221+
});
193222
});

0 commit comments

Comments
 (0)