@dangao/bun-server 1.8.0 → 1.8.1
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/package.json +1 -1
- package/tests/auth/auth-decorators.test.ts +241 -0
- package/tests/auth/oauth2-service.test.ts +318 -0
- package/tests/cache/cache-decorators-extended.test.ts +272 -0
- package/tests/cache/cache-interceptors.test.ts +534 -0
- package/tests/cache/cache-service-proxy.test.ts +246 -0
- package/tests/cache/memory-cache-store.test.ts +155 -0
- package/tests/cache/redis-cache-store.test.ts +199 -0
- package/tests/config/config-center-integration.test.ts +334 -0
- package/tests/config/config-module-extended.test.ts +165 -0
- package/tests/controller/param-binder.test.ts +333 -0
- package/tests/error/error-handler.test.ts +166 -57
- package/tests/error/i18n-extended.test.ts +105 -0
- package/tests/events/event-listener-scanner.test.ts +114 -0
- package/tests/events/event-module.test.ts +133 -302
- package/tests/extensions/logger-module.test.ts +158 -0
- package/tests/files/file-storage.test.ts +136 -0
- package/tests/interceptor/base-interceptor.test.ts +605 -0
- package/tests/interceptor/builtin/cache-interceptor.test.ts +233 -86
- package/tests/interceptor/builtin/log-interceptor.test.ts +469 -0
- package/tests/interceptor/builtin/permission-interceptor.test.ts +219 -120
- package/tests/interceptor/interceptor-chain.test.ts +241 -189
- package/tests/interceptor/interceptor-metadata.test.ts +221 -0
- package/tests/microservice/circuit-breaker.test.ts +221 -0
- package/tests/microservice/service-client-decorators.test.ts +86 -0
- package/tests/microservice/service-client-interceptors.test.ts +274 -0
- package/tests/microservice/service-registry-decorators.test.ts +147 -0
- package/tests/microservice/tracer.test.ts +213 -0
- package/tests/microservice/tracing-collectors.test.ts +168 -0
- package/tests/middleware/builtin/middleware-builtin-extended.test.ts +237 -0
- package/tests/middleware/builtin/rate-limit.test.ts +257 -0
- package/tests/middleware/middleware-decorators.test.ts +222 -0
- package/tests/middleware/middleware-pipeline.test.ts +160 -0
- package/tests/queue/queue-decorators.test.ts +139 -0
- package/tests/queue/queue-service.test.ts +191 -0
- package/tests/request/body-parser-extended.test.ts +291 -0
- package/tests/request/request-wrapper.test.ts +319 -0
- package/tests/router/router-decorators.test.ts +260 -0
- package/tests/router/router-extended.test.ts +298 -0
- package/tests/security/guards/reflector.test.ts +188 -0
- package/tests/security/security-filter.test.ts +182 -0
- package/tests/security/security-module-extended.test.ts +133 -0
- package/tests/session/memory-session-store.test.ts +172 -0
- package/tests/session/session-decorators.test.ts +163 -0
- package/tests/swagger/ui.test.ts +212 -0
package/package.json
CHANGED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Auth,
|
|
6
|
+
getAuthMetadata,
|
|
7
|
+
requiresAuth,
|
|
8
|
+
checkRoles,
|
|
9
|
+
type AuthConfig,
|
|
10
|
+
} from '../../src/auth/decorators';
|
|
11
|
+
|
|
12
|
+
describe('Auth Decorator', () => {
|
|
13
|
+
describe('@Auth', () => {
|
|
14
|
+
test('should set auth metadata with default options', () => {
|
|
15
|
+
class TestController {
|
|
16
|
+
@Auth()
|
|
17
|
+
public protectedMethod(): void {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const metadata = getAuthMetadata(TestController.prototype, 'protectedMethod');
|
|
21
|
+
expect(metadata).toBeDefined();
|
|
22
|
+
expect(metadata?.required).toBe(true);
|
|
23
|
+
expect(metadata?.roles).toEqual([]);
|
|
24
|
+
expect(metadata?.allowAnonymous).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should set auth metadata with required=false', () => {
|
|
28
|
+
class TestController {
|
|
29
|
+
@Auth({ required: false })
|
|
30
|
+
public optionalAuthMethod(): void {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const metadata = getAuthMetadata(TestController.prototype, 'optionalAuthMethod');
|
|
34
|
+
expect(metadata?.required).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('should set auth metadata with roles', () => {
|
|
38
|
+
class TestController {
|
|
39
|
+
@Auth({ roles: ['admin', 'moderator'] })
|
|
40
|
+
public adminMethod(): void {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const metadata = getAuthMetadata(TestController.prototype, 'adminMethod');
|
|
44
|
+
expect(metadata?.roles).toEqual(['admin', 'moderator']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should set auth metadata with allowAnonymous', () => {
|
|
48
|
+
class TestController {
|
|
49
|
+
@Auth({ allowAnonymous: true })
|
|
50
|
+
public publicMethod(): void {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const metadata = getAuthMetadata(TestController.prototype, 'publicMethod');
|
|
54
|
+
expect(metadata?.allowAnonymous).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should set all options together', () => {
|
|
58
|
+
const config: AuthConfig = {
|
|
59
|
+
required: true,
|
|
60
|
+
roles: ['admin'],
|
|
61
|
+
allowAnonymous: false,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
class TestController {
|
|
65
|
+
@Auth(config)
|
|
66
|
+
public fullConfigMethod(): void {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const metadata = getAuthMetadata(TestController.prototype, 'fullConfigMethod');
|
|
70
|
+
expect(metadata?.required).toBe(true);
|
|
71
|
+
expect(metadata?.roles).toEqual(['admin']);
|
|
72
|
+
expect(metadata?.allowAnonymous).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should handle multiple methods with different configs', () => {
|
|
76
|
+
class TestController {
|
|
77
|
+
@Auth({ roles: ['admin'] })
|
|
78
|
+
public adminMethod(): void {}
|
|
79
|
+
|
|
80
|
+
@Auth({ roles: ['user'] })
|
|
81
|
+
public userMethod(): void {}
|
|
82
|
+
|
|
83
|
+
@Auth({ allowAnonymous: true })
|
|
84
|
+
public publicMethod(): void {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const adminMetadata = getAuthMetadata(TestController.prototype, 'adminMethod');
|
|
88
|
+
const userMetadata = getAuthMetadata(TestController.prototype, 'userMethod');
|
|
89
|
+
const publicMetadata = getAuthMetadata(TestController.prototype, 'publicMethod');
|
|
90
|
+
|
|
91
|
+
expect(adminMetadata?.roles).toEqual(['admin']);
|
|
92
|
+
expect(userMetadata?.roles).toEqual(['user']);
|
|
93
|
+
expect(publicMetadata?.allowAnonymous).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('getAuthMetadata', () => {
|
|
98
|
+
test('should return undefined for non-decorated method', () => {
|
|
99
|
+
class TestController {
|
|
100
|
+
public normalMethod(): void {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const metadata = getAuthMetadata(TestController.prototype, 'normalMethod');
|
|
104
|
+
expect(metadata).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should return metadata for decorated method', () => {
|
|
108
|
+
class TestController {
|
|
109
|
+
@Auth({ roles: ['admin'] })
|
|
110
|
+
public protectedMethod(): void {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const metadata = getAuthMetadata(TestController.prototype, 'protectedMethod');
|
|
114
|
+
expect(metadata).toBeDefined();
|
|
115
|
+
expect(metadata?.roles).toEqual(['admin']);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('requiresAuth', () => {
|
|
120
|
+
test('should return false for non-decorated method', () => {
|
|
121
|
+
class TestController {
|
|
122
|
+
public normalMethod(): void {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const requires = requiresAuth(TestController.prototype, 'normalMethod');
|
|
126
|
+
expect(requires).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('should return true for decorated method with default config', () => {
|
|
130
|
+
class TestController {
|
|
131
|
+
@Auth()
|
|
132
|
+
public protectedMethod(): void {}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const requires = requiresAuth(TestController.prototype, 'protectedMethod');
|
|
136
|
+
expect(requires).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('should return true when required is explicitly true', () => {
|
|
140
|
+
class TestController {
|
|
141
|
+
@Auth({ required: true })
|
|
142
|
+
public protectedMethod(): void {}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const requires = requiresAuth(TestController.prototype, 'protectedMethod');
|
|
146
|
+
expect(requires).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('should return false when required is false', () => {
|
|
150
|
+
class TestController {
|
|
151
|
+
@Auth({ required: false })
|
|
152
|
+
public optionalMethod(): void {}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const requires = requiresAuth(TestController.prototype, 'optionalMethod');
|
|
156
|
+
expect(requires).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('checkRoles', () => {
|
|
161
|
+
test('should return true when no roles required', () => {
|
|
162
|
+
class TestController {
|
|
163
|
+
@Auth()
|
|
164
|
+
public anyAuthMethod(): void {}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const hasAccess = checkRoles(TestController.prototype, 'anyAuthMethod', ['user']);
|
|
168
|
+
expect(hasAccess).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should return true when roles is empty array', () => {
|
|
172
|
+
class TestController {
|
|
173
|
+
@Auth({ roles: [] })
|
|
174
|
+
public anyAuthMethod(): void {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const hasAccess = checkRoles(TestController.prototype, 'anyAuthMethod', ['user']);
|
|
178
|
+
expect(hasAccess).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('should return true when user has required role', () => {
|
|
182
|
+
class TestController {
|
|
183
|
+
@Auth({ roles: ['admin'] })
|
|
184
|
+
public adminMethod(): void {}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const hasAccess = checkRoles(TestController.prototype, 'adminMethod', ['admin']);
|
|
188
|
+
expect(hasAccess).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('should return true when user has one of required roles', () => {
|
|
192
|
+
class TestController {
|
|
193
|
+
@Auth({ roles: ['admin', 'moderator'] })
|
|
194
|
+
public modMethod(): void {}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const hasAccess = checkRoles(TestController.prototype, 'modMethod', ['moderator']);
|
|
198
|
+
expect(hasAccess).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('should return false when user lacks required role', () => {
|
|
202
|
+
class TestController {
|
|
203
|
+
@Auth({ roles: ['admin'] })
|
|
204
|
+
public adminMethod(): void {}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const hasAccess = checkRoles(TestController.prototype, 'adminMethod', ['user']);
|
|
208
|
+
expect(hasAccess).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('should return false when user has no roles', () => {
|
|
212
|
+
class TestController {
|
|
213
|
+
@Auth({ roles: ['admin'] })
|
|
214
|
+
public adminMethod(): void {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const hasAccess = checkRoles(TestController.prototype, 'adminMethod', []);
|
|
218
|
+
expect(hasAccess).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('should return true for non-decorated method', () => {
|
|
222
|
+
class TestController {
|
|
223
|
+
public normalMethod(): void {}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const hasAccess = checkRoles(TestController.prototype, 'normalMethod', []);
|
|
227
|
+
expect(hasAccess).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('should handle default empty userRoles parameter', () => {
|
|
231
|
+
class TestController {
|
|
232
|
+
@Auth({ roles: ['admin'] })
|
|
233
|
+
public adminMethod(): void {}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// checkRoles 默认参数为空数组
|
|
237
|
+
const hasAccess = checkRoles(TestController.prototype, 'adminMethod');
|
|
238
|
+
expect(hasAccess).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
|
|
4
|
+
import { OAuth2Service } from '../../src/auth/oauth2';
|
|
5
|
+
import { JWTUtil } from '../../src/auth/jwt';
|
|
6
|
+
import type { OAuth2Client, UserInfo } from '../../src/auth/types';
|
|
7
|
+
|
|
8
|
+
describe('OAuth2Service', () => {
|
|
9
|
+
let jwtUtil: JWTUtil;
|
|
10
|
+
let oauth2Service: OAuth2Service;
|
|
11
|
+
let testClient: OAuth2Client;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jwtUtil = new JWTUtil({
|
|
15
|
+
secret: 'test-secret-key-for-oauth2-testing',
|
|
16
|
+
accessTokenExpiresIn: 3600,
|
|
17
|
+
refreshTokenExpiresIn: 86400,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
testClient = {
|
|
21
|
+
clientId: 'test-client',
|
|
22
|
+
clientSecret: 'test-secret',
|
|
23
|
+
redirectUris: ['http://localhost:3000/callback'],
|
|
24
|
+
grantTypes: ['authorization_code'],
|
|
25
|
+
scopes: ['read', 'write'],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
oauth2Service = new OAuth2Service(jwtUtil, [testClient]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('registerClient', () => {
|
|
32
|
+
test('should register a new client', () => {
|
|
33
|
+
const newClient: OAuth2Client = {
|
|
34
|
+
clientId: 'new-client',
|
|
35
|
+
clientSecret: 'new-secret',
|
|
36
|
+
redirectUris: ['http://example.com/callback'],
|
|
37
|
+
grantTypes: ['authorization_code'],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
oauth2Service.registerClient(newClient);
|
|
41
|
+
|
|
42
|
+
// Verify by trying to validate a request with this client
|
|
43
|
+
const result = oauth2Service.validateAuthorizationRequest({
|
|
44
|
+
clientId: 'new-client',
|
|
45
|
+
responseType: 'code',
|
|
46
|
+
redirectUri: 'http://example.com/callback',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(result.valid).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('validateAuthorizationRequest', () => {
|
|
54
|
+
test('should return valid for correct request', () => {
|
|
55
|
+
const result = oauth2Service.validateAuthorizationRequest({
|
|
56
|
+
clientId: 'test-client',
|
|
57
|
+
responseType: 'code',
|
|
58
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.valid).toBe(true);
|
|
62
|
+
expect(result.error).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should return invalid for unknown client', () => {
|
|
66
|
+
const result = oauth2Service.validateAuthorizationRequest({
|
|
67
|
+
clientId: 'unknown-client',
|
|
68
|
+
responseType: 'code',
|
|
69
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result.valid).toBe(false);
|
|
73
|
+
expect(result.error).toBe('invalid_client');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should return invalid for unsupported response type', () => {
|
|
77
|
+
const result = oauth2Service.validateAuthorizationRequest({
|
|
78
|
+
clientId: 'test-client',
|
|
79
|
+
responseType: 'token' as any,
|
|
80
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result.valid).toBe(false);
|
|
84
|
+
expect(result.error).toBe('unsupported_response_type');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('should return invalid for wrong redirect uri', () => {
|
|
88
|
+
const result = oauth2Service.validateAuthorizationRequest({
|
|
89
|
+
clientId: 'test-client',
|
|
90
|
+
responseType: 'code',
|
|
91
|
+
redirectUri: 'http://evil.com/callback',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result.valid).toBe(false);
|
|
95
|
+
expect(result.error).toBe('invalid_redirect_uri');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('generateAuthorizationCode', () => {
|
|
100
|
+
test('should generate a code', () => {
|
|
101
|
+
const code = oauth2Service.generateAuthorizationCode(
|
|
102
|
+
'test-client',
|
|
103
|
+
'http://localhost:3000/callback',
|
|
104
|
+
'user-123',
|
|
105
|
+
'read write',
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(code).toBeDefined();
|
|
109
|
+
expect(typeof code).toBe('string');
|
|
110
|
+
expect(code.length).toBe(32);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('should generate unique codes', () => {
|
|
114
|
+
const code1 = oauth2Service.generateAuthorizationCode(
|
|
115
|
+
'test-client',
|
|
116
|
+
'http://localhost:3000/callback',
|
|
117
|
+
'user-123',
|
|
118
|
+
);
|
|
119
|
+
const code2 = oauth2Service.generateAuthorizationCode(
|
|
120
|
+
'test-client',
|
|
121
|
+
'http://localhost:3000/callback',
|
|
122
|
+
'user-456',
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(code1).not.toBe(code2);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('exchangeCodeForToken', () => {
|
|
130
|
+
test('should exchange valid code for tokens', async () => {
|
|
131
|
+
const code = oauth2Service.generateAuthorizationCode(
|
|
132
|
+
'test-client',
|
|
133
|
+
'http://localhost:3000/callback',
|
|
134
|
+
'user-123',
|
|
135
|
+
'read',
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const result = await oauth2Service.exchangeCodeForToken({
|
|
139
|
+
grantType: 'authorization_code',
|
|
140
|
+
code,
|
|
141
|
+
clientId: 'test-client',
|
|
142
|
+
clientSecret: 'test-secret',
|
|
143
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(result).not.toBeNull();
|
|
147
|
+
expect(result?.accessToken).toBeDefined();
|
|
148
|
+
expect(result?.refreshToken).toBeDefined();
|
|
149
|
+
expect(result?.tokenType).toBe('Bearer');
|
|
150
|
+
expect(result?.scope).toBe('read');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('should return null for wrong grant type', async () => {
|
|
154
|
+
const result = await oauth2Service.exchangeCodeForToken({
|
|
155
|
+
grantType: 'client_credentials' as any,
|
|
156
|
+
code: 'some-code',
|
|
157
|
+
clientId: 'test-client',
|
|
158
|
+
clientSecret: 'test-secret',
|
|
159
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('should return null for invalid code', async () => {
|
|
166
|
+
const result = await oauth2Service.exchangeCodeForToken({
|
|
167
|
+
grantType: 'authorization_code',
|
|
168
|
+
code: 'invalid-code',
|
|
169
|
+
clientId: 'test-client',
|
|
170
|
+
clientSecret: 'test-secret',
|
|
171
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(result).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should return null for wrong client secret', async () => {
|
|
178
|
+
const code = oauth2Service.generateAuthorizationCode(
|
|
179
|
+
'test-client',
|
|
180
|
+
'http://localhost:3000/callback',
|
|
181
|
+
'user-123',
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const result = await oauth2Service.exchangeCodeForToken({
|
|
185
|
+
grantType: 'authorization_code',
|
|
186
|
+
code,
|
|
187
|
+
clientId: 'test-client',
|
|
188
|
+
clientSecret: 'wrong-secret',
|
|
189
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(result).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('should return null for wrong redirect uri', async () => {
|
|
196
|
+
const code = oauth2Service.generateAuthorizationCode(
|
|
197
|
+
'test-client',
|
|
198
|
+
'http://localhost:3000/callback',
|
|
199
|
+
'user-123',
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const result = await oauth2Service.exchangeCodeForToken({
|
|
203
|
+
grantType: 'authorization_code',
|
|
204
|
+
code,
|
|
205
|
+
clientId: 'test-client',
|
|
206
|
+
clientSecret: 'test-secret',
|
|
207
|
+
redirectUri: 'http://different.com/callback',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(result).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('should not allow code reuse', async () => {
|
|
214
|
+
const code = oauth2Service.generateAuthorizationCode(
|
|
215
|
+
'test-client',
|
|
216
|
+
'http://localhost:3000/callback',
|
|
217
|
+
'user-123',
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// First exchange should work
|
|
221
|
+
const result1 = await oauth2Service.exchangeCodeForToken({
|
|
222
|
+
grantType: 'authorization_code',
|
|
223
|
+
code,
|
|
224
|
+
clientId: 'test-client',
|
|
225
|
+
clientSecret: 'test-secret',
|
|
226
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(result1).not.toBeNull();
|
|
230
|
+
|
|
231
|
+
// Second exchange should fail
|
|
232
|
+
const result2 = await oauth2Service.exchangeCodeForToken({
|
|
233
|
+
grantType: 'authorization_code',
|
|
234
|
+
code,
|
|
235
|
+
clientId: 'test-client',
|
|
236
|
+
clientSecret: 'test-secret',
|
|
237
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(result2).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('refreshToken', () => {
|
|
245
|
+
test('should refresh token', async () => {
|
|
246
|
+
const code = oauth2Service.generateAuthorizationCode(
|
|
247
|
+
'test-client',
|
|
248
|
+
'http://localhost:3000/callback',
|
|
249
|
+
'user-123',
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const tokenResponse = await oauth2Service.exchangeCodeForToken({
|
|
253
|
+
grantType: 'authorization_code',
|
|
254
|
+
code,
|
|
255
|
+
clientId: 'test-client',
|
|
256
|
+
clientSecret: 'test-secret',
|
|
257
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(tokenResponse?.refreshToken).toBeDefined();
|
|
261
|
+
|
|
262
|
+
const refreshResult = await oauth2Service.refreshToken(tokenResponse!.refreshToken!);
|
|
263
|
+
|
|
264
|
+
expect(refreshResult).not.toBeNull();
|
|
265
|
+
expect(refreshResult?.accessToken).toBeDefined();
|
|
266
|
+
expect(refreshResult?.refreshToken).toBeDefined();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('should return null for invalid refresh token', async () => {
|
|
270
|
+
const result = await oauth2Service.refreshToken('invalid-token');
|
|
271
|
+
|
|
272
|
+
expect(result).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('with userProvider', () => {
|
|
277
|
+
test('should use userProvider for token generation', async () => {
|
|
278
|
+
const userProvider = async (userId: string): Promise<UserInfo | null> => {
|
|
279
|
+
if (userId === 'user-123') {
|
|
280
|
+
return {
|
|
281
|
+
id: userId,
|
|
282
|
+
username: 'alice',
|
|
283
|
+
roles: ['admin', 'user'],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const serviceWithProvider = new OAuth2Service(
|
|
290
|
+
jwtUtil,
|
|
291
|
+
[testClient],
|
|
292
|
+
{},
|
|
293
|
+
userProvider,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const code = serviceWithProvider.generateAuthorizationCode(
|
|
297
|
+
'test-client',
|
|
298
|
+
'http://localhost:3000/callback',
|
|
299
|
+
'user-123',
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const result = await serviceWithProvider.exchangeCodeForToken({
|
|
303
|
+
grantType: 'authorization_code',
|
|
304
|
+
code,
|
|
305
|
+
clientId: 'test-client',
|
|
306
|
+
clientSecret: 'test-secret',
|
|
307
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(result).not.toBeNull();
|
|
311
|
+
|
|
312
|
+
// Verify the token contains the user info
|
|
313
|
+
const decoded = jwtUtil.verify(result!.accessToken);
|
|
314
|
+
expect(decoded?.username).toBe('alice');
|
|
315
|
+
expect(decoded?.roles).toContain('admin');
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|