-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.test.ts
More file actions
141 lines (126 loc) · 5.4 KB
/
client.test.ts
File metadata and controls
141 lines (126 loc) · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import { describe, expect, it, vi } from 'vitest';
import { ApiClient, ApiError } from './client';
// The client only reads `status`, `ok`, and `text()` off the response,
// so a minimal stub keeps the test free of any global-`Response`
// polyfill assumption.
function fakeResponse(status: number, body: unknown): Response {
return {
status,
ok: status >= 200 && status < 300,
text: async () => JSON.stringify(body),
} as unknown as Response;
}
function clientReturning(response: Response, onAuthFailure: () => void) {
const fetchImpl = vi.fn(async () => response) as unknown as typeof fetch;
return new ApiClient({ mount: '/admin-react/', fetchImpl, onAuthFailure });
}
describe('ApiClient — session-level auth failure (#414)', () => {
it('invokes onAuthFailure on a 403 `session_expired` (and still throws ApiError)', async () => {
const onAuthFailure = vi.fn();
const client = clientReturning(
fakeResponse(403, { error: { code: 'session_expired', message: 'Session expired.' } }),
onAuthFailure,
);
await expect(client.getRegistry()).rejects.toBeInstanceOf(ApiError);
expect(onAuthFailure).toHaveBeenCalledOnce();
});
it('invokes onAuthFailure on a 401', async () => {
const onAuthFailure = vi.fn();
const client = clientReturning(
fakeResponse(401, { error: { code: 'not_authenticated', message: 'No session.' } }),
onAuthFailure,
);
await expect(client.getRegistry()).rejects.toBeInstanceOf(ApiError);
expect(onAuthFailure).toHaveBeenCalledOnce();
});
it('does NOT invoke onAuthFailure on a plain `forbidden` 403 (renders inline instead)', async () => {
const onAuthFailure = vi.fn();
const client = clientReturning(
fakeResponse(403, { error: { code: 'forbidden', message: 'You do not have permission.' } }),
onAuthFailure,
);
await expect(client.getRegistry()).rejects.toBeInstanceOf(ApiError);
expect(onAuthFailure).not.toHaveBeenCalled();
});
it('does NOT invoke onAuthFailure on a successful response', async () => {
const onAuthFailure = vi.fn();
const client = clientReturning(
fakeResponse(200, { mount: '/admin-react/', user: {}, apps: [] }),
onAuthFailure,
);
await client.getRegistry();
expect(onAuthFailure).not.toHaveBeenCalled();
});
});
describe('ApiClient.getRecentActions (#502)', () => {
it('GETs recent-actions/ and parses the actions array', async () => {
const fetchImpl = vi.fn(async () =>
fakeResponse(200, {
actions: [
{
id: 1,
action: 'changed',
action_time: '2026-05-28T00:00:00Z',
object_repr: 'thing',
target: { app_label: 'auth', model_name: 'group', pk: '3' },
},
],
}),
) as unknown as typeof fetch;
const client = new ApiClient({ mount: '/admin-react/', fetchImpl });
const res = await client.getRecentActions();
expect(res.actions).toHaveLength(1);
expect(res.actions[0]?.target).toEqual({ app_label: 'auth', model_name: 'group', pk: '3' });
const calls = (fetchImpl as unknown as ReturnType<typeof vi.fn>).mock.calls;
expect(calls[0]?.[0]).toBe('/admin-react/api/v1/recent-actions/');
});
it('appends the limit query param when given', async () => {
const fetchImpl = vi.fn(
async () => fakeResponse(200, { actions: [] }),
) as unknown as typeof fetch;
const client = new ApiClient({ mount: '/admin-react/', fetchImpl });
await client.getRecentActions(5);
const calls = (fetchImpl as unknown as ReturnType<typeof vi.fn>).mock.calls;
expect(calls[0]?.[0]).toBe('/admin-react/api/v1/recent-actions/?limit=5');
});
});
describe('ApiClient — apiPrefix override (#559)', () => {
it('routes every request through `apiPrefix` instead of `<mount>api/v1/`', async () => {
const fetchImpl = vi.fn(
async () => fakeResponse(200, { actions: [] }),
) as unknown as typeof fetch;
// The consumer mounted django-admin-rest-api at /api/ separately
// and set DJANGO_ADMIN_REACT["API_URL_PREFIX"] = "/api/api/v1/" so
// the SPA talks to that mount instead of <spa-mount>/api/v1/.
const client = new ApiClient({
mount: '/admin-react/',
apiPrefix: '/api/api/v1/',
fetchImpl,
});
await client.getRecentActions();
const calls = (fetchImpl as unknown as ReturnType<typeof vi.fn>).mock.calls;
expect(calls[0]?.[0]).toBe('/api/api/v1/recent-actions/');
});
it('adds a trailing slash to `apiPrefix` when missing (concat invariant)', async () => {
const fetchImpl = vi.fn(
async () => fakeResponse(200, { actions: [] }),
) as unknown as typeof fetch;
const client = new ApiClient({
mount: '/admin-react/',
apiPrefix: '/custom-api/v1',
fetchImpl,
});
await client.getRecentActions();
const calls = (fetchImpl as unknown as ReturnType<typeof vi.fn>).mock.calls;
expect(calls[0]?.[0]).toBe('/custom-api/v1/recent-actions/');
});
it('defaults to `<mount>api/v1/` when `apiPrefix` is omitted (no change for existing consumers)', async () => {
const fetchImpl = vi.fn(
async () => fakeResponse(200, { actions: [] }),
) as unknown as typeof fetch;
const client = new ApiClient({ mount: '/admin-react/', fetchImpl });
await client.getRecentActions();
const calls = (fetchImpl as unknown as ReturnType<typeof vi.fn>).mock.calls;
expect(calls[0]?.[0]).toBe('/admin-react/api/v1/recent-actions/');
});
});