fix(auth): Phase 2.3.1.1 truthful giveaway detail capability based on credential status
This commit is contained in:
parent
0ce91d7b0d
commit
9927e74421
5 changed files with 328 additions and 21 deletions
|
|
@ -0,0 +1,17 @@
|
|||
# Task Done: Phase 2.3.1.1 — Giveaway Detail Capability Truthfulness
|
||||
|
||||
**Status:** DONE
|
||||
**Assigned to:** Antigravity (@orchestrator)
|
||||
**Date:** 2026-08-18
|
||||
**Base Commit:** `4e4370d`
|
||||
|
||||
## Summary of Changes
|
||||
- Implemented `getCredentialStatus(userId: string)` in `TokenRefresher` with exact states:
|
||||
- `AVAILABLE`: valid, non-expired USER access token present.
|
||||
- `REFRESHABLE`: expired or unknown expiry, but refresh token is present.
|
||||
- `REAUTH_REQUIRED`: expired or unknown expiry, without refresh token.
|
||||
- `MISSING`: no credentials stored for user.
|
||||
- Updated `GET /api/giveaways/[id]` to query `defaultTokenRefresher.getCredentialStatus(sessionUser.id)` without performing network calls or leaking tokens into response.
|
||||
- `resolveEffectiveCapabilities` assigns `accessMode: 'ORGANIZER_USER'` only when `status` is `AVAILABLE` or `REFRESHABLE`. For `MISSING` or `REAUTH_REQUIRED`, it strictly defaults to `PUBLIC_SERVICE`.
|
||||
- Expanded `tests/effective-capabilities-truthfulness.test.ts` to 11 tests covering all credential states, expiration boundaries, refresh token presence, and token secrecy.
|
||||
- All 273 tests passing across 47 test files (100% green).
|
||||
|
|
@ -4,7 +4,7 @@ import { generalApiRateLimiter } from '@/lib/rate-limiter';
|
|||
import { resolveClientIp } from '@/lib/client-ip';
|
||||
import { requireGiveawayOwner } from '@/lib/auth/auth-guard';
|
||||
import { resolveEffectiveCapabilities } from '@/providers/vk/vk-capabilities';
|
||||
import { defaultUserRepository } from '@/lib/repository/user-repository';
|
||||
import { defaultTokenRefresher } from '@/lib/auth/token-refresher';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
|
@ -20,16 +20,17 @@ export async function GET(
|
|||
// Enforce giveaway ownership authorization
|
||||
const { giveaway, sessionUser } = await requireGiveawayOwner(req, id);
|
||||
|
||||
// Resolve runtime effective capabilities truthfully based on stored organizer credentials
|
||||
let authType: 'SERVICE' | 'USER' = 'SERVICE';
|
||||
// Resolve runtime effective capabilities truthfully based on stored organizer credential status
|
||||
let credentialStatus: 'AVAILABLE' | 'REFRESHABLE' | 'REAUTH_REQUIRED' | 'MISSING' = 'MISSING';
|
||||
if (sessionUser?.id) {
|
||||
const cred = await defaultUserRepository.getUserCredentials(sessionUser.id);
|
||||
if (cred?.encryptedAccessToken) {
|
||||
authType = 'USER';
|
||||
}
|
||||
credentialStatus = await defaultTokenRefresher.getCredentialStatus(sessionUser.id);
|
||||
}
|
||||
|
||||
const effectiveCapabilities = resolveEffectiveCapabilities({ type: authType });
|
||||
const isUserAuthUsable = credentialStatus === 'AVAILABLE' || credentialStatus === 'REFRESHABLE';
|
||||
const effectiveCapabilities = resolveEffectiveCapabilities({
|
||||
type: isUserAuthUsable ? 'USER' : 'SERVICE',
|
||||
credentialStatus,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -31,13 +31,15 @@ import { VkReauthenticationRequiredError, VkAuthError } from '@/integrations/vk/
|
|||
* - VK token refresh responses include user_id. We verify it matches the stored User.vkUserId.
|
||||
* - Mismatch → VkReauthenticationRequiredError. Tokens are NOT persisted.
|
||||
*/
|
||||
export type UserCredentialStatus = 'AVAILABLE' | 'REFRESHABLE' | 'REAUTH_REQUIRED' | 'MISSING';
|
||||
|
||||
export class TokenRefresher {
|
||||
private inFlightRefreshes = new Map<string, Promise<string>>();
|
||||
|
||||
constructor(
|
||||
private userRepo: IUserRepository = defaultUserRepository,
|
||||
private tokenVault: ITokenVault = defaultTokenVault,
|
||||
private oauthClient: IVkOAuthClient = defaultVkOAuthClient
|
||||
private userRepo?: IUserRepository,
|
||||
private tokenVault?: ITokenVault,
|
||||
private oauthClient?: IVkOAuthClient
|
||||
) {}
|
||||
|
||||
public setDependencies(deps: {
|
||||
|
|
@ -50,12 +52,53 @@ export class TokenRefresher {
|
|||
if (deps.oauthClient) this.oauthClient = deps.oauthClient;
|
||||
}
|
||||
|
||||
private getUserRepo(): IUserRepository {
|
||||
return this.userRepo || defaultUserRepository;
|
||||
}
|
||||
|
||||
private getTokenVault(): ITokenVault {
|
||||
return this.tokenVault || defaultTokenVault;
|
||||
}
|
||||
|
||||
private getOAuthClient(): IVkOAuthClient {
|
||||
return this.oauthClient || defaultVkOAuthClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast, server-side status check of user credentials without performing network calls
|
||||
* or decrypting secrets.
|
||||
*/
|
||||
public async getCredentialStatus(userId: string): Promise<UserCredentialStatus> {
|
||||
try {
|
||||
const cred = await this.getUserRepo().getUserCredentials(userId);
|
||||
if (!cred || !cred.encryptedAccessToken) {
|
||||
return 'MISSING';
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const hasRefreshToken = Boolean(cred.encryptedRefreshToken);
|
||||
|
||||
if (cred.expiresAt === null || cred.expiresAt === undefined) {
|
||||
return hasRefreshToken ? 'REFRESHABLE' : 'REAUTH_REQUIRED';
|
||||
}
|
||||
|
||||
const isExpiredOrExpiring = now >= cred.expiresAt.getTime() - 30_000;
|
||||
if (!isExpiredOrExpiring) {
|
||||
return 'AVAILABLE';
|
||||
}
|
||||
|
||||
return hasRefreshToken ? 'REFRESHABLE' : 'REAUTH_REQUIRED';
|
||||
} catch {
|
||||
return 'REAUTH_REQUIRED';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid plaintext VK access token for the given userId.
|
||||
* Refreshes if expired or if expiry is unknown. Uses single-flight mutex.
|
||||
*/
|
||||
public async getOrRefreshUserToken(userId: string): Promise<string> {
|
||||
const cred = await this.userRepo.getUserCredentials(userId);
|
||||
const cred = await this.getUserRepo().getUserCredentials(userId);
|
||||
|
||||
if (!cred || !cred.encryptedAccessToken) {
|
||||
throw new VkReauthenticationRequiredError(
|
||||
|
|
@ -85,7 +128,7 @@ export class TokenRefresher {
|
|||
}
|
||||
|
||||
if (!isExpiredOrExpiring) {
|
||||
return await this.tokenVault.decrypt(cred.encryptedAccessToken);
|
||||
return await this.getTokenVault().decrypt(cred.encryptedAccessToken);
|
||||
}
|
||||
|
||||
// Token is expired or expiry is unknown. Check if refresh token is available.
|
||||
|
|
@ -125,11 +168,11 @@ export class TokenRefresher {
|
|||
credentialUpdatedAt: Date
|
||||
): Promise<string> {
|
||||
try {
|
||||
const refreshToken = await this.tokenVault.decrypt(encryptedRefreshToken);
|
||||
const refreshToken = await this.getTokenVault().decrypt(encryptedRefreshToken);
|
||||
const clientId = process.env.VK_APP_ID || (process.env.NODE_ENV === 'test' ? 'test_vk_app_id' : '');
|
||||
const clientSecret = process.env.VK_CLIENT_SECRET;
|
||||
|
||||
const refreshResponse = await this.oauthClient.refreshToken({
|
||||
const refreshResponse = await this.getOAuthClient().refreshToken({
|
||||
refreshToken,
|
||||
clientId,
|
||||
clientSecret,
|
||||
|
|
@ -143,7 +186,7 @@ export class TokenRefresher {
|
|||
|
||||
// --- IDENTITY BINDING ---
|
||||
// Verify the refreshed token belongs to the same VK user.
|
||||
const user = await this.userRepo.getUserById(userId);
|
||||
const user = await this.getUserRepo().getUserById(userId);
|
||||
if (!user) {
|
||||
throw new VkReauthenticationRequiredError(
|
||||
'User account not found during token refresh'
|
||||
|
|
@ -162,11 +205,11 @@ export class TokenRefresher {
|
|||
// VK ID may issue a new refresh_token. If it does, rotate it.
|
||||
// If the response omits refresh_token, retain the previous encrypted token.
|
||||
// IMPORTANT: only store defined non-null values.
|
||||
const encryptedAccessToken = await this.tokenVault.encrypt(refreshResponse.access_token);
|
||||
const encryptedAccessToken = await this.getTokenVault().encrypt(refreshResponse.access_token);
|
||||
let newEncryptedRefreshToken: string;
|
||||
if (refreshResponse.refresh_token) {
|
||||
// New (or same) refresh token returned — encrypt and store
|
||||
newEncryptedRefreshToken = await this.tokenVault.encrypt(refreshResponse.refresh_token);
|
||||
newEncryptedRefreshToken = await this.getTokenVault().encrypt(refreshResponse.refresh_token);
|
||||
} else {
|
||||
// No refresh token in response — retain the previous encrypted token
|
||||
newEncryptedRefreshToken = encryptedRefreshToken;
|
||||
|
|
@ -181,7 +224,7 @@ export class TokenRefresher {
|
|||
// since we read it (i.e., no re-login or concurrent refresh has won the race).
|
||||
// On CAS miss, we still return the freshly-computed access token — it's valid —
|
||||
// but we do NOT overwrite the newer credential in the DB.
|
||||
const written = await this.userRepo.updateCredentialConditionally(
|
||||
const written = await this.getUserRepo().updateCredentialConditionally(
|
||||
userId,
|
||||
{
|
||||
encryptedAccessToken,
|
||||
|
|
@ -222,4 +265,8 @@ export class TokenRefresher {
|
|||
}
|
||||
}
|
||||
|
||||
export const defaultTokenRefresher = new TokenRefresher();
|
||||
export let defaultTokenRefresher: TokenRefresher = new TokenRefresher();
|
||||
|
||||
export function setTokenRefresher(refresher: TokenRefresher): void {
|
||||
defaultTokenRefresher = refresher;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { ProviderCapabilities } from '../types';
|
||||
import { VkAuthContext } from '@/integrations/vk/vk-types';
|
||||
|
||||
export type UserCredentialStatus = 'AVAILABLE' | 'REFRESHABLE' | 'REAUTH_REQUIRED' | 'MISSING';
|
||||
export type VkAccessMode = 'PUBLIC_SERVICE' | 'ORGANIZER_USER' | 'COMMUNITY_GROUP';
|
||||
|
||||
export interface EffectiveCapabilities extends ProviderCapabilities {
|
||||
accessMode: VkAccessMode;
|
||||
credentialStatus?: UserCredentialStatus;
|
||||
}
|
||||
|
||||
export const STATIC_VK_CAPABILITIES: ProviderCapabilities = {
|
||||
|
|
@ -20,7 +22,9 @@ export const STATIC_VK_CAPABILITIES: ProviderCapabilities = {
|
|||
/**
|
||||
* Derives effective capabilities at runtime based on the resolved auth context and target resource.
|
||||
*/
|
||||
export function resolveEffectiveCapabilities(authContext?: { type: 'SERVICE' | 'USER' | 'COMMUNITY' } | VkAuthContext | null): EffectiveCapabilities {
|
||||
export function resolveEffectiveCapabilities(
|
||||
authContext?: { type: 'SERVICE' | 'USER' | 'COMMUNITY'; credentialStatus?: UserCredentialStatus } | VkAuthContext | null
|
||||
): EffectiveCapabilities {
|
||||
const accessMode: VkAccessMode = !authContext || authContext.type === 'SERVICE'
|
||||
? 'PUBLIC_SERVICE'
|
||||
: authContext.type === 'USER'
|
||||
|
|
@ -28,6 +32,7 @@ export function resolveEffectiveCapabilities(authContext?: { type: 'SERVICE' | '
|
|||
: 'COMMUNITY_GROUP';
|
||||
|
||||
const isCommunityAdmin = authContext?.type === 'COMMUNITY';
|
||||
const credentialStatus = authContext && 'credentialStatus' in authContext ? authContext.credentialStatus : undefined;
|
||||
|
||||
return {
|
||||
...STATIC_VK_CAPABILITIES,
|
||||
|
|
@ -36,5 +41,6 @@ export function resolveEffectiveCapabilities(authContext?: { type: 'SERVICE' | '
|
|||
? undefined
|
||||
: 'Требует прямого подключения токена сообщества с правами администратора',
|
||||
accessMode,
|
||||
...(credentialStatus ? { credentialStatus } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
|
|||
|
||||
expect(resAlice.status).toBe(200);
|
||||
expect(jsonAlice.effectiveCapabilities.accessMode).toBe('ORGANIZER_USER');
|
||||
expect(jsonAlice.effectiveCapabilities.credentialStatus).toBe('AVAILABLE');
|
||||
|
||||
// 6b. Create giveaway for Charlie (no credentials)
|
||||
const gwCharlie = await GiveawayStore.create({
|
||||
|
|
@ -359,5 +360,240 @@ describe('Phase 2.3.1 — Effective Capabilities Truthfulness Gate', () => {
|
|||
|
||||
expect(resCharlie.status).toBe(200);
|
||||
expect(jsonCharlie.effectiveCapabilities.accessMode).toBe('PUBLIC_SERVICE');
|
||||
expect(jsonCharlie.effectiveCapabilities.credentialStatus).toBe('MISSING');
|
||||
});
|
||||
|
||||
// ─── Test 7: Expired credential + refresh token available → ORGANIZER_USER (REFRESHABLE) ─
|
||||
it('7. expired credential + refresh token reports ORGANIZER_USER with REFRESHABLE status', async () => {
|
||||
const encryptedAccessToken = await tokenVault.encrypt('old_token');
|
||||
const encryptedRefreshToken = await tokenVault.encrypt('valid_refresh_token');
|
||||
|
||||
const expiredUser = await userRepo.upsertUserWithTokens({
|
||||
vkUserId: '55556666',
|
||||
firstName: 'David',
|
||||
lastName: 'Refreshable',
|
||||
encryptedAccessToken,
|
||||
encryptedRefreshToken,
|
||||
expiresIn: -60, // expired 1 minute ago
|
||||
});
|
||||
|
||||
const sessionId = await defaultSessionStore.createSession(expiredUser);
|
||||
const cookie = `${SESSION_COOKIE_NAME}=${sessionId}`;
|
||||
|
||||
const gw = await GiveawayStore.create({
|
||||
sourceUrl: 'https://vk.com/wall-555_555',
|
||||
platform: 'VK',
|
||||
platformOwnerId: '-555',
|
||||
platformPostId: '555',
|
||||
title: 'David Giveaway',
|
||||
organizerId: expiredUser.id,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
ownerId: '-555',
|
||||
postId: '555',
|
||||
sourceUrl: 'https://vk.com/wall-555_555',
|
||||
title: 'David Giveaway',
|
||||
text: 'Text',
|
||||
likesCount: 5,
|
||||
commentsCount: 1,
|
||||
repostsCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const req = new NextRequest(`http://localhost:3000/api/giveaways/${gw.id}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Cookie': cookie },
|
||||
});
|
||||
|
||||
const res = await giveawayDetailGet(req, { params: { id: gw.id } });
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.effectiveCapabilities.accessMode).toBe('ORGANIZER_USER');
|
||||
expect(json.effectiveCapabilities.credentialStatus).toBe('REFRESHABLE');
|
||||
});
|
||||
|
||||
// ─── Test 8: Expired credential WITHOUT refresh token → PUBLIC_SERVICE (REAUTH_REQUIRED) ─
|
||||
it('8. expired credential without refresh token reports PUBLIC_SERVICE with REAUTH_REQUIRED status', async () => {
|
||||
const encryptedAccessToken = await tokenVault.encrypt('old_token_no_refresh');
|
||||
|
||||
const expiredNoRefreshUser = await userRepo.upsertUserWithTokens({
|
||||
vkUserId: '77779999',
|
||||
firstName: 'Eve',
|
||||
lastName: 'NoRefresh',
|
||||
encryptedAccessToken,
|
||||
expiresIn: -60, // expired 1 minute ago
|
||||
});
|
||||
|
||||
const sessionId = await defaultSessionStore.createSession(expiredNoRefreshUser);
|
||||
const cookie = `${SESSION_COOKIE_NAME}=${sessionId}`;
|
||||
|
||||
const gw = await GiveawayStore.create({
|
||||
sourceUrl: 'https://vk.com/wall-777_777',
|
||||
platform: 'VK',
|
||||
platformOwnerId: '-777',
|
||||
platformPostId: '777',
|
||||
title: 'Eve Giveaway',
|
||||
organizerId: expiredNoRefreshUser.id,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
ownerId: '-777',
|
||||
postId: '777',
|
||||
sourceUrl: 'https://vk.com/wall-777_777',
|
||||
title: 'Eve Giveaway',
|
||||
text: 'Text',
|
||||
likesCount: 5,
|
||||
commentsCount: 1,
|
||||
repostsCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const req = new NextRequest(`http://localhost:3000/api/giveaways/${gw.id}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Cookie': cookie },
|
||||
});
|
||||
|
||||
const res = await giveawayDetailGet(req, { params: { id: gw.id } });
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
// Must NOT claim ORGANIZER_USER
|
||||
expect(json.effectiveCapabilities.accessMode).toBe('PUBLIC_SERVICE');
|
||||
expect(json.effectiveCapabilities.credentialStatus).toBe('REAUTH_REQUIRED');
|
||||
});
|
||||
|
||||
// ─── Test 9: Null expiresAt WITHOUT refresh token → PUBLIC_SERVICE (REAUTH_REQUIRED) ─
|
||||
it('9. null expiresAt without refresh token reports PUBLIC_SERVICE with REAUTH_REQUIRED status', async () => {
|
||||
const encryptedAccessToken = await tokenVault.encrypt('legacy_token');
|
||||
|
||||
const legacyUser = await userRepo.upsertUserWithTokens({
|
||||
vkUserId: '88880000',
|
||||
firstName: 'Frank',
|
||||
lastName: 'LegacyNoExpiry',
|
||||
encryptedAccessToken,
|
||||
// expiresIn: undefined -> expiresAt: null
|
||||
});
|
||||
|
||||
const sessionId = await defaultSessionStore.createSession(legacyUser);
|
||||
const cookie = `${SESSION_COOKIE_NAME}=${sessionId}`;
|
||||
|
||||
const gw = await GiveawayStore.create({
|
||||
sourceUrl: 'https://vk.com/wall-888_888',
|
||||
platform: 'VK',
|
||||
platformOwnerId: '-888',
|
||||
platformPostId: '888',
|
||||
title: 'Frank Giveaway',
|
||||
organizerId: legacyUser.id,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
ownerId: '-888',
|
||||
postId: '888',
|
||||
sourceUrl: 'https://vk.com/wall-888_888',
|
||||
title: 'Frank Giveaway',
|
||||
text: 'Text',
|
||||
likesCount: 5,
|
||||
commentsCount: 1,
|
||||
repostsCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const req = new NextRequest(`http://localhost:3000/api/giveaways/${gw.id}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Cookie': cookie },
|
||||
});
|
||||
|
||||
const res = await giveawayDetailGet(req, { params: { id: gw.id } });
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.effectiveCapabilities.accessMode).toBe('PUBLIC_SERVICE');
|
||||
expect(json.effectiveCapabilities.credentialStatus).toBe('REAUTH_REQUIRED');
|
||||
});
|
||||
|
||||
// ─── Test 10: Null expiresAt WITH refresh token → ORGANIZER_USER (REFRESHABLE) ─
|
||||
it('10. null expiresAt with refresh token reports ORGANIZER_USER with REFRESHABLE status', async () => {
|
||||
const encryptedAccessToken = await tokenVault.encrypt('legacy_token');
|
||||
const encryptedRefreshToken = await tokenVault.encrypt('valid_refresh_token');
|
||||
|
||||
const legacyRefreshUser = await userRepo.upsertUserWithTokens({
|
||||
vkUserId: '99991111',
|
||||
firstName: 'Grace',
|
||||
lastName: 'LegacyRefreshable',
|
||||
encryptedAccessToken,
|
||||
encryptedRefreshToken,
|
||||
// expiresIn: undefined -> expiresAt: null
|
||||
});
|
||||
|
||||
const sessionId = await defaultSessionStore.createSession(legacyRefreshUser);
|
||||
const cookie = `${SESSION_COOKIE_NAME}=${sessionId}`;
|
||||
|
||||
const gw = await GiveawayStore.create({
|
||||
sourceUrl: 'https://vk.com/wall-999_999',
|
||||
platform: 'VK',
|
||||
platformOwnerId: '-999',
|
||||
platformPostId: '999',
|
||||
title: 'Grace Giveaway',
|
||||
organizerId: legacyRefreshUser.id,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
ownerId: '-999',
|
||||
postId: '999',
|
||||
sourceUrl: 'https://vk.com/wall-999_999',
|
||||
title: 'Grace Giveaway',
|
||||
text: 'Text',
|
||||
likesCount: 5,
|
||||
commentsCount: 1,
|
||||
repostsCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const req = new NextRequest(`http://localhost:3000/api/giveaways/${gw.id}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Cookie': cookie },
|
||||
});
|
||||
|
||||
const res = await giveawayDetailGet(req, { params: { id: gw.id } });
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.effectiveCapabilities.accessMode).toBe('ORGANIZER_USER');
|
||||
expect(json.effectiveCapabilities.credentialStatus).toBe('REFRESHABLE');
|
||||
});
|
||||
|
||||
// ─── Test 11: Giveaway detail does not leak tokens or secret fields ──────────
|
||||
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,
|
||||
post: {
|
||||
platform: 'VK',
|
||||
ownerId: '-101',
|
||||
postId: '101',
|
||||
sourceUrl: 'https://vk.com/wall-101_101',
|
||||
title: 'Alice Giveaway',
|
||||
text: 'Text',
|
||||
likesCount: 10,
|
||||
commentsCount: 2,
|
||||
repostsCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const reqAlice = new NextRequest(`http://localhost:3000/api/giveaways/${gwAlice.id}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Cookie': sessionCookie },
|
||||
});
|
||||
|
||||
const resAlice = await giveawayDetailGet(reqAlice, { params: { id: gwAlice.id } });
|
||||
const rawText = await resAlice.text();
|
||||
|
||||
expect(rawText).not.toContain(userTokenPlain);
|
||||
expect(rawText).not.toContain(serviceTokenPlain);
|
||||
expect(rawText).not.toContain('encryptedAccessToken');
|
||||
expect(rawText).not.toContain('encryptedRefreshToken');
|
||||
expect(rawText).not.toContain('tokenVault');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue