chore: Task 11 dead code cleanup and capability validation unification
This commit is contained in:
parent
1883a86402
commit
906148813e
15 changed files with 143 additions and 125 deletions
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Task 11: Удаление мёртвого кода Report
|
||||||
|
|
||||||
|
**Date:** 2026-08-21
|
||||||
|
**Base Commit SHA:** `1883a864023b0b10fe6674a035fbcffa7461c3c8`
|
||||||
|
**Status:** COMPLETED / PASS
|
||||||
|
**Assigned Agent:** Antigravity (Implementation Orchestrator)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Executive Summary
|
||||||
|
|
||||||
|
Проведена чистка мертвого и дублирующего кода, устранен технический долг без изменения боевых инвариантов:
|
||||||
|
|
||||||
|
1. **Удален неиспользуемый импорт:**
|
||||||
|
- `generateCryptoSecureSeed` удален из `src/app/api/giveaways/[id]/draw/route.ts`.
|
||||||
|
2. **Ликвидация дублирующей фабрики `ProviderRegistry` (`src/providers/registry.ts`):**
|
||||||
|
- Файл `src/providers/registry.ts` удален.
|
||||||
|
- Метод `getProvider(platform)` перенесен в каноническую фабрику `ProviderFactory` (`src/providers/factory.ts`), реализующую строгий fail-closed контроль наличия `VK_SERVICE_TOKEN` в production.
|
||||||
|
- Все тестовые вызовы (`tests/concurrency.test.ts`, `tests/payload-summary-regression.test.ts`, `tests/winner-count-contract.test.ts`, `tests/security.test.ts`) и документация `docs/ARCHITECTURE.md` переведены на `ProviderFactory`.
|
||||||
|
3. **Объединение валидации возможностей провайдера (`validateProviderCapabilities`):**
|
||||||
|
- Проверка `requireSubscription` перенесена в канонический валидатор `validateProviderCapabilities` (`src/core/validation/giveaway-schemas.ts`).
|
||||||
|
- Дублирующий модуль `src/core/filtering/rule-validation.ts` удален.
|
||||||
|
- Тестовый набор `tests/provider-capabilities.test.ts` перенастроен на тестирование `validateProviderCapabilities` (все 11 тестов успешны).
|
||||||
|
4. **Удаление `GiveawayStore.listAll` (`src/lib/giveaway-store.ts`):**
|
||||||
|
- Неиспользуемый метод `listAll` удален из класса `GiveawayStore`.
|
||||||
|
5. **Упрощение `getOAuthClient()` (`src/integrations/vk/vk-oauth-client.ts`):**
|
||||||
|
- Удалены избыточные тождественные ветви `if/else`, возвращавшие одну и ту же переменную `defaultVkOAuthClient`.
|
||||||
|
6. **Сохранение статусов FSM (`DRAFT`, `FETCHING`, `PUBLISHED`, `CANCELLED`):**
|
||||||
|
- Статусы сохранены в `GiveawayStatusType` и таблице переходов FSM согласно контракту (являются заделом под фичи публикации итогов в группу VK и отмены конкурса).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Modified & Deleted Files
|
||||||
|
|
||||||
|
| File | Status | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `src/core/filtering/rule-validation.ts` | DELETED | Удален дублирующий валидатор правил. |
|
||||||
|
| `src/providers/registry.ts` | DELETED | Удалена неконсистентная фабрика `ProviderRegistry`. |
|
||||||
|
| `src/app/api/giveaways/[id]/draw/route.ts` | MODIFIED | Удален неиспользуемый импорт `generateCryptoSecureSeed`. |
|
||||||
|
| `src/providers/factory.ts` | MODIFIED | Добавлен метод `getProvider(platform: PlatformType)` с fail-fast проверкой неподдерживаемых платформ. |
|
||||||
|
| `src/core/validation/giveaway-schemas.ts` | MODIFIED | В `validateProviderCapabilities` добавлена проверка `requireSubscription`. |
|
||||||
|
| `src/lib/giveaway-store.ts` | MODIFIED | Удален неиспользуемый метод `listAll`. |
|
||||||
|
| `src/integrations/vk/vk-oauth-client.ts` | MODIFIED | Упрощена функция `getOAuthClient()`. |
|
||||||
|
| `tests/provider-capabilities.test.ts` | MODIFIED | Переведен на `validateProviderCapabilities` и `ProviderFactory`. |
|
||||||
|
| `tests/concurrency.test.ts` | MODIFIED | Удален импорт и вызовы `ProviderRegistry`. |
|
||||||
|
| `tests/payload-summary-regression.test.ts` | MODIFIED | Удален импорт и вызовы `ProviderRegistry`. |
|
||||||
|
| `tests/winner-count-contract.test.ts` | MODIFIED | Удален импорт и вызовы `ProviderRegistry`. |
|
||||||
|
| `tests/security.test.ts` | MODIFIED | Переведен на `ProviderFactory`. |
|
||||||
|
| `docs/ARCHITECTURE.md` | MODIFIED | Обновлена ссылка на `ProviderFactory`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Предложение следующей задачи (FSM Statuses Lifecycle)
|
||||||
|
|
||||||
|
Статусы `DRAFT`, `FETCHING`, `PUBLISHED`, `CANCELLED` сохранены в `src/core/fsm/giveaway-fsm.ts`.
|
||||||
|
Рекомендуется оформить отдельную задачу на реализацию недостающих пользовательских сценариев:
|
||||||
|
1. `POST /api/giveaways/[id]/cancel` — отмена активного розыгрыша организатором с фиксацией аудиторного события (`status: 'CANCELLED'`).
|
||||||
|
2. `POST /api/giveaways/[id]/publish` — автоматическая публикация карточки с итогами и победителями на стену сообщества через VK API (`status: 'PUBLISHED'`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Verification Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
npx prisma generate -> EXIT 0
|
||||||
|
npx tsc --noEmit -> EXIT 0
|
||||||
|
npm test -> EXIT 0 (57 suites, 333 tests passed)
|
||||||
|
npm run lint -> EXIT 0 (0 errors, 6 warnings on no-img-element)
|
||||||
|
npm run build -> EXIT 0 (All 17 routes compiled successfully)
|
||||||
|
npm audit --omit=dev -> EXIT 0 (0 vulnerabilities)
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Task 11: Удаление мёртвого кода
|
||||||
|
|
||||||
|
**Assigned to:** Antigravity (Implementation Orchestrator)
|
||||||
|
**Priority:** LOW (tech debt cleanup)
|
||||||
|
**Date:** 2026-08-21
|
||||||
|
**Base SHA:** `1883a864023b0b10fe6674a035fbcffa7461c3c8`
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
1. **Unused import in `src/app/api/giveaways/[id]/draw/route.ts`**:
|
||||||
|
- Remove unused import `generateCryptoSecureSeed`.
|
||||||
|
2. **`ProviderRegistry` (`src/providers/registry.ts`)**:
|
||||||
|
- Delete dead `src/providers/registry.ts` which duplicates `ProviderFactory` with inconsistent token lengths and non-fail-fast behavior.
|
||||||
|
3. **`rule-validation.ts` (`src/core/filtering/rule-validation.ts`) vs `validateProviderCapabilities`**:
|
||||||
|
- Inspect `requireSubscription` check in `src/core/filtering/rule-validation.ts`.
|
||||||
|
- Transfer required capability checks (including `requireSubscription` checking `supportsSubscriptions`) into canonical `validateProviderCapabilities` in `src/core/validation/giveaway-schemas.ts`.
|
||||||
|
- Remove redundant `src/core/filtering/rule-validation.ts`.
|
||||||
|
- Update `tests/provider-capabilities.test.ts` to test canonical `validateProviderCapabilities`.
|
||||||
|
4. **`GiveawayStore.listAll` (`src/lib/giveaway-store.ts`)**:
|
||||||
|
- Remove unused `listAll` method.
|
||||||
|
5. **`getOAuthClient()` in `src/integrations/vk/vk-oauth-client.ts`**:
|
||||||
|
- Clean up redundant branches in `getOAuthClient()`.
|
||||||
|
6. **FSM unreachable statuses (`DRAFT`, `FETCHING`, `PUBLISHED`, `CANCELLED`)**:
|
||||||
|
- Per instructions, **DO NOT delete** from `GiveawayStatusType` or FSM transitions. Document them as reserved for future publication & cancellation features.
|
||||||
|
7. Verification gate:
|
||||||
|
- `npm test`, `npx prisma generate`, `npx tsc --noEmit`, `npm run lint`, `npm run build`, `npm audit --omit=dev`.
|
||||||
|
- Save report to `agents/antigravity/done/TASK-2026-08-21-11-dead-code-cleanup.md`.
|
||||||
|
|
@ -33,7 +33,7 @@ graph TD
|
||||||
- `checkSubscription(userIds: string[], groupId: string)`: Проверка подписки на сообщество.
|
- `checkSubscription(userIds: string[], groupId: string)`: Проверка подписки на сообщество.
|
||||||
- **`VkProvider`**: Боевой клиент к VK API с поддержкой пакетных запросов `execute`.
|
- **`VkProvider`**: Боевой клиент к VK API с поддержкой пакетных запросов `execute`.
|
||||||
- **`VkMockProvider`**: Тестовый провайдер для демонстрации, локальной разработки и оффлайн-тестирования.
|
- **`VkMockProvider`**: Тестовый провайдер для демонстрации, локальной разработки и оффлайн-тестирования.
|
||||||
- **`ProviderRegistry`**: Фабрика для получения провайдера по типу платформы (`vk`, `telegram`, `youtube`).
|
- **`ProviderFactory`**: Фабрика для получения провайдера по типу платформы (`VK`, `TELEGRAM`, `YOUTUBE`).
|
||||||
|
|
||||||
### 2.3. Data & Persistence Layer (`prisma/` + `src/lib/`)
|
### 2.3. Data & Persistence Layer (`prisma/` + `src/lib/`)
|
||||||
- **PostgreSQL** в качестве надежного реляционного хранилища.
|
- **PostgreSQL** в качестве надежного реляционного хранилища.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { GiveawayStore } from '@/lib/giveaway-store';
|
import { GiveawayStore } from '@/lib/giveaway-store';
|
||||||
import { generateCryptoSecureSeed } from '@/core/randomizer/hasher';
|
|
||||||
import { executeDeterministicDrawV1 } from '@/core/randomizer/deterministic';
|
import { executeDeterministicDrawV1 } from '@/core/randomizer/deterministic';
|
||||||
import { executeDrawSchema } from '@/core/validation/giveaway-schemas';
|
import { executeDrawSchema } from '@/core/validation/giveaway-schemas';
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import { FilterRules } from '../types/giveaway';
|
|
||||||
import { ProviderCapabilities } from '../../providers/types';
|
|
||||||
|
|
||||||
export interface RuleValidationResult {
|
|
||||||
valid: boolean;
|
|
||||||
errors: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates that the requested filter rules can actually be verified by the
|
|
||||||
* selected social-media provider. This prevents organizers from configuring
|
|
||||||
* giveaways with conditions (e.g. reposts) that the provider cannot check.
|
|
||||||
*/
|
|
||||||
export function validateFilterRulesAgainstProviderCapabilities(
|
|
||||||
rules: FilterRules,
|
|
||||||
capabilities: ProviderCapabilities
|
|
||||||
): RuleValidationResult {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
if (rules.requireRepost && !capabilities.reposts) {
|
|
||||||
errors.push(
|
|
||||||
`requireRepost is not supported by the ${capabilities.repostsNote || 'current provider'}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rules.requireSubscription && !capabilities.subscriptions) {
|
|
||||||
errors.push('requireSubscription is not supported by the current provider');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rules.excludeAdmins && !capabilities.adminDetection) {
|
|
||||||
errors.push(
|
|
||||||
`excludeAdmins is not supported by the ${capabilities.adminDetectionNote || 'current provider'}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// likes/comments are considered universally supported by providers that have
|
|
||||||
// them declared; the engine itself still needs a provider to fetch them.
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: errors.length === 0,
|
|
||||||
errors,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -80,6 +80,13 @@ export function validateProviderCapabilities(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rules.requireSubscription && !capabilities.subscriptions) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'Subscription verification is not supported by the current provider',
|
||||||
|
{ condition: 'requireSubscription' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (rules.excludeAdmins && !capabilities.adminDetection) {
|
if (rules.excludeAdmins && !capabilities.adminDetection) {
|
||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
'Admin detection requires VK ID organizer authorization',
|
'Admin detection requires VK ID organizer authorization',
|
||||||
|
|
|
||||||
|
|
@ -271,8 +271,5 @@ export function setOAuthClient(client: IVkOAuthClient): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOAuthClient(): IVkOAuthClient {
|
export function getOAuthClient(): IVkOAuthClient {
|
||||||
if (process.env.USE_VK_MOCK === 'true' || (process.env.NODE_ENV === 'test' && !process.env.VK_APP_ID)) {
|
|
||||||
return defaultVkOAuthClient;
|
|
||||||
}
|
|
||||||
return defaultVkOAuthClient;
|
return defaultVkOAuthClient;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,6 @@ export class GiveawayStore {
|
||||||
return await activeRepository.getGiveawayById(id);
|
return await activeRepository.getGiveawayById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async listAll(organizerId?: string): Promise<StoredGiveaway[]> {
|
|
||||||
return await activeRepository.listGiveaways(organizerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async listSummaries(organizerId?: string): Promise<GiveawaySummary[]> {
|
static async listSummaries(organizerId?: string): Promise<GiveawaySummary[]> {
|
||||||
return await activeRepository.listGiveawaysSummary(organizerId);
|
return await activeRepository.listGiveawaysSummary(organizerId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
import { PlatformType } from '../core/types/giveaway';
|
||||||
import { SocialMediaProvider } from './types';
|
import { SocialMediaProvider } from './types';
|
||||||
import { VkProvider } from './vk/vk-provider';
|
import { VkProvider } from './vk/vk-provider';
|
||||||
import { VkMockProvider } from './vk/vk-mock-provider';
|
import { VkMockProvider } from './vk/vk-mock-provider';
|
||||||
import { DependencyUnavailableError } from '../core/errors/http-errors';
|
import { DependencyUnavailableError, ValidationError } from '../core/errors/http-errors';
|
||||||
|
|
||||||
export class ProviderFactory {
|
export class ProviderFactory {
|
||||||
public static getVkProvider(): SocialMediaProvider {
|
public static getVkProvider(): SocialMediaProvider {
|
||||||
|
|
@ -26,4 +27,16 @@ export class ProviderFactory {
|
||||||
'VK provider credentials are not configured. Configure VK_SERVICE_TOKEN or set USE_VK_MOCK=true for staging/test.'
|
'VK provider credentials are not configured. Configure VK_SERVICE_TOKEN or set USE_VK_MOCK=true for staging/test.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getProvider(platform: PlatformType): SocialMediaProvider {
|
||||||
|
switch (platform) {
|
||||||
|
case 'VK':
|
||||||
|
return this.getVkProvider();
|
||||||
|
case 'TELEGRAM':
|
||||||
|
case 'YOUTUBE':
|
||||||
|
throw new ValidationError(`Platform "${platform}" is not currently supported`);
|
||||||
|
default:
|
||||||
|
throw new ValidationError(`Unknown platform: ${platform}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import { PlatformType } from '../core/types/giveaway';
|
|
||||||
import { SocialMediaProvider } from './types';
|
|
||||||
import { VkMockProvider } from './vk/vk-mock-provider';
|
|
||||||
import { VkProvider } from './vk/vk-provider';
|
|
||||||
|
|
||||||
export class ProviderRegistry {
|
|
||||||
private static providers: Map<PlatformType, SocialMediaProvider> = new Map();
|
|
||||||
|
|
||||||
static {
|
|
||||||
// Determine whether to use real VK provider or mock provider
|
|
||||||
const useRealVk = Boolean(process.env.VK_SERVICE_TOKEN && process.env.VK_SERVICE_TOKEN.trim().length > 10);
|
|
||||||
const vkProvider = useRealVk ? new VkProvider() : new VkMockProvider();
|
|
||||||
|
|
||||||
this.providers.set('VK', vkProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register or override a provider for a platform
|
|
||||||
*/
|
|
||||||
static registerProvider(platform: PlatformType, provider: SocialMediaProvider): void {
|
|
||||||
this.providers.set(platform, provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get provider by platform type
|
|
||||||
*/
|
|
||||||
static getProvider(platform: PlatformType): SocialMediaProvider {
|
|
||||||
const provider = this.providers.get(platform);
|
|
||||||
if (!provider) {
|
|
||||||
throw new Error(`Provider for platform "${platform}" is not registered`);
|
|
||||||
}
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force set mock provider for testing purposes
|
|
||||||
*/
|
|
||||||
static useMockVk(): void {
|
|
||||||
this.providers.set('VK', new VkMockProvider());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
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 { ProviderRegistry } from '../src/providers/registry';
|
|
||||||
import { POST as drawPost } from '../src/app/api/giveaways/[id]/draw/route';
|
import { POST as drawPost } from '../src/app/api/giveaways/[id]/draw/route';
|
||||||
import { POST as participantsPost } from '../src/app/api/giveaways/[id]/participants/route';
|
import { POST as participantsPost } from '../src/app/api/giveaways/[id]/participants/route';
|
||||||
import { POST as snapshotPost } from '../src/app/api/giveaways/[id]/snapshot/route';
|
import { POST as snapshotPost } from '../src/app/api/giveaways/[id]/snapshot/route';
|
||||||
|
|
@ -55,7 +54,6 @@ async function createReadyGiveaway() {
|
||||||
describe('Concurrency analysis', () => {
|
describe('Concurrency analysis', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
GiveawayStore.setRepository(new MemoryGiveawayRepository());
|
GiveawayStore.setRepository(new MemoryGiveawayRepository());
|
||||||
ProviderRegistry.useMockVk();
|
|
||||||
defaultSessionStore.clear();
|
defaultSessionStore.clear();
|
||||||
sessionId = await defaultSessionStore.createSession(testUser);
|
sessionId = await defaultSessionStore.createSession(testUser);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { GiveawayStore } from '../src/lib/giveaway-store';
|
||||||
import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository';
|
import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository';
|
||||||
import { POST as participantsPost } from '../src/app/api/giveaways/[id]/participants/route';
|
import { POST as participantsPost } from '../src/app/api/giveaways/[id]/participants/route';
|
||||||
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
|
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
|
||||||
import { ProviderRegistry } from '../src/providers/registry';
|
|
||||||
import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session';
|
import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session';
|
||||||
|
|
||||||
describe('POST /participants Payload Summary Regression Test', () => {
|
describe('POST /participants Payload Summary Regression Test', () => {
|
||||||
|
|
@ -13,7 +12,6 @@ describe('POST /participants Payload Summary Regression Test', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
GiveawayStore.setRepository(new MemoryGiveawayRepository());
|
GiveawayStore.setRepository(new MemoryGiveawayRepository());
|
||||||
ProviderRegistry.useMockVk();
|
|
||||||
defaultSessionStore.clear();
|
defaultSessionStore.clear();
|
||||||
sessionId = await defaultSessionStore.createSession(user);
|
sessionId = await defaultSessionStore.createSession(user);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { validateFilterRulesAgainstProviderCapabilities } from '../src/core/filtering/rule-validation';
|
import { validateProviderCapabilities } from '../src/core/validation/giveaway-schemas';
|
||||||
import { VkMockProvider } from '../src/providers/vk/vk-mock-provider';
|
import { VkMockProvider } from '../src/providers/vk/vk-mock-provider';
|
||||||
import { VkProvider } from '../src/providers/vk/vk-provider';
|
import { VkProvider } from '../src/providers/vk/vk-provider';
|
||||||
import { ProviderRegistry } from '../src/providers/registry';
|
|
||||||
import { FilterRules } from '../src/core/types/giveaway';
|
import { FilterRules } from '../src/core/types/giveaway';
|
||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { POST as participantsPost } from '../src/app/api/giveaways/[id]/participants/route';
|
import { POST as participantsPost } from '../src/app/api/giveaways/[id]/participants/route';
|
||||||
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 { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session';
|
import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session';
|
||||||
|
import { ValidationError } from '../src/core/errors/http-errors';
|
||||||
|
|
||||||
const testUser = { id: 'usr_capabilities_tester', vkUserId: '77777' };
|
const testUser = { id: 'usr_capabilities_tester', vkUserId: '77777' };
|
||||||
let sessionId: string;
|
let sessionId: string;
|
||||||
|
|
@ -34,7 +34,6 @@ async function createGiveaway(store: typeof GiveawayStore) {
|
||||||
requireSubscription: false,
|
requireSubscription: false,
|
||||||
excludeAdmins: false,
|
excludeAdmins: false,
|
||||||
excludeBlacklistedIds: [],
|
excludeBlacklistedIds: [],
|
||||||
excludeDuplicateComments: true,
|
|
||||||
},
|
},
|
||||||
winnersCount: 1,
|
winnersCount: 1,
|
||||||
reserveWinnersCount: 0,
|
reserveWinnersCount: 0,
|
||||||
|
|
@ -56,7 +55,6 @@ function buildReq(id: string, body: object): NextRequest {
|
||||||
describe('Provider capabilities', () => {
|
describe('Provider capabilities', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
GiveawayStore.setRepository(new MemoryGiveawayRepository());
|
GiveawayStore.setRepository(new MemoryGiveawayRepository());
|
||||||
ProviderRegistry.useMockVk();
|
|
||||||
defaultSessionStore.clear();
|
defaultSessionStore.clear();
|
||||||
sessionId = await defaultSessionStore.createSession(testUser);
|
sessionId = await defaultSessionStore.createSession(testUser);
|
||||||
});
|
});
|
||||||
|
|
@ -95,12 +93,10 @@ describe('Provider capabilities', () => {
|
||||||
requireSubscription: false,
|
requireSubscription: false,
|
||||||
excludeAdmins: false,
|
excludeAdmins: false,
|
||||||
excludeBlacklistedIds: [],
|
excludeBlacklistedIds: [],
|
||||||
excludeDuplicateComments: true,
|
|
||||||
};
|
};
|
||||||
const provider = new VkMockProvider();
|
const provider = new VkMockProvider();
|
||||||
const result = validateFilterRulesAgainstProviderCapabilities(rules, provider.capabilities);
|
expect(() => validateProviderCapabilities(rules, provider.capabilities)).toThrow(ValidationError);
|
||||||
expect(result.valid).toBe(false);
|
expect(() => validateProviderCapabilities(rules, provider.capabilities)).toThrow(/repost/i);
|
||||||
expect(result.errors.some(e => e.includes('requireRepost'))).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validation rejects excludeAdmins when provider cannot detect admins', () => {
|
it('validation rejects excludeAdmins when provider cannot detect admins', () => {
|
||||||
|
|
@ -111,12 +107,24 @@ describe('Provider capabilities', () => {
|
||||||
requireSubscription: false,
|
requireSubscription: false,
|
||||||
excludeAdmins: true,
|
excludeAdmins: true,
|
||||||
excludeBlacklistedIds: [],
|
excludeBlacklistedIds: [],
|
||||||
excludeDuplicateComments: true,
|
|
||||||
};
|
};
|
||||||
const provider = new VkMockProvider();
|
const provider = new VkMockProvider();
|
||||||
const result = validateFilterRulesAgainstProviderCapabilities(rules, provider.capabilities);
|
expect(() => validateProviderCapabilities(rules, provider.capabilities)).toThrow(ValidationError);
|
||||||
expect(result.valid).toBe(false);
|
expect(() => validateProviderCapabilities(rules, provider.capabilities)).toThrow(/admin/i);
|
||||||
expect(result.errors.some(e => e.includes('excludeAdmins'))).toBe(true);
|
});
|
||||||
|
|
||||||
|
it('validation rejects requireSubscription when provider does not support subscriptions', () => {
|
||||||
|
const rules: FilterRules = {
|
||||||
|
requireLike: false,
|
||||||
|
requireComment: false,
|
||||||
|
requireRepost: false,
|
||||||
|
requireSubscription: true,
|
||||||
|
excludeAdmins: false,
|
||||||
|
excludeBlacklistedIds: [],
|
||||||
|
};
|
||||||
|
const capabilities = { ...new VkMockProvider().capabilities, subscriptions: false };
|
||||||
|
expect(() => validateProviderCapabilities(rules, capabilities)).toThrow(ValidationError);
|
||||||
|
expect(() => validateProviderCapabilities(rules, capabilities)).toThrow(/subscription/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validation accepts supported combinations', () => {
|
it('validation accepts supported combinations', () => {
|
||||||
|
|
@ -128,12 +136,9 @@ describe('Provider capabilities', () => {
|
||||||
targetGroupId: '-100',
|
targetGroupId: '-100',
|
||||||
excludeAdmins: false,
|
excludeAdmins: false,
|
||||||
excludeBlacklistedIds: [],
|
excludeBlacklistedIds: [],
|
||||||
excludeDuplicateComments: true,
|
|
||||||
};
|
};
|
||||||
const provider = new VkMockProvider();
|
const provider = new VkMockProvider();
|
||||||
const result = validateFilterRulesAgainstProviderCapabilities(rules, provider.capabilities);
|
expect(() => validateProviderCapabilities(rules, provider.capabilities)).not.toThrow();
|
||||||
expect(result.valid).toBe(true);
|
|
||||||
expect(result.errors).toHaveLength(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('participants route returns 400 when requireRepost is requested for VK', async () => {
|
it('participants route returns 400 when requireRepost is requested for VK', async () => {
|
||||||
|
|
@ -146,7 +151,6 @@ describe('Provider capabilities', () => {
|
||||||
requireSubscription: false,
|
requireSubscription: false,
|
||||||
excludeAdmins: false,
|
excludeAdmins: false,
|
||||||
excludeBlacklistedIds: [],
|
excludeBlacklistedIds: [],
|
||||||
excludeDuplicateComments: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -167,7 +171,6 @@ describe('Provider capabilities', () => {
|
||||||
requireSubscription: false,
|
requireSubscription: false,
|
||||||
excludeAdmins: true,
|
excludeAdmins: true,
|
||||||
excludeBlacklistedIds: [],
|
excludeBlacklistedIds: [],
|
||||||
excludeDuplicateComments: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -189,7 +192,6 @@ describe('Provider capabilities', () => {
|
||||||
targetGroupId: '-100',
|
targetGroupId: '-100',
|
||||||
excludeAdmins: false,
|
excludeAdmins: false,
|
||||||
excludeBlacklistedIds: [],
|
excludeBlacklistedIds: [],
|
||||||
excludeDuplicateComments: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { ProviderRegistry } from '../src/providers/registry';
|
import { ProviderFactory } from '../src/providers/factory';
|
||||||
import { VkProvider } from '../src/providers/vk/vk-provider';
|
import { VkProvider } from '../src/providers/vk/vk-provider';
|
||||||
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';
|
||||||
|
|
@ -14,13 +14,11 @@ describe('Security: VK_SERVICE_TOKEN handling', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
GiveawayStore.setRepository(new MemoryGiveawayRepository());
|
GiveawayStore.setRepository(new MemoryGiveawayRepository());
|
||||||
ProviderRegistry.useMockVk();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ProviderRegistry does not expose VK_SERVICE_TOKEN in public API', () => {
|
it('ProviderFactory does not expose VK_SERVICE_TOKEN in public API', () => {
|
||||||
process.env.VK_SERVICE_TOKEN = secretToken;
|
process.env.VK_SERVICE_TOKEN = secretToken;
|
||||||
// Re-initialize registry (static block already ran, but we can inspect provider)
|
const provider = ProviderFactory.getProvider('VK');
|
||||||
const provider = ProviderRegistry.getProvider('VK');
|
|
||||||
expect(provider.platform).toBe('VK');
|
expect(provider.platform).toBe('VK');
|
||||||
expect(provider).not.toHaveProperty('serviceToken');
|
expect(provider).not.toHaveProperty('serviceToken');
|
||||||
delete process.env.VK_SERVICE_TOKEN;
|
delete process.env.VK_SERVICE_TOKEN;
|
||||||
|
|
@ -63,7 +61,6 @@ describe('Security: VK_SERVICE_TOKEN handling', () => {
|
||||||
|
|
||||||
it('Post preview response does not contain VK_SERVICE_TOKEN', async () => {
|
it('Post preview response does not contain VK_SERVICE_TOKEN', async () => {
|
||||||
process.env.VK_SERVICE_TOKEN = secretToken;
|
process.env.VK_SERVICE_TOKEN = secretToken;
|
||||||
ProviderRegistry.useMockVk(); // mock so no real API call
|
|
||||||
const req = new NextRequest('http://localhost/api/posts/preview', {
|
const req = new NextRequest('http://localhost/api/posts/preview', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ url: 'https://vk.com/wall-1_1' }),
|
body: JSON.stringify({ url: 'https://vk.com/wall-1_1' }),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
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 { ProviderRegistry } from '../src/providers/registry';
|
|
||||||
import { POST as drawPost } from '../src/app/api/giveaways/[id]/draw/route';
|
import { POST as drawPost } from '../src/app/api/giveaways/[id]/draw/route';
|
||||||
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
|
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
|
||||||
import { FilteredParticipant } from '../src/core/types/participant';
|
import { FilteredParticipant } from '../src/core/types/participant';
|
||||||
|
|
@ -15,7 +14,6 @@ describe('Winner Count Contract & Draw Retry Invariants', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
GiveawayStore.setRepository(new MemoryGiveawayRepository());
|
GiveawayStore.setRepository(new MemoryGiveawayRepository());
|
||||||
ProviderRegistry.useMockVk();
|
|
||||||
defaultSessionStore.clear();
|
defaultSessionStore.clear();
|
||||||
sessionId = await defaultSessionStore.createSession(testUser);
|
sessionId = await defaultSessionStore.createSession(testUser);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue