Skip to content

Commit 3f57cc1

Browse files
committed
Add comprehensive test suite for auth extension
1 parent d7aabfe commit 3f57cc1

3 files changed

Lines changed: 534 additions & 0 deletions

File tree

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* @file Tests for JWTAuthAdapter
3+
*/
4+
5+
import { describe, it, expect, beforeEach } from 'vitest';
6+
import { JWTAuthAdapter } from '../src/adapters/JWTAuthAdapter.js';
7+
8+
describe('JWTAuthAdapter', () => {
9+
let auth: JWTAuthAdapter;
10+
11+
beforeEach(() => {
12+
auth = new JWTAuthAdapter({
13+
jwtSecret: 'test-secret-key-for-testing',
14+
jwtExpiresIn: '1h',
15+
bcryptSaltRounds: 4, // Lower for faster tests
16+
});
17+
});
18+
19+
describe('Token Generation', () => {
20+
it('should generate a valid JWT token', () => {
21+
const token = auth.generateToken('user123');
22+
expect(token).toBeTruthy();
23+
expect(typeof token).toBe('string');
24+
expect(token.split('.')).toHaveLength(3); // JWT has 3 parts
25+
});
26+
27+
it('should include additional claims in token', () => {
28+
const token = auth.generateToken('user123', {
29+
email: 'test@example.com',
30+
roles: ['admin'],
31+
tier: 'pro',
32+
});
33+
34+
const decoded = JSON.parse(
35+
Buffer.from(token.split('.')[1], 'base64').toString()
36+
);
37+
38+
expect(decoded.sub).toBe('user123');
39+
expect(decoded.email).toBe('test@example.com');
40+
expect(decoded.roles).toEqual(['admin']);
41+
expect(decoded.tier).toBe('pro');
42+
});
43+
});
44+
45+
describe('Token Validation', () => {
46+
it('should validate a valid token', async () => {
47+
const token = auth.generateToken('user123', {
48+
email: 'test@example.com',
49+
tier: 'pro',
50+
});
51+
52+
const user = await auth.validateToken(token);
53+
54+
expect(user).toBeTruthy();
55+
expect(user?.id).toBe('user123');
56+
expect(user?.email).toBe('test@example.com');
57+
expect(user?.tier).toBe('pro');
58+
});
59+
60+
it('should reject an invalid token', async () => {
61+
const user = await auth.validateToken('invalid.token.here');
62+
expect(user).toBeNull();
63+
});
64+
65+
it('should reject an empty token', async () => {
66+
const user = await auth.validateToken('');
67+
expect(user).toBeNull();
68+
});
69+
70+
it('should reject a token signed with wrong secret', async () => {
71+
const otherAuth = new JWTAuthAdapter({ jwtSecret: 'different-secret' });
72+
const token = otherAuth.generateToken('user123');
73+
74+
const user = await auth.validateToken(token);
75+
expect(user).toBeNull();
76+
});
77+
});
78+
79+
describe('Token Revocation', () => {
80+
it('should revoke a token', async () => {
81+
const token = auth.generateToken('user123');
82+
83+
// Token should be valid initially
84+
const user1 = await auth.validateToken(token);
85+
expect(user1).toBeTruthy();
86+
87+
// Revoke the token
88+
await auth.revokeToken(token);
89+
90+
// Token should now be invalid
91+
const user2 = await auth.validateToken(token);
92+
expect(user2).toBeNull();
93+
});
94+
});
95+
96+
describe('Token Refresh', () => {
97+
it('should refresh a token within refresh window', async () => {
98+
// Create auth with short expiry for testing
99+
const shortAuth = new JWTAuthAdapter({
100+
jwtSecret: 'test-secret',
101+
jwtExpiresIn: '10s',
102+
refreshWindow: 20, // 20 seconds
103+
enableTokenRefresh: true,
104+
});
105+
106+
const originalToken = shortAuth.generateToken('user123', {
107+
email: 'test@example.com',
108+
});
109+
110+
const newToken = await shortAuth.refreshToken(originalToken);
111+
112+
expect(newToken).toBeTruthy();
113+
expect(newToken).not.toBe(originalToken);
114+
115+
// New token should be valid
116+
const user = await shortAuth.validateToken(newToken!);
117+
expect(user?.id).toBe('user123');
118+
expect(user?.email).toBe('test@example.com');
119+
});
120+
121+
it('should not refresh when disabled', async () => {
122+
const noRefreshAuth = new JWTAuthAdapter({
123+
jwtSecret: 'test-secret',
124+
enableTokenRefresh: false,
125+
});
126+
127+
const token = noRefreshAuth.generateToken('user123');
128+
const refreshed = await noRefreshAuth.refreshToken(token);
129+
130+
expect(refreshed).toBeNull();
131+
});
132+
});
133+
134+
describe('Password Hashing', () => {
135+
it('should hash a password', async () => {
136+
const password = 'secure-password-123';
137+
const hash = await auth.hashPassword(password);
138+
139+
expect(hash).toBeTruthy();
140+
expect(hash).not.toBe(password);
141+
expect(hash.startsWith('$2')).toBe(true); // BCrypt hashes start with $2
142+
});
143+
144+
it('should verify correct password', async () => {
145+
const password = 'secure-password-123';
146+
const hash = await auth.hashPassword(password);
147+
148+
const isValid = await auth.verifyPassword(password, hash);
149+
expect(isValid).toBe(true);
150+
});
151+
152+
it('should reject incorrect password', async () => {
153+
const password = 'secure-password-123';
154+
const hash = await auth.hashPassword(password);
155+
156+
const isValid = await auth.verifyPassword('wrong-password', hash);
157+
expect(isValid).toBe(false);
158+
});
159+
160+
it('should handle invalid hash gracefully', async () => {
161+
const isValid = await auth.verifyPassword('password', 'invalid-hash');
162+
expect(isValid).toBe(false);
163+
});
164+
});
165+
166+
describe('Initialization', () => {
167+
it('should initialize with config', async () => {
168+
const newAuth = new JWTAuthAdapter({ jwtSecret: 'initial-secret' });
169+
170+
await newAuth.initialize({ jwtSecret: 'updated-secret' });
171+
172+
// Should use updated secret
173+
const token = newAuth.generateToken('user123');
174+
expect(token).toBeTruthy();
175+
});
176+
});
177+
});
178+
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* @file Tests for SubscriptionAdapter
3+
*/
4+
5+
import { describe, it, expect, beforeEach } from 'vitest';
6+
import { SubscriptionAdapter } from '../src/adapters/SubscriptionAdapter.js';
7+
8+
describe('SubscriptionAdapter', () => {
9+
let subscriptions: SubscriptionAdapter;
10+
11+
beforeEach(() => {
12+
subscriptions = new SubscriptionAdapter({
13+
defaultTier: 'free',
14+
tiers: [
15+
{ name: 'free', level: 0, features: [], isActive: true },
16+
{ name: 'basic', level: 1, features: ['FEATURE_A'], isActive: true },
17+
{ name: 'pro', level: 2, features: ['FEATURE_A', 'FEATURE_B'], isActive: true },
18+
{ name: 'enterprise', level: 3, features: ['FEATURE_A', 'FEATURE_B', 'FEATURE_C'], isActive: true },
19+
],
20+
});
21+
});
22+
23+
describe('User Tier Management', () => {
24+
it('should return default tier for new user', async () => {
25+
const tier = await subscriptions.getUserSubscription('user123');
26+
27+
expect(tier).toBeTruthy();
28+
expect(tier?.name).toBe('free');
29+
expect(tier?.level).toBe(0);
30+
});
31+
32+
it('should set and get user tier', async () => {
33+
subscriptions.setUserTier('user123', 'pro');
34+
35+
const tier = await subscriptions.getUserSubscription('user123');
36+
expect(tier?.name).toBe('pro');
37+
expect(tier?.level).toBe(2);
38+
});
39+
40+
it('should throw error for invalid tier', () => {
41+
expect(() => {
42+
subscriptions.setUserTier('user123', 'invalid-tier');
43+
}).toThrow();
44+
});
45+
46+
it('should return null for empty userId', async () => {
47+
const tier = await subscriptions.getUserSubscription('');
48+
expect(tier).toBeNull();
49+
});
50+
});
51+
52+
describe('Tier Information', () => {
53+
it('should get tier by name', async () => {
54+
const tier = await subscriptions.getTierByName('pro');
55+
56+
expect(tier).toBeTruthy();
57+
expect(tier?.name).toBe('pro');
58+
expect(tier?.level).toBe(2);
59+
expect(tier?.features).toContain('FEATURE_A');
60+
expect(tier?.features).toContain('FEATURE_B');
61+
});
62+
63+
it('should return null for unknown tier', async () => {
64+
const tier = await subscriptions.getTierByName('unknown');
65+
expect(tier).toBeNull();
66+
});
67+
68+
it('should list all tiers in order', async () => {
69+
const tiers = await subscriptions.listTiers();
70+
71+
expect(tiers).toHaveLength(4);
72+
expect(tiers[0].name).toBe('free');
73+
expect(tiers[1].name).toBe('basic');
74+
expect(tiers[2].name).toBe('pro');
75+
expect(tiers[3].name).toBe('enterprise');
76+
77+
// Check they're sorted by level
78+
for (let i = 1; i < tiers.length; i++) {
79+
expect(tiers[i].level).toBeGreaterThan(tiers[i - 1].level);
80+
}
81+
});
82+
});
83+
84+
describe('Feature Access', () => {
85+
it('should allow access to feature in tier', async () => {
86+
subscriptions.setUserTier('user123', 'pro');
87+
88+
const hasFeatureA = await subscriptions.validateAccess('user123', 'FEATURE_A');
89+
const hasFeatureB = await subscriptions.validateAccess('user123', 'FEATURE_B');
90+
91+
expect(hasFeatureA).toBe(true);
92+
expect(hasFeatureB).toBe(true);
93+
});
94+
95+
it('should deny access to feature not in tier', async () => {
96+
subscriptions.setUserTier('user123', 'basic');
97+
98+
const hasFeatureB = await subscriptions.validateAccess('user123', 'FEATURE_B');
99+
expect(hasFeatureB).toBe(false);
100+
});
101+
102+
it('should deny access for user with no tier', async () => {
103+
const hasFeature = await subscriptions.validateAccess('', 'FEATURE_A');
104+
expect(hasFeature).toBe(false);
105+
});
106+
});
107+
108+
describe('Tier Comparison', () => {
109+
it('should validate user meets minimum tier', async () => {
110+
subscriptions.setUserTier('user123', 'pro');
111+
112+
const meetsBasic = await subscriptions.validateTierAccess('user123', 'basic');
113+
expect(meetsBasic).toBe(true);
114+
115+
const meetsPro = await subscriptions.validateTierAccess('user123', 'pro');
116+
expect(meetsPro).toBe(true);
117+
});
118+
119+
it('should reject user below minimum tier', async () => {
120+
subscriptions.setUserTier('user123', 'basic');
121+
122+
const meetsPro = await subscriptions.validateTierAccess('user123', 'pro');
123+
expect(meetsPro).toBe(false);
124+
});
125+
126+
it('should handle unknown minimum tier', async () => {
127+
subscriptions.setUserTier('user123', 'pro');
128+
129+
const result = await subscriptions.validateTierAccess('user123', 'unknown');
130+
expect(result).toBe(false);
131+
});
132+
});
133+
134+
describe('Tier Management', () => {
135+
it('should add new tier', async () => {
136+
subscriptions.addTier({
137+
name: 'custom',
138+
level: 4,
139+
features: ['FEATURE_CUSTOM'],
140+
isActive: true,
141+
});
142+
143+
const tier = await subscriptions.getTierByName('custom');
144+
expect(tier).toBeTruthy();
145+
expect(tier?.level).toBe(4);
146+
});
147+
148+
it('should update existing tier', async () => {
149+
subscriptions.addTier({
150+
name: 'pro',
151+
level: 2,
152+
features: ['FEATURE_A', 'FEATURE_B', 'FEATURE_NEW'],
153+
isActive: true,
154+
});
155+
156+
const tier = await subscriptions.getTierByName('pro');
157+
expect(tier?.features).toContain('FEATURE_NEW');
158+
});
159+
});
160+
161+
describe('Initialization', () => {
162+
it('should initialize successfully', async () => {
163+
await expect(subscriptions.initialize()).resolves.not.toThrow();
164+
});
165+
});
166+
});
167+

0 commit comments

Comments
 (0)