randomayzer/tests/vk-provider-authenticated.test.ts

170 lines
6.2 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { VkProvider } from '../src/providers/vk/vk-provider';
import { VkAuthContextResolver } from '../src/integrations/vk/vk-auth-resolver';
import { TokenRefresher } from '../src/lib/auth/token-refresher';
import { MemoryUserRepository } from '../src/lib/repository/user-repository';
import { AesGcmTokenVault } from '../src/lib/auth/token-vault';
import { MockVkOAuthClient } from '../src/integrations/vk/mock-oauth-client';
import { IVkClient } from '../src/integrations/vk/vk-client';
import { VkAuthContext } from '../src/integrations/vk/vk-types';
import {
VkPrivateResourceError,
VkRateLimitError,
VkTemporaryError
} from '../src/integrations/vk/vk-errors';
import { resolveEffectiveCapabilities } from '../src/providers/vk/vk-capabilities';
describe('Phase 2.3 Authenticated VK Provider & Controlled Fallback Gate', () => {
let userRepo: MemoryUserRepository;
let tokenVault: AesGcmTokenVault;
let oauthClient: MockVkOAuthClient;
let tokenRefresher: TokenRefresher;
let authResolver: VkAuthContextResolver;
let organizerId: string;
const userTokenPlain = 'vk1.a.organizer_user_access_token_abc';
const serviceTokenPlain = 'vk_service_token_xyz';
beforeEach(async () => {
userRepo = new MemoryUserRepository();
tokenVault = new AesGcmTokenVault('test-master-token-encryption-key-32b!');
oauthClient = new MockVkOAuthClient();
tokenRefresher = new TokenRefresher(userRepo, tokenVault, oauthClient);
authResolver = new VkAuthContextResolver(tokenRefresher);
process.env.VK_SERVICE_TOKEN = serviceTokenPlain;
const encryptedAccessToken = await tokenVault.encrypt(userTokenPlain);
const user = await userRepo.upsertUserWithTokens({
vkUserId: '77778888',
firstName: 'Dmitry',
lastName: 'Organizer',
encryptedAccessToken,
expiresIn: 7200,
});
organizerId = user.id;
});
it('uses SERVICE token for public posts by default (least privilege)', async () => {
let capturedAuth: VkAuthContext | undefined;
const mockClient: IVkClient = {
call: async (_method, _params, auth) => {
capturedAuth = auth;
return {
items: [
{
id: 100,
owner_id: -100,
date: 1700000000,
text: 'Public post',
likes: { count: 5 },
comments: { count: 2 },
reposts: { count: 1 },
},
],
} as any;
},
};
const provider = new VkProvider(serviceTokenPlain, mockClient, authResolver);
const post = await provider.fetchPost('https://vk.com/wall-100_100', { organizerId });
expect(post.title).toBe('Public post...');
expect(capturedAuth?.type).toBe('SERVICE');
expect(capturedAuth?.token).toBe(serviceTokenPlain);
});
it('performs controlled fallback to USER token when SERVICE token receives private resource error', async () => {
const authSequence: VkAuthContext[] = [];
const mockClient: IVkClient = {
call: async (_method, _params, auth) => {
authSequence.push(auth!);
if (auth?.type === 'SERVICE') {
// Simulate VK API error 15 / 30 (Access denied to private group/profile)
throw new VkPrivateResourceError('Access denied: post is in a private group', { errorCode: 15 });
}
// USER token succeeds
return {
items: [
{
id: 200,
owner_id: -200,
date: 1700000000,
text: 'Private group post visible to organizer',
likes: { count: 10 },
comments: { count: 4 },
reposts: { count: 0 },
},
],
} as any;
},
};
const provider = new VkProvider(serviceTokenPlain, mockClient, authResolver);
const post = await provider.fetchPost('https://vk.com/wall-200_200', { organizerId });
expect(post.title).toBe('Private group post visible to organizer...');
expect(authSequence).toHaveLength(2);
expect(authSequence[0].type).toBe('SERVICE');
expect(authSequence[1].type).toBe('USER');
expect(authSequence[1].token).toBe(userTokenPlain);
});
it('strictly forbids fallback on rate limits (HTTP 429 / error 6/29)', async () => {
const authSequence: VkAuthContext[] = [];
const mockClient: IVkClient = {
call: async (_method, _params, auth) => {
authSequence.push(auth!);
throw new VkRateLimitError('VK rate limit reached', { errorCode: 6 });
},
};
const provider = new VkProvider(serviceTokenPlain, mockClient, authResolver);
await expect(
provider.fetchPost('https://vk.com/wall-100_100', { organizerId })
).rejects.toThrow(VkRateLimitError);
// Fallback was NOT attempted on rate limit
expect(authSequence).toHaveLength(1);
expect(authSequence[0].type).toBe('SERVICE');
});
it('strictly forbids fallback on VK server errors (HTTP 500 / error 10)', async () => {
const authSequence: VkAuthContext[] = [];
const mockClient: IVkClient = {
call: async (_method, _params, auth) => {
authSequence.push(auth!);
throw new VkTemporaryError('VK Internal Server Error', { errorCode: 10 });
},
};
const provider = new VkProvider(serviceTokenPlain, mockClient, authResolver);
await expect(
provider.fetchPost('https://vk.com/wall-100_100', { organizerId })
).rejects.toThrow(VkTemporaryError);
expect(authSequence).toHaveLength(1);
expect(authSequence[0].type).toBe('SERVICE');
});
it('derives effective capabilities accurately depending on accessMode', () => {
const serviceCapabilities = resolveEffectiveCapabilities({ type: 'SERVICE', token: 's' });
expect(serviceCapabilities.accessMode).toBe('PUBLIC_SERVICE');
expect(serviceCapabilities.adminDetection).toBe(false);
const userCapabilities = resolveEffectiveCapabilities({ type: 'USER', token: 'u' });
expect(userCapabilities.accessMode).toBe('ORGANIZER_USER');
expect(userCapabilities.adminDetection).toBe(false);
const communityCapabilities = resolveEffectiveCapabilities({ type: 'COMMUNITY', token: 'c', communityId: '100' });
expect(communityCapabilities.accessMode).toBe('COMMUNITY_GROUP');
expect(communityCapabilities.adminDetection).toBe(true);
});
});