it_solutions_test/test/profile.e2e-spec.ts
Илья Братских bd22447bd3 Add a GraphQL digital card that migrates and seeds itself on startup.
The NestJS API serves the public profile through Apollo Sandbox and loads only the relations selected by the query.
2026-11-02 05:30:54 +03:00

196 lines
5.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { existsSync } from 'node:fs';
import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
import { applyMigrations } from '../src/prisma/apply-migrations.js';
import { PrismaService } from '../src/prisma/prisma.service.js';
type GraphqlBody<T> = {
data?: T;
errors?: Array<{ message: string }>;
};
function readBody<T>(response: request.Response): GraphqlBody<T> {
const payload: unknown = response.body;
if (typeof payload !== 'object' || payload === null) {
throw new Error('GraphQL response was not an object');
}
const record = payload as Record<string, unknown>;
const errors = Array.isArray(record.errors)
? record.errors.filter(isErrorItem)
: undefined;
return {
data: record.data as T | undefined,
errors,
};
}
function isErrorItem(value: unknown): value is { message: string } {
return (
typeof value === 'object' &&
value !== null &&
'message' in value &&
typeof value.message === 'string'
);
}
function http(app: INestApplication) {
// The Nest server handle is typed as any.
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return request(app.getHttpServer());
}
if (existsSync('.env')) {
process.loadEnvFile('.env');
}
const EXAMPLE = `
query {
profile {
name
description
skills { name }
experience { company position }
projects { name }
}
}
`;
describe('digital card', () => {
let app: INestApplication;
beforeAll(async () => {
await applyMigrations();
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('opens Apollo Sandbox and a health check', async () => {
const sandbox = await http(app).get('/graphql').set('Accept', 'text/html');
expect(sandbox.status).toBe(200);
expect(sandbox.text.toLowerCase()).toMatch(/sandbox|apollo/);
const health = await http(app).get('/health');
expect(health.status).toBe(200);
expect(health.body).toEqual({ status: 'ok' });
});
it('returns the profile for the assignment query', async () => {
const prisma = app.get(PrismaService);
prisma.takeQueries();
const response = await http(app).post('/graphql').send({ query: EXAMPLE });
expect(response.status).toBe(200);
const body = readBody<{
profile: {
name: string;
description: string;
skills: Array<{ name: string }>;
experience: Array<{ company: string; position: string }>;
projects: Array<{ name: string }>;
};
}>(response);
expect(body.errors).toBeUndefined();
const profile = body.data?.profile;
expect(profile?.name).toBe('Илья Братских');
expect(profile?.description.length).toBeGreaterThan(0);
expect(profile?.skills.map((skill) => skill.name)).toContain('NestJS');
expect(profile?.experience).toEqual(
expect.arrayContaining([
expect.objectContaining({
company: 'АО «Синтелс»',
position: 'Fullstack Developer & Researcher',
}),
]),
);
expect(profile?.projects.map((project) => project.name)).toContain(
'AI-обучение и чат-боты',
);
const statements = prisma.takeQueries().filter(isApplicationSql);
expect(statements.length).toBeGreaterThan(0);
expect(statements.length).toBeLessThanOrEqual(6);
expect(statements.filter((sql) => /Achievement/i.test(sql))).toHaveLength(
0,
);
});
it('loads nested achievements without a query per job', async () => {
const prisma = app.get(PrismaService);
prisma.takeQueries();
const response = await http(app)
.post('/graphql')
.send({
query: `
query {
profile {
experience { company achievements { description } }
projects { name url skills { name } highlights }
}
}
`,
});
expect(response.status).toBe(200);
const body = readBody<{
profile: {
experience: Array<{
company: string;
achievements: Array<{ description: string }>;
}>;
projects: Array<{
name: string;
url: string | null;
skills: Array<{ name: string }>;
}>;
};
}>(response);
expect(body.errors).toBeUndefined();
const jobs = body.data?.profile.experience ?? [];
const sintels = jobs.find((job) => job.company.includes('Синтелс'));
expect(sintels?.achievements).toHaveLength(4);
const projects = body.data?.profile.projects ?? [];
const ai = projects.find((project) => project.name.startsWith('AI-'));
expect(ai?.url).toBe('https://github.com/KBAHTNET/ai_learning');
expect(ai?.skills.map((skill) => skill.name)).toEqual(
expect.arrayContaining(['Python', 'PyTorch']),
);
const statements = prisma.takeQueries().filter(isApplicationSql);
expect(statements.filter((sql) => /Achievement/i.test(sql))).toHaveLength(
1,
);
expect(statements.length).toBeLessThanOrEqual(8);
});
it('rejects an unknown or malformed slug', async () => {
const missing = await http(app).post('/graphql').send({
query: 'query { profile(slug: "nobody") { name } }',
});
expect(readBody(missing).errors?.[0]?.message).toContain('не найден');
const invalid = await http(app).post('/graphql').send({
query: 'query { profile(slug: "Bad Slug") { name } }',
});
expect(readBody(invalid).errors?.[0]?.message).toContain('Некорректный');
});
});
function isApplicationSql(sql: string): boolean {
return /profile|skill|experience|achievement|project|education|award|link/i.test(
sql,
);
}