Skip to content

Commit 55db633

Browse files
authored
Merge pull request #98 from chrispoupart/fix/assigned-quest-bug
test(dashboard): 🧪 add tests for quests and repeatable quests
2 parents e99e0cc + 99cd9e7 commit 55db633

File tree

4 files changed

+384
-2
lines changed

4 files changed

+384
-2
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to alter the column `title` on the `quests` table. The data in that column could be lost. The data in that column will be cast from `String` to `Unsupported("TEXT COLLATE NOCASE")`.
5+
- You are about to alter the column `description` on the `quests` table. The data in that column could be lost. The data in that column will be cast from `String` to `Unsupported("TEXT COLLATE NOCASE")`.
6+
7+
*/
8+
-- Create new table with COLLATE NOCASE
9+
CREATE TABLE "new_quests" (
10+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
11+
"title" TEXT NOT NULL COLLATE NOCASE,
12+
"description" TEXT COLLATE NOCASE,
13+
"bounty" REAL NOT NULL,
14+
"status" TEXT NOT NULL DEFAULT 'AVAILABLE',
15+
"created_by" INTEGER NOT NULL,
16+
"claimed_by" INTEGER,
17+
"claimed_at" DATETIME,
18+
"completed_at" DATETIME,
19+
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
20+
"updated_at" DATETIME NOT NULL,
21+
"due_date" DATETIME,
22+
"user_id" INTEGER,
23+
"is_repeatable" BOOLEAN NOT NULL DEFAULT false,
24+
"cooldown_days" INTEGER,
25+
"last_completed_at" DATETIME,
26+
CONSTRAINT "quests_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
27+
CONSTRAINT "quests_claimed_by_fkey" FOREIGN KEY ("claimed_by") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
28+
CONSTRAINT "quests_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE
29+
);
30+
31+
-- Copy data from old table to new table
32+
INSERT INTO "new_quests" ("id", "title", "description", "bounty", "status", "created_by", "claimed_by", "claimed_at", "completed_at", "created_at", "updated_at", "due_date", "user_id", "is_repeatable", "cooldown_days", "last_completed_at")
33+
SELECT "id", "title", "description", "bounty", "status", "created_by", "claimed_by", "claimed_at", "completed_at", "created_at", "updated_at", "due_date", "user_id", "is_repeatable", "cooldown_days", "last_completed_at" FROM "quests";
34+
35+
-- Drop old table
36+
DROP TABLE "quests";
37+
38+
-- Rename new table
39+
ALTER TABLE "new_quests" RENAME TO "quests";
40+
41+
-- Recreate indexes
42+
CREATE INDEX "quests_user_id_idx" ON "quests"("user_id");

backend/src/controllers/dashboardController.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ export class DashboardController {
204204
if (search && typeof search === 'string') {
205205
whereConditions.push({
206206
OR: [
207-
{ title: { contains: search, mode: 'insensitive' } },
208-
{ description: { contains: search, mode: 'insensitive' } }
207+
{ title: { contains: search } },
208+
{ description: { contains: search } }
209209
]
210210
});
211211
}

backend/tests/dashboard.test.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,211 @@ describe('Leaderboard API', () => {
501501
});
502502
});
503503

504+
describe('GET /dashboard/quests', () => {
505+
beforeAll(async () => {
506+
await setupTestDatabase();
507+
});
508+
509+
afterAll(async () => {
510+
await teardownTestDatabase();
511+
});
512+
513+
beforeEach(async () => {
514+
await clearTestData();
515+
resetUserCounter();
516+
});
517+
518+
it('should only show personalized quests to the assigned user', async () => {
519+
const questGiver = await createTestUser({ role: 'EDITOR' });
520+
const assignedPlayer = await createTestUser({ role: 'PLAYER', email: 'assigned@test.com' });
521+
const otherPlayer = await createTestUser({ role: 'PLAYER', email: 'other@test.com' });
522+
523+
// Create a global quest
524+
await createTestQuest(questGiver.id, {
525+
title: 'Global Quest',
526+
status: 'AVAILABLE',
527+
bounty: 100
528+
});
529+
530+
// Create a personalized quest for assignedPlayer
531+
await createTestQuest(questGiver.id, {
532+
title: 'Personalized Quest',
533+
status: 'AVAILABLE',
534+
bounty: 150,
535+
userId: assignedPlayer.id
536+
});
537+
538+
// Create a personalized quest for otherPlayer
539+
await createTestQuest(questGiver.id, {
540+
title: 'Other Player Quest',
541+
status: 'AVAILABLE',
542+
bounty: 200,
543+
userId: otherPlayer.id
544+
});
545+
546+
// Assigned player should see global quest and their personalized quest
547+
const assignedPlayerToken = createTestToken(assignedPlayer.id, assignedPlayer.email, assignedPlayer.role);
548+
let response = await request(app)
549+
.get('/dashboard/quests')
550+
.set('Authorization', `Bearer ${assignedPlayerToken}`);
551+
552+
expect(response.status).toBe(200);
553+
expect(response.body.data.quests).toHaveLength(2);
554+
const assignedPlayerTitles = response.body.data.quests.map((q: any) => q.title);
555+
expect(assignedPlayerTitles).toContain('Global Quest');
556+
expect(assignedPlayerTitles).toContain('Personalized Quest');
557+
expect(assignedPlayerTitles).not.toContain('Other Player Quest');
558+
559+
// Other player should only see global quest and their own personalized quest
560+
const otherPlayerToken = createTestToken(otherPlayer.id, otherPlayer.email, otherPlayer.role);
561+
response = await request(app)
562+
.get('/dashboard/quests')
563+
.set('Authorization', `Bearer ${otherPlayerToken}`);
564+
565+
expect(response.status).toBe(200);
566+
expect(response.body.data.quests).toHaveLength(2);
567+
const otherPlayerTitles = response.body.data.quests.map((q: any) => q.title);
568+
expect(otherPlayerTitles).toContain('Global Quest');
569+
expect(otherPlayerTitles).toContain('Other Player Quest');
570+
expect(otherPlayerTitles).not.toContain('Personalized Quest');
571+
});
572+
573+
it('admin should see all quests including personalized ones', async () => {
574+
const questGiver = await createTestUser({ role: 'EDITOR' });
575+
const assignedPlayer = await createTestUser({ role: 'PLAYER', email: 'assigned@test.com' });
576+
const admin = await createTestUser({ role: 'ADMIN', email: 'admin@test.com' });
577+
578+
// Create a global quest
579+
await createTestQuest(questGiver.id, {
580+
title: 'Global Quest',
581+
status: 'AVAILABLE',
582+
bounty: 100
583+
});
584+
585+
// Create a personalized quest
586+
await createTestQuest(questGiver.id, {
587+
title: 'Personalized Quest',
588+
status: 'AVAILABLE',
589+
bounty: 150,
590+
userId: assignedPlayer.id
591+
});
592+
593+
const adminToken = createTestToken(admin.id, admin.email, admin.role);
594+
const response = await request(app)
595+
.get('/dashboard/quests')
596+
.set('Authorization', `Bearer ${adminToken}`);
597+
598+
expect(response.status).toBe(200);
599+
expect(response.body.data.quests).toHaveLength(2);
600+
const titles = response.body.data.quests.map((q: any) => q.title);
601+
expect(titles).toContain('Global Quest');
602+
expect(titles).toContain('Personalized Quest');
603+
});
604+
605+
it('should exclude expired quests', async () => {
606+
const questGiver = await createTestUser({ role: 'EDITOR' });
607+
const player = await createTestUser({ role: 'PLAYER' });
608+
const token = createTestToken(player.id, player.email, player.role);
609+
610+
// Create a quest with past due date
611+
const pastDate = new Date();
612+
pastDate.setDate(pastDate.getDate() - 1); // Yesterday
613+
614+
await createTestQuest(questGiver.id, {
615+
title: 'Expired Quest',
616+
status: 'AVAILABLE',
617+
bounty: 100,
618+
dueDate: pastDate
619+
});
620+
621+
// Create a valid quest
622+
await createTestQuest(questGiver.id, {
623+
title: 'Valid Quest',
624+
status: 'AVAILABLE',
625+
bounty: 100
626+
});
627+
628+
const response = await request(app)
629+
.get('/dashboard/quests')
630+
.set('Authorization', `Bearer ${token}`);
631+
632+
expect(response.status).toBe(200);
633+
expect(response.body.data.quests).toHaveLength(1);
634+
expect(response.body.data.quests[0].title).toBe('Valid Quest');
635+
});
636+
637+
it('should support status filtering', async () => {
638+
const questGiver = await createTestUser({ role: 'EDITOR' });
639+
const player = await createTestUser({ role: 'PLAYER' });
640+
const token = createTestToken(player.id, player.email, player.role);
641+
642+
// Create quests with different statuses
643+
await createTestQuest(questGiver.id, {
644+
title: 'Available Quest',
645+
status: 'AVAILABLE',
646+
bounty: 100
647+
});
648+
649+
await createTestQuest(questGiver.id, {
650+
title: 'Claimed Quest',
651+
status: 'CLAIMED',
652+
claimedBy: player.id,
653+
bounty: 200
654+
});
655+
656+
// Test filtering by AVAILABLE status
657+
let response = await request(app)
658+
.get('/dashboard/quests?status=AVAILABLE')
659+
.set('Authorization', `Bearer ${token}`);
660+
661+
expect(response.status).toBe(200);
662+
expect(response.body.data.quests).toHaveLength(1);
663+
expect(response.body.data.quests[0].title).toBe('Available Quest');
664+
665+
// Test filtering by CLAIMED status
666+
response = await request(app)
667+
.get('/dashboard/quests?status=CLAIMED')
668+
.set('Authorization', `Bearer ${token}`);
669+
670+
expect(response.status).toBe(200);
671+
expect(response.body.data.quests).toHaveLength(1);
672+
expect(response.body.data.quests[0].title).toBe('Claimed Quest');
673+
});
674+
675+
it('should support search functionality', async () => {
676+
const questGiver = await createTestUser({ role: 'EDITOR' });
677+
const player = await createTestUser({ role: 'PLAYER' });
678+
const token = createTestToken(player.id, player.email, player.role);
679+
680+
// Create quests with different titles
681+
await createTestQuest(questGiver.id, {
682+
title: 'Dragon Slayer Quest',
683+
status: 'AVAILABLE',
684+
bounty: 100
685+
});
686+
687+
await createTestQuest(questGiver.id, {
688+
title: 'Treasure Hunt Quest',
689+
status: 'AVAILABLE',
690+
bounty: 200
691+
});
692+
693+
// Test search by title
694+
const response = await request(app)
695+
.get('/dashboard/quests?search=dragon') // Using lowercase 'd' to test case-insensitivity
696+
.set('Authorization', `Bearer ${token}`);
697+
698+
expect(response.status).toBe(200);
699+
expect(response.body.data.quests).toHaveLength(1);
700+
expect(response.body.data.quests[0].title).toBe('Dragon Slayer Quest');
701+
});
702+
703+
it('should require authentication', async () => {
704+
const response = await request(app).get('/dashboard/quests');
705+
expect(response.status).toBe(401);
706+
});
707+
});
708+
504709
describe('Reward Config API', () => {
505710
let adminUser: any;
506711
let regularUser: any;

0 commit comments

Comments
 (0)