forked from Nova-reward/Nova-Rewards
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuserRepository.test.js
More file actions
309 lines (256 loc) · 11.2 KB
/
Copy pathuserRepository.test.js
File metadata and controls
309 lines (256 loc) · 11.2 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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
// Unit tests for userRepository
jest.mock('../db/index', () => ({ query: jest.fn() }));
const { query } = require('../db/index');
const repo = require('../db/userRepository');
const { UserFactory, STELLAR_ADDRESSES } = require('./fixtures');
beforeEach(() => jest.clearAllMocks());
describe('getUserByWallet', () => {
test('returns user when found', async () => {
const user = UserFactory.build();
query.mockResolvedValue({ rows: [user] });
expect(await repo.getUserByWallet(user.wallet_address)).toEqual(user);
});
test('returns null when not found', async () => {
query.mockResolvedValue({ rows: [] });
expect(await repo.getUserByWallet(STELLAR_ADDRESSES[0])).toBeNull();
});
});
describe('getUserById', () => {
test('returns user when found', async () => {
const user = UserFactory.build();
query.mockResolvedValue({ rows: [user] });
expect(await repo.getUserById(user.id)).toEqual(user);
});
test('returns null when not found', async () => {
query.mockResolvedValue({ rows: [] });
expect(await repo.getUserById(999)).toBeNull();
});
});
describe('createUser', () => {
test('creates user without referral', async () => {
const user = UserFactory.build({ referred_by: null });
query.mockResolvedValue({ rows: [user] });
const result = await repo.createUser({ walletAddress: user.wallet_address });
expect(result).toEqual(user);
expect(query.mock.calls[0][1][1]).toBeNull(); // referredBy = null
});
test('creates user with referral', async () => {
const referrer = UserFactory.build();
const user = UserFactory.build({ referred_by: referrer.id });
query.mockResolvedValue({ rows: [user] });
const result = await repo.createUser({ walletAddress: user.wallet_address, referredBy: referrer.id });
expect(result).toEqual(user);
expect(query.mock.calls[0][1][1]).toBe(referrer.id);
});
});
describe('markReferralBonusClaimed', () => {
test('updates and returns user', async () => {
const user = UserFactory.build({ referral_bonus_claimed: true });
query.mockResolvedValue({ rows: [user] });
expect(await repo.markReferralBonusClaimed(user.id)).toEqual(user);
});
});
describe('getReferredUsers', () => {
test('returns list of referred users', async () => {
const referrer = UserFactory.build();
const referred = UserFactory.buildList(2, { referred_by: referrer.id });
query.mockResolvedValue({ rows: referred });
expect(await repo.getReferredUsers(referrer.id)).toEqual(referred);
});
});
describe('getReferralPointsEarned', () => {
test('returns total as string', async () => {
query.mockResolvedValue({ rows: [{ total: '200' }] });
expect(await repo.getReferralPointsEarned(1)).toBe('200');
});
});
describe('hasReferralBonusBeenClaimed', () => {
test('returns true when row exists', async () => {
const user = UserFactory.build();
query.mockResolvedValue({ rows: [{ id: user.id }] });
expect(await repo.hasReferralBonusBeenClaimed(1, 2)).toBe(true);
});
test('returns false when no row', async () => {
query.mockResolvedValue({ rows: [] });
expect(await repo.hasReferralBonusBeenClaimed(1, 2)).toBe(false);
});
});
describe('getUnprocessedReferrals', () => {
test('returns unprocessed referrals', async () => {
const referrer = UserFactory.build();
const rows = [UserFactory.build({ referred_by: referrer.id })];
query.mockResolvedValue({ rows });
expect(await repo.getUnprocessedReferrals(24)).toEqual(rows);
});
});
describe('profile functions', () => {
test('findById returns user', async () => {
const user = UserFactory.build();
query.mockResolvedValue({ rows: [user] });
expect(await repo.findById(user.id)).toEqual(user);
});
test('findById returns null when not found', async () => {
query.mockResolvedValue({ rows: [] });
expect(await repo.findById(999)).toBeNull();
});
test('findByWalletAddress returns user', async () => {
const user = UserFactory.build();
query.mockResolvedValue({ rows: [user] });
expect(await repo.findByWalletAddress(user.wallet_address)).toEqual(user);
});
test('getPublicProfile returns limited fields', async () => {
const user = UserFactory.build();
const profile = { id: user.id, first_name: user.first_name };
query.mockResolvedValue({ rows: [profile] });
expect(await repo.getPublicProfile(user.id)).toEqual(profile);
});
test('getPrivateProfile returns all fields', async () => {
const user = UserFactory.build();
query.mockResolvedValue({ rows: [user] });
expect(await repo.getPrivateProfile(user.id)).toEqual(user);
});
test('update returns updated user', async () => {
const user = UserFactory.build();
const updated = { ...user, first_name: 'Jane' };
query.mockResolvedValue({ rows: [updated] });
expect(await repo.update(user.id, { first_name: 'Jane' })).toEqual(updated);
});
test('update with no valid fields calls findById', async () => {
const user = UserFactory.build();
query.mockResolvedValue({ rows: [user] });
const result = await repo.update(user.id, {});
expect(result).toEqual(user);
});
test('softDelete returns true on success', async () => {
query.mockResolvedValue({ rowCount: 1 });
expect(await repo.softDelete(1)).toBe(true);
});
test('softDelete returns false when user not found', async () => {
query.mockResolvedValue({ rowCount: 0 });
expect(await repo.softDelete(999)).toBe(false);
});
test('exists returns true when user found', async () => {
query.mockResolvedValue({ rows: [{ 1: 1 }] });
expect(await repo.exists(1)).toBe(true);
});
test('exists returns false when not found', async () => {
query.mockResolvedValue({ rows: [] });
expect(await repo.exists(999)).toBe(false);
});
test('isAdmin returns true for admin role', async () => {
query.mockResolvedValue({ rows: [{ role: 'admin' }] });
expect(await repo.isAdmin(1)).toBe(true);
});
test('isAdmin returns false for non-admin', async () => {
const user = UserFactory.build({ role: 'user' });
query.mockResolvedValue({ rows: [{ role: user.role }] });
expect(await repo.isAdmin(user.id)).toBe(false);
});
});
// ---------------------------------------------------------------------------
// Task 6.2 — Unit tests: updateUser ignores role, updateUserRole only updates role
// ---------------------------------------------------------------------------
describe('updateUser — role field exclusion (Requirements 5.3)', () => {
test('ignores role key and does not include role = in SQL', async () => {
const updated = { id: 1, first_name: 'Alice', role: 'user' };
query.mockResolvedValue({ rows: [updated] });
await repo.update(1, { first_name: 'Alice', role: 'admin' });
const sql = query.mock.calls[0][0];
expect(sql).not.toMatch(/role\s*=/i);
});
test('still updates allowed fields when role is mixed in', async () => {
const updated = { id: 1, last_name: 'Smith', role: 'user' };
query.mockResolvedValue({ rows: [updated] });
const result = await repo.update(1, { last_name: 'Smith', role: 'admin' });
const sql = query.mock.calls[0][0];
expect(sql).toMatch(/last_name\s*=/i);
expect(sql).not.toMatch(/role\s*=/i);
expect(result).toEqual(updated);
});
test('returns findById result when only role is provided (no valid fields)', async () => {
const user = { id: 1, role: 'user' };
query.mockResolvedValue({ rows: [user] });
const result = await repo.update(1, { role: 'admin' });
// Should have called findById (one query), not an UPDATE
const sql = query.mock.calls[0][0];
expect(sql).not.toMatch(/UPDATE/i);
expect(result).toEqual(user);
});
});
describe('updateUserRole (Requirements 6.2)', () => {
test('executes UPDATE with only role column', async () => {
const updated = { id: 1, role: 'merchant', updated_at: new Date() };
query.mockResolvedValue({ rows: [updated] });
const result = await repo.updateUserRole(1, 'merchant');
const sql = query.mock.calls[0][0];
expect(sql).toMatch(/UPDATE users/i);
expect(sql).toMatch(/SET role = \$1/i);
// Should not touch other columns
expect(sql).not.toMatch(/first_name/i);
expect(sql).not.toMatch(/last_name/i);
expect(result).toEqual(updated);
});
test('returns null when user not found', async () => {
query.mockResolvedValue({ rows: [] });
const result = await repo.updateUserRole(999, 'user');
expect(result).toBeNull();
});
test('uses trx.query when transaction client is provided', async () => {
const updated = { id: 1, role: 'user', updated_at: new Date() };
const trx = { query: jest.fn().mockResolvedValue({ rows: [updated] }) };
const result = await repo.updateUserRole(1, 'user', trx);
// The shared pool query should NOT have been called
expect(query).not.toHaveBeenCalled();
// The transaction client query SHOULD have been called
expect(trx.query).toHaveBeenCalledTimes(1);
const [sql, params] = trx.query.mock.calls[0];
expect(sql).toMatch(/UPDATE users/i);
expect(sql).toMatch(/SET role = \$1/i);
expect(params).toEqual(['user', 1]);
expect(result).toEqual(updated);
});
test('falls back to shared query when trx is undefined', async () => {
const updated = { id: 1, role: 'merchant', updated_at: new Date() };
query.mockResolvedValue({ rows: [updated] });
await repo.updateUserRole(1, 'merchant', undefined);
expect(query).toHaveBeenCalledTimes(1);
});
});
// ---------------------------------------------------------------------------
// Task 6.1 — Property test: updateUser never persists a role change (Property 5)
// Feature: admin-auth-privilege-escalation, Property 5: updateUser repository never persists a role change
// Validates: Requirements 5.3
// ---------------------------------------------------------------------------
const fc = require('fast-check');
describe('Property 5: updateUser repository never persists a role change', () => {
test('SQL never contains role = for any update object that includes a role key', () => {
// Capture the SQL text passed to query for each generated input
query.mockImplementation((sql) => Promise.resolve({ rows: [{ id: 1 }] }));
fc.assert(
fc.asyncProperty(
fc.record({
first_name: fc.option(fc.string({ minLength: 1, maxLength: 30 }), { nil: undefined }),
last_name: fc.option(fc.string({ minLength: 1, maxLength: 30 }), { nil: undefined }),
bio: fc.option(fc.string({ minLength: 0, maxLength: 200 }), { nil: undefined }),
stellar_public_key: fc.option(fc.string({ minLength: 1, maxLength: 60 }), { nil: undefined }),
role: fc.oneof(
fc.constantFrom('admin', 'user', 'merchant'),
fc.string({ minLength: 1, maxLength: 20 })
),
}),
async (updates) => {
jest.clearAllMocks();
query.mockResolvedValue({ rows: [{ id: 1 }] });
await repo.update(1, updates);
// If query was called (i.e., there were valid fields besides role),
// the SQL must not contain a role assignment.
if (query.mock.calls.length > 0) {
const sql = query.mock.calls[0][0];
expect(sql).not.toMatch(/\brole\s*=/i);
}
}
),
{ numRuns: 100 }
);
});
});