Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 41s
299 lines
12 KiB
TypeScript
299 lines
12 KiB
TypeScript
// src/tests/integration/user.integration.test.ts
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import app from '../../../server';
|
|
import { logger } from '../../services/logger.server';
|
|
import { getPool } from '../../services/db/connection.db';
|
|
import type { UserProfile, MasterGroceryItem, ShoppingList } from '../../types';
|
|
import { createAndLoginUser, TEST_PASSWORD } from '../utils/testHelpers';
|
|
import { cleanupDb } from '../utils/cleanup';
|
|
|
|
/**
|
|
* @vitest-environment node
|
|
*/
|
|
|
|
const request = supertest(app);
|
|
|
|
describe('User API Routes Integration Tests', () => {
|
|
let testUser: UserProfile;
|
|
let authToken: string;
|
|
const createdUserIds: string[] = [];
|
|
|
|
// 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', request });
|
|
testUser = user;
|
|
authToken = token;
|
|
createdUserIds.push(user.user.user_id);
|
|
});
|
|
|
|
// 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 () => {
|
|
await cleanupDb({ userIds: createdUserIds });
|
|
});
|
|
|
|
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 request
|
|
.get('/api/users/profile')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
const profile = response.body;
|
|
|
|
// Assert: Verify the profile data matches the created user.
|
|
expect(response.status).toBe(200);
|
|
expect(profile.user.user_id).toBe(testUser.user.user_id);
|
|
expect(profile.user.email).toBe(testUser.user.email); // This was already correct
|
|
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 request
|
|
.put('/api/users/profile')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send(profileUpdates);
|
|
const updatedProfile = response.body;
|
|
|
|
// Assert: Check that the returned profile reflects the changes.
|
|
expect(response.status).toBe(200);
|
|
expect(updatedProfile.full_name).toBe('Updated Test User');
|
|
|
|
// Also, fetch the profile again to ensure the change was persisted.
|
|
const refetchResponse = await request
|
|
.get('/api/users/profile')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
const refetchedProfile = refetchResponse.body;
|
|
expect(refetchedProfile.full_name).toBe('Updated Test User');
|
|
});
|
|
|
|
it('should allow updating the profile with an empty string for avatar_url', async () => {
|
|
// Arrange: Define the profile updates.
|
|
const profileUpdates = {
|
|
full_name: 'Empty Avatar User',
|
|
avatar_url: '',
|
|
};
|
|
|
|
// Act: Call the update endpoint with the new data and the auth token.
|
|
const response = await request
|
|
.put('/api/users/profile')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send(profileUpdates);
|
|
const updatedProfile = response.body;
|
|
|
|
// Assert: Check that the returned profile reflects the changes.
|
|
expect(response.status).toBe(200);
|
|
expect(updatedProfile.full_name).toBe('Empty Avatar User');
|
|
expect(updatedProfile.avatar_url).toBeNull();
|
|
|
|
// Also, fetch the profile again to ensure the change was persisted in the database as NULL.
|
|
const refetchResponse = await request
|
|
.get('/api/users/profile')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
expect(refetchResponse.body.avatar_url).toBeNull();
|
|
});
|
|
|
|
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 request
|
|
.put('/api/users/profile/preferences')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send(preferenceUpdates);
|
|
const updatedProfile = response.body;
|
|
|
|
// Assert: Check that the preferences object in the returned profile is updated.
|
|
expect(response.status).toBe(200);
|
|
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 request.post('/api/auth/register').send({
|
|
email,
|
|
password: weakPassword,
|
|
full_name: 'Weak Password User',
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
const errorData = response.body as { message: string; errors: { message: string }[] };
|
|
// For validation errors, the detailed messages are in the `errors` array.
|
|
// We join them to check for the specific feedback from the password strength checker.
|
|
const detailedErrorMessage = errorData.errors?.map((e) => e.message).join(' ');
|
|
expect(detailedErrorMessage).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 { user: deletionUser, token: deletionToken } = await createAndLoginUser({ email: deletionEmail, request });
|
|
createdUserIds.push(deletionUser.user.user_id);
|
|
|
|
// Act: Call the delete endpoint with the correct password and token.
|
|
const response = await request
|
|
.delete('/api/users/account')
|
|
.set('Authorization', `Bearer ${deletionToken}`)
|
|
.send({ password: TEST_PASSWORD });
|
|
const deleteResponse = response.body;
|
|
|
|
// Assert: Check for a successful deletion message.
|
|
expect(response.status).toBe(200);
|
|
expect(deleteResponse.message).toBe('Account deleted successfully.');
|
|
|
|
// Assert (Verification): Attempting to log in again with the same credentials should now fail.
|
|
const loginResponse = await request
|
|
.post('/api/auth/login')
|
|
.send({ email: deletionEmail, password: TEST_PASSWORD });
|
|
expect(loginResponse.status).toBe(401);
|
|
const errorData = loginResponse.body;
|
|
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, request });
|
|
createdUserIds.push(resetUser.user.user_id);
|
|
|
|
// Act 1: Request a password reset. In our test environment, the token is returned in the response.
|
|
const resetRequestRawResponse = await request
|
|
.post('/api/auth/forgot-password')
|
|
.send({ email: resetEmail });
|
|
if (resetRequestRawResponse.status !== 200) {
|
|
const errorData = resetRequestRawResponse.body;
|
|
throw new Error(errorData.message || 'Password reset request failed');
|
|
}
|
|
const resetRequestResponse = resetRequestRawResponse.body;
|
|
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 request
|
|
.post('/api/auth/reset-password')
|
|
.send({ token: resetToken!, newPassword });
|
|
if (resetRawResponse.status !== 200) {
|
|
const errorData = resetRawResponse.body;
|
|
throw new Error(errorData.message || 'Password reset failed');
|
|
}
|
|
const resetResponse = resetRawResponse.body;
|
|
|
|
// 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 request
|
|
.post('/api/auth/login')
|
|
.send({ email: resetEmail, password: newPassword });
|
|
const loginData = loginResponse.body;
|
|
expect(loginData.userprofile).toBeDefined();
|
|
expect(loginData.userprofile.user.user_id).toBe(resetUser.user.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 request
|
|
.post('/api/users/watched-items')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({ itemName: 'Integration Test Item', category: 'Other/Miscellaneous' });
|
|
const newItem = addResponse.body;
|
|
|
|
// Assert 1: Check that the item was created correctly.
|
|
expect(addResponse.status).toBe(201);
|
|
expect(newItem.name).toBe('Integration Test Item');
|
|
|
|
// Act 2: Fetch all watched items for the user.
|
|
const watchedItemsResponse = await request
|
|
.get('/api/users/watched-items')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
const watchedItems = watchedItemsResponse.body;
|
|
|
|
// 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.
|
|
const removeResponse = await request
|
|
.delete(`/api/users/watched-items/${newItem.master_grocery_item_id}`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
expect(removeResponse.status).toBe(204);
|
|
|
|
// Assert 3: Fetch again and verify the item is gone.
|
|
const finalWatchedItemsResponse = await request
|
|
.get('/api/users/watched-items')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
const finalWatchedItems = finalWatchedItemsResponse.body;
|
|
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 request
|
|
.post('/api/users/shopping-lists')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({ name: 'My Integration Test List' });
|
|
const newList = createListResponse.body;
|
|
|
|
// Assert 1: Check that the list was created.
|
|
expect(createListResponse.status).toBe(201);
|
|
expect(newList.name).toBe('My Integration Test List');
|
|
|
|
// Act 2: Add an item to the new list.
|
|
const addItemResponse = await request
|
|
.post(`/api/users/shopping-lists/${newList.shopping_list_id}/items`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({ customItemName: 'Custom Test Item' });
|
|
const addedItem = addItemResponse.body;
|
|
|
|
// Assert 2: Check that the item was added.
|
|
expect(addItemResponse.status).toBe(201);
|
|
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 request
|
|
.get('/api/users/shopping-lists')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
const lists = fetchResponse.body;
|
|
expect(fetchResponse.status).toBe(200);
|
|
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),
|
|
);
|
|
});
|
|
});
|
|
});
|