Skip to content

Commit cb63496

Browse files
authored
Merge pull request #105 from chrispoupart/fix/reward-management
Fix/reward management
2 parents 3a0775c + dbdbb4a commit cb63496

File tree

6 files changed

+82
-35
lines changed

6 files changed

+82
-35
lines changed

backend/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"db:migrate:deploy": "prisma migrate deploy",
2020
"db:seed": "ts-node prisma/seed.ts",
2121
"db:studio": "prisma studio",
22-
"db:reset": "prisma migrate reset --force",
2322
"promote-admin": "node dist/scripts/promote-to-admin.js",
2423
"db:migrate:backup": "node dist/scripts/db-migrate-with-backup.js",
2524
"db:migrate:backup:dev": "ts-node scripts/db-migrate-with-backup.ts",

backend/src/controllers/rewardsController.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,21 @@ export class RewardsController {
77
const config = await prisma.rewardConfig.findFirst();
88
if (!config) {
99
// Return default config if none exists.
10-
// This object is a template for a new config, so it shouldn't have an id
11-
// or other DB-generated fields.
1210
res.status(200).json({
13-
monthlyBountyReward: 0,
14-
monthlyQuestReward: 0,
15-
quarterlyCollectiveGoal: 0,
16-
quarterlyCollectiveReward: '',
11+
success: true,
12+
data: {
13+
monthlyBountyReward: 0,
14+
monthlyQuestReward: 0,
15+
quarterlyCollectiveGoal: 0,
16+
quarterlyCollectiveReward: '',
17+
}
1718
});
1819
} else {
19-
res.status(200).json(config);
20+
res.status(200).json({ success: true, data: config });
2021
}
2122
} catch (error) {
2223
console.error('Error getting reward config:', error);
23-
res.status(500).json({ success: false, error: 'Internal server error' });
24+
res.status(500).json({ success: false, error: { message: 'Internal server error' } });
2425
}
2526
}
2627

@@ -37,7 +38,7 @@ export class RewardsController {
3738
) {
3839
res.status(400).json({
3940
success: false,
40-
error: 'All fields are required and must be of the correct type.'
41+
error: { message: 'All fields are required and must be of the correct type.' }
4142
});
4243
return;
4344
}
@@ -66,22 +67,22 @@ export class RewardsController {
6667
res.status(200).json({ success: true });
6768
} catch (error) {
6869
console.error('Error updating reward config:', error);
69-
res.status(500).json({ success: false, error: 'Internal server error' });
70+
res.status(500).json({ success: false, error: { message: 'Internal server error' } });
7071
}
7172
}
7273

7374
static async getCollectiveProgress(req: Request, res: Response): Promise<void> {
7475
try {
7576
const { quarter } = req.query;
7677
if (!quarter || typeof quarter !== 'string' || !/^\d{4}-Q[1-4]$/.test(quarter)) {
77-
res.status(400).json({ error: 'Invalid or missing quarter parameter (expected YYYY-QN)' });
78+
res.status(400).json({ success: false, error: { message: 'Invalid or missing quarter parameter (expected YYYY-QN)' } });
7879
return;
7980
}
8081
const [yearStr, qStr] = quarter.split('-Q');
8182
const year = parseInt(yearStr, 10);
8283
const q = parseInt(qStr, 10);
8384
if (isNaN(year) || isNaN(q) || q < 1 || q > 4) {
84-
res.status(400).json({ error: 'Invalid quarter parameter' });
85+
res.status(400).json({ success: false, error: { message: 'Invalid quarter parameter' } });
8586
return;
8687
}
8788
// Calculate start and end of the quarter
@@ -116,10 +117,10 @@ export class RewardsController {
116117
}
117118
}
118119
const percent = goal > 0 ? (progress / goal) * 100 : 0;
119-
res.status(200).json({ goal, reward, progress, percent });
120+
res.status(200).json({ success: true, data: { goal, reward, progress, percent } });
120121
} catch (error) {
121122
console.error('Error getting collective reward progress:', error);
122-
res.status(500).json({ error: 'Internal server error' });
123+
res.status(500).json({ success: false, error: { message: 'Internal server error' } });
123124
}
124125
}
125126
}

backend/tests/dashboard.test.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -714,10 +714,6 @@ describe('Reward Config API', () => {
714714

715715
beforeAll(async () => {
716716
await setupTestDatabase();
717-
adminUser = await createTestUser({ role: 'ADMIN', email: 'admin@example.com' });
718-
regularUser = await createTestUser({ role: 'PLAYER', email: 'user@example.com' });
719-
adminToken = createTestToken(adminUser.id, adminUser.email, adminUser.role);
720-
userToken = createTestToken(regularUser.id, regularUser.email, regularUser.role);
721717
});
722718

723719
afterAll(async () => {
@@ -727,6 +723,10 @@ describe('Reward Config API', () => {
727723
beforeEach(async () => {
728724
await clearTestData();
729725
resetUserCounter();
726+
adminUser = await createTestUser({ role: 'ADMIN', email: 'admin@example.com' });
727+
regularUser = await createTestUser({ role: 'PLAYER', email: 'user@example.com' });
728+
adminToken = createTestToken(adminUser.id, adminUser.email, adminUser.role);
729+
userToken = createTestToken(regularUser.id, regularUser.email, regularUser.role);
730730
});
731731

732732
it('should allow admin to get and update reward config', async () => {
@@ -735,10 +735,11 @@ describe('Reward Config API', () => {
735735
.get('/rewards/config')
736736
.set('Authorization', `Bearer ${adminToken}`)
737737
.expect(200);
738-
expect(res.body).toHaveProperty('monthlyBountyReward');
739-
expect(res.body).toHaveProperty('monthlyQuestReward');
740-
expect(res.body).toHaveProperty('quarterlyCollectiveGoal');
741-
expect(res.body).toHaveProperty('quarterlyCollectiveReward');
738+
expect(res.body.success).toBe(true);
739+
expect(res.body.data).toHaveProperty('monthlyBountyReward');
740+
expect(res.body.data).toHaveProperty('monthlyQuestReward');
741+
expect(res.body.data).toHaveProperty('quarterlyCollectiveGoal');
742+
expect(res.body.data).toHaveProperty('quarterlyCollectiveReward');
742743

743744
// Admin can update config
744745
const newConfig = {
@@ -759,10 +760,11 @@ describe('Reward Config API', () => {
759760
.get('/rewards/config')
760761
.set('Authorization', `Bearer ${adminToken}`)
761762
.expect(200);
762-
expect(res.body.monthlyBountyReward).toBe(100);
763-
expect(res.body.monthlyQuestReward).toBe(50);
764-
expect(res.body.quarterlyCollectiveGoal).toBe(1000);
765-
expect(res.body.quarterlyCollectiveReward).toBe('Pizza Party!');
763+
expect(res.body.success).toBe(true);
764+
expect(res.body.data.monthlyBountyReward).toBe(100);
765+
expect(res.body.data.monthlyQuestReward).toBe(50);
766+
expect(res.body.data.quarterlyCollectiveGoal).toBe(1000);
767+
expect(res.body.data.quarterlyCollectiveReward).toBe('Pizza Party!');
766768
});
767769

768770
it('should not allow non-admins to update reward config', async () => {
@@ -860,12 +862,14 @@ describe('Collective Reward Progress API', () => {
860862
.set('Authorization', `Bearer ${token}`)
861863
.expect(200);
862864

863-
expect(res.body).toHaveProperty('goal', 1000);
864-
expect(res.body).toHaveProperty('reward', 'Team Pizza Party!');
865-
expect(res.body).toHaveProperty('progress', totalBounty);
866-
expect(res.body).toHaveProperty('percent');
867-
expect(typeof res.body.percent).toBe('number');
868-
expect(res.body.percent).toBeCloseTo((totalBounty / 1000) * 100, 1);
865+
expect(res.body).toHaveProperty('success', true);
866+
expect(res.body).toHaveProperty('data');
867+
expect(res.body.data).toHaveProperty('goal', 1000);
868+
expect(res.body.data).toHaveProperty('reward', 'Team Pizza Party!');
869+
expect(res.body.data).toHaveProperty('progress', totalBounty);
870+
expect(res.body.data).toHaveProperty('percent');
871+
expect(typeof res.body.data.percent).toBe('number');
872+
expect(res.body.data.percent).toBeCloseTo((totalBounty / 1000) * 100, 1);
869873
});
870874

871875
it('should require authentication', async () => {

cspell.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@
1616
"cooldowns",
1717
"Eldoria",
1818
"headlessui",
19+
"keepalive",
20+
"nodelay",
21+
"nopush",
22+
"nosniff",
23+
"referer",
1924
"Reqs",
25+
"SAMEORIGIN",
26+
"sendfile",
2027
"shellcheck",
2128
"sonner"
2229
],

frontend/src/components/admin/ApprovalWorkflow.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,16 @@ const ApprovalWorkflow: React.FC<ApprovalWorkflowProps> = () => {
189189
{quest.claimedBy && (
190190
<div className="flex items-center gap-2 text-foreground">
191191
<User className="w-4 h-4" />
192-
<span>Claimed by User #{quest.claimedBy}</span>
192+
{quest.claimer ? (
193+
<>
194+
{quest.claimer.avatarUrl && (
195+
<img src={quest.claimer.avatarUrl} alt={quest.claimer.characterName || quest.claimer.name} className="w-5 h-5 rounded-full inline-block mr-1" />
196+
)}
197+
<span>{quest.claimer.characterName || quest.claimer.name || `User #${quest.claimer.id}`}</span>
198+
</>
199+
) : (
200+
<span>User #{quest.claimedBy}</span>
201+
)}
193202
</div>
194203
)}
195204

frontend/src/setupTests.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,28 @@
1-
import '@testing-library/jest-dom';
1+
import '@testing-library/jest-dom';
2+
import axios from 'axios';
3+
4+
// Fail tests if any unmocked fetch is called
5+
const originalFetch = global.fetch;
6+
beforeAll(() => {
7+
global.fetch = (...args) => {
8+
throw new Error(
9+
`Unexpected fetch call in test: ${args[0]}. You must mock all API requests.`
10+
);
11+
};
12+
});
13+
afterAll(() => {
14+
global.fetch = originalFetch;
15+
});
16+
17+
// Fail tests if any unmocked axios request is made
18+
const originalAxios = axios.request;
19+
beforeAll(() => {
20+
axios.request = function (...args) {
21+
throw new Error(
22+
`Unexpected axios request in test: ${JSON.stringify(args[0])}. You must mock all API requests.`
23+
);
24+
};
25+
});
26+
afterAll(() => {
27+
axios.request = originalAxios;
28+
});

0 commit comments

Comments
 (0)