Files
flyer-crawler.projectium.com/src/tests/integration/gamification.integration.test.ts
Torben Sorensen 1f3f99d430
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m0s
fix tests + flyer upload (anon)
2025-12-30 14:56:25 -08:00

131 lines
5.8 KiB
TypeScript

// src/tests/integration/gamification.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import supertest from 'supertest';
import app from '../../../server';
import path from 'path';
import fs from 'node:fs/promises';
import { createAndLoginUser } from '../utils/testHelpers';
import { generateFileChecksum } from '../../utils/checksum';
import * as db from '../../services/db/index.db';
import { cleanupDb } from '../utils/cleanup';
import { logger } from '../../services/logger.server';
import type { UserProfile, UserAchievement, LeaderboardUser, Achievement } from '../../types';
import { cleanupFiles } from '../utils/cleanupFiles';
/**
* @vitest-environment node
*/
const request = supertest(app);
describe('Gamification Flow Integration Test', () => {
let testUser: UserProfile;
let authToken: string;
const createdFlyerIds: number[] = [];
const createdFilePaths: string[] = [];
beforeAll(async () => {
// Create a new user specifically for this test suite to ensure a clean slate.
({ user: testUser, token: authToken } = await createAndLoginUser({
email: `gamification-user-${Date.now()}@example.com`,
fullName: 'Gamification Tester',
request,
}));
});
afterAll(async () => {
await cleanupDb({
userIds: testUser ? [testUser.user.user_id] : [],
flyerIds: createdFlyerIds,
});
await cleanupFiles(createdFilePaths);
});
it(
'should award the "First Upload" achievement after a user successfully uploads and processes their first flyer',
async () => {
// --- Arrange: Prepare a unique flyer file for upload ---
const imagePath = path.resolve(__dirname, '../assets/test-flyer-image.jpg');
const imageBuffer = await fs.readFile(imagePath);
const uniqueContent = Buffer.concat([imageBuffer, Buffer.from(Date.now().toString())]);
const uniqueFileName = `gamification-test-flyer-${Date.now()}.jpg`;
const mockImageFile = new File([uniqueContent], uniqueFileName, { type: 'image/jpeg' });
const checksum = await generateFileChecksum(mockImageFile);
// Track created files for cleanup
const uploadDir = path.resolve(__dirname, '../../../flyer-images');
createdFilePaths.push(path.join(uploadDir, uniqueFileName));
const iconFileName = `icon-${path.parse(uniqueFileName).name}.webp`;
createdFilePaths.push(path.join(uploadDir, 'icons', iconFileName));
// --- Act 1: Upload the flyer to trigger the background job ---
const uploadResponse = await request
.post('/api/ai/upload-and-process')
.set('Authorization', `Bearer ${authToken}`)
.field('checksum', checksum)
.attach('flyerFile', uniqueContent, uniqueFileName);
const { jobId } = uploadResponse.body;
expect(jobId).toBeTypeOf('string');
// --- Act 2: Poll for job completion ---
let jobStatus;
const maxRetries = 30; // Poll for up to 90 seconds
for (let i = 0; i < maxRetries; i++) {
await new Promise((resolve) => setTimeout(resolve, 3000));
const statusResponse = await request
.get(`/api/ai/jobs/${jobId}/status`)
.set('Authorization', `Bearer ${authToken}`);
jobStatus = statusResponse.body;
if (jobStatus.state === 'completed' || jobStatus.state === 'failed') {
break;
}
}
// --- Assert 1: Verify the job completed successfully ---
if (jobStatus?.state === 'failed') {
console.error('[DEBUG] Gamification test job failed:', jobStatus.failedReason);
}
expect(jobStatus?.state).toBe('completed');
const flyerId = jobStatus?.returnValue?.flyerId;
expect(flyerId).toBeTypeOf('number');
createdFlyerIds.push(flyerId); // Track for cleanup
// --- Assert 1.5: Verify the flyer was saved with the correct original filename ---
const savedFlyer = await db.flyerRepo.findFlyerByChecksum(checksum, logger);
expect(savedFlyer).toBeDefined();
expect(savedFlyer?.file_name).toBe(uniqueFileName);
// Also add the final processed image path to the cleanup list.
// This is important because JPEGs are re-processed to strip EXIF data, creating a new file.
const savedImagePath = path.join(uploadDir, path.basename(savedFlyer!.image_url));
createdFilePaths.push(savedImagePath);
// --- Act 3: Fetch the user's achievements ---
const achievementsResponse = await request
.get('/api/achievements/me')
.set('Authorization', `Bearer ${authToken}`);
const userAchievements: (UserAchievement & Achievement)[] = achievementsResponse.body;
// --- Assert 2: Verify the "First-Upload" achievement was awarded ---
// The 'user_registered' achievement is awarded on creation, so we expect at least two.
expect(userAchievements.length).toBeGreaterThanOrEqual(2);
const firstUploadAchievement = userAchievements.find((ach) => ach.name === 'First-Upload');
expect(firstUploadAchievement).toBeDefined();
expect(firstUploadAchievement?.points_value).toBeGreaterThan(0);
// --- Act 4: Fetch the leaderboard ---
const leaderboardResponse = await request.get('/api/achievements/leaderboard');
const leaderboard: LeaderboardUser[] = leaderboardResponse.body;
// --- Assert 3: Verify the user is on the leaderboard with points ---
const userOnLeaderboard = leaderboard.find((u) => u.user_id === testUser.user.user_id);
expect(userOnLeaderboard).toBeDefined();
// The user should have points from 'user_registered' and 'First-Upload'.
// We check that the points are greater than or equal to the points from the upload achievement.
expect(Number(userOnLeaderboard?.points)).toBeGreaterThanOrEqual(
firstUploadAchievement!.points_value,
);
},
120000, // Increase timeout to 120 seconds for this long-running test
);
});