Files
pwa/backend/tests/profile.test.ts
T
ar.badieiandCursor 8c37c15697 Fix minimum age 12 and ensure assignment catalog seeds on startup.
Persian age validation messages, clearer homework entry points, and seed assignment definitions so «تکالیف من» is not empty after deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 07:24:40 +03:30

171 lines
5.4 KiB
TypeScript

import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { prisma } from '../src/lib/prisma.js';
import {
assertNoSecrets,
authHeader,
createTestApp,
isDatabaseAvailable,
requestJson,
} from './helpers.js';
const app = createTestApp();
const createdUserIds: bigint[] = [];
describe('profile API', () => {
let dbAvailable = false;
before(async () => {
dbAvailable = await isDatabaseAvailable();
});
after(async () => {
if (dbAvailable && createdUserIds.length > 0) {
await prisma.user.deleteMany({ where: { id: { in: createdUserIds } } });
}
});
async function createAuthenticatedUser() {
const email = `profile-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
const registered = await requestJson(app, '/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password: 'password12' }),
});
const id = (registered.body.data?.user as { id: string }).id;
const user = await prisma.user.findUnique({ where: { externalId: id } });
if (user) {
createdUserIds.push(user.id);
}
return {
token: registered.body.data?.sessionToken as string,
id,
};
}
it('rejects unauthenticated profile access', async () => {
const missing = await requestJson(app, '/users/me');
assert.equal(missing.status, 401);
});
it('lets an authenticated user read and update their own profile', async (t) => {
if (!dbAvailable) {
t.skip('MySQL is not running');
return;
}
const { token } = await createAuthenticatedUser();
const before = await requestJson(app, '/users/me', { headers: authHeader(token) });
assert.equal(before.status, 200);
assert.equal((before.body.data as { profile: unknown }).profile, null);
assert.equal((before.body.data as { profileComplete: boolean }).profileComplete, false);
const updated = await requestJson(app, '/users/me/profile', {
method: 'PATCH',
headers: authHeader(token),
body: JSON.stringify({ name: ' علی رضایی ', age: 29 }),
});
assert.equal(updated.status, 200);
assert.equal((updated.body.data as { profileComplete: boolean }).profileComplete, true);
assert.equal((updated.body.data as { profile: { name: string; age: number } }).profile.name, 'علی رضایی');
assert.equal((updated.body.data as { profile: { name: string; age: number } }).profile.age, 29);
assertNoSecrets(updated.raw);
});
it('validates name and requires an integer age', async (t) => {
if (!dbAvailable) {
t.skip('MySQL is not running');
return;
}
const { token } = await createAuthenticatedUser();
const emptyName = await requestJson(app, '/users/me/profile', {
method: 'PATCH',
headers: authHeader(token),
body: JSON.stringify({ name: ' ', age: 20 }),
});
assert.equal(emptyName.status, 422);
const decimalAge = await requestJson(app, '/users/me/profile', {
method: 'PATCH',
headers: authHeader(token),
body: JSON.stringify({ name: 'Sara', age: 17.5 }),
});
assert.equal(decimalAge.status, 422);
const stringAge = await requestJson(app, '/users/me/profile', {
method: 'PATCH',
headers: authHeader(token),
body: JSON.stringify({ name: 'Sara', age: '17' }),
});
assert.equal(stringAge.status, 422);
const underage = await requestJson(app, '/users/me/profile', {
method: 'PATCH',
headers: authHeader(token),
body: JSON.stringify({ name: 'Sara', age: 11 }),
});
assert.equal(underage.status, 422);
assert.match(String(underage.body.error?.message ?? ''), /حداقل سن 12/);
const teenOk = await requestJson(app, '/users/me/profile', {
method: 'PATCH',
headers: authHeader(token),
body: JSON.stringify({ name: 'Sara', age: 12 }),
});
assert.equal(teenOk.status, 200);
const seniorOk = await requestJson(app, '/users/me/profile', {
method: 'PATCH',
headers: authHeader(token),
body: JSON.stringify({ name: 'Sara', age: 101 }),
});
assert.equal(seniorOk.status, 200);
});
it('does not expose a userId parameter for modifying another user', async (t) => {
if (!dbAvailable) {
t.skip('MySQL is not running');
return;
}
const first = await createAuthenticatedUser();
const second = await createAuthenticatedUser();
const missing = await requestJson(app, `/users/${second.id}/profile`, {
method: 'PATCH',
headers: authHeader(first.token),
body: JSON.stringify({ name: 'Hacker', age: 40 }),
});
assert.equal(missing.status, 404);
const other = await requestJson(app, '/users/me/profile', {
method: 'PATCH',
headers: authHeader(first.token),
body: JSON.stringify({ name: 'Only Me', age: 22 }),
});
assert.equal(other.status, 200);
const secondRead = await requestJson(app, '/users/me', { headers: authHeader(second.token) });
assert.equal((secondRead.body.data as { profile: unknown }).profile, null);
});
it('requires both name and age when creating a profile', async (t) => {
if (!dbAvailable) {
t.skip('MySQL is not running');
return;
}
const { token } = await createAuthenticatedUser();
const partial = await requestJson(app, '/users/me/profile', {
method: 'PATCH',
headers: authHeader(token),
body: JSON.stringify({ name: 'Only Name' }),
});
assert.equal(partial.status, 422);
});
});