Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/api-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ jobs:
- name: 'Start containers'
run: npm run start:anon:api -- --wait -d

- name: Wait for MariaDB to be ready
run: |
echo "Waiting for MariaDB to be ready..."
for i in {1..30}; do
if docker exec nowdb-db-anon mariadb -h localhost -u now_test -pnow_test -e "SELECT 1;" >/dev/null 2>&1; then
echo "MariaDB is ready!"
break
fi
echo "Waiting for MariaDB... ($i/30)"
sleep 2
done

- name: Run api-tests
run: npm run test:ci:api

Expand Down
1 change: 1 addition & 0 deletions backend/jest-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = {
testPathIgnorePatterns: [
"<rootDir>/build",
"<rootDir>/node_modules",
"<rootDir>/../src/api-tests",
],
collectCoverageFrom: [
"./src/**",
Expand Down
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"dev": "npx tsx watch --clear-screen=false src/index.ts",
"build": "tsc",
"test:api": "jest src/api-tests --runInBand --coverage --config jest-config.js --detectOpenHandles --forceExit --silent",
"test:api": "jest ./src/api-tests --runInBand --coverage --config jest-config.js --detectOpenHandles --forceExit --silent",
"test:unit": "jest src/unit-tests --coverage --config jest-config.js --detectOpenHandles --forceExit --silent",
"test:api:local": "DOTENV_CONFIG_PATH=../.test.env npm run test:api -- --setupFiles dotenv/config",
"start": "node build/index.js",
Expand Down
9 changes: 9 additions & 0 deletions backend/src/api-tests/helpers/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { send } from '../utils'

export async function getTestAuthToken(): Promise<string> {
const result = await send<{ token: string }>('user/login', 'POST', {

Check failure on line 4 in backend/src/api-tests/helpers/auth.ts

View workflow job for this annotation

GitHub Actions / Lint & tsc

Delete `·`
username: 'testSu',

Check failure on line 5 in backend/src/api-tests/helpers/auth.ts

View workflow job for this annotation

GitHub Actions / Lint & tsc

Delete `·`
password: 'test'

Check failure on line 6 in backend/src/api-tests/helpers/auth.ts

View workflow job for this annotation

GitHub Actions / Lint & tsc

Replace `·` with `,`
})
return result.body.token
}
23 changes: 23 additions & 0 deletions backend/src/api-tests/helpers/locality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { PrismaClient } from '@prisma/client';

Check failure on line 1 in backend/src/api-tests/helpers/locality.ts

View workflow job for this annotation

GitHub Actions / Lint & tsc

Delete `;`
import { testPrisma as prisma } from './prisma';

Check failure on line 2 in backend/src/api-tests/helpers/locality.ts

View workflow job for this annotation

GitHub Actions / Lint & tsc

Delete `;`

export const createLocality = async (

Check failure on line 4 in backend/src/api-tests/helpers/locality.ts

View workflow job for this annotation

GitHub Actions / Lint & tsc

Replace `⏎··localityData?:·Parameters<PrismaClient['locality']['create']>[0]['data']⏎` with `localityData?:·Parameters<PrismaClient['locality']['create']>[0]['data']`
localityData?: Parameters<PrismaClient['locality']['create']>[0]['data']
) => {

Check failure on line 6 in backend/src/api-tests/helpers/locality.ts

View workflow job for this annotation

GitHub Actions / Lint & tsc

Async arrow function 'createLocality' has no 'await' expression
return prisma.locality.create({

Check failure on line 7 in backend/src/api-tests/helpers/locality.ts

View workflow job for this annotation

GitHub Actions / Lint & tsc

Unsafe member access .locality on an `any` value

Check failure on line 7 in backend/src/api-tests/helpers/locality.ts

View workflow job for this annotation

GitHub Actions / Lint & tsc

Unsafe call of a(n) `any` typed value

Check failure on line 7 in backend/src/api-tests/helpers/locality.ts

View workflow job for this annotation

GitHub Actions / Lint & tsc

Unsafe return of a value of type `any`
data: localityData ?? {
loc_name: 'Test Locality',
country: 'Test Country',
},
});
};

export const deleteLocality = async (id: number) => {
return prisma.locality.delete({
where: { lid: id },
});
};

export const cleanupLocalities = async () => {
return prisma.locality.deleteMany({});
};
4 changes: 4 additions & 0 deletions backend/src/api-tests/helpers/prisma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PrismaClient } from '@prisma/client';

// Singleton PrismaClient instance for all tests to prevent connection pool exhaustion
export const testPrisma = new PrismaClient();
188 changes: 188 additions & 0 deletions backend/src/api-tests/helpers/userHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { testPrisma as prisma } from './prisma';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';

interface TestUser {
id: string;
email: string;
username: string;
password: string;
}

interface CreateTestUserOptions {
email?: string;
username?: string;
password?: string;
role?: string;
verified?: boolean;
}

/**
* Creates a test user in the database with optional custom properties
* @param options - Optional configuration for the test user
* @returns The created test user object
*/
export async function createTestUser(
options: CreateTestUserOptions = {}
): Promise<TestUser> {
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(7);

const email = options.email || `test-${timestamp}-${randomSuffix}@example.com`;
const username = options.username || `testuser-${timestamp}-${randomSuffix}`;
const password = options.password || 'TestPassword123!';
const role = options.role || 'user';
const verified = options.verified !== undefined ? options.verified : true;

const hashedPassword = await bcrypt.hash(password, 10);

const user = await prisma.user.create({
data: {
email,
username,
password: hashedPassword,
role,
emailVerified: verified,
},
});

return {
id: user.id,
email: user.email,
username: user.username,
password, // Return the plain text password for testing purposes
};
}

/**
* Generates an authentication token for a test user
* @param userId - The ID of the user
* @param options - Optional JWT options
* @returns JWT authentication token
*/
export function getAuthToken(
userId: string,
options: { expiresIn?: string; role?: string } = {}
): string {
const secret = process.env.JWT_SECRET || 'test-secret-key';
const expiresIn = options.expiresIn || '1h';

const payload = {
userId,
role: options.role || 'user',
};

return jwt.sign(payload, secret, { expiresIn });
}

/**
* Cleans up a test user from the database
* @param userId - The ID of the user to delete
*/
export async function cleanupTestUser(userId: string): Promise<void> {
try {
// Delete related data first (adjust based on your schema)
await prisma.session.deleteMany({
where: { userId },
});

await prisma.refreshToken.deleteMany({
where: { userId },
});

// Delete the user
await prisma.user.delete({
where: { id: userId },
});
} catch (error) {
console.error(`Failed to cleanup test user ${userId}:`, error);
// Don't throw to prevent test failures during cleanup
}
}

/**
* Cleans up multiple test users from the database
* @param userIds - Array of user IDs to delete
*/
export async function cleanupTestUsers(userIds: string[]): Promise<void> {
await Promise.all(userIds.map(id => cleanupTestUser(id)));
}

/**
* Creates a test user and returns both user data and auth token
* @param options - Optional configuration for the test user
* @returns Object containing user data and auth token
*/
export async function createAuthenticatedTestUser(
options: CreateTestUserOptions = {}
) {
const user = await createTestUser(options);
const token = getAuthToken(user.id, { role: options.role });

return {
user,
token,
authHeader: `Bearer ${token}`,
};
}

/**
* Finds a user by email
* @param email - The email address to search for
* @returns The user object or null if not found
*/
export async function findUserByEmail(email: string) {
return prisma.user.findUnique({
where: { email },
});
}

/**
* Finds a user by username
* @param username - The username to search for
* @returns The user object or null if not found
*/
export async function findUserByUsername(username: string) {
return prisma.user.findUnique({
where: { username },
});
}

/**
* Updates a test user's properties
* @param userId - The ID of the user to update
* @param data - The data to update
*/
export async function updateTestUser(userId: string, data: any) {
return prisma.user.update({
where: { id: userId },
data,
});
}

/**
* Cleanup function to be used in afterEach/afterAll hooks
* Removes all test users created during tests
*/
export async function cleanupAllTestUsers(): Promise<void> {
try {
await prisma.user.deleteMany({
where: {
OR: [
{ email: { contains: 'test-' } },
{ username: { contains: 'testuser-' } },
],
},
});
} catch (error) {
console.error('Failed to cleanup all test users:', error);
}
}

/**
* Closes the Prisma client connection
* Should be called after all tests complete
*/
export async function disconnectPrisma(): Promise<void> {
await prisma.$disconnect();
}
Loading
Loading