@tenxyte/core 0.1.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -0
- package/dist/index.cjs +951 -496
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1709 -1265
- package/dist/index.d.ts +1709 -1265
- package/dist/index.js +919 -464
- package/dist/index.js.map +1 -1
- package/package.json +70 -66
- package/src/client.ts +50 -21
- package/src/http/client.ts +162 -162
- package/src/http/index.ts +1 -1
- package/src/http/interceptors.ts +117 -117
- package/src/index.ts +7 -7
- package/src/modules/ai.ts +178 -0
- package/src/modules/auth.ts +116 -95
- package/src/modules/b2b.ts +177 -0
- package/src/modules/rbac.ts +207 -160
- package/src/modules/security.ts +313 -122
- package/src/modules/user.ts +95 -80
- package/src/storage/cookie.ts +39 -39
- package/src/storage/index.ts +29 -29
- package/src/storage/localStorage.ts +75 -75
- package/src/storage/memory.ts +30 -30
- package/src/types/index.ts +152 -150
- package/src/utils/base64url.ts +25 -0
- package/src/utils/device_info.ts +94 -94
- package/src/utils/events.ts +71 -71
- package/src/utils/jwt.ts +51 -51
- package/tests/http.test.ts +144 -144
- package/tests/modules/auth.test.ts +93 -93
- package/tests/modules/rbac.test.ts +95 -95
- package/tests/modules/security.test.ts +85 -75
- package/tests/modules/user.test.ts +76 -76
- package/tests/storage.test.ts +96 -96
- package/tests/utils.test.ts +71 -71
- package/tsup.config.ts +10 -10
- package/vitest.config.ts +7 -7
package/tests/http.test.ts
CHANGED
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { TenxyteHttpClient } from '../src/http/client';
|
|
3
|
-
import { MemoryStorage } from '../src/storage';
|
|
4
|
-
import { createAuthInterceptor, createRefreshInterceptor } from '../src/http/interceptors';
|
|
5
|
-
|
|
6
|
-
// Mock global fetch
|
|
7
|
-
const mockFetch = vi.fn();
|
|
8
|
-
global.fetch = mockFetch;
|
|
9
|
-
|
|
10
|
-
describe('TenxyteHttpClient', () => {
|
|
11
|
-
let client: TenxyteHttpClient;
|
|
12
|
-
let storage: MemoryStorage;
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
mockFetch.mockReset();
|
|
16
|
-
storage = new MemoryStorage();
|
|
17
|
-
client = new TenxyteHttpClient({ baseUrl: 'https://api.tenxyte.com/v1' });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
describe('Core HTTP', () => {
|
|
21
|
-
it('should format URL correctly and apply default headers', async () => {
|
|
22
|
-
mockFetch.mockResolvedValueOnce({
|
|
23
|
-
ok: true,
|
|
24
|
-
status: 200,
|
|
25
|
-
headers: new Headers({ 'content-type': 'application/json' }),
|
|
26
|
-
json: () => Promise.resolve({ data: 'ok' })
|
|
27
|
-
} as any);
|
|
28
|
-
|
|
29
|
-
const res = await client.get<{ data: string }>('/users', { params: { limit: 10 } });
|
|
30
|
-
|
|
31
|
-
expect(res.data).toBe('ok');
|
|
32
|
-
expect(mockFetch).toHaveBeenCalledWith('https://api.tenxyte.com/v1/users?limit=10', expect.objectContaining({
|
|
33
|
-
method: 'GET',
|
|
34
|
-
headers: expect.objectContaining({
|
|
35
|
-
'Content-Type': 'application/json',
|
|
36
|
-
'Accept': 'application/json'
|
|
37
|
-
})
|
|
38
|
-
}));
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should throw normalized TenxyteError on failure', async () => {
|
|
42
|
-
mockFetch.mockResolvedValueOnce({
|
|
43
|
-
ok: false,
|
|
44
|
-
status: 400,
|
|
45
|
-
headers: new Headers({ 'content-type': 'application/json' }),
|
|
46
|
-
json: () => Promise.resolve({ error: 'Bad data', code: 'INVALID_CREDENTIALS', details: { email: ['invalid'] } })
|
|
47
|
-
} as any);
|
|
48
|
-
|
|
49
|
-
await expect(client.post('/auth/login', { bad: true })).rejects.toMatchObject({
|
|
50
|
-
error: 'Bad data',
|
|
51
|
-
code: 'INVALID_CREDENTIALS',
|
|
52
|
-
details: { email: ['invalid'] }
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should handle 204 No Content correctly without parsing JSON', async () => {
|
|
57
|
-
mockFetch.mockResolvedValueOnce({
|
|
58
|
-
ok: true,
|
|
59
|
-
status: 204,
|
|
60
|
-
headers: new Headers()
|
|
61
|
-
} as any);
|
|
62
|
-
|
|
63
|
-
const res = await client.delete('/users/1');
|
|
64
|
-
expect(res).toEqual({});
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('Interceptors', () => {
|
|
69
|
-
it('should inject Authorization and Context headers', async () => {
|
|
70
|
-
storage.setItem('tx_access', 'jwt.token.123');
|
|
71
|
-
const authInterceptor = createAuthInterceptor(storage, { activeOrgSlug: 'tenxyte-labs', agentTraceId: 'trc_999' });
|
|
72
|
-
client.addRequestInterceptor(authInterceptor);
|
|
73
|
-
|
|
74
|
-
mockFetch.mockResolvedValueOnce({
|
|
75
|
-
ok: true,
|
|
76
|
-
status: 200,
|
|
77
|
-
headers: new Headers({ 'content-type': 'application/json' }),
|
|
78
|
-
json: () => Promise.resolve({ ok: true })
|
|
79
|
-
} as any);
|
|
80
|
-
|
|
81
|
-
await client.get('/me');
|
|
82
|
-
expect(mockFetch).toHaveBeenCalledWith('https://api.tenxyte.com/v1/me', expect.objectContaining({
|
|
83
|
-
headers: expect.objectContaining({
|
|
84
|
-
Authorization: 'Bearer jwt.token.123',
|
|
85
|
-
'X-Org-Slug': 'tenxyte-labs',
|
|
86
|
-
'X-Prompt-Trace-ID': 'trc_999'
|
|
87
|
-
})
|
|
88
|
-
}));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should seamlessly refresh token on 401', async () => {
|
|
92
|
-
storage.setItem('tx_access', 'expired.token');
|
|
93
|
-
storage.setItem('tx_refresh', 'valid.refresh.token');
|
|
94
|
-
|
|
95
|
-
const onSessionExpired = vi.fn();
|
|
96
|
-
const authInterceptor = createAuthInterceptor(storage, { activeOrgSlug: null, agentTraceId: null });
|
|
97
|
-
const refreshInterceptor = createRefreshInterceptor(client, storage, onSessionExpired);
|
|
98
|
-
|
|
99
|
-
client.addRequestInterceptor(authInterceptor);
|
|
100
|
-
client.addResponseInterceptor(refreshInterceptor);
|
|
101
|
-
|
|
102
|
-
// 1. Initial request gets 401
|
|
103
|
-
// 2. Interceptor triggers refresh (/auth/refresh/) returning 200 with new token
|
|
104
|
-
// 3. Interceptor retries initial request with new token, returning 200
|
|
105
|
-
|
|
106
|
-
mockFetch
|
|
107
|
-
// First fail call '/me' with 401
|
|
108
|
-
.mockResolvedValueOnce({
|
|
109
|
-
ok: false,
|
|
110
|
-
status: 401,
|
|
111
|
-
headers: new Headers(),
|
|
112
|
-
json: () => Promise.resolve({ error: 'Token expired', code: 'TOKEN_EXPIRED' })
|
|
113
|
-
} as any)
|
|
114
|
-
// Refresh call '/auth/refresh/' succeeds
|
|
115
|
-
.mockResolvedValueOnce({
|
|
116
|
-
ok: true,
|
|
117
|
-
status: 200,
|
|
118
|
-
headers: new Headers({ 'content-type': 'application/json' }),
|
|
119
|
-
json: () => Promise.resolve({ access: 'new.access', refresh: 'new.refresh' })
|
|
120
|
-
} as any)
|
|
121
|
-
// Retry call '/me' succeeds
|
|
122
|
-
.mockResolvedValueOnce({
|
|
123
|
-
ok: true,
|
|
124
|
-
status: 200,
|
|
125
|
-
headers: new Headers({ 'content-type': 'application/json' }),
|
|
126
|
-
json: () => Promise.resolve({ id: 1, name: 'Bob' })
|
|
127
|
-
} as any);
|
|
128
|
-
|
|
129
|
-
const res = await client.get('/me');
|
|
130
|
-
|
|
131
|
-
expect(res).toEqual({ id: 1, name: 'Bob' });
|
|
132
|
-
expect(storage.getItem('tx_access')).toBe('new.access');
|
|
133
|
-
expect(storage.getItem('tx_refresh')).toBe('new.refresh');
|
|
134
|
-
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
135
|
-
|
|
136
|
-
// Verify the retry request had the NEW token!
|
|
137
|
-
expect(mockFetch).toHaveBeenNthCalledWith(3, 'https://api.tenxyte.com/v1/me', expect.objectContaining({
|
|
138
|
-
headers: expect.objectContaining({
|
|
139
|
-
Authorization: 'Bearer new.access'
|
|
140
|
-
})
|
|
141
|
-
}));
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
});
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { TenxyteHttpClient } from '../src/http/client';
|
|
3
|
+
import { MemoryStorage } from '../src/storage';
|
|
4
|
+
import { createAuthInterceptor, createRefreshInterceptor } from '../src/http/interceptors';
|
|
5
|
+
|
|
6
|
+
// Mock global fetch
|
|
7
|
+
const mockFetch = vi.fn();
|
|
8
|
+
global.fetch = mockFetch;
|
|
9
|
+
|
|
10
|
+
describe('TenxyteHttpClient', () => {
|
|
11
|
+
let client: TenxyteHttpClient;
|
|
12
|
+
let storage: MemoryStorage;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockFetch.mockReset();
|
|
16
|
+
storage = new MemoryStorage();
|
|
17
|
+
client = new TenxyteHttpClient({ baseUrl: 'https://api.tenxyte.com/v1' });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('Core HTTP', () => {
|
|
21
|
+
it('should format URL correctly and apply default headers', async () => {
|
|
22
|
+
mockFetch.mockResolvedValueOnce({
|
|
23
|
+
ok: true,
|
|
24
|
+
status: 200,
|
|
25
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
26
|
+
json: () => Promise.resolve({ data: 'ok' })
|
|
27
|
+
} as any);
|
|
28
|
+
|
|
29
|
+
const res = await client.get<{ data: string }>('/users', { params: { limit: 10 } });
|
|
30
|
+
|
|
31
|
+
expect(res.data).toBe('ok');
|
|
32
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.tenxyte.com/v1/users?limit=10', expect.objectContaining({
|
|
33
|
+
method: 'GET',
|
|
34
|
+
headers: expect.objectContaining({
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'Accept': 'application/json'
|
|
37
|
+
})
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should throw normalized TenxyteError on failure', async () => {
|
|
42
|
+
mockFetch.mockResolvedValueOnce({
|
|
43
|
+
ok: false,
|
|
44
|
+
status: 400,
|
|
45
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
46
|
+
json: () => Promise.resolve({ error: 'Bad data', code: 'INVALID_CREDENTIALS', details: { email: ['invalid'] } })
|
|
47
|
+
} as any);
|
|
48
|
+
|
|
49
|
+
await expect(client.post('/auth/login', { bad: true })).rejects.toMatchObject({
|
|
50
|
+
error: 'Bad data',
|
|
51
|
+
code: 'INVALID_CREDENTIALS',
|
|
52
|
+
details: { email: ['invalid'] }
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle 204 No Content correctly without parsing JSON', async () => {
|
|
57
|
+
mockFetch.mockResolvedValueOnce({
|
|
58
|
+
ok: true,
|
|
59
|
+
status: 204,
|
|
60
|
+
headers: new Headers()
|
|
61
|
+
} as any);
|
|
62
|
+
|
|
63
|
+
const res = await client.delete('/users/1');
|
|
64
|
+
expect(res).toEqual({});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('Interceptors', () => {
|
|
69
|
+
it('should inject Authorization and Context headers', async () => {
|
|
70
|
+
storage.setItem('tx_access', 'jwt.token.123');
|
|
71
|
+
const authInterceptor = createAuthInterceptor(storage, { activeOrgSlug: 'tenxyte-labs', agentTraceId: 'trc_999' });
|
|
72
|
+
client.addRequestInterceptor(authInterceptor);
|
|
73
|
+
|
|
74
|
+
mockFetch.mockResolvedValueOnce({
|
|
75
|
+
ok: true,
|
|
76
|
+
status: 200,
|
|
77
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
78
|
+
json: () => Promise.resolve({ ok: true })
|
|
79
|
+
} as any);
|
|
80
|
+
|
|
81
|
+
await client.get('/me');
|
|
82
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.tenxyte.com/v1/me', expect.objectContaining({
|
|
83
|
+
headers: expect.objectContaining({
|
|
84
|
+
Authorization: 'Bearer jwt.token.123',
|
|
85
|
+
'X-Org-Slug': 'tenxyte-labs',
|
|
86
|
+
'X-Prompt-Trace-ID': 'trc_999'
|
|
87
|
+
})
|
|
88
|
+
}));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should seamlessly refresh token on 401', async () => {
|
|
92
|
+
storage.setItem('tx_access', 'expired.token');
|
|
93
|
+
storage.setItem('tx_refresh', 'valid.refresh.token');
|
|
94
|
+
|
|
95
|
+
const onSessionExpired = vi.fn();
|
|
96
|
+
const authInterceptor = createAuthInterceptor(storage, { activeOrgSlug: null, agentTraceId: null });
|
|
97
|
+
const refreshInterceptor = createRefreshInterceptor(client, storage, onSessionExpired);
|
|
98
|
+
|
|
99
|
+
client.addRequestInterceptor(authInterceptor);
|
|
100
|
+
client.addResponseInterceptor(refreshInterceptor);
|
|
101
|
+
|
|
102
|
+
// 1. Initial request gets 401
|
|
103
|
+
// 2. Interceptor triggers refresh (/auth/refresh/) returning 200 with new token
|
|
104
|
+
// 3. Interceptor retries initial request with new token, returning 200
|
|
105
|
+
|
|
106
|
+
mockFetch
|
|
107
|
+
// First fail call '/me' with 401
|
|
108
|
+
.mockResolvedValueOnce({
|
|
109
|
+
ok: false,
|
|
110
|
+
status: 401,
|
|
111
|
+
headers: new Headers(),
|
|
112
|
+
json: () => Promise.resolve({ error: 'Token expired', code: 'TOKEN_EXPIRED' })
|
|
113
|
+
} as any)
|
|
114
|
+
// Refresh call '/auth/refresh/' succeeds
|
|
115
|
+
.mockResolvedValueOnce({
|
|
116
|
+
ok: true,
|
|
117
|
+
status: 200,
|
|
118
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
119
|
+
json: () => Promise.resolve({ access: 'new.access', refresh: 'new.refresh' })
|
|
120
|
+
} as any)
|
|
121
|
+
// Retry call '/me' succeeds
|
|
122
|
+
.mockResolvedValueOnce({
|
|
123
|
+
ok: true,
|
|
124
|
+
status: 200,
|
|
125
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
126
|
+
json: () => Promise.resolve({ id: 1, name: 'Bob' })
|
|
127
|
+
} as any);
|
|
128
|
+
|
|
129
|
+
const res = await client.get('/me');
|
|
130
|
+
|
|
131
|
+
expect(res).toEqual({ id: 1, name: 'Bob' });
|
|
132
|
+
expect(storage.getItem('tx_access')).toBe('new.access');
|
|
133
|
+
expect(storage.getItem('tx_refresh')).toBe('new.refresh');
|
|
134
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
135
|
+
|
|
136
|
+
// Verify the retry request had the NEW token!
|
|
137
|
+
expect(mockFetch).toHaveBeenNthCalledWith(3, 'https://api.tenxyte.com/v1/me', expect.objectContaining({
|
|
138
|
+
headers: expect.objectContaining({
|
|
139
|
+
Authorization: 'Bearer new.access'
|
|
140
|
+
})
|
|
141
|
+
}));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { AuthModule } from '../../src/modules/auth';
|
|
3
|
-
import { TenxyteHttpClient } from '../../src/http/client';
|
|
4
|
-
|
|
5
|
-
describe('AuthModule', () => {
|
|
6
|
-
let client: TenxyteHttpClient;
|
|
7
|
-
let auth: AuthModule;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
client = new TenxyteHttpClient({ baseUrl: 'http://localhost:8000' });
|
|
11
|
-
auth = new AuthModule(client);
|
|
12
|
-
|
|
13
|
-
// Mock the underlying request method
|
|
14
|
-
vi.spyOn(client, 'request').mockImplementation(async () => {
|
|
15
|
-
return {};
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('loginWithEmail should POST to /api/v1/auth/login/email/', async () => {
|
|
20
|
-
const mockResponse = { access_token: 'acc', refresh_token: 'ref', token_type: 'Bearer', expires_in: 3600, device_summary: null };
|
|
21
|
-
vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
|
|
22
|
-
|
|
23
|
-
const data = { email: 'test@example.com', password: 'password123' };
|
|
24
|
-
const result = await auth.loginWithEmail(data);
|
|
25
|
-
|
|
26
|
-
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/login/email/', {
|
|
27
|
-
method: 'POST',
|
|
28
|
-
body: data,
|
|
29
|
-
});
|
|
30
|
-
expect(result).toEqual(mockResponse);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('logout should POST to /api/v1/auth/logout/ with refresh_token', async () => {
|
|
34
|
-
vi.mocked(client.request).mockResolvedValueOnce(undefined);
|
|
35
|
-
|
|
36
|
-
await auth.logout('some_refresh_token');
|
|
37
|
-
|
|
38
|
-
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/logout/', {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
body: { refresh_token: 'some_refresh_token' },
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('requestMagicLink should POST to /api/v1/auth/magic-link/request/', async () => {
|
|
45
|
-
vi.mocked(client.request).mockResolvedValueOnce(undefined);
|
|
46
|
-
|
|
47
|
-
const data = { email: 'magic@example.com' };
|
|
48
|
-
await auth.requestMagicLink(data);
|
|
49
|
-
|
|
50
|
-
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/magic-link/request/', {
|
|
51
|
-
method: 'POST',
|
|
52
|
-
body: data,
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('verifyMagicLink should GET from /api/v1/auth/magic-link/verify/ with token in query', async () => {
|
|
57
|
-
const mockResponse = { access_token: 'acc' };
|
|
58
|
-
vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
|
|
59
|
-
|
|
60
|
-
const result = await auth.verifyMagicLink('magic_token_123');
|
|
61
|
-
|
|
62
|
-
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/magic-link/verify/', {
|
|
63
|
-
method: 'GET',
|
|
64
|
-
params: { token: 'magic_token_123' },
|
|
65
|
-
});
|
|
66
|
-
expect(result).toEqual(mockResponse);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('loginWithSocial should POST to /api/v1/auth/social/:provider/', async () => {
|
|
70
|
-
const mockResponse = { access_token: 'acc' };
|
|
71
|
-
vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
|
|
72
|
-
|
|
73
|
-
const data = { access_token: 'google_token' };
|
|
74
|
-
const result = await auth.loginWithSocial('google', data);
|
|
75
|
-
|
|
76
|
-
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/social/google/', {
|
|
77
|
-
method: 'POST',
|
|
78
|
-
body: data,
|
|
79
|
-
});
|
|
80
|
-
expect(result).toEqual(mockResponse);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('handleSocialCallback should GET from callback endpoint with correct params', async () => {
|
|
84
|
-
vi.mocked(client.request).mockResolvedValueOnce({ access_token: 'acc' });
|
|
85
|
-
|
|
86
|
-
await auth.handleSocialCallback('github', 'auth_code', 'http://localhost/callback');
|
|
87
|
-
|
|
88
|
-
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/social/github/callback/', {
|
|
89
|
-
method: 'GET',
|
|
90
|
-
params: { code: 'auth_code', redirect_uri: 'http://localhost/callback' },
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
});
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { AuthModule } from '../../src/modules/auth';
|
|
3
|
+
import { TenxyteHttpClient } from '../../src/http/client';
|
|
4
|
+
|
|
5
|
+
describe('AuthModule', () => {
|
|
6
|
+
let client: TenxyteHttpClient;
|
|
7
|
+
let auth: AuthModule;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
client = new TenxyteHttpClient({ baseUrl: 'http://localhost:8000' });
|
|
11
|
+
auth = new AuthModule(client);
|
|
12
|
+
|
|
13
|
+
// Mock the underlying request method
|
|
14
|
+
vi.spyOn(client, 'request').mockImplementation(async () => {
|
|
15
|
+
return {};
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('loginWithEmail should POST to /api/v1/auth/login/email/', async () => {
|
|
20
|
+
const mockResponse = { access_token: 'acc', refresh_token: 'ref', token_type: 'Bearer', expires_in: 3600, device_summary: null };
|
|
21
|
+
vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
|
|
22
|
+
|
|
23
|
+
const data = { email: 'test@example.com', password: 'password123' };
|
|
24
|
+
const result = await auth.loginWithEmail(data);
|
|
25
|
+
|
|
26
|
+
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/login/email/', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
body: data,
|
|
29
|
+
});
|
|
30
|
+
expect(result).toEqual(mockResponse);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('logout should POST to /api/v1/auth/logout/ with refresh_token', async () => {
|
|
34
|
+
vi.mocked(client.request).mockResolvedValueOnce(undefined);
|
|
35
|
+
|
|
36
|
+
await auth.logout('some_refresh_token');
|
|
37
|
+
|
|
38
|
+
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/logout/', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
body: { refresh_token: 'some_refresh_token' },
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('requestMagicLink should POST to /api/v1/auth/magic-link/request/', async () => {
|
|
45
|
+
vi.mocked(client.request).mockResolvedValueOnce(undefined);
|
|
46
|
+
|
|
47
|
+
const data = { email: 'magic@example.com' };
|
|
48
|
+
await auth.requestMagicLink(data);
|
|
49
|
+
|
|
50
|
+
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/magic-link/request/', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
body: data,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('verifyMagicLink should GET from /api/v1/auth/magic-link/verify/ with token in query', async () => {
|
|
57
|
+
const mockResponse = { access_token: 'acc' };
|
|
58
|
+
vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
|
|
59
|
+
|
|
60
|
+
const result = await auth.verifyMagicLink('magic_token_123');
|
|
61
|
+
|
|
62
|
+
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/magic-link/verify/', {
|
|
63
|
+
method: 'GET',
|
|
64
|
+
params: { token: 'magic_token_123' },
|
|
65
|
+
});
|
|
66
|
+
expect(result).toEqual(mockResponse);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('loginWithSocial should POST to /api/v1/auth/social/:provider/', async () => {
|
|
70
|
+
const mockResponse = { access_token: 'acc' };
|
|
71
|
+
vi.mocked(client.request).mockResolvedValueOnce(mockResponse);
|
|
72
|
+
|
|
73
|
+
const data = { access_token: 'google_token' };
|
|
74
|
+
const result = await auth.loginWithSocial('google', data);
|
|
75
|
+
|
|
76
|
+
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/social/google/', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
body: data,
|
|
79
|
+
});
|
|
80
|
+
expect(result).toEqual(mockResponse);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('handleSocialCallback should GET from callback endpoint with correct params', async () => {
|
|
84
|
+
vi.mocked(client.request).mockResolvedValueOnce({ access_token: 'acc' });
|
|
85
|
+
|
|
86
|
+
await auth.handleSocialCallback('github', 'auth_code', 'http://localhost/callback');
|
|
87
|
+
|
|
88
|
+
expect(client.request).toHaveBeenCalledWith('/api/v1/auth/social/github/callback/', {
|
|
89
|
+
method: 'GET',
|
|
90
|
+
params: { code: 'auth_code', redirect_uri: 'http://localhost/callback' },
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|