mirror of
https://github.com/kamask-dev/pchelpmaster.git
synced 2026-11-11 05:49:38 +00:00
test(api): type-check harness and cover automigrate and auth isolation
- add tsconfig.tests.json and run tsc in test:api - move vitest ProvidedContext types to provided-context.d.ts - rename vitest.api.config.ts to .mts - kill signal-test fixture with SIGTERM first so cleanup still runs - add tests: no files in pb_migrations, independent user/guest clients - docs: drop superuser env vars from tests in README, note tsc in testing.md Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvmtStx737hu8gXe15e55W
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
248a2e1e90
commit
1109f68e11
@@ -106,6 +106,6 @@ PB_URL=http://127.0.0.1:8090 PORT=4000 node dist/pchelpcrm/server/server.mjs
|
|||||||
| `PB_URL` | внутренний адрес PocketBase для SSR-сервера и dev-прокси, по умолчанию `http://127.0.0.1:8090` |
|
| `PB_URL` | внутренний адрес PocketBase для SSR-сервера и dev-прокси, по умолчанию `http://127.0.0.1:8090` |
|
||||||
| `PORT` | порт Node SSR-сервера, по умолчанию `4000` |
|
| `PORT` | порт Node SSR-сервера, по умолчанию `4000` |
|
||||||
| `PB_DIR` | папка данных PocketBase для скрипта `pb:superuser` |
|
| `PB_DIR` | папка данных PocketBase для скрипта `pb:superuser` |
|
||||||
| `PB_SUPERUSER_EMAIL`, `PB_SUPERUSER_PASSWORD` | суперпользователь PocketBase (скрипт `pb:superuser` и тесты) |
|
| `PB_SUPERUSER_EMAIL`, `PB_SUPERUSER_PASSWORD` | суперпользователь PocketBase (скрипт `pb:superuser`) |
|
||||||
|
|
||||||
Резервная копия — копирование папки `pb_data`. После первого релиза приложение планируется поставлять одним Docker-контейнером (SSR-сервер и PocketBase вместе).
|
Резервная копия — копирование папки `pb_data`. После первого релиза приложение планируется поставлять одним Docker-контейнером (SSR-сервер и PocketBase вместе).
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
Команды появляются по мере того, как настраиваются соответствующие стенды.
|
Команды появляются по мере того, как настраиваются соответствующие стенды.
|
||||||
|
|
||||||
|
`pnpm test:api` сначала проверяет типы тестового кода (`tsconfig.tests.json`: `tests/**` и `vitest.api.config.mts`), затем запускает Vitest.
|
||||||
|
|
||||||
## Контрактные тесты репозиториев
|
## Контрактные тесты репозиториев
|
||||||
|
|
||||||
У каждого репозитория две реализации: PocketBase (`data/`) и фейк в памяти (`testing/`). Чтобы фейк не расходился с реальностью, поведение описывается **один раз**:
|
У каждого репозитория две реализации: PocketBase (`data/`) и фейк в памяти (`testing/`). Чтобы фейк не расходился с реальностью, поведение описывается **один раз**:
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"test:api": "vitest run --config vitest.api.config.ts",
|
"test:api": "tsc -p tsconfig.tests.json && vitest run --config vitest.api.config.mts",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"serve:ssr:pchelpcrm": "node dist/pchelpcrm/server/server.mjs",
|
"serve:ssr:pchelpcrm": "node dist/pchelpcrm/server/server.mjs",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { TestProject } from 'vitest/node';
|
import type { TestProject } from 'vitest/node';
|
||||||
import { startPocketBase } from '../support/pocketbase-server';
|
import { startPocketBase } from '../support/pocketbase-server';
|
||||||
import '../support/clients';
|
|
||||||
|
|
||||||
export default async function setup(project: TestProject) {
|
export default async function setup(project: TestProject) {
|
||||||
const { url, dataDir, superuser, stop } = await startPocketBase();
|
const { url, dataDir, superuser, stop } = await startPocketBase();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
import { once } from 'node:events';
|
import { once } from 'node:events';
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync, readdirSync } from 'node:fs';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { describe, expect, inject, it, vi } from 'vitest';
|
import { describe, expect, inject, it, vi } from 'vitest';
|
||||||
@@ -9,6 +10,7 @@ import { type PocketBaseServer, startPocketBase } from '../support/pocketbase-se
|
|||||||
|
|
||||||
const health = (url: string) => fetch(`${url}/api/health`);
|
const health = (url: string) => fetch(`${url}/api/health`);
|
||||||
const holdFixture = path.resolve('tests/support/fixtures/hold-pocketbase.ts');
|
const holdFixture = path.resolve('tests/support/fixtures/hold-pocketbase.ts');
|
||||||
|
const migrationsDir = path.resolve('pocketbase/pb_migrations');
|
||||||
|
|
||||||
describe('стенд API-тестов', () => {
|
describe('стенд API-тестов', () => {
|
||||||
it('отвечает 200 на GET /api/health', async () => {
|
it('отвечает 200 на GET /api/health', async () => {
|
||||||
@@ -60,13 +62,32 @@ describe('стенд API-тестов', () => {
|
|||||||
expect(pb.authStore.record?.['email']).toBe(user.email);
|
expect(pb.authStore.record?.['email']).toBe(user.email);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('у каждого хелпера свой экземпляр SDK', async () => {
|
it('клиенты двух пользователей и гостя не делят авторизацию', async () => {
|
||||||
const superuser = await asSuperuser();
|
const a = await createUser();
|
||||||
|
const b = await createUser();
|
||||||
|
|
||||||
|
const pbA = await asUser(a);
|
||||||
|
const pbB = await asUser(b);
|
||||||
const guest = asGuest();
|
const guest = asGuest();
|
||||||
|
|
||||||
expect(guest).not.toBe(superuser);
|
expect(pbA.authStore.record?.id).toBe(a.id);
|
||||||
expect(guest).not.toBe(asGuest());
|
expect(pbB.authStore.record?.id).toBe(b.id);
|
||||||
expect(guest.authStore.isValid).toBe(false);
|
expect(guest.authStore.isValid).toBe(false);
|
||||||
|
expect((await pbA.collection('users').getOne(a.id)).id).toBe(a.id);
|
||||||
|
expect((await pbB.collection('users').getOne(b.id)).id).toBe(b.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('изменения коллекций не порождают файлы в pocketbase/pb_migrations', async () => {
|
||||||
|
const before = readdirSync(migrationsDir);
|
||||||
|
|
||||||
|
await (
|
||||||
|
await asSuperuser()
|
||||||
|
).collections.create({
|
||||||
|
name: `automigrate_probe_${randomUUID().replaceAll('-', '')}`,
|
||||||
|
type: 'base',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(readdirSync(migrationsDir)).toEqual(before);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('работает на отдельном временном PocketBase, а не на pocketbase/pb_data', async () => {
|
it('работает на отдельном временном PocketBase, а не на pocketbase/pb_data', async () => {
|
||||||
@@ -134,7 +155,14 @@ describe('startPocketBase', { timeout: 30_000 }, () => {
|
|||||||
await expect(health(url)).rejects.toThrow();
|
await expect(health(url)).rejects.toThrow();
|
||||||
expect(existsSync(dataDir)).toBe(false);
|
expect(existsSync(dataDir)).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
child.kill('SIGKILL');
|
// Если тест упал до сигнала, даём владельцу убрать PocketBase сам; SIGKILL — только по таймауту.
|
||||||
|
if (child.exitCode === null && child.signalCode === null) {
|
||||||
|
const exited = once(child, 'exit');
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
const timer = setTimeout(() => child.kill('SIGKILL'), 5_000);
|
||||||
|
await exited;
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,14 +3,6 @@ import { randomUUID } from 'node:crypto';
|
|||||||
import PocketBase, { BaseAuthStore } from 'pocketbase';
|
import PocketBase, { BaseAuthStore } from 'pocketbase';
|
||||||
import { inject } from 'vitest';
|
import { inject } from 'vitest';
|
||||||
|
|
||||||
declare module 'vitest' {
|
|
||||||
export interface ProvidedContext {
|
|
||||||
pbUrl: string;
|
|
||||||
pbDataDir: string;
|
|
||||||
pbSuperuser: { email: string; password: string };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestUser {
|
export interface TestUser {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
// Типы значений, которые global-setup отдаёт тестам через `project.provide` / `inject`.
|
||||||
|
import 'vitest';
|
||||||
|
|
||||||
|
declare module 'vitest' {
|
||||||
|
export interface ProvidedContext {
|
||||||
|
pbUrl: string;
|
||||||
|
pbDataDir: string;
|
||||||
|
pbSuperuser: { email: string; password: string };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.spec.json"
|
"path": "./tsconfig.spec.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.tests.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/* Проверка типов стенда API-тестов и вспомогательного кода: запускается из `pnpm test:api`. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": ["tests/**/*.ts", "vitest.api.config.mts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user