All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m0s
131 lines
5.8 KiB
TypeScript
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
|
|
);
|
|
}); |