Files
flyer-crawler.projectium.com/src/tests/integration/user.integration.test.ts
Torben Sorensen 56981236ab
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 1h7m5s
ensure mocks are used wherever possible, more test fixes
2025-12-20 15:33:08 -08:00

223 lines
11 KiB
TypeScript

// src/tests/integration/user.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as apiClient from '../../services/apiClient';
import { logger } from '../../services/logger.server';
import { getPool } from '../../services/db/connection.db';
import type { User, MasterGroceryItem, ShoppingList } from '../../types';
import { createAndLoginUser, TEST_PASSWORD } from '../utils/testHelpers';
/**
* @vitest-environment node
*/
describe('User API Routes Integration Tests', () => {
let testUser: User;
let authToken: string;
// --- START DEBUG LOGGING ---
// Query the DB from within the test file to see its state.
beforeAll(async () => {
const res = await getPool().query('SELECT u.user_id, u.email, p.role FROM public.users u JOIN public.profiles p ON u.user_id = p.user_id');
console.log('\n--- [user.integration.test.ts] Users found in DB from TEST perspective (beforeAll): ---');
console.table(res.rows);
console.log('-------------------------------------------------------------------------------------\n');
});
// --- END DEBUG LOGGING ---
// Before any tests run, create a new user and log them in.
// The token will be used for all subsequent API calls in this test suite.
beforeAll(async () => {
const email = `user-test-${Date.now()}@example.com`;
const { user, token } = await createAndLoginUser({ email, fullName: 'Test User' });
testUser = user;
authToken = token;
});
// After all tests, clean up by deleting the created user.
// This now cleans up ALL users created by this test suite to prevent pollution.
afterAll(async () => {
const pool = getPool();
try {
// Find all users created during this test run by their email pattern.
const res = await pool.query("SELECT user_id FROM public.users WHERE email LIKE 'user-test-%' OR email LIKE 'delete-me-%' OR email LIKE 'reset-me-%'");
if (res.rows.length > 0) {
const userIds = res.rows.map(r => r.user_id);
logger.debug(`[user.integration.test.ts afterAll] Cleaning up ${userIds.length} test users...`);
// Use a direct DB query for cleanup, which is faster and more reliable than API calls.
await pool.query('DELETE FROM public.users WHERE user_id = ANY($1::uuid[])', [userIds]);
}
} catch (error) {
logger.error({ error }, 'Failed to clean up test users from database.');
}
});
it('should fetch the authenticated user profile via GET /api/users/profile', async () => {
// Act: Call the API endpoint using the authenticated token.
const response = await apiClient.getAuthenticatedUserProfile({ tokenOverride: authToken });
const profile = await response.json();
// Assert: Verify the profile data matches the created user.
expect(profile).toBeDefined();
expect(profile.user_id).toBe(testUser.user_id);
expect(profile.user.email).toBe(testUser.email);
expect(profile.full_name).toBe('Test User');
expect(profile.role).toBe('user');
});
it('should update the user profile via PUT /api/users/profile', async () => {
// Arrange: Define the profile updates.
const profileUpdates = {
full_name: 'Updated Test User',
};
// Act: Call the update endpoint with the new data and the auth token.
const response = await apiClient.updateUserProfile(profileUpdates, { tokenOverride: authToken });
const updatedProfile = await response.json();
// Assert: Check that the returned profile reflects the changes.
expect(updatedProfile).toBeDefined();
expect(updatedProfile.full_name).toBe('Updated Test User');
// Also, fetch the profile again to ensure the change was persisted.
const refetchResponse = await apiClient.getAuthenticatedUserProfile({ tokenOverride: authToken });
const refetchedProfile = await refetchResponse.json();
expect(refetchedProfile.full_name).toBe('Updated Test User');
});
it('should update user preferences via PUT /api/users/profile/preferences', async () => {
// Arrange: Define the preference updates.
const preferenceUpdates = {
darkMode: true,
};
// Act: Call the update endpoint.
const response = await apiClient.updateUserPreferences(preferenceUpdates, { tokenOverride: authToken });
const updatedProfile = await response.json();
// Assert: Check that the preferences object in the returned profile is updated.
expect(updatedProfile).toBeDefined();
expect(updatedProfile.preferences).toBeDefined();
expect(updatedProfile.preferences?.darkMode).toBe(true);
});
it('should reject registration with a weak password', async () => {
// Arrange: Define a new user with a known weak password.
const email = `weak-password-user-${Date.now()}@example.com`;
const weakPassword = 'password';
// Act & Assert: Attempt to register and expect the promise to reject
// with an error message indicating the password is too weak.
const response = await apiClient.registerUser(email, weakPassword, 'Weak Password User');
expect(response.ok).toBe(false);
const errorData = await response.json();
expect(errorData.message).toMatch(/Password is too weak/);
});
it('should allow a user to delete their own account and then fail to log in', async () => {
// Arrange: Create a new, separate user just for this deletion test.
const deletionEmail = `delete-me-${Date.now()}@example.com`;
const { token: deletionToken } = await createAndLoginUser({ email: deletionEmail });
// Act: Call the delete endpoint with the correct password and token.
const response = await apiClient.deleteUserAccount(TEST_PASSWORD, { tokenOverride: deletionToken });
const deleteResponse = await response.json();
// Assert: Check for a successful deletion message.
expect(deleteResponse.message).toBe('Account deleted successfully.');
// Assert (Verification): Attempting to log in again with the same credentials should now fail.
const loginResponse = await apiClient.loginUser(deletionEmail, TEST_PASSWORD, false);
expect(loginResponse.ok).toBe(false);
const errorData = await loginResponse.json();
expect(errorData.message).toBe('Incorrect email or password.');
});
it('should allow a user to reset their password and log in with the new one', async () => {
// Arrange: Create a new user for the password reset flow.
const resetEmail = `reset-me-${Date.now()}@example.com`;
const { user: resetUser } = await createAndLoginUser({ email: resetEmail });
// Act 1: Request a password reset. In our test environment, the token is returned in the response.
const resetRequestRawResponse = await apiClient.requestPasswordReset(resetEmail);
if (!resetRequestRawResponse.ok) {
const errorData = await resetRequestRawResponse.json();
throw new Error(errorData.message || 'Password reset request failed');
}
const resetRequestResponse = await resetRequestRawResponse.json();
const resetToken = resetRequestResponse.token;
// Assert 1: Check that we received a token.
expect(resetToken).toBeDefined();
expect(resetToken).toBeTypeOf('string');
// Act 2: Use the token to set a new password.
const newPassword = 'my-new-secure-password-!@#$';
const resetRawResponse = await apiClient.resetPassword(resetToken!, newPassword);
if (!resetRawResponse.ok) {
const errorData = await resetRawResponse.json();
throw new Error(errorData.message || 'Password reset failed');
}
const resetResponse = await resetRawResponse.json();
// Assert 2: Check for a successful password reset message.
expect(resetResponse.message).toBe('Password has been reset successfully.');
// Act 3 & Assert 3 (Verification): Log in with the NEW password to confirm the change.
const loginResponse = await apiClient.loginUser(resetEmail, newPassword, false);
const loginData = await loginResponse.json();
expect(loginData.user).toBeDefined();
expect(loginData.user.user_id).toBe(resetUser.user_id);
});
describe('User Data Routes (Watched Items & Shopping Lists)', () => {
it('should allow a user to add and remove a watched item', async () => {
// Act 1: Add a new watched item. The API returns the created master item.
const addResponse = await apiClient.addWatchedItem('Integration Test Item', 'Other/Miscellaneous', authToken);
const newItem = await addResponse.json();
// Assert 1: Check that the item was created correctly.
expect(newItem).toBeDefined();
expect(newItem.name).toBe('Integration Test Item');
// Act 2: Fetch all watched items for the user.
const watchedItemsResponse = await apiClient.fetchWatchedItems(authToken);
const watchedItems = await watchedItemsResponse.json();
// Assert 2: Verify the new item is in the user's watched list.
expect(watchedItems.some((item: MasterGroceryItem) => item.master_grocery_item_id === newItem.master_grocery_item_id)).toBe(true);
// Act 3: Remove the watched item.
await apiClient.removeWatchedItem(newItem.master_grocery_item_id, authToken);
// Assert 3: Fetch again and verify the item is gone.
const finalWatchedItemsResponse = await apiClient.fetchWatchedItems(authToken);
const finalWatchedItems = await finalWatchedItemsResponse.json();
expect(finalWatchedItems.some((item: MasterGroceryItem) => item.master_grocery_item_id === newItem.master_grocery_item_id)).toBe(false);
});
it('should allow a user to manage a shopping list', async () => {
// Act 1: Create a new shopping list.
const createListResponse = await apiClient.createShoppingList('My Integration Test List', authToken);
const newList = await createListResponse.json();
// Assert 1: Check that the list was created.
expect(newList).toBeDefined();
expect(newList.name).toBe('My Integration Test List');
// Act 2: Add an item to the new list.
const addItemResponse = await apiClient.addShoppingListItem(newList.shopping_list_id, { customItemName: 'Custom Test Item' }, authToken);
const addedItem = await addItemResponse.json();
// Assert 2: Check that the item was added.
expect(addedItem).toBeDefined();
expect(addedItem.custom_item_name).toBe('Custom Test Item');
// Assert 3: Fetch all lists and verify the new item is present in the correct list.
const fetchResponse = await apiClient.fetchShoppingLists(authToken);
const lists = await fetchResponse.json();
const updatedList = lists.find((l: ShoppingList) => l.shopping_list_id === newList.shopping_list_id);
// The `add` endpoint returns the new ID as a string, while the `fetch` endpoint returns it as a number.
// To make the test robust, we convert both to strings before comparing.
expect(String(updatedList?.items[0].shopping_list_item_id)).toEqual(String(addedItem.shopping_list_item_id));
});
});
});