feat(auth): Phase 2.2.2 Authorization Ownership Invariant Fix - strict requireGiveawayOwner null check, Prisma non-null organizerId with onDelete Restrict, repository non-null organizer invariant, migration documentation, and regression tests
This commit is contained in:
parent
5d2554f33a
commit
02a04df271
16 changed files with 150 additions and 13 deletions
57
docs/MIGRATION_OWNERSHIP_INVARIANT.md
Normal file
57
docs/MIGRATION_OWNERSHIP_INVARIANT.md
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Database Migration Strategy: Mandatory Organizer Ownership Invariant
|
||||||
|
|
||||||
|
This document details the database schema migration strategy for enforcing mandatory, non-null `organizerId` on the `Giveaway` table in production.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Context & Invariant
|
||||||
|
|
||||||
|
In Randomayzer, every giveaway is owned by an authenticated Organizer (represented by the `User` model via VK ID OAuth 2.1).
|
||||||
|
To prevent broken access control and ambiguous authorization states:
|
||||||
|
- `Giveaway.organizerId` is strictly `NOT NULL`.
|
||||||
|
- Foreign key relation is defined with `onDelete: Restrict`, strictly preventing the deletion of an organizer account while owned giveaways exist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Prisma Schema Definition
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
vkUserId String @unique
|
||||||
|
giveaways Giveaway[]
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
model Giveaway {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
organizerId String
|
||||||
|
organizer User @relation(fields: [organizerId], references: [id], onDelete: Restrict)
|
||||||
|
...
|
||||||
|
@@index([organizerId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Migration Plan for Existing Records
|
||||||
|
|
||||||
|
### A. Development & Staging Environments
|
||||||
|
Existing anonymous test records created prior to Phase 2.2 can be purged:
|
||||||
|
```sql
|
||||||
|
DELETE FROM "ParticipantSnapshot" WHERE "giveawayId" IN (SELECT "id" FROM "Giveaway" WHERE "organizerId" IS NULL);
|
||||||
|
DELETE FROM "Participant" WHERE "giveawayId" IN (SELECT "id" FROM "Giveaway" WHERE "organizerId" IS NULL);
|
||||||
|
DELETE FROM "Giveaway" WHERE "organizerId" IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. Production Migration Protocol
|
||||||
|
1. **Never assign legacy records to arbitrary users**: If unassigned giveaways exist in a production database, do not automatically bind them to random organizers.
|
||||||
|
2. **Quarantine or Admin Assignment**: Migrate legacy orphan giveaways to a designated system quarantine table or assign to an explicitly verified administrative organizer.
|
||||||
|
3. **Execute SQL Migration**:
|
||||||
|
```sql
|
||||||
|
-- Step 1: Verify 0 null records remain
|
||||||
|
SELECT COUNT(*) FROM "Giveaway" WHERE "organizerId" IS NULL;
|
||||||
|
|
||||||
|
-- Step 2: Enforce NOT NULL and Restrict constraint
|
||||||
|
ALTER TABLE "Giveaway" ALTER COLUMN "organizerId" SET NOT NULL;
|
||||||
|
```
|
||||||
|
|
@ -73,8 +73,8 @@ model Giveaway {
|
||||||
winnersCount Int @default(1)
|
winnersCount Int @default(1)
|
||||||
reserveWinnersCount Int @default(0)
|
reserveWinnersCount Int @default(0)
|
||||||
seed String?
|
seed String?
|
||||||
organizerId String?
|
organizerId String
|
||||||
organizer User? @relation(fields: [organizerId], references: [id], onDelete: SetNull)
|
organizer User @relation(fields: [organizerId], references: [id], onDelete: Restrict)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
drawnAt DateTime?
|
drawnAt DateTime?
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export async function requireAuthenticatedUser(req: NextRequest): Promise<Sessio
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enforces that a request is authenticated AND that the current user is the verified organizer (owner) of the giveaway.
|
* Enforces that a request is authenticated AND that the current user is the verified organizer (owner) of the giveaway.
|
||||||
|
* Invariant: A giveaway without an owner (organizerId is null/empty) must NEVER authorize any user.
|
||||||
*/
|
*/
|
||||||
export async function requireGiveawayOwner(
|
export async function requireGiveawayOwner(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
|
|
@ -42,8 +43,12 @@ export async function requireGiveawayOwner(
|
||||||
throw new NotFoundError(`Giveaway with id "${giveawayId}" not found`);
|
throw new NotFoundError(`Giveaway with id "${giveawayId}" not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the giveaway has an organizerId, strict owner match is enforced
|
// Mandatory Ownership Invariant: Null organizer must NEVER authorize
|
||||||
if (giveaway.organizerId && giveaway.organizerId !== sessionUser.id) {
|
if (!giveaway.organizerId) {
|
||||||
|
throw new ForbiddenError('Access denied: giveaway has no valid organizer assigned (ownership integrity error)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (giveaway.organizerId !== sessionUser.id) {
|
||||||
throw new ForbiddenError('Access denied: you are not the organizer of this giveaway');
|
throw new ForbiddenError('Access denied: you are not the organizer of this giveaway');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export interface GiveawayWithRelations {
|
||||||
winnersCount: number;
|
winnersCount: number;
|
||||||
reserveWinnersCount: number;
|
reserveWinnersCount: number;
|
||||||
seed: string | null;
|
seed: string | null;
|
||||||
organizerId?: string | null;
|
organizerId: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
drawnAt: string | null;
|
drawnAt: string | null;
|
||||||
|
|
@ -43,7 +43,7 @@ export interface GiveawaySummary {
|
||||||
status: GiveawayStatusType;
|
status: GiveawayStatusType;
|
||||||
winnersCount: number;
|
winnersCount: number;
|
||||||
reserveWinnersCount: number;
|
reserveWinnersCount: number;
|
||||||
organizerId?: string | null;
|
organizerId: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
drawnAt: string | null;
|
drawnAt: string | null;
|
||||||
|
|
@ -70,7 +70,7 @@ export interface CreateGiveawayInput {
|
||||||
winnersCount?: number;
|
winnersCount?: number;
|
||||||
reserveWinnersCount?: number;
|
reserveWinnersCount?: number;
|
||||||
seed?: string;
|
seed?: string;
|
||||||
organizerId?: string;
|
organizerId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGiveawayRepository {
|
export interface IGiveawayRepository {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ export class MemoryGiveawayRepository implements IGiveawayRepository {
|
||||||
private drawLocks: Set<string> = new Set();
|
private drawLocks: Set<string> = new Set();
|
||||||
|
|
||||||
async createGiveaway(input: CreateGiveawayInput): Promise<GiveawayWithRelations> {
|
async createGiveaway(input: CreateGiveawayInput): Promise<GiveawayWithRelations> {
|
||||||
|
if (!input.organizerId) {
|
||||||
|
throw new Error('FATAL: organizerId is strictly required to create a giveaway in repository');
|
||||||
|
}
|
||||||
|
|
||||||
const id = 'gw_' + Math.random().toString(36).slice(2, 10);
|
const id = 'gw_' + Math.random().toString(36).slice(2, 10);
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
|
@ -38,7 +42,7 @@ export class MemoryGiveawayRepository implements IGiveawayRepository {
|
||||||
winnersCount: input.winnersCount || 1,
|
winnersCount: input.winnersCount || 1,
|
||||||
reserveWinnersCount: input.reserveWinnersCount || 0,
|
reserveWinnersCount: input.reserveWinnersCount || 0,
|
||||||
seed: input.seed || null,
|
seed: input.seed || null,
|
||||||
organizerId: input.organizerId || null,
|
organizerId: input.organizerId,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
drawnAt: null,
|
drawnAt: null,
|
||||||
|
|
@ -99,7 +103,7 @@ export class MemoryGiveawayRepository implements IGiveawayRepository {
|
||||||
status: gw.status,
|
status: gw.status,
|
||||||
winnersCount: gw.winnersCount,
|
winnersCount: gw.winnersCount,
|
||||||
reserveWinnersCount: gw.reserveWinnersCount,
|
reserveWinnersCount: gw.reserveWinnersCount,
|
||||||
organizerId: gw.organizerId || null,
|
organizerId: gw.organizerId,
|
||||||
createdAt: gw.createdAt,
|
createdAt: gw.createdAt,
|
||||||
updatedAt: gw.updatedAt,
|
updatedAt: gw.updatedAt,
|
||||||
drawnAt: gw.drawnAt,
|
drawnAt: gw.drawnAt,
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
|
||||||
winnersCount: raw.winnersCount,
|
winnersCount: raw.winnersCount,
|
||||||
reserveWinnersCount: raw.reserveWinnersCount,
|
reserveWinnersCount: raw.reserveWinnersCount,
|
||||||
seed: raw.seed,
|
seed: raw.seed,
|
||||||
organizerId: raw.organizerId || null,
|
organizerId: raw.organizerId,
|
||||||
createdAt: raw.createdAt.toISOString(),
|
createdAt: raw.createdAt.toISOString(),
|
||||||
updatedAt: raw.updatedAt.toISOString(),
|
updatedAt: raw.updatedAt.toISOString(),
|
||||||
drawnAt: raw.drawnAt ? raw.drawnAt.toISOString() : null,
|
drawnAt: raw.drawnAt ? raw.drawnAt.toISOString() : null,
|
||||||
|
|
@ -115,7 +115,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
|
||||||
winnersCount: input.winnersCount || 1,
|
winnersCount: input.winnersCount || 1,
|
||||||
reserveWinnersCount: input.reserveWinnersCount || 0,
|
reserveWinnersCount: input.reserveWinnersCount || 0,
|
||||||
seed: input.seed,
|
seed: input.seed,
|
||||||
organizerId: input.organizerId || null,
|
organizerId: input.organizerId,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
participants: true,
|
participants: true,
|
||||||
|
|
@ -212,7 +212,7 @@ export class PrismaGiveawayRepository implements IGiveawayRepository {
|
||||||
status: item.status as GiveawayStatusType,
|
status: item.status as GiveawayStatusType,
|
||||||
winnersCount: item.winnersCount,
|
winnersCount: item.winnersCount,
|
||||||
reserveWinnersCount: item.reserveWinnersCount,
|
reserveWinnersCount: item.reserveWinnersCount,
|
||||||
organizerId: item.organizerId || null,
|
organizerId: item.organizerId,
|
||||||
createdAt: item.createdAt.toISOString(),
|
createdAt: item.createdAt.toISOString(),
|
||||||
updatedAt: item.updatedAt.toISOString(),
|
updatedAt: item.updatedAt.toISOString(),
|
||||||
drawnAt: item.drawnAt ? item.drawnAt.toISOString() : null,
|
drawnAt: item.drawnAt ? item.drawnAt.toISOString() : null,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { POST as drawPost } from '../src/app/api/giveaways/[id]/draw/route';
|
||||||
import { GET as verifyGet } from '../src/app/api/giveaways/[id]/verify/route';
|
import { GET as verifyGet } from '../src/app/api/giveaways/[id]/verify/route';
|
||||||
import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session';
|
import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session';
|
||||||
|
|
||||||
describe('Phase 2.2.1 Giveaway Ownership & AuthZ Guard Security Suite', () => {
|
describe('Phase 2.2.2 Giveaway Ownership Invariant & AuthZ Guard Security Suite', () => {
|
||||||
let memoryRepo: MemoryGiveawayRepository;
|
let memoryRepo: MemoryGiveawayRepository;
|
||||||
const ownerUser = { id: 'usr_organizer_1', vkUserId: '111111', firstName: 'Alice' };
|
const ownerUser = { id: 'usr_organizer_1', vkUserId: '111111', firstName: 'Alice' };
|
||||||
const intruderUser = { id: 'usr_intruder_2', vkUserId: '222222', firstName: 'Eve' };
|
const intruderUser = { id: 'usr_intruder_2', vkUserId: '222222', firstName: 'Eve' };
|
||||||
|
|
@ -215,6 +215,64 @@ describe('Phase 2.2.1 Giveaway Ownership & AuthZ Guard Security Suite', () => {
|
||||||
expect(ownerDrawRes.status).toBe(200);
|
expect(ownerDrawRes.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('null organizer giveaway must NEVER authorize any user (fails with 403 Forbidden)', async () => {
|
||||||
|
// Manually inject a corrupted/legacy giveaway with empty organizerId
|
||||||
|
const corruptedId = 'gw_corrupted_null_owner';
|
||||||
|
(memoryRepo as any).giveaways.set(corruptedId, {
|
||||||
|
id: corruptedId,
|
||||||
|
platform: 'VK',
|
||||||
|
sourceUrl: 'https://vk.com/wall-1_1',
|
||||||
|
platformOwnerId: '-1',
|
||||||
|
platformPostId: '1',
|
||||||
|
title: 'Corrupted',
|
||||||
|
organizerId: '', // Empty/null owner
|
||||||
|
status: 'READY',
|
||||||
|
participants: [],
|
||||||
|
filterRules: validPostData.filterRules,
|
||||||
|
winnersCount: 1,
|
||||||
|
reserveWinnersCount: 0,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
drawnAt: null,
|
||||||
|
snapshots: [],
|
||||||
|
latestSnapshot: null,
|
||||||
|
drawResult: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Authenticated user attempts GET -> 403
|
||||||
|
const getReq = new NextRequest(`http://localhost:3000/api/giveaways/${corruptedId}`, {
|
||||||
|
headers: { cookie: `${SESSION_COOKIE_NAME}=${ownerSessionId}` },
|
||||||
|
});
|
||||||
|
const getRes = await giveawayDetailGet(getReq, { params: { id: corruptedId } });
|
||||||
|
expect(getRes.status).toBe(403);
|
||||||
|
const getBody = await getRes.json();
|
||||||
|
expect(getBody.error?.message).toMatch(/no valid organizer assigned/i);
|
||||||
|
|
||||||
|
// 2. Authenticated user attempts POST participants -> 403
|
||||||
|
const partReq = new NextRequest(`http://localhost:3000/api/giveaways/${corruptedId}/participants`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
cookie: `${SESSION_COOKIE_NAME}=${ownerSessionId}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ filterRules: validPostData.filterRules }),
|
||||||
|
});
|
||||||
|
const partRes = await participantsPost(partReq, { params: { id: corruptedId } });
|
||||||
|
expect(partRes.status).toBe(403);
|
||||||
|
|
||||||
|
// 3. Authenticated user attempts POST draw -> 403
|
||||||
|
const drawReq = new NextRequest(`http://localhost:3000/api/giveaways/${corruptedId}/draw`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
cookie: `${SESSION_COOKIE_NAME}=${ownerSessionId}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ winnersCount: 1, reserveWinnersCount: 0 }),
|
||||||
|
});
|
||||||
|
const drawRes = await drawPost(drawReq, { params: { id: corruptedId } });
|
||||||
|
expect(drawRes.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
it('GET /api/giveaways/[id]/verify remains public without requiring authentication', async () => {
|
it('GET /api/giveaways/[id]/verify remains public without requiring authentication', async () => {
|
||||||
const created = await GiveawayStore.create({
|
const created = await GiveawayStore.create({
|
||||||
sourceUrl: validPostData.sourceUrl,
|
sourceUrl: validPostData.sourceUrl,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ describe('100-Draw Concurrency Regression & Mixed Race', () => {
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
winnersCount: 3,
|
winnersCount: 3,
|
||||||
reserveWinnersCount: 1,
|
reserveWinnersCount: 1,
|
||||||
|
organizerId: 'usr_conc_100',
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshot = await repo.createAndLockSnapshot(gw.id, participants, DEFAULT_FILTER_RULES);
|
const snapshot = await repo.createAndLockSnapshot(gw.id, participants, DEFAULT_FILTER_RULES);
|
||||||
|
|
@ -104,6 +105,7 @@ describe('100-Draw Concurrency Regression & Mixed Race', () => {
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
winnersCount: 1,
|
winnersCount: 1,
|
||||||
reserveWinnersCount: 0,
|
reserveWinnersCount: 0,
|
||||||
|
organizerId: 'usr_conc_100',
|
||||||
});
|
});
|
||||||
|
|
||||||
await repo.saveParticipants(gw.id, participants);
|
await repo.saveParticipants(gw.id, participants);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ describe('Concurrency Double Draw Protection', () => {
|
||||||
repostsCount: 10,
|
repostsCount: 10,
|
||||||
},
|
},
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
|
organizerId: 'usr_conc_draw',
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshot = await repo.createAndLockSnapshot(gw.id, participants, DEFAULT_FILTER_RULES);
|
const snapshot = await repo.createAndLockSnapshot(gw.id, participants, DEFAULT_FILTER_RULES);
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ describe('Concurrency: Participants Update vs Snapshot Lock Race', () => {
|
||||||
repostsCount: 0,
|
repostsCount: 0,
|
||||||
},
|
},
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
|
organizerId: 'usr_conc_part_snap',
|
||||||
});
|
});
|
||||||
|
|
||||||
await repo.saveParticipants(gw.id, initialParticipants);
|
await repo.saveParticipants(gw.id, initialParticipants);
|
||||||
|
|
@ -84,6 +85,7 @@ describe('Concurrency: Participants Update vs Snapshot Lock Race', () => {
|
||||||
repostsCount: 0,
|
repostsCount: 0,
|
||||||
},
|
},
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
|
organizerId: 'usr_conc_part_snap',
|
||||||
});
|
});
|
||||||
|
|
||||||
await repo.saveParticipants(gw.id, initialParticipants);
|
await repo.saveParticipants(gw.id, initialParticipants);
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ describe('Concurrency Snapshot Locking & Participant Isolation', () => {
|
||||||
repostsCount: 1,
|
repostsCount: 1,
|
||||||
},
|
},
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
|
organizerId: 'usr_conc_snap',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Valid transition: READY -> SNAPSHOT_LOCKED -> DRAWN
|
// Valid transition: READY -> SNAPSHOT_LOCKED -> DRAWN
|
||||||
|
|
@ -77,6 +78,7 @@ describe('Concurrency Snapshot Locking & Participant Isolation', () => {
|
||||||
repostsCount: 1,
|
repostsCount: 1,
|
||||||
},
|
},
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
|
organizerId: 'usr_conc_snap',
|
||||||
});
|
});
|
||||||
|
|
||||||
await repo.createAndLockSnapshot(gw.id, participants, DEFAULT_FILTER_RULES);
|
await repo.createAndLockSnapshot(gw.id, participants, DEFAULT_FILTER_RULES);
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ describe('Payload Scalability & Pagination', () => {
|
||||||
repostsCount: 10,
|
repostsCount: 10,
|
||||||
},
|
},
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
|
organizerId: 'usr_payload_test',
|
||||||
});
|
});
|
||||||
|
|
||||||
await repo.saveParticipants(gw.id, participants);
|
await repo.saveParticipants(gw.id, participants);
|
||||||
|
|
@ -65,6 +66,7 @@ describe('Payload Scalability & Pagination', () => {
|
||||||
repostsCount: 10,
|
repostsCount: 10,
|
||||||
},
|
},
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
|
organizerId: 'usr_payload_test',
|
||||||
});
|
});
|
||||||
|
|
||||||
await repo.saveParticipants(gw.id, participants);
|
await repo.saveParticipants(gw.id, participants);
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ describe('Repository Persistence & Lifecycle Scenario', () => {
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
winnersCount: 1,
|
winnersCount: 1,
|
||||||
reserveWinnersCount: 1,
|
reserveWinnersCount: 1,
|
||||||
|
organizerId: 'usr_persist_test',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(gw.status).toBe('READY');
|
expect(gw.status).toBe('READY');
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ describe('Security: VK_SERVICE_TOKEN handling', () => {
|
||||||
excludeBlacklistedIds: [],
|
excludeBlacklistedIds: [],
|
||||||
excludeDuplicateComments: true,
|
excludeDuplicateComments: true,
|
||||||
},
|
},
|
||||||
|
organizerId: 'usr_security_test',
|
||||||
});
|
});
|
||||||
|
|
||||||
const json = JSON.stringify(gw);
|
const json = JSON.stringify(gw);
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ describe('DrawResult Snapshot Binding Regression Tests', () => {
|
||||||
repostsCount: 10,
|
repostsCount: 10,
|
||||||
},
|
},
|
||||||
filterRules: DEFAULT_FILTER_RULES,
|
filterRules: DEFAULT_FILTER_RULES,
|
||||||
|
organizerId: 'usr_snap_bind_test',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Create snapshot V1
|
// 2. Create snapshot V1
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ describe('Storage Driver Policy & No Silent Fallback', () => {
|
||||||
repostsCount: 2,
|
repostsCount: 2,
|
||||||
},
|
},
|
||||||
filterRules: {} as any,
|
filterRules: {} as any,
|
||||||
|
organizerId: 'usr_test_driver',
|
||||||
})
|
})
|
||||||
).rejects.toThrow(/Can't reach database server/);
|
).rejects.toThrow(/Can't reach database server/);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue