docs(report): update Phase 2.4 report with UNVERIFIED and migration details and fix test typechecks

This commit is contained in:
Ochenstarik 2026-08-21 08:44:34 +07:00
parent 3279f287fb
commit b2888950cd
15 changed files with 90 additions and 70 deletions

View file

@ -2,7 +2,8 @@
**Date:** 2026-08-20 **Date:** 2026-08-20
**Base Commit SHA:** `9927e74421223135a170de640255803ab513fd48` **Base Commit SHA:** `9927e74421223135a170de640255803ab513fd48`
**Status:** COMPLETED / PASS **Result Commit SHA:** `78151572bd2ae01645d70a0768c6ece517e2cab0`
**Status:** IMPLEMENTED / READY FOR INDEPENDENT RE-REVIEW
**Assigned Agent:** Antigravity (Implementation Orchestrator) **Assigned Agent:** Antigravity (Implementation Orchestrator)
--- ---
@ -12,9 +13,9 @@
Закрыта критическая уязвимость манипуляции результатами розыгрышей (**Seed Grinding / Pre-computation attack**), при которой организатор мог локально перебрать seed'ы на открытом списке участников и передать в `POST /api/giveaways/[id]/draw` подобранный seed, гарантирующий победу нужного участника при успешном статусе верификации `verified: true`. Закрыта критическая уязвимость манипуляции результатами розыгрышей (**Seed Grinding / Pre-computation attack**), при которой организатор мог локально перебрать seed'ы на открытом списке участников и передать в `POST /api/giveaways/[id]/draw` подобранный seed, гарантирующий победу нужного участника при успешном статусе верификации `verified: true`.
Реализована схема **Cryptographic Seed Pre-Commitment**: Реализована схема **Cryptographic Seed Pre-Commitment**:
1. Клиентский `seed` полностью исключён из входных схем (`createGiveawaySchema`, `executeDrawSchema`). Попытка передать `seed` в теле запроса строго отклоняется со статусом `400 VALIDATION_ERROR`. 1. Клиентский `seed` полностью исключён из входных схем (`createGiveawaySchema`, `executeDrawSchema`). Попытка передать `seed` в теле запроса `POST /draw` строго отклоняется со статусом `400 VALIDATION_ERROR`.
2. Seed генерируется на сервере исключительно через CSPRNG (`crypto.randomBytes(16).toString('hex')`) в момент создания и блокировки неизменяемого слепка участников (`createAndLockSnapshot`) и сохраняется в БД (`Giveaway.seed`) в единой атомарной транзакции. 2. Seed генерируется на сервере исключительно через CSPRNG (`generateCryptoSecureSeed()`) в момент создания и блокировки неизменяемого слепка участников (`createAndLockSnapshot`) и сохраняется в БД (`Giveaway.seed`) в единой атомарной операции.
3. До момента проведения жеребьёвки (`DRAWN`) открытый `seed` скрыт от клиента во всех публичных и приватных эндпоинтах (`POST /api/giveaways/[id]/snapshot`, `GET /api/giveaways/[id]`, `GET /api/giveaways`, `GET /api/giveaways/[id]/participants`). Клиенту отдаётся только криптографическое обязательство `seedCommitment = sha256(seed)`. 3. До момента проведения жеребьёвки (`DRAWN`) открытый `seed` скрыт от клиента во всех эндпоинтах (`POST /api/giveaways/[id]/snapshot`, `GET /api/giveaways/[id]`, `GET /api/giveaways`, `GET /api/giveaways/[id]/participants`). Клиенту отдаётся только криптографическое обязательство `seedCommitment = sha256(seed)`.
4. Роут жеребьёвки `POST /api/giveaways/[id]/draw` читает seed строго из базы данных (`giveaway.seed`). Любой fallback на генерацию seed в роуте жеребьёвки удалён. Если seed отсутствует — возвращается `409 CONFLICT`. 4. Роут жеребьёвки `POST /api/giveaways/[id]/draw` читает seed строго из базы данных (`giveaway.seed`). Любой fallback на генерацию seed в роуте жеребьёвки удалён. Если seed отсутствует — возвращается `409 CONFLICT`.
5. После завершения жеребьёвки `seed` раскрывается публично (`giveaway.seed` и `drawResult.seedUsed`), позволяя любому участнику подтвердить равенство `sha256(drawResult.seedUsed) === seedCommitment` и математическую честность через независимый `GET /api/giveaways/[id]/verify`. 5. После завершения жеребьёвки `seed` раскрывается публично (`giveaway.seed` и `drawResult.seedUsed`), позволяя любому участнику подтвердить равенство `sha256(drawResult.seedUsed) === seedCommitment` и математическую честность через независимый `GET /api/giveaways/[id]/verify`.
@ -24,10 +25,10 @@
| File | Type | Description | | File | Type | Description |
|------|------|-------------| |------|------|-------------|
| `src/core/randomizer/hasher.ts` | Backend | Добавлена функция `computeSeedCommitment(seed: string): string` (SHA-256 hex digest). | | `src/core/randomizer/hasher.ts` | Core | Добавлена функция `computeSeedCommitment(seed: string): string` (SHA-256 hex digest). |
| `src/core/validation/giveaway-schemas.ts` | Validation | Удалено поле `seed` из `createGiveawaySchema` и `executeDrawSchema` (строгая валидация). | | `src/core/validation/giveaway-schemas.ts` | Validation | Удалено поле `seed` из `createGiveawaySchema` и `executeDrawSchema` (строгая `.strict()` валидация на draw). |
| `src/lib/repository/giveaway-repository.ts` | Repository | Добавлено поле `seedCommitment?: string \| null` в `GiveawayWithRelations`, удален `seed` из `CreateGiveawayInput`. | | `src/lib/repository/giveaway-repository.ts` | Repository | Добавлено поле `seedCommitment?: string \| null` в `GiveawayWithRelations`, удален `seed` из `CreateGiveawayInput`. |
| `src/lib/repository/memory-repository.ts` | Storage Driver | Инициализация `seed: null`, атомарная генерация и фиксация `seed` + `seedCommitment` в `createAndLockSnapshot`. | | `src/lib/repository/memory-repository.ts` | Storage Driver | Инициализация `seed: null`, генерация и фиксация `seed` + `seedCommitment` в `createAndLockSnapshot`. |
| `src/lib/repository/prisma-repository.ts` | Storage Driver | Фиксация `seed` в БД внутри `$transaction` при `createAndLockSnapshot`, маппинг `seedCommitment`. | | `src/lib/repository/prisma-repository.ts` | Storage Driver | Фиксация `seed` в БД внутри `$transaction` при `createAndLockSnapshot`, маппинг `seedCommitment`. |
| `src/app/api/giveaways/route.ts` | API Route | Удалена передача клиентского seed при создании розыгрыша. | | `src/app/api/giveaways/route.ts` | API Route | Удалена передача клиентского seed при создании розыгрыша. |
| `src/app/api/giveaways/[id]/snapshot/route.ts` | API Route | Возврат `seedCommitment` вместо раскрытия plaintext seed. | | `src/app/api/giveaways/[id]/snapshot/route.ts` | API Route | Возврат `seedCommitment` вместо раскрытия plaintext seed. |
@ -36,6 +37,7 @@
| `src/app/giveaways/new/page.tsx` | Frontend UI | Удалено поле ручного ввода seed из шага 4; добавлен индикатор защиты от подбора (Seed Pre-Commitment) со значением SHA-256 commitment. | | `src/app/giveaways/new/page.tsx` | Frontend UI | Удалено поле ручного ввода seed из шага 4; добавлен индикатор защиты от подбора (Seed Pre-Commitment) со значением SHA-256 commitment. |
| `tests/api-validation.test.ts` | Tests | Обновлены тесты валидации на строгое отклонение `seed`. | | `tests/api-validation.test.ts` | Tests | Обновлены тесты валидации на строгое отклонение `seed`. |
| `tests/seed-precommit-gate.test.ts` | Tests (NEW) | Комплексный adversarial & regression test suite (7 тестов). | | `tests/seed-precommit-gate.test.ts` | Tests (NEW) | Комплексный adversarial & regression test suite (7 тестов). |
| `tests/storage-driver.test.ts` | Tests | Исправлен мок `IGiveawayRepository` (добавлены `listGiveawaysSummary` и `getParticipantsPaginated`). |
--- ---
@ -53,30 +55,46 @@
--- ---
## 4. Verification Evidence & Test Gate ## 4. API Contract & Database Migration
Фактически выполненные команды: - **Database Migration Required:** `NO` (Поле `Giveaway.seed` уже существует в `prisma/schema.prisma` как nullable `String?` и готово к сохранению CSPRNG seed).
- **API Contract Changes:**
1. `npx prisma generate` → Exit code 0 (Prisma Client v5.22.0 generated). - `POST /api/giveaways`: поле `seed` удалено из входящего тела (автоматически отбрасывается `.strip()`).
2. `npm test` → Exit code 0 (48 test files, 280 tests passed, 0 failed). - `POST /api/giveaways/[id]/draw`: поле `seed` строго запрещено в теле запроса (`.strict()`), возвращает `400 VALIDATION_ERROR` при попытке передачи.
3. `npm run lint` → Exit code 0 (Next.js ESLint passed clean). - `POST /api/giveaways/[id]/snapshot`: в ответ добавлено поле `seedCommitment: string` (SHA-256 hex от сгенерированного seed).
4. `npm run build` → Exit code 0 (Production build & static generation compiled successfully). - `GET /api/giveaways/[id]`: в объекте `giveaway` возвращается `seedCommitment: string | null`. До статуса `DRAWN` поле `giveaway.seed` маскируется (`null`), после проведения розыгрыша раскрывается исходный `seed`.
- `GET /api/giveaways`: поле `seed` отсутствует в `GiveawaySummary` и не утекает в списках.
### Regression Tests Summary (`tests/seed-precommit-gate.test.ts`):
- `adversarial attempt to pass custom seed in draw body fails with 400 and keeps status SNAPSHOT_LOCKED` → PASS
- `grinding regression: local brute-force of 100 seeds cannot alter the pre-committed API winner` → PASS
- `draw attempt on giveaway without locked snapshot and seed returns 409 Conflict` → PASS
- `GET /api/giveaways/[id] masks seed before DRAWN and exposes seedCommitment` → PASS
- `after DRAWN, sha256(seedUsed) strictly equals seedCommitment and verify endpoint succeeds` → PASS
- `MemoryGiveawayRepository generates and locks seed during createAndLockSnapshot` → PASS
- `PrismaGiveawayRepository maps seedCommitment correctly` → PASS
--- ---
## 5. Security & Risk Assessment ## 5. Verification Evidence & Test Gate
- **CRITICAL/HIGH findings:** 0 open Фактически выполненные команды:
- **Seed Grinding Attack:** ELIMINATED & MATHEMATICALLY PREVENTED
- **IDOR / Cross-User Access:** PRESERVED (protected by session & `requireGiveawayOwner`) ```text
- **Secrets:** 0 leaked npx prisma generate -> Exit code 0 (Prisma Client v5.22.0 generated)
- **UNVERIFIED statements:** None npm test -> Exit code 0 (49 test files, 284 tests passed, 0 failed)
npm run lint -> Exit code 0 (Next.js ESLint passed clean)
npm run build -> Exit code 0 (Next.js production build compiled successfully)
npx tsc --noEmit -> Exit code 0 (Clean TypeScript check)
```
### Regression Tests Summary (`tests/seed-precommit-gate.test.ts`):
- `adversarial attempt to pass custom seed in draw body fails with 400 and keeps status SNAPSHOT_LOCKED` → **PASS**
- `grinding regression: local brute-force of 100 seeds cannot alter the pre-committed API winner` → **PASS**
- `draw attempt on giveaway without locked snapshot and seed returns 409 Conflict` → **PASS**
- `GET /api/giveaways/[id] masks seed before DRAWN and exposes seedCommitment` → **PASS**
- `after DRAWN, sha256(seedUsed) strictly equals seedCommitment and verify endpoint succeeds` → **PASS**
- `MemoryGiveawayRepository generates and locks seed during createAndLockSnapshot` → **PASS**
- `PrismaGiveawayRepository maps seedCommitment correctly` → **PASS**
---
## 6. UNVERIFIED Assertions & Tech Debt
1. **UNVERIFIED: Prisma integration harness with live DB:**
- В текущем тестовом сьюте все функциональные тесты выполняются с драйвером `STORAGE_DRIVER=memory`. Хотя `PrismaGiveawayRepository` полностью реализован, компилируется (`tsc --noEmit`), собирается (`npm run build`) и покрыт маппинг-тестами, его сквозное выполнение в интеграционном тесте с реальной БД PostgreSQL не автоматизировано в Vitest.
- **Рекомендация / Proposed Next Task:** Добавить тестовый сьют `tests/prisma-integration.test.ts` для запуска прогона репозитория против тестового экземпляра PostgreSQL.
2. **CRITICAL Finding Status:**
- Исполнитель не объявляет CRITICAL finding автоматически закрытым самостоятельно. Требуется независимое re-review ревизии.

View file

@ -31,6 +31,7 @@ describe('100-Draw Concurrency Regression & Mixed Race', () => {
postId: '100', postId: '100',
sourceUrl: 'https://vk.com/wall-100_100', sourceUrl: 'https://vk.com/wall-100_100',
title: '100 Concurrency Test', title: '100 Concurrency Test',
text: 'Test description',
likesCount: 50, likesCount: 50,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,
@ -98,6 +99,7 @@ describe('100-Draw Concurrency Regression & Mixed Race', () => {
postId: '200', postId: '200',
sourceUrl: 'https://vk.com/wall-100_200', sourceUrl: 'https://vk.com/wall-100_200',
title: 'Mixed Race Test', title: 'Mixed Race Test',
text: 'Test description',
likesCount: 50, likesCount: 50,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,

View file

@ -43,6 +43,7 @@ describe('Concurrency: Participants Update vs Snapshot Lock Race', () => {
postId: '1', postId: '1',
sourceUrl: 'https://vk.com/wall-100_1', sourceUrl: 'https://vk.com/wall-100_1',
title: 'Race Test', title: 'Race Test',
text: 'Test description',
likesCount: 10, likesCount: 10,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,
@ -80,6 +81,7 @@ describe('Concurrency: Participants Update vs Snapshot Lock Race', () => {
postId: '2', postId: '2',
sourceUrl: 'https://vk.com/wall-100_2', sourceUrl: 'https://vk.com/wall-100_2',
title: 'Simultaneous Race Test', title: 'Simultaneous Race Test',
text: 'Test description',
likesCount: 10, likesCount: 10,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,

View file

@ -14,6 +14,7 @@ import { VkPrivateResourceError } from '../src/integrations/vk/vk-errors';
import { ProviderFactory } from '../src/providers/factory'; import { ProviderFactory } from '../src/providers/factory';
import { GiveawayStore } from '../src/lib/giveaway-store'; import { GiveawayStore } from '../src/lib/giveaway-store';
import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository'; import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository';
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => { describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
let userRepo: MemoryUserRepository; let userRepo: MemoryUserRepository;
@ -299,11 +300,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
// 6a. Create giveaway for Alice (has credentials) // 6a. Create giveaway for Alice (has credentials)
const gwAlice = await GiveawayStore.create({ const gwAlice = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-101_101', sourceUrl: 'https://vk.com/wall-101_101',
platform: 'VK',
platformOwnerId: '-101',
platformPostId: '101',
title: 'Alice Giveaway',
organizerId: loggedInUser.id, organizerId: loggedInUser.id,
filterRules: DEFAULT_FILTER_RULES,
post: { post: {
platform: 'VK', platform: 'VK',
ownerId: '-101', ownerId: '-101',
@ -332,11 +330,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
// 6b. Create giveaway for Charlie (no credentials) // 6b. Create giveaway for Charlie (no credentials)
const gwCharlie = await GiveawayStore.create({ const gwCharlie = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-303_303', sourceUrl: 'https://vk.com/wall-303_303',
platform: 'VK',
platformOwnerId: '-303',
platformPostId: '303',
title: 'Charlie Giveaway',
organizerId: loggedInUserWithoutCreds.id, organizerId: loggedInUserWithoutCreds.id,
filterRules: DEFAULT_FILTER_RULES,
post: { post: {
platform: 'VK', platform: 'VK',
ownerId: '-303', ownerId: '-303',
@ -382,11 +377,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
const gw = await GiveawayStore.create({ const gw = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-555_555', sourceUrl: 'https://vk.com/wall-555_555',
platform: 'VK',
platformOwnerId: '-555',
platformPostId: '555',
title: 'David Giveaway',
organizerId: expiredUser.id, organizerId: expiredUser.id,
filterRules: DEFAULT_FILTER_RULES,
post: { post: {
platform: 'VK', platform: 'VK',
ownerId: '-555', ownerId: '-555',
@ -430,11 +422,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
const gw = await GiveawayStore.create({ const gw = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-777_777', sourceUrl: 'https://vk.com/wall-777_777',
platform: 'VK',
platformOwnerId: '-777',
platformPostId: '777',
title: 'Eve Giveaway',
organizerId: expiredNoRefreshUser.id, organizerId: expiredNoRefreshUser.id,
filterRules: DEFAULT_FILTER_RULES,
post: { post: {
platform: 'VK', platform: 'VK',
ownerId: '-777', ownerId: '-777',
@ -479,11 +468,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
const gw = await GiveawayStore.create({ const gw = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-888_888', sourceUrl: 'https://vk.com/wall-888_888',
platform: 'VK',
platformOwnerId: '-888',
platformPostId: '888',
title: 'Frank Giveaway',
organizerId: legacyUser.id, organizerId: legacyUser.id,
filterRules: DEFAULT_FILTER_RULES,
post: { post: {
platform: 'VK', platform: 'VK',
ownerId: '-888', ownerId: '-888',
@ -529,11 +515,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
const gw = await GiveawayStore.create({ const gw = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-999_999', sourceUrl: 'https://vk.com/wall-999_999',
platform: 'VK',
platformOwnerId: '-999',
platformPostId: '999',
title: 'Grace Giveaway',
organizerId: legacyRefreshUser.id, organizerId: legacyRefreshUser.id,
filterRules: DEFAULT_FILTER_RULES,
post: { post: {
platform: 'VK', platform: 'VK',
ownerId: '-999', ownerId: '-999',
@ -564,11 +547,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
it('11. giveaway detail response never leaks token or secret fields', async () => { it('11. giveaway detail response never leaks token or secret fields', async () => {
const gwAlice = await GiveawayStore.create({ const gwAlice = await GiveawayStore.create({
sourceUrl: 'https://vk.com/wall-101_101', sourceUrl: 'https://vk.com/wall-101_101',
platform: 'VK',
platformOwnerId: '-101',
platformPostId: '101',
title: 'Alice Giveaway',
organizerId: loggedInUser.id, organizerId: loggedInUser.id,
filterRules: DEFAULT_FILTER_RULES,
post: { post: {
platform: 'VK', platform: 'VK',
ownerId: '-101', ownerId: '-101',

View file

@ -27,6 +27,7 @@ describe('True Partial Fisher-Yates (HMAC_SHA256_FY_V1)', () => {
version: 1, version: 1,
createdAt: '2026-08-17T12:00:00.000Z', createdAt: '2026-08-17T12:00:00.000Z',
eligibleParticipants: participants, eligibleParticipants: participants,
filterRulesSnapshot: DEFAULT_FILTER_RULES,
participantCount: size, participantCount: size,
participantsSnapshotHash: computeParticipantsSnapshotHash(participants), participantsSnapshotHash: computeParticipantsSnapshotHash(participants),
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES), conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),

View file

@ -35,6 +35,7 @@ describe('Phase 2.2.3 Claude PoC: GET /api/giveaways IDOR & Scoped Query Gate',
postId: '100', postId: '100',
sourceUrl: 'https://vk.com/wall-10_100', sourceUrl: 'https://vk.com/wall-10_100',
title: 'Secret Giveaway of Alice', title: 'Secret Giveaway of Alice',
text: 'Text',
likesCount: 10, likesCount: 10,
commentsCount: 2, commentsCount: 2,
repostsCount: 0, repostsCount: 0,
@ -52,6 +53,7 @@ describe('Phase 2.2.3 Claude PoC: GET /api/giveaways IDOR & Scoped Query Gate',
postId: '200', postId: '200',
sourceUrl: 'https://vk.com/wall-20_200', sourceUrl: 'https://vk.com/wall-20_200',
title: 'Confidential Giveaway of Bob', title: 'Confidential Giveaway of Bob',
text: 'Text',
likesCount: 50, likesCount: 50,
commentsCount: 15, commentsCount: 15,
repostsCount: 5, repostsCount: 5,

View file

@ -18,21 +18,21 @@ describe('Phase 2.2.3 Origin, CSRF Trusted Host & Rate Limiting Gate', () => {
describe('Production Base URL & VK_REDIRECT_URI Fail-Fast Policy', () => { describe('Production Base URL & VK_REDIRECT_URI Fail-Fast Policy', () => {
it('fails fast in production when APP_BASE_URL is missing', () => { it('fails fast in production when APP_BASE_URL is missing', () => {
process.env.NODE_ENV = 'production'; (process.env as any).NODE_ENV = 'production';
delete process.env.APP_BASE_URL; delete process.env.APP_BASE_URL;
expect(() => getAppBaseUrl()).toThrow(/APP_BASE_URL environment variable is strictly required in production/i); expect(() => getAppBaseUrl()).toThrow(/APP_BASE_URL environment variable is strictly required in production/i);
}); });
it('fails fast in production when APP_BASE_URL is not HTTPS', () => { it('fails fast in production when APP_BASE_URL is not HTTPS', () => {
process.env.NODE_ENV = 'production'; (process.env as any).NODE_ENV = 'production';
process.env.APP_BASE_URL = 'http://insecure-http-url.com'; process.env.APP_BASE_URL = 'http://insecure-http-url.com';
expect(() => getAppBaseUrl()).toThrow(/must be a valid HTTPS URL in production/i); expect(() => getAppBaseUrl()).toThrow(/must be a valid HTTPS URL in production/i);
}); });
it('fails fast in production when VK_REDIRECT_URI is missing', () => { it('fails fast in production when VK_REDIRECT_URI is missing', () => {
process.env.NODE_ENV = 'production'; (process.env as any).NODE_ENV = 'production';
process.env.APP_BASE_URL = 'https://randomayzer.org'; process.env.APP_BASE_URL = 'https://randomayzer.org';
delete process.env.VK_REDIRECT_URI; delete process.env.VK_REDIRECT_URI;
@ -40,7 +40,7 @@ describe('Phase 2.2.3 Origin, CSRF Trusted Host & Rate Limiting Gate', () => {
}); });
it('accepts valid HTTPS configuration in production', () => { it('accepts valid HTTPS configuration in production', () => {
process.env.NODE_ENV = 'production'; (process.env as any).NODE_ENV = 'production';
process.env.APP_BASE_URL = 'https://randomayzer.org'; process.env.APP_BASE_URL = 'https://randomayzer.org';
process.env.VK_REDIRECT_URI = 'https://randomayzer.org/api/auth/vk/callback'; process.env.VK_REDIRECT_URI = 'https://randomayzer.org/api/auth/vk/callback';
@ -51,7 +51,7 @@ describe('Phase 2.2.3 Origin, CSRF Trusted Host & Rate Limiting Gate', () => {
describe('CSRF Trusted Host & Host-Spoofing Immunity', () => { describe('CSRF Trusted Host & Host-Spoofing Immunity', () => {
it('rejects attacker sending evil Origin even if attacker injects spoofed X-Forwarded-Host', () => { it('rejects attacker sending evil Origin even if attacker injects spoofed X-Forwarded-Host', () => {
process.env.NODE_ENV = 'production'; (process.env as any).NODE_ENV = 'production';
process.env.APP_BASE_URL = 'https://trusted-randomayzer.org'; process.env.APP_BASE_URL = 'https://trusted-randomayzer.org';
const req = new NextRequest('http://localhost/api/auth/logout', { const req = new NextRequest('http://localhost/api/auth/logout', {
@ -78,7 +78,7 @@ describe('Phase 2.2.3 Origin, CSRF Trusted Host & Rate Limiting Gate', () => {
}); });
it('accepts valid origin matching configured trusted host', () => { it('accepts valid origin matching configured trusted host', () => {
process.env.NODE_ENV = 'production'; (process.env as any).NODE_ENV = 'production';
process.env.APP_BASE_URL = 'https://trusted-randomayzer.org'; process.env.APP_BASE_URL = 'https://trusted-randomayzer.org';
const req = new NextRequest('https://trusted-randomayzer.org/api/auth/logout', { const req = new NextRequest('https://trusted-randomayzer.org/api/auth/logout', {

View file

@ -27,6 +27,7 @@ describe('POST /participants Payload Summary Regression Test', () => {
postId: '1', postId: '1',
sourceUrl: 'https://vk.com/wall-100_1', sourceUrl: 'https://vk.com/wall-100_1',
title: 'Large Payload Test', title: 'Large Payload Test',
text: 'Test description',
likesCount: 100000, likesCount: 100000,
commentsCount: 50000, commentsCount: 50000,
repostsCount: 0, repostsCount: 0,

View file

@ -29,7 +29,7 @@ describe('Provider Safety (No unconfigured mocks in Production)', () => {
}); });
it('should throw DependencyUnavailableError in production when VK credentials and USE_VK_MOCK are missing', () => { it('should throw DependencyUnavailableError in production when VK credentials and USE_VK_MOCK are missing', () => {
process.env.NODE_ENV = 'production'; (process.env as any).NODE_ENV = 'production';
delete process.env.USE_VK_MOCK; delete process.env.USE_VK_MOCK;
delete process.env.VK_SERVICE_TOKEN; delete process.env.VK_SERVICE_TOKEN;

View file

@ -51,6 +51,7 @@ describe('Phase 2.4 — Seed Pre-Commit Gate (Seed Grinding Elimination)', () =>
postId: '1054', postId: '1054',
sourceUrl: 'https://vk.com/wall-22446688_1054', sourceUrl: 'https://vk.com/wall-22446688_1054',
title: 'Fairness Test Post', title: 'Fairness Test Post',
text: 'Test description',
likesCount: 100, likesCount: 100,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,
@ -191,6 +192,7 @@ describe('Phase 2.4 — Seed Pre-Commit Gate (Seed Grinding Elimination)', () =>
postId: '1054', postId: '1054',
sourceUrl: 'https://vk.com/wall-22446688_1054', sourceUrl: 'https://vk.com/wall-22446688_1054',
title: 'No Seed Post', title: 'No Seed Post',
text: 'Test description',
likesCount: 10, likesCount: 10,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,
@ -295,6 +297,7 @@ describe('Phase 2.4 — Seed Pre-Commit Gate (Seed Grinding Elimination)', () =>
postId: '1', postId: '1',
sourceUrl: 'https://vk.com/wall-1_1', sourceUrl: 'https://vk.com/wall-1_1',
title: 'Memory Parity', title: 'Memory Parity',
text: 'Test description',
likesCount: 10, likesCount: 10,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,

View file

@ -47,6 +47,7 @@ describe('Phase 2.4.1 — Atomic Snapshot + Seed Commitment Binding', () => {
postId: '789', postId: '789',
sourceUrl: 'https://vk.com/wall-33445566_789', sourceUrl: 'https://vk.com/wall-33445566_789',
title: 'Atomic Snapshot Test Post', title: 'Atomic Snapshot Test Post',
text: 'Test description',
likesCount: 50, likesCount: 50,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,
@ -70,6 +71,7 @@ describe('Phase 2.4.1 — Atomic Snapshot + Seed Commitment Binding', () => {
postId: '1', postId: '1',
sourceUrl: 'https://vk.com/wall-1_1', sourceUrl: 'https://vk.com/wall-1_1',
title: 'Parity Test', title: 'Parity Test',
text: 'Test description',
likesCount: 50, likesCount: 50,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,

View file

@ -40,6 +40,12 @@ describe('Storage Driver Policy & No Silent Fallback', () => {
listGiveaways: async () => { listGiveaways: async () => {
throw new Error('Database connection failed'); throw new Error('Database connection failed');
}, },
listGiveawaysSummary: async () => {
throw new Error('Database connection failed');
},
getParticipantsPaginated: async () => {
throw new Error('Database connection failed');
},
updateStatus: async () => { throw new Error('DB error'); }, updateStatus: async () => { throw new Error('DB error'); },
saveParticipants: async () => { throw new Error('DB error'); }, saveParticipants: async () => { throw new Error('DB error'); },
createAndLockSnapshot: async () => { throw new Error('DB error'); }, createAndLockSnapshot: async () => { throw new Error('DB error'); },

View file

@ -138,7 +138,7 @@ describe('Phase 2.3.1 — Token Refresh Correctness Gate', () => {
callCount++; callCount++;
await new Promise(r => setTimeout(r, 20)); await new Promise(r => setTimeout(r, 20));
const res = await orig(p); const res = await orig(p);
delete res.user_id; delete (res as any).user_id;
return res; return res;
}; };
@ -323,7 +323,6 @@ describe('Phase 2.3.1 — Token Refresh Correctness Gate', () => {
const provider = new VkProvider('svc_token', mockClient as any, mockResolver as any); const provider = new VkProvider('svc_token', mockClient as any, mockResolver as any);
const results = await provider.fetchParticipants({ const results = await provider.fetchParticipants({
platform: 'VK',
ownerId: '-123', ownerId: '-123',
postId: '456', postId: '456',
organizerId: 'org1', organizerId: 'org1',

View file

@ -13,21 +13,21 @@ describe('Phase 2.2.1 Token Vault Security & Fail-Fast Policies', () => {
}); });
it('fails fast with fatal configuration error in production when TOKEN_ENCRYPTION_KEY is missing', () => { it('fails fast with fatal configuration error in production when TOKEN_ENCRYPTION_KEY is missing', () => {
process.env.NODE_ENV = 'production'; (process.env as any).NODE_ENV = 'production';
delete process.env.TOKEN_ENCRYPTION_KEY; delete process.env.TOKEN_ENCRYPTION_KEY;
expect(() => new AesGcmTokenVault()).toThrow(/TOKEN_ENCRYPTION_KEY environment variable is strictly required in production/i); expect(() => new AesGcmTokenVault()).toThrow(/TOKEN_ENCRYPTION_KEY environment variable is strictly required in production/i);
}); });
it('fails fast in production when TOKEN_ENCRYPTION_KEY is shorter than 32 characters', () => { it('fails fast in production when TOKEN_ENCRYPTION_KEY is shorter than 32 characters', () => {
process.env.NODE_ENV = 'production'; (process.env as any).NODE_ENV = 'production';
process.env.TOKEN_ENCRYPTION_KEY = 'too-short-key'; process.env.TOKEN_ENCRYPTION_KEY = 'too-short-key';
expect(() => new AesGcmTokenVault()).toThrow(/must be at least 32 characters long in production/i); expect(() => new AesGcmTokenVault()).toThrow(/must be at least 32 characters long in production/i);
}); });
it('successfully initializes and encrypts/decrypts with valid 32+ character key in production', async () => { it('successfully initializes and encrypts/decrypts with valid 32+ character key in production', async () => {
process.env.NODE_ENV = 'production'; (process.env as any).NODE_ENV = 'production';
process.env.TOKEN_ENCRYPTION_KEY = 'a-super-secret-production-encryption-key-32chars!'; process.env.TOKEN_ENCRYPTION_KEY = 'a-super-secret-production-encryption-key-32chars!';
const vault = new AesGcmTokenVault(); const vault = new AesGcmTokenVault();
@ -42,7 +42,7 @@ describe('Phase 2.2.1 Token Vault Security & Fail-Fast Policies', () => {
}); });
it('allows explicit dev/test key when in development or test environment', async () => { it('allows explicit dev/test key when in development or test environment', async () => {
process.env.NODE_ENV = 'test'; (process.env as any).NODE_ENV = 'test';
delete process.env.TOKEN_ENCRYPTION_KEY; delete process.env.TOKEN_ENCRYPTION_KEY;
const vault = new AesGcmTokenVault(); const vault = new AesGcmTokenVault();

View file

@ -43,6 +43,7 @@ describe('Winner Count Contract & Draw Retry Invariants', () => {
postId: '1', postId: '1',
sourceUrl: 'https://vk.com/wall-100_1', sourceUrl: 'https://vk.com/wall-100_1',
title: 'Title', title: 'Title',
text: 'Test description',
likesCount: 3, likesCount: 3,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,
@ -79,6 +80,7 @@ describe('Winner Count Contract & Draw Retry Invariants', () => {
postId: '2', postId: '2',
sourceUrl: 'https://vk.com/wall-100_2', sourceUrl: 'https://vk.com/wall-100_2',
title: 'Title', title: 'Title',
text: 'Test description',
likesCount: 3, likesCount: 3,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,
@ -115,6 +117,7 @@ describe('Winner Count Contract & Draw Retry Invariants', () => {
postId: '3', postId: '3',
sourceUrl: 'https://vk.com/wall-100_3', sourceUrl: 'https://vk.com/wall-100_3',
title: 'Title', title: 'Title',
text: 'Test description',
likesCount: 3, likesCount: 3,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,
@ -149,6 +152,7 @@ describe('Winner Count Contract & Draw Retry Invariants', () => {
postId: '4', postId: '4',
sourceUrl: 'https://vk.com/wall-100_4', sourceUrl: 'https://vk.com/wall-100_4',
title: 'Title', title: 'Title',
text: 'Test description',
likesCount: 3, likesCount: 3,
commentsCount: 0, commentsCount: 0,
repostsCount: 0, repostsCount: 0,