163 lines
5.1 KiB
TypeScript
163 lines
5.1 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: 17 }),
|
||
|
|
});
|
||
|
|
assert.equal(underage.status, 422);
|
||
|
|
|
||
|
|
const overage = await requestJson(app, '/users/me/profile', {
|
||
|
|
method: 'PATCH',
|
||
|
|
headers: authHeader(token),
|
||
|
|
body: JSON.stringify({ name: 'Sara', age: 101 }),
|
||
|
|
});
|
||
|
|
assert.equal(overage.status, 422);
|
||
|
|
});
|
||
|
|
|
||
|
|
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);
|
||
|
|
});
|
||
|
|
});
|