Skip to content

Commit f17ecff

Browse files
committed
feat: improve test coverage
1 parent fc1cc58 commit f17ecff

72 files changed

Lines changed: 24904 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/__tests__/unit/reputation.test.ts

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ jest.unstable_mockModule('../../src/utils/config.js', () => ({
4040
},
4141
}));
4242

43-
const { calculateSenpaiScore, calculateSenseiScore } =
43+
const { calculateSenpaiScore, calculateSenseiScore, checkPromotion, getUserStats } =
4444
await import('../../src/services/reputation.js');
4545

46+
import { Role } from '../../src/types.js';
47+
4648
describe('Reputation Service', () => {
4749
beforeEach(() => {
4850
jest.clearAllMocks();
@@ -289,4 +291,209 @@ describe('Reputation Service', () => {
289291
expect(score.uniqueRequired).toBe(2); // CEIL(6 * 0.20) = 2
290292
});
291293
});
294+
295+
describe('checkPromotion', () => {
296+
test('should promote Kohai to Senpai when thresholds met', async () => {
297+
const guild = createMockGuild();
298+
const mockMember = {
299+
id: 'user-1',
300+
user: { id: 'user-1', tag: 'User#0001' },
301+
};
302+
guild.members.cache.set('user-1', mockMember);
303+
304+
mockGetUserRole.mockReturnValue(Role.Kohai);
305+
mockGetRoleCounts.mockReturnValue({ kohai: 50, senpai: 70, sensei: 30 });
306+
mockGetReactionCount.mockResolvedValue(60);
307+
mockGetUniqueReactors.mockResolvedValue(Array.from({ length: 15 }, (_, i) => `reactor-${i}`));
308+
mockAssignRole.mockResolvedValue(undefined);
309+
mockInsertRoleHistory.mockResolvedValue(undefined);
310+
mockSendDM.mockResolvedValue(true);
311+
312+
const result = await checkPromotion(guild as any, 'user-1');
313+
314+
expect(result.promoted).toBe(true);
315+
expect(result.oldRole).toBe(Role.Kohai);
316+
expect(result.newRole).toBe(Role.Senpai);
317+
expect(mockAssignRole).toHaveBeenCalledWith(guild, 'user-1', Role.Senpai);
318+
expect(mockInsertRoleHistory).toHaveBeenCalledWith('user-1', Role.Senpai, 'promotion');
319+
expect(mockSendDM).toHaveBeenCalled();
320+
});
321+
322+
test('should promote Senpai to Sensei when thresholds met', async () => {
323+
const guild = createMockGuild();
324+
const mockMember = {
325+
id: 'user-1',
326+
user: { id: 'user-1', tag: 'User#0001' },
327+
};
328+
guild.members.cache.set('user-1', mockMember);
329+
330+
mockGetUserRole.mockReturnValue(Role.Senpai);
331+
mockGetRoleCounts.mockReturnValue({ kohai: 50, senpai: 70, sensei: 50 });
332+
mockGetReactionCount.mockResolvedValue(35);
333+
mockGetUniqueReactors.mockResolvedValue(Array.from({ length: 12 }, (_, i) => `sensei-${i}`));
334+
mockAssignRole.mockResolvedValue(undefined);
335+
mockInsertRoleHistory.mockResolvedValue(undefined);
336+
mockSendDM.mockResolvedValue(true);
337+
338+
const result = await checkPromotion(guild as any, 'user-1');
339+
340+
expect(result.promoted).toBe(true);
341+
expect(result.oldRole).toBe(Role.Senpai);
342+
expect(result.newRole).toBe(Role.Sensei);
343+
expect(mockAssignRole).toHaveBeenCalledWith(guild, 'user-1', Role.Sensei);
344+
expect(mockInsertRoleHistory).toHaveBeenCalledWith('user-1', Role.Sensei, 'promotion');
345+
});
346+
347+
test('should not promote Kohai when threshold not met', async () => {
348+
const guild = createMockGuild();
349+
350+
mockGetUserRole.mockReturnValue(Role.Kohai);
351+
mockGetRoleCounts.mockReturnValue({ kohai: 50, senpai: 70, sensei: 30 });
352+
mockGetReactionCount.mockResolvedValue(40); // Below 50 threshold
353+
mockGetUniqueReactors.mockResolvedValue(Array.from({ length: 15 }, (_, i) => `reactor-${i}`));
354+
355+
const result = await checkPromotion(guild as any, 'user-1');
356+
357+
expect(result.promoted).toBe(false);
358+
expect(mockAssignRole).not.toHaveBeenCalled();
359+
expect(mockInsertRoleHistory).not.toHaveBeenCalled();
360+
});
361+
362+
test('should not promote Kohai when unique requirement not met', async () => {
363+
const guild = createMockGuild();
364+
365+
mockGetUserRole.mockReturnValue(Role.Kohai);
366+
mockGetRoleCounts.mockReturnValue({ kohai: 50, senpai: 70, sensei: 30 });
367+
mockGetReactionCount.mockResolvedValue(60);
368+
mockGetUniqueReactors.mockResolvedValue(['reactor-1', 'reactor-2']); // Only 2, need 10
369+
370+
const result = await checkPromotion(guild as any, 'user-1');
371+
372+
expect(result.promoted).toBe(false);
373+
expect(mockAssignRole).not.toHaveBeenCalled();
374+
});
375+
376+
test('should not check promotion for user without role', async () => {
377+
const guild = createMockGuild();
378+
379+
mockGetUserRole.mockReturnValue(null);
380+
381+
const result = await checkPromotion(guild as any, 'user-1');
382+
383+
expect(result.promoted).toBe(false);
384+
expect(mockGetReactionCount).not.toHaveBeenCalled();
385+
});
386+
387+
test('should not promote Sensei (already at max)', async () => {
388+
const guild = createMockGuild();
389+
390+
mockGetUserRole.mockReturnValue(Role.Sensei);
391+
392+
const result = await checkPromotion(guild as any, 'user-1');
393+
394+
expect(result.promoted).toBe(false);
395+
expect(mockGetReactionCount).not.toHaveBeenCalled();
396+
});
397+
398+
test('should handle errors gracefully', async () => {
399+
const guild = createMockGuild();
400+
401+
mockGetUserRole.mockReturnValue(Role.Kohai);
402+
mockGetRoleCounts.mockReturnValue({ kohai: 50, senpai: 70, sensei: 30 });
403+
mockGetReactionCount.mockRejectedValue(new Error('Database error'));
404+
405+
const result = await checkPromotion(guild as any, 'user-1');
406+
407+
expect(result.promoted).toBe(false);
408+
});
409+
});
410+
411+
describe('getUserStats', () => {
412+
test('should return stats for Kohai with senpaiScore', async () => {
413+
const guild = createMockGuild();
414+
415+
mockGetUserRole.mockReturnValue(Role.Kohai);
416+
mockGetReactionBreakdown.mockResolvedValue({
417+
total: 38,
418+
fromKohai: 15,
419+
fromSenpai: 18,
420+
fromSensei: 5,
421+
});
422+
mockGetRoleCounts.mockReturnValue({ kohai: 50, senpai: 70, sensei: 30 });
423+
mockGetReactionCount.mockResolvedValue(23);
424+
mockGetUniqueReactors.mockResolvedValue(Array.from({ length: 8 }, (_, i) => `reactor-${i}`));
425+
426+
const stats = await getUserStats(guild as any, 'user-1');
427+
428+
expect(stats.userId).toBe('user-1');
429+
expect(stats.currentRole).toBe(Role.Kohai);
430+
expect(stats.breakdown.total).toBe(38);
431+
expect(stats.breakdown.fromSenpai).toBe(18);
432+
expect(stats.senpaiScore).toBeDefined();
433+
expect(stats.senpaiScore?.totalReactions).toBe(23);
434+
expect(stats.senseiScore).toBeUndefined();
435+
});
436+
437+
test('should return stats for Senpai with senseiScore', async () => {
438+
const guild = createMockGuild();
439+
440+
mockGetUserRole.mockReturnValue(Role.Senpai);
441+
mockGetReactionBreakdown.mockResolvedValue({
442+
total: 120,
443+
fromKohai: 30,
444+
fromSenpai: 50,
445+
fromSensei: 40,
446+
});
447+
mockGetRoleCounts.mockReturnValue({ kohai: 50, senpai: 70, sensei: 30 });
448+
mockGetReactionCount.mockResolvedValue(25);
449+
mockGetUniqueReactors.mockResolvedValue(Array.from({ length: 5 }, (_, i) => `sensei-${i}`));
450+
451+
const stats = await getUserStats(guild as any, 'user-1');
452+
453+
expect(stats.userId).toBe('user-1');
454+
expect(stats.currentRole).toBe(Role.Senpai);
455+
expect(stats.senseiScore).toBeDefined();
456+
expect(stats.senseiScore?.totalReactions).toBe(25);
457+
expect(stats.senpaiScore).toBeUndefined();
458+
});
459+
460+
test('should return stats for Sensei without progression scores', async () => {
461+
const guild = createMockGuild();
462+
463+
mockGetUserRole.mockReturnValue(Role.Sensei);
464+
mockGetReactionBreakdown.mockResolvedValue({
465+
total: 250,
466+
fromKohai: 80,
467+
fromSenpai: 100,
468+
fromSensei: 70,
469+
});
470+
471+
const stats = await getUserStats(guild as any, 'user-1');
472+
473+
expect(stats.userId).toBe('user-1');
474+
expect(stats.currentRole).toBe(Role.Sensei);
475+
expect(stats.senpaiScore).toBeUndefined();
476+
expect(stats.senseiScore).toBeUndefined();
477+
});
478+
479+
test('should return stats for user without role', async () => {
480+
const guild = createMockGuild();
481+
482+
mockGetUserRole.mockReturnValue(null);
483+
mockGetReactionBreakdown.mockResolvedValue({
484+
total: 0,
485+
fromKohai: 0,
486+
fromSenpai: 0,
487+
fromSensei: 0,
488+
});
489+
490+
const stats = await getUserStats(guild as any, 'user-1');
491+
492+
expect(stats.userId).toBe('user-1');
493+
expect(stats.currentRole).toBeNull();
494+
expect(stats.breakdown.total).toBe(0);
495+
expect(stats.senpaiScore).toBeUndefined();
496+
expect(stats.senseiScore).toBeUndefined();
497+
});
498+
});
292499
});

0 commit comments

Comments
 (0)