All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m34s
259 lines
8.6 KiB
TypeScript
259 lines
8.6 KiB
TypeScript
// src/tests/e2e/upc-journey.e2e.test.ts
|
|
/**
|
|
* End-to-End test for the UPC scanning user journey.
|
|
* Tests the complete flow from user registration to scanning UPCs and viewing history.
|
|
*/
|
|
import { describe, it, expect, afterAll } from 'vitest';
|
|
import * as apiClient from '../../services/apiClient';
|
|
import { cleanupDb } from '../utils/cleanup';
|
|
import { poll } from '../utils/poll';
|
|
import { getPool } from '../../services/db/connection.db';
|
|
|
|
/**
|
|
* @vitest-environment node
|
|
*/
|
|
|
|
const API_BASE_URL = process.env.VITE_API_BASE_URL || 'http://localhost:3000/api';
|
|
|
|
// Helper to make authenticated API calls
|
|
const authedFetch = async (
|
|
path: string,
|
|
options: RequestInit & { token?: string } = {},
|
|
): Promise<Response> => {
|
|
const { token, ...fetchOptions } = options;
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(fetchOptions.headers as Record<string, string>),
|
|
};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
return fetch(`${API_BASE_URL}${path}`, {
|
|
...fetchOptions,
|
|
headers,
|
|
});
|
|
};
|
|
|
|
describe('E2E UPC Scanning Journey', () => {
|
|
const uniqueId = Date.now();
|
|
const userEmail = `upc-e2e-${uniqueId}@example.com`;
|
|
const userPassword = 'StrongUpcPassword123!';
|
|
|
|
let authToken: string;
|
|
let userId: string | null = null;
|
|
const createdScanIds: number[] = [];
|
|
const createdProductIds: number[] = [];
|
|
|
|
afterAll(async () => {
|
|
const pool = getPool();
|
|
|
|
// Clean up scan history
|
|
if (createdScanIds.length > 0) {
|
|
await pool.query('DELETE FROM public.upc_scan_history WHERE scan_id = ANY($1::int[])', [
|
|
createdScanIds,
|
|
]);
|
|
}
|
|
|
|
// Clean up test products
|
|
if (createdProductIds.length > 0) {
|
|
await pool.query('DELETE FROM public.products WHERE product_id = ANY($1::int[])', [
|
|
createdProductIds,
|
|
]);
|
|
}
|
|
|
|
// Clean up user
|
|
await cleanupDb({
|
|
userIds: [userId],
|
|
});
|
|
});
|
|
|
|
it('should complete full UPC scanning journey: Register -> Scan -> Lookup -> History -> Stats', async () => {
|
|
// Step 1: Register a new user
|
|
const registerResponse = await apiClient.registerUser(userEmail, userPassword, 'UPC E2E User');
|
|
expect(registerResponse.status).toBe(201);
|
|
|
|
// Step 2: Login to get auth token
|
|
const { response: loginResponse, responseBody: loginResponseBody } = await poll(
|
|
async () => {
|
|
const response = await apiClient.loginUser(userEmail, userPassword, false);
|
|
const responseBody = response.ok ? await response.clone().json() : {};
|
|
return { response, responseBody };
|
|
},
|
|
(result) => result.response.ok,
|
|
{ timeout: 10000, interval: 1000, description: 'user login after registration' },
|
|
);
|
|
|
|
expect(loginResponse.status).toBe(200);
|
|
authToken = loginResponseBody.data.token;
|
|
userId = loginResponseBody.data.userprofile.user.user_id;
|
|
expect(authToken).toBeDefined();
|
|
|
|
// Step 3: Create a test product with UPC in the database
|
|
// Products table requires master_item_id (FK to master_grocery_items), has optional brand_id
|
|
const pool = getPool();
|
|
const testUpc = `${Date.now()}`.slice(-12).padStart(12, '0');
|
|
|
|
// First, create or get a master grocery item
|
|
const masterItemResult = await pool.query(
|
|
`INSERT INTO public.master_grocery_items (name)
|
|
VALUES ('E2E Test Product Item')
|
|
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
|
RETURNING master_grocery_item_id`,
|
|
);
|
|
const masterItemId = masterItemResult.rows[0].master_grocery_item_id;
|
|
|
|
const productResult = await pool.query(
|
|
`INSERT INTO public.products (name, master_item_id, upc_code, description)
|
|
VALUES ('E2E Test Product', $1, $2, 'Product for E2E testing')
|
|
RETURNING product_id`,
|
|
[masterItemId, testUpc],
|
|
);
|
|
const productId = productResult.rows[0].product_id;
|
|
createdProductIds.push(productId);
|
|
|
|
// Step 4: Scan the UPC code
|
|
const scanResponse = await authedFetch('/upc/scan', {
|
|
method: 'POST',
|
|
token: authToken,
|
|
body: JSON.stringify({
|
|
upc_code: testUpc,
|
|
scan_source: 'manual_entry',
|
|
}),
|
|
});
|
|
|
|
expect(scanResponse.status).toBe(200);
|
|
const scanData = await scanResponse.json();
|
|
expect(scanData.success).toBe(true);
|
|
expect(scanData.data.upc_code).toBe(testUpc);
|
|
const scanId = scanData.data.scan_id;
|
|
createdScanIds.push(scanId);
|
|
|
|
// Step 5: Lookup the product by UPC
|
|
const lookupResponse = await authedFetch(`/upc/lookup?upc_code=${testUpc}`, {
|
|
method: 'GET',
|
|
token: authToken,
|
|
});
|
|
|
|
expect(lookupResponse.status).toBe(200);
|
|
const lookupData = await lookupResponse.json();
|
|
expect(lookupData.success).toBe(true);
|
|
expect(lookupData.data.product).toBeDefined();
|
|
expect(lookupData.data.product.name).toBe('E2E Test Product');
|
|
|
|
// Step 6: Scan a few more items to build history
|
|
for (let i = 0; i < 3; i++) {
|
|
const additionalScan = await authedFetch('/upc/scan', {
|
|
method: 'POST',
|
|
token: authToken,
|
|
body: JSON.stringify({
|
|
upc_code: `00000000000${i}`,
|
|
scan_source: i % 2 === 0 ? 'manual_entry' : 'image_upload',
|
|
}),
|
|
});
|
|
|
|
if (additionalScan.ok) {
|
|
const additionalData = await additionalScan.json();
|
|
if (additionalData.data?.scan_id) {
|
|
createdScanIds.push(additionalData.data.scan_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 7: View scan history
|
|
const historyResponse = await authedFetch('/upc/history', {
|
|
method: 'GET',
|
|
token: authToken,
|
|
});
|
|
|
|
expect(historyResponse.status).toBe(200);
|
|
const historyData = await historyResponse.json();
|
|
expect(historyData.success).toBe(true);
|
|
expect(historyData.data.scans.length).toBeGreaterThanOrEqual(4); // At least our 4 scans
|
|
expect(historyData.data.total).toBeGreaterThanOrEqual(4);
|
|
|
|
// Step 8: View specific scan details
|
|
const scanDetailResponse = await authedFetch(`/upc/history/${scanId}`, {
|
|
method: 'GET',
|
|
token: authToken,
|
|
});
|
|
|
|
expect(scanDetailResponse.status).toBe(200);
|
|
const scanDetailData = await scanDetailResponse.json();
|
|
expect(scanDetailData.data.scan_id).toBe(scanId);
|
|
expect(scanDetailData.data.upc_code).toBe(testUpc);
|
|
|
|
// Step 9: Check user scan statistics
|
|
const statsResponse = await authedFetch('/upc/stats', {
|
|
method: 'GET',
|
|
token: authToken,
|
|
});
|
|
|
|
expect(statsResponse.status).toBe(200);
|
|
const statsData = await statsResponse.json();
|
|
expect(statsData.success).toBe(true);
|
|
expect(statsData.data.total_scans).toBeGreaterThanOrEqual(4);
|
|
|
|
// Step 10: Test history filtering by scan_source
|
|
const filteredHistoryResponse = await authedFetch('/upc/history?scan_source=manual_entry', {
|
|
method: 'GET',
|
|
token: authToken,
|
|
});
|
|
|
|
expect(filteredHistoryResponse.status).toBe(200);
|
|
const filteredData = await filteredHistoryResponse.json();
|
|
filteredData.data.scans.forEach((scan: { scan_source: string }) => {
|
|
expect(scan.scan_source).toBe('manual_entry');
|
|
});
|
|
|
|
// Step 11: Verify another user cannot see our scans
|
|
const otherUserEmail = `other-upc-e2e-${uniqueId}@example.com`;
|
|
await apiClient.registerUser(otherUserEmail, userPassword, 'Other UPC User');
|
|
|
|
const { responseBody: otherLoginData } = await poll(
|
|
async () => {
|
|
const response = await apiClient.loginUser(otherUserEmail, userPassword, false);
|
|
const responseBody = response.ok ? await response.clone().json() : {};
|
|
return { response, responseBody };
|
|
},
|
|
(result) => result.response.ok,
|
|
{ timeout: 10000, interval: 1000, description: 'other user login' },
|
|
);
|
|
|
|
const otherToken = otherLoginData.data.token;
|
|
const otherUserId = otherLoginData.data.userprofile.user.user_id;
|
|
|
|
// Other user should not see our scan
|
|
const otherScanDetailResponse = await authedFetch(`/upc/history/${scanId}`, {
|
|
method: 'GET',
|
|
token: otherToken,
|
|
});
|
|
|
|
expect(otherScanDetailResponse.status).toBe(404);
|
|
|
|
// Other user's history should be empty
|
|
const otherHistoryResponse = await authedFetch('/upc/history', {
|
|
method: 'GET',
|
|
token: otherToken,
|
|
});
|
|
|
|
expect(otherHistoryResponse.status).toBe(200);
|
|
const otherHistoryData = await otherHistoryResponse.json();
|
|
expect(otherHistoryData.data.total).toBe(0);
|
|
|
|
// Clean up other user
|
|
await cleanupDb({ userIds: [otherUserId] });
|
|
|
|
// Step 12: Delete account (self-service)
|
|
const deleteAccountResponse = await apiClient.deleteUserAccount(userPassword, {
|
|
tokenOverride: authToken,
|
|
});
|
|
|
|
expect(deleteAccountResponse.status).toBe(200);
|
|
|
|
// Mark userId as null to avoid double deletion in afterAll
|
|
userId = null;
|
|
});
|
|
});
|