ryauth 1.0.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.
@@ -0,0 +1,202 @@
1
+ import { describe, it, expect, beforeAll, afterAll, jest } from '@jest/globals';
2
+ import { hashPassword, verifyPassword, signAccessToken, signRefreshToken, verifyJWT } from '../src/core/crypto.js';
3
+ import argon2 from 'argon2';
4
+
5
+ // Mock environment variables
6
+ const originalEnv = process.env;
7
+
8
+ beforeAll(() => {
9
+ // Set up test secrets
10
+ process.env.ACCESS_TOKEN_SECRET = 'test_access_secret_32_characters_long_1234567890';
11
+ process.env.REFRESH_TOKEN_SECRET = 'test_refresh_secret_32_characters_long_1234567890';
12
+ });
13
+
14
+ afterAll(() => {
15
+ process.env = originalEnv;
16
+ });
17
+
18
+ describe('Core Crypto Module - Password Hashing', () => {
19
+ describe('hashPassword', () => {
20
+ it('should return a valid Argon2 string', async () => {
21
+ const password = 'securePassword123!';
22
+ const hash = await hashPassword(password);
23
+
24
+ // Verify it's a valid Argon2 hash
25
+ expect(typeof hash).toBe('string');
26
+ expect(hash).toBeTruthy();
27
+
28
+ // Verify argon2 can verify it
29
+ const isValid = await argon2.verify(hash, password);
30
+ expect(isValid).toBe(true);
31
+ });
32
+
33
+ it('should throw error if input is not a string', async () => {
34
+ await expect(hashPassword(123)).rejects.toThrow('Password must be a string');
35
+ await expect(hashPassword(null)).rejects.toThrow('Password must be a string');
36
+ await expect(hashPassword(undefined)).rejects.toThrow('Password must be a string');
37
+ await expect(hashPassword({})).rejects.toThrow('Password must be a string');
38
+ await expect(hashPassword([])).rejects.toThrow('Password must be a string');
39
+ });
40
+ });
41
+
42
+ describe('verifyPassword', () => {
43
+ let testHash;
44
+
45
+ beforeAll(async () => {
46
+ testHash = await hashPassword('testPassword123');
47
+ });
48
+
49
+ it('should return true for correct credentials using timing-safe comparison', async () => {
50
+ const result = await verifyPassword(testHash, 'testPassword123');
51
+ expect(result).toBe(true);
52
+ });
53
+
54
+ it('should return false for incorrect credentials', async () => {
55
+ const result = await verifyPassword(testHash, 'wrongPassword');
56
+ expect(result).toBe(false);
57
+ });
58
+
59
+ it('should throw error if hash is not a string', async () => {
60
+ await expect(verifyPassword(123, 'password')).rejects.toThrow('Hash and plain text must be strings');
61
+ await expect(verifyPassword(null, 'password')).rejects.toThrow('Hash and plain text must be strings');
62
+ });
63
+
64
+ it('should throw error if plain text is not a string', async () => {
65
+ await expect(verifyPassword(testHash, 123)).rejects.toThrow('Hash and plain text must be strings');
66
+ await expect(verifyPassword(testHash, null)).rejects.toThrow('Hash and plain text must be strings');
67
+ });
68
+ });
69
+ });
70
+
71
+ describe('Core Crypto Module - JWT Management', () => {
72
+ const testPayload = {
73
+ userId: '12345',
74
+ email: 'test@example.com',
75
+ roles: ['user']
76
+ };
77
+
78
+ describe('signAccessToken', () => {
79
+ it('should generate a JWT with a 15-minute expiry', async () => {
80
+ const token = await signAccessToken(testPayload);
81
+
82
+ expect(typeof token).toBe('string');
83
+ expect(token).toBeTruthy();
84
+
85
+ // Verify the token can be decoded
86
+ const payload = await verifyJWT(token, 'access');
87
+ expect(payload.userId).toBe(testPayload.userId);
88
+ expect(payload.email).toBe(testPayload.email);
89
+ });
90
+
91
+ it('should throw error if ACCESS_TOKEN_SECRET is missing', async () => {
92
+ const originalSecret = process.env.ACCESS_TOKEN_SECRET;
93
+ delete process.env.ACCESS_TOKEN_SECRET;
94
+
95
+ await expect(signAccessToken(testPayload)).rejects.toThrow('ACCESS_TOKEN_SECRET must be at least 32 characters');
96
+
97
+ process.env.ACCESS_TOKEN_SECRET = originalSecret;
98
+ });
99
+
100
+ it('should throw error if ACCESS_TOKEN_SECRET is less than 32 characters', async () => {
101
+ const originalSecret = process.env.ACCESS_TOKEN_SECRET;
102
+ process.env.ACCESS_TOKEN_SECRET = 'short';
103
+
104
+ await expect(signAccessToken(testPayload)).rejects.toThrow('ACCESS_TOKEN_SECRET must be at least 32 characters');
105
+
106
+ process.env.ACCESS_TOKEN_SECRET = originalSecret;
107
+ });
108
+ });
109
+
110
+ describe('signRefreshToken', () => {
111
+ it('should generate a JWT with a 7-day expiry', async () => {
112
+ const token = await signRefreshToken(testPayload);
113
+
114
+ expect(typeof token).toBe('string');
115
+ expect(token).toBeTruthy();
116
+
117
+ // Verify the token can be decoded
118
+ const payload = await verifyJWT(token, 'refresh');
119
+ expect(payload.userId).toBe(testPayload.userId);
120
+ expect(payload.email).toBe(testPayload.email);
121
+ });
122
+
123
+ it('should throw error if REFRESH_TOKEN_SECRET is missing', async () => {
124
+ const originalSecret = process.env.REFRESH_TOKEN_SECRET;
125
+ delete process.env.REFRESH_TOKEN_SECRET;
126
+
127
+ await expect(signRefreshToken(testPayload)).rejects.toThrow('REFRESH_TOKEN_SECRET must be at least 32 characters');
128
+
129
+ process.env.REFRESH_TOKEN_SECRET = originalSecret;
130
+ });
131
+
132
+ it('should throw error if REFRESH_TOKEN_SECRET is less than 32 characters', async () => {
133
+ const originalSecret = process.env.REFRESH_TOKEN_SECRET;
134
+ process.env.REFRESH_TOKEN_SECRET = 'short';
135
+
136
+ await expect(signRefreshToken(testPayload)).rejects.toThrow('REFRESH_TOKEN_SECRET must be at least 32 characters');
137
+
138
+ process.env.REFRESH_TOKEN_SECRET = originalSecret;
139
+ });
140
+ });
141
+
142
+ describe('verifyJWT', () => {
143
+ let accessToken;
144
+ let refreshToken;
145
+
146
+ beforeAll(async () => {
147
+ accessToken = await signAccessToken(testPayload);
148
+ refreshToken = await signRefreshToken(testPayload);
149
+ });
150
+
151
+ it('should successfully decode access tokens', async () => {
152
+ const payload = await verifyJWT(accessToken, 'access');
153
+ expect(payload.userId).toBe(testPayload.userId);
154
+ expect(payload.email).toBe(testPayload.email);
155
+ expect(payload.roles).toEqual(testPayload.roles);
156
+ });
157
+
158
+ it('should successfully decode refresh tokens', async () => {
159
+ const payload = await verifyJWT(refreshToken, 'refresh');
160
+ expect(payload.userId).toBe(testPayload.userId);
161
+ expect(payload.email).toBe(testPayload.email);
162
+ expect(payload.roles).toEqual(testPayload.roles);
163
+ });
164
+
165
+ it('should throw error if token is used for the wrong type (type-check validation)', async () => {
166
+ // Try to verify access token as refresh token
167
+ await expect(verifyJWT(accessToken, 'refresh')).rejects.toThrow('Invalid token');
168
+
169
+ // Try to verify refresh token as access token
170
+ await expect(verifyJWT(refreshToken, 'access')).rejects.toThrow('Invalid token');
171
+ });
172
+
173
+ it('should throw error if token is invalid', async () => {
174
+ await expect(verifyJWT('invalid.token.here', 'access')).rejects.toThrow('Invalid token');
175
+ await expect(verifyJWT('', 'access')).rejects.toThrow('Invalid token');
176
+ });
177
+
178
+ it('should throw error if token is not a string', async () => {
179
+ await expect(verifyJWT(123, 'access')).rejects.toThrow('Token must be a string');
180
+ await expect(verifyJWT(null, 'access')).rejects.toThrow('Token must be a string');
181
+ await expect(verifyJWT(undefined, 'access')).rejects.toThrow('Token must be a string');
182
+ });
183
+
184
+ it('should throw error if ACCESS_TOKEN_SECRET is missing during verification', async () => {
185
+ const originalSecret = process.env.ACCESS_TOKEN_SECRET;
186
+ delete process.env.ACCESS_TOKEN_SECRET;
187
+
188
+ await expect(verifyJWT(accessToken, 'access')).rejects.toThrow('ACCESS_TOKEN_SECRET must be at least 32 characters');
189
+
190
+ process.env.ACCESS_TOKEN_SECRET = originalSecret;
191
+ });
192
+
193
+ it('should throw error if REFRESH_TOKEN_SECRET is missing during verification', async () => {
194
+ const originalSecret = process.env.REFRESH_TOKEN_SECRET;
195
+ delete process.env.REFRESH_TOKEN_SECRET;
196
+
197
+ await expect(verifyJWT(refreshToken, 'refresh')).rejects.toThrow('REFRESH_TOKEN_SECRET must be at least 32 characters');
198
+
199
+ process.env.REFRESH_TOKEN_SECRET = originalSecret;
200
+ });
201
+ });
202
+ });