fix(validation): Task 14 reject excludeDuplicateComments in API schemas and upgrade CI node-version to 22

This commit is contained in:
Ochenstarik 2026-08-21 22:49:37 +07:00
parent 6510ea2a04
commit 8e39ce0cbd
7 changed files with 128 additions and 4 deletions

View file

@ -37,7 +37,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies

View file

@ -0,0 +1,47 @@
# Task 14: Два LOW-хвоста из ревью 9061488 Report
**Date:** 2026-08-21
**Base Commit SHA:** `6510ea2a048aa9bbeec5af8d80e61a420f32c471`
**Status:** COMPLETED / PASS
**Assigned Agent:** Antigravity (Implementation Orchestrator)
---
## 1. Executive Summary
Устранены два LOW-хвоста, выявленные в ходе независимого ревью:
1. **Запрет `excludeDuplicateComments` на границе API (`src/core/validation/giveaway-schemas.ts`):**
- Поле `excludeDuplicateComments` удалено из `filterRulesSchema`.
- Поскольку `filterRulesSchema` определена со `.strict()`, любая попытка внешнего API-клиента передать `excludeDuplicateComments` при создании розыгрыша (`POST /api/giveaways`), импорте участников (`POST /api/giveaways/[id]/participants`) или фиксации слепка (`POST /api/giveaways/[id]/snapshot`) немедленно отвергается с кодом `400 Bad Request` (`ValidationError: Unrecognized key(s) in object: 'excludeDuplicateComments'`).
- Поле физически не может попасть в `filterRulesSnapshot` нового розыгрыша и изменить `conditionsHash`.
- **Обратная совместимость legacy-снапшотов:** Функция `computeConditionsHash` (`src/core/randomizer/canonical.ts:46`) и верификатор `verifyDrawResult` сохраняют поддержку старых слепков, где это поле было записано до Задания 07 (`verified: true`, `conditionsIntegrity: true`).
2. **Обновление версии Node.js в CI (`.github/workflows/ci.yml`):**
- Версия `node-version` в GitHub Actions поднята с 20 до 22 (текущая Active LTS).
- Все шаги CI (Prisma generate/migrations, Unit/Integration tests, ESLint, Next.js build) валидированы.
---
## 2. Modified Files
| File | Status | Description |
|------|--------|-------------|
| `src/core/validation/giveaway-schemas.ts` | MODIFIED | Удалено `excludeDuplicateComments` из `filterRulesSchema`. |
| `.github/workflows/ci.yml` | MODIFIED | Обновлен `node-version: 22` в CI workflow. |
| `tests/duplicate-comments-rule.test.ts` | MODIFIED | Добавлены тесты отсечения `excludeDuplicateComments` на уровне схем Zod. |
| `tests/auth-guard.test.ts` | MODIFIED | Удалено устаревшее поле из тестовых фикстур `validPostData`. |
| `tests/security.test.ts` | MODIFIED | Удалено устаревшее поле из тестовой фикстуры. |
---
## 3. Verification Evidence
```text
npx prisma generate -> EXIT 0 (Prisma Client v5.22.0)
npx tsc --noEmit -> EXIT 0 (0 ошибок типизации)
npm test -> EXIT 0 (59 тест-сьютов, 343 теста пройдены успешно)
npm run lint -> EXIT 0 (0 ошибок, 6 warnings на no-img-element)
npm run build -> EXIT 0 (Все 17 маршрутов скомпилированы успешно в Next.js 16.3.2)
npm audit --omit=dev -> EXIT 0 (0 vulnerabilities)
```

View file

@ -0,0 +1,24 @@
# Task 14: Два LOW-хвоста из ревью 9061488
**Assigned to:** Antigravity (Implementation Orchestrator)
**Priority:** LOW
**Date:** 2026-08-21
**Base SHA:** `6510ea2a048aa9bbeec5af8d80e61a420f32c471`
## Проблемы
1. **`excludeDuplicateComments` на входе API (`filterRulesSchema`)**:
- `src/core/validation/giveaway-schemas.ts` по-прежнему объявляет `excludeDuplicateComments: z.boolean().optional()`.
- Если внешний API-клиент передаст этот флаг, он попадет в `filterRulesSnapshot` и в `computeConditionsHash`, изменив хеш условий для нового розыгрыша, хотя фактически на фильтрацию не влияет.
- Необходимо удалить поле из `filterRulesSchema` (или отбрасывать/валидировать), сохранив чтение legacy-снапшотов в `computeConditionsHash` и `verifyDrawResult`.
2. **`node-version: 20` в `.github/workflows/ci.yml`**:
- Обновить версию Node.js в CI до актуальной активной LTS (Node 22 / 24) с сохранением поддержки `engines.node >= 20.9.0`.
## Scope
1. Схема `filterRulesSchema`:
- Удалить `excludeDuplicateComments` из `filterRulesSchema` (так как схемы используют `.strict()`, передача этого поля будет отвергаться с 400 ValidationError, либо при strip отбрасываться без записи в БД).
- Сохранить в `FilterRules` тип `excludeDuplicateComments?: boolean` (deprecated) для совместимости со старыми записями в БД.
- Проверить, что `computeConditionsHash` по-прежнему поддерживает legacy-снапшоты с этим полем.
2. CI Workflow `.github/workflows/ci.yml`:
- Обновить `node-version: 22` (текущая Active LTS).
3. Дополнить `tests/duplicate-comments-rule.test.ts`.
4. Verification gate & Report.

View file

@ -9,7 +9,6 @@ export const filterRulesSchema = z.object({
requireRepost: z.boolean().default(false), requireRepost: z.boolean().default(false),
requireSubscription: z.boolean().default(false), requireSubscription: z.boolean().default(false),
excludeAdmins: z.boolean().default(false), excludeAdmins: z.boolean().default(false),
excludeDuplicateComments: z.boolean().optional(),
excludeBlacklistedIds: z.array(z.string().max(128)).max(1000).default([]), excludeBlacklistedIds: z.array(z.string().max(128)).max(1000).default([]),
targetGroupId: z.string().max(128).optional(), targetGroupId: z.string().max(128).optional(),
minEligibleParticipants: z.number().int().min(1).max(100000).default(1), minEligibleParticipants: z.number().int().min(1).max(100000).default(1),

View file

@ -46,7 +46,6 @@ describe('Phase 2.2.2 Giveaway Ownership Invariant & AuthZ Guard Security Suite'
requireSubscription: false, requireSubscription: false,
excludeAdmins: false, excludeAdmins: false,
excludeBlacklistedIds: [], excludeBlacklistedIds: [],
excludeDuplicateComments: true,
minEligibleParticipants: 1, minEligibleParticipants: 1,
}, },
winnersCount: 1, winnersCount: 1,

View file

@ -262,4 +262,60 @@ describe('Task 07: Duplicate Comments Rule & Backward Compatibility', () => {
expect(verification.conditionsIntegrity).toBe(true); expect(verification.conditionsIntegrity).toBe(true);
expect(verification.verified).toBe(true); expect(verification.verified).toBe(true);
}); });
// ─── 5. API Schemas Reject excludeDuplicateComments on Input ──────────────────
it('API schemas strictly reject excludeDuplicateComments preventing it from reaching snapshot', async () => {
const { filterRulesSchema, fetchParticipantsSchema, createSnapshotSchema, createGiveawaySchema } = await import(
'../src/core/validation/giveaway-schemas'
);
// 1. filterRulesSchema is strict and rejects the key
const parseResult = filterRulesSchema.safeParse({
requireLike: true,
excludeDuplicateComments: true,
});
expect(parseResult.success).toBe(false);
if (!parseResult.success) {
expect(parseResult.error.issues.some(i => i.message.includes('unrecognized_keys') || (i as any).keys?.includes('excludeDuplicateComments'))).toBe(true);
}
// 2. fetchParticipantsSchema rejects
const fetchResult = fetchParticipantsSchema.safeParse({
filterRules: {
requireLike: true,
excludeDuplicateComments: true,
},
});
expect(fetchResult.success).toBe(false);
// 3. createSnapshotSchema rejects
const snapResult = createSnapshotSchema.safeParse({
filterRules: {
requireLike: true,
excludeDuplicateComments: true,
},
});
expect(snapResult.success).toBe(false);
// 4. createGiveawaySchema rejects
const createResult = createGiveawaySchema.safeParse({
sourceUrl: 'https://vk.com/wall-1_1',
post: {
platform: 'VK',
ownerId: '-1',
postId: '1',
sourceUrl: 'https://vk.com/wall-1_1',
title: 'Test',
text: 'Test',
likesCount: 1,
commentsCount: 0,
repostsCount: 0,
},
filterRules: {
requireLike: true,
excludeDuplicateComments: true,
},
});
expect(createResult.success).toBe(false);
});
}); });

View file

@ -49,7 +49,6 @@ describe('Security: VK_SERVICE_TOKEN handling', () => {
requireSubscription: false, requireSubscription: false,
excludeAdmins: false, excludeAdmins: false,
excludeBlacklistedIds: [], excludeBlacklistedIds: [],
excludeDuplicateComments: true,
}, },
}), }),
}); });