docs(report): update Phase 2.4 report with UNVERIFIED and migration details and fix test typechecks
This commit is contained in:
parent
3279f287fb
commit
b2888950cd
15 changed files with 90 additions and 70 deletions
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
**Date:** 2026-08-20
|
||||
**Base Commit SHA:** `9927e74421223135a170de640255803ab513fd48`
|
||||
**Status:** COMPLETED / PASS
|
||||
**Result Commit SHA:** `78151572bd2ae01645d70a0768c6ece517e2cab0`
|
||||
**Status:** IMPLEMENTED / READY FOR INDEPENDENT RE-REVIEW
|
||||
**Assigned Agent:** Antigravity (Implementation Orchestrator)
|
||||
|
||||
---
|
||||
|
|
@ -12,9 +13,9 @@
|
|||
Закрыта критическая уязвимость манипуляции результатами розыгрышей (**Seed Grinding / Pre-computation attack**), при которой организатор мог локально перебрать seed'ы на открытом списке участников и передать в `POST /api/giveaways/[id]/draw` подобранный seed, гарантирующий победу нужного участника при успешном статусе верификации `verified: true`.
|
||||
|
||||
Реализована схема **Cryptographic Seed Pre-Commitment**:
|
||||
1. Клиентский `seed` полностью исключён из входных схем (`createGiveawaySchema`, `executeDrawSchema`). Попытка передать `seed` в теле запроса строго отклоняется со статусом `400 VALIDATION_ERROR`.
|
||||
2. Seed генерируется на сервере исключительно через CSPRNG (`crypto.randomBytes(16).toString('hex')`) в момент создания и блокировки неизменяемого слепка участников (`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)`.
|
||||
1. Клиентский `seed` полностью исключён из входных схем (`createGiveawaySchema`, `executeDrawSchema`). Попытка передать `seed` в теле запроса `POST /draw` строго отклоняется со статусом `400 VALIDATION_ERROR`.
|
||||
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)`.
|
||||
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`.
|
||||
|
||||
|
|
@ -24,10 +25,10 @@
|
|||
|
||||
| File | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `src/core/randomizer/hasher.ts` | Backend | Добавлена функция `computeSeedCommitment(seed: string): string` (SHA-256 hex digest). |
|
||||
| `src/core/validation/giveaway-schemas.ts` | Validation | Удалено поле `seed` из `createGiveawaySchema` и `executeDrawSchema` (строгая валидация). |
|
||||
| `src/core/randomizer/hasher.ts` | Core | Добавлена функция `computeSeedCommitment(seed: string): string` (SHA-256 hex digest). |
|
||||
| `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/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/app/api/giveaways/route.ts` | API Route | Удалена передача клиентского 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. |
|
||||
| `tests/api-validation.test.ts` | Tests | Обновлены тесты валидации на строгое отклонение `seed`. |
|
||||
| `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
|
||||
|
||||
Фактически выполненные команды:
|
||||
|
||||
1. `npx prisma generate` → Exit code 0 (Prisma Client v5.22.0 generated).
|
||||
2. `npm test` → Exit code 0 (48 test files, 280 tests passed, 0 failed).
|
||||
3. `npm run lint` → Exit code 0 (Next.js ESLint passed clean).
|
||||
4. `npm run build` → Exit code 0 (Production build & static generation compiled successfully).
|
||||
|
||||
### 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
|
||||
- **Database Migration Required:** `NO` (Поле `Giveaway.seed` уже существует в `prisma/schema.prisma` как nullable `String?` и готово к сохранению CSPRNG seed).
|
||||
- **API Contract Changes:**
|
||||
- `POST /api/giveaways`: поле `seed` удалено из входящего тела (автоматически отбрасывается `.strip()`).
|
||||
- `POST /api/giveaways/[id]/draw`: поле `seed` строго запрещено в теле запроса (`.strict()`), возвращает `400 VALIDATION_ERROR` при попытке передачи.
|
||||
- `POST /api/giveaways/[id]/snapshot`: в ответ добавлено поле `seedCommitment: string` (SHA-256 hex от сгенерированного seed).
|
||||
- `GET /api/giveaways/[id]`: в объекте `giveaway` возвращается `seedCommitment: string | null`. До статуса `DRAWN` поле `giveaway.seed` маскируется (`null`), после проведения розыгрыша раскрывается исходный `seed`.
|
||||
- `GET /api/giveaways`: поле `seed` отсутствует в `GiveawaySummary` и не утекает в списках.
|
||||
|
||||
---
|
||||
|
||||
## 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`)
|
||||
- **Secrets:** 0 leaked
|
||||
- **UNVERIFIED statements:** None
|
||||
Фактически выполненные команды:
|
||||
|
||||
```text
|
||||
npx prisma generate -> Exit code 0 (Prisma Client v5.22.0 generated)
|
||||
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 ревизии.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ describe('100-Draw Concurrency Regression & Mixed Race', () => {
|
|||
postId: '100',
|
||||
sourceUrl: 'https://vk.com/wall-100_100',
|
||||
title: '100 Concurrency Test',
|
||||
text: 'Test description',
|
||||
likesCount: 50,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
@ -98,6 +99,7 @@ describe('100-Draw Concurrency Regression & Mixed Race', () => {
|
|||
postId: '200',
|
||||
sourceUrl: 'https://vk.com/wall-100_200',
|
||||
title: 'Mixed Race Test',
|
||||
text: 'Test description',
|
||||
likesCount: 50,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ describe('Concurrency: Participants Update vs Snapshot Lock Race', () => {
|
|||
postId: '1',
|
||||
sourceUrl: 'https://vk.com/wall-100_1',
|
||||
title: 'Race Test',
|
||||
text: 'Test description',
|
||||
likesCount: 10,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
@ -80,6 +81,7 @@ describe('Concurrency: Participants Update vs Snapshot Lock Race', () => {
|
|||
postId: '2',
|
||||
sourceUrl: 'https://vk.com/wall-100_2',
|
||||
title: 'Simultaneous Race Test',
|
||||
text: 'Test description',
|
||||
likesCount: 10,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { VkPrivateResourceError } from '../src/integrations/vk/vk-errors';
|
|||
import { ProviderFactory } from '../src/providers/factory';
|
||||
import { GiveawayStore } from '../src/lib/giveaway-store';
|
||||
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', () => {
|
||||
let userRepo: MemoryUserRepository;
|
||||
|
|
@ -299,11 +300,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
|
|||
// 6a. Create giveaway for Alice (has credentials)
|
||||
const gwAlice = await GiveawayStore.create({
|
||||
sourceUrl: 'https://vk.com/wall-101_101',
|
||||
platform: 'VK',
|
||||
platformOwnerId: '-101',
|
||||
platformPostId: '101',
|
||||
title: 'Alice Giveaway',
|
||||
organizerId: loggedInUser.id,
|
||||
filterRules: DEFAULT_FILTER_RULES,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
ownerId: '-101',
|
||||
|
|
@ -332,11 +330,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
|
|||
// 6b. Create giveaway for Charlie (no credentials)
|
||||
const gwCharlie = await GiveawayStore.create({
|
||||
sourceUrl: 'https://vk.com/wall-303_303',
|
||||
platform: 'VK',
|
||||
platformOwnerId: '-303',
|
||||
platformPostId: '303',
|
||||
title: 'Charlie Giveaway',
|
||||
organizerId: loggedInUserWithoutCreds.id,
|
||||
filterRules: DEFAULT_FILTER_RULES,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
ownerId: '-303',
|
||||
|
|
@ -382,11 +377,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
|
|||
|
||||
const gw = await GiveawayStore.create({
|
||||
sourceUrl: 'https://vk.com/wall-555_555',
|
||||
platform: 'VK',
|
||||
platformOwnerId: '-555',
|
||||
platformPostId: '555',
|
||||
title: 'David Giveaway',
|
||||
organizerId: expiredUser.id,
|
||||
filterRules: DEFAULT_FILTER_RULES,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
ownerId: '-555',
|
||||
|
|
@ -430,11 +422,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
|
|||
|
||||
const gw = await GiveawayStore.create({
|
||||
sourceUrl: 'https://vk.com/wall-777_777',
|
||||
platform: 'VK',
|
||||
platformOwnerId: '-777',
|
||||
platformPostId: '777',
|
||||
title: 'Eve Giveaway',
|
||||
organizerId: expiredNoRefreshUser.id,
|
||||
filterRules: DEFAULT_FILTER_RULES,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
ownerId: '-777',
|
||||
|
|
@ -479,11 +468,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
|
|||
|
||||
const gw = await GiveawayStore.create({
|
||||
sourceUrl: 'https://vk.com/wall-888_888',
|
||||
platform: 'VK',
|
||||
platformOwnerId: '-888',
|
||||
platformPostId: '888',
|
||||
title: 'Frank Giveaway',
|
||||
organizerId: legacyUser.id,
|
||||
filterRules: DEFAULT_FILTER_RULES,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
ownerId: '-888',
|
||||
|
|
@ -529,11 +515,8 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
|
|||
|
||||
const gw = await GiveawayStore.create({
|
||||
sourceUrl: 'https://vk.com/wall-999_999',
|
||||
platform: 'VK',
|
||||
platformOwnerId: '-999',
|
||||
platformPostId: '999',
|
||||
title: 'Grace Giveaway',
|
||||
organizerId: legacyRefreshUser.id,
|
||||
filterRules: DEFAULT_FILTER_RULES,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
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 () => {
|
||||
const gwAlice = await GiveawayStore.create({
|
||||
sourceUrl: 'https://vk.com/wall-101_101',
|
||||
platform: 'VK',
|
||||
platformOwnerId: '-101',
|
||||
platformPostId: '101',
|
||||
title: 'Alice Giveaway',
|
||||
organizerId: loggedInUser.id,
|
||||
filterRules: DEFAULT_FILTER_RULES,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
ownerId: '-101',
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ describe('True Partial Fisher-Yates (HMAC_SHA256_FY_V1)', () => {
|
|||
version: 1,
|
||||
createdAt: '2026-08-17T12:00:00.000Z',
|
||||
eligibleParticipants: participants,
|
||||
filterRulesSnapshot: DEFAULT_FILTER_RULES,
|
||||
participantCount: size,
|
||||
participantsSnapshotHash: computeParticipantsSnapshotHash(participants),
|
||||
conditionsHash: computeConditionsHash(DEFAULT_FILTER_RULES),
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ describe('Phase 2.2.3 Claude PoC: GET /api/giveaways IDOR & Scoped Query Gate',
|
|||
postId: '100',
|
||||
sourceUrl: 'https://vk.com/wall-10_100',
|
||||
title: 'Secret Giveaway of Alice',
|
||||
text: 'Text',
|
||||
likesCount: 10,
|
||||
commentsCount: 2,
|
||||
repostsCount: 0,
|
||||
|
|
@ -52,6 +53,7 @@ describe('Phase 2.2.3 Claude PoC: GET /api/giveaways IDOR & Scoped Query Gate',
|
|||
postId: '200',
|
||||
sourceUrl: 'https://vk.com/wall-20_200',
|
||||
title: 'Confidential Giveaway of Bob',
|
||||
text: 'Text',
|
||||
likesCount: 50,
|
||||
commentsCount: 15,
|
||||
repostsCount: 5,
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
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;
|
||||
|
||||
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', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
(process.env as any).NODE_ENV = 'production';
|
||||
process.env.APP_BASE_URL = 'http://insecure-http-url.com';
|
||||
|
||||
expect(() => getAppBaseUrl()).toThrow(/must be a valid HTTPS URL in production/i);
|
||||
});
|
||||
|
||||
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';
|
||||
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', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
(process.env as any).NODE_ENV = 'production';
|
||||
process.env.APP_BASE_URL = 'https://randomayzer.org';
|
||||
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', () => {
|
||||
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';
|
||||
|
||||
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', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
(process.env as any).NODE_ENV = 'production';
|
||||
process.env.APP_BASE_URL = 'https://trusted-randomayzer.org';
|
||||
|
||||
const req = new NextRequest('https://trusted-randomayzer.org/api/auth/logout', {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ describe('POST /participants Payload Summary Regression Test', () => {
|
|||
postId: '1',
|
||||
sourceUrl: 'https://vk.com/wall-100_1',
|
||||
title: 'Large Payload Test',
|
||||
text: 'Test description',
|
||||
likesCount: 100000,
|
||||
commentsCount: 50000,
|
||||
repostsCount: 0,
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
(process.env as any).NODE_ENV = 'production';
|
||||
delete process.env.USE_VK_MOCK;
|
||||
delete process.env.VK_SERVICE_TOKEN;
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ describe('Phase 2.4 — Seed Pre-Commit Gate (Seed Grinding Elimination)', () =>
|
|||
postId: '1054',
|
||||
sourceUrl: 'https://vk.com/wall-22446688_1054',
|
||||
title: 'Fairness Test Post',
|
||||
text: 'Test description',
|
||||
likesCount: 100,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
@ -191,6 +192,7 @@ describe('Phase 2.4 — Seed Pre-Commit Gate (Seed Grinding Elimination)', () =>
|
|||
postId: '1054',
|
||||
sourceUrl: 'https://vk.com/wall-22446688_1054',
|
||||
title: 'No Seed Post',
|
||||
text: 'Test description',
|
||||
likesCount: 10,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
@ -295,6 +297,7 @@ describe('Phase 2.4 — Seed Pre-Commit Gate (Seed Grinding Elimination)', () =>
|
|||
postId: '1',
|
||||
sourceUrl: 'https://vk.com/wall-1_1',
|
||||
title: 'Memory Parity',
|
||||
text: 'Test description',
|
||||
likesCount: 10,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ describe('Phase 2.4.1 — Atomic Snapshot + Seed Commitment Binding', () => {
|
|||
postId: '789',
|
||||
sourceUrl: 'https://vk.com/wall-33445566_789',
|
||||
title: 'Atomic Snapshot Test Post',
|
||||
text: 'Test description',
|
||||
likesCount: 50,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
@ -70,6 +71,7 @@ describe('Phase 2.4.1 — Atomic Snapshot + Seed Commitment Binding', () => {
|
|||
postId: '1',
|
||||
sourceUrl: 'https://vk.com/wall-1_1',
|
||||
title: 'Parity Test',
|
||||
text: 'Test description',
|
||||
likesCount: 50,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ describe('Storage Driver Policy & No Silent Fallback', () => {
|
|||
listGiveaways: async () => {
|
||||
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'); },
|
||||
saveParticipants: async () => { throw new Error('DB error'); },
|
||||
createAndLockSnapshot: async () => { throw new Error('DB error'); },
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ describe('Phase 2.3.1 — Token Refresh Correctness Gate', () => {
|
|||
callCount++;
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
const res = await orig(p);
|
||||
delete res.user_id;
|
||||
delete (res as any).user_id;
|
||||
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 results = await provider.fetchParticipants({
|
||||
platform: 'VK',
|
||||
ownerId: '-123',
|
||||
postId: '456',
|
||||
organizerId: 'org1',
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
(process.env as any).NODE_ENV = 'production';
|
||||
delete process.env.TOKEN_ENCRYPTION_KEY;
|
||||
|
||||
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', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
(process.env as any).NODE_ENV = 'production';
|
||||
process.env.TOKEN_ENCRYPTION_KEY = 'too-short-key';
|
||||
|
||||
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 () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
(process.env as any).NODE_ENV = 'production';
|
||||
process.env.TOKEN_ENCRYPTION_KEY = 'a-super-secret-production-encryption-key-32chars!';
|
||||
|
||||
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 () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
(process.env as any).NODE_ENV = 'test';
|
||||
delete process.env.TOKEN_ENCRYPTION_KEY;
|
||||
|
||||
const vault = new AesGcmTokenVault();
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ describe('Winner Count Contract & Draw Retry Invariants', () => {
|
|||
postId: '1',
|
||||
sourceUrl: 'https://vk.com/wall-100_1',
|
||||
title: 'Title',
|
||||
text: 'Test description',
|
||||
likesCount: 3,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
@ -79,6 +80,7 @@ describe('Winner Count Contract & Draw Retry Invariants', () => {
|
|||
postId: '2',
|
||||
sourceUrl: 'https://vk.com/wall-100_2',
|
||||
title: 'Title',
|
||||
text: 'Test description',
|
||||
likesCount: 3,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
@ -115,6 +117,7 @@ describe('Winner Count Contract & Draw Retry Invariants', () => {
|
|||
postId: '3',
|
||||
sourceUrl: 'https://vk.com/wall-100_3',
|
||||
title: 'Title',
|
||||
text: 'Test description',
|
||||
likesCount: 3,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
@ -149,6 +152,7 @@ describe('Winner Count Contract & Draw Retry Invariants', () => {
|
|||
postId: '4',
|
||||
sourceUrl: 'https://vk.com/wall-100_4',
|
||||
title: 'Title',
|
||||
text: 'Test description',
|
||||
likesCount: 3,
|
||||
commentsCount: 0,
|
||||
repostsCount: 0,
|
||||
|
|
|
|||
Loading…
Reference in a new issue