90 lines
3.3 KiB
TypeScript
90 lines
3.3 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
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 { VkReauthenticationRequiredError } from '../src/integrations/vk/vk-errors';
|
|
|
|
describe('Phase 2.3 VkAuthContextResolver & Token Selection Policy', () => {
|
|
let userRepo: MemoryUserRepository;
|
|
let tokenVault: AesGcmTokenVault;
|
|
let oauthClient: MockVkOAuthClient;
|
|
let tokenRefresher: TokenRefresher;
|
|
let resolver: VkAuthContextResolver;
|
|
|
|
let organizerId: string;
|
|
const rawAccessToken = 'vk1.a.alice_valid_user_access_token_12345';
|
|
const rawRefreshToken = 'vk1.a.alice_valid_refresh_token_67890';
|
|
|
|
beforeEach(async () => {
|
|
userRepo = new MemoryUserRepository();
|
|
tokenVault = new AesGcmTokenVault('test-master-token-encryption-key-32b!');
|
|
oauthClient = new MockVkOAuthClient();
|
|
tokenRefresher = new TokenRefresher(userRepo, tokenVault, oauthClient);
|
|
resolver = new VkAuthContextResolver(tokenRefresher);
|
|
|
|
process.env.VK_SERVICE_TOKEN = 'vk_service_token_secret_12345';
|
|
|
|
// Store encrypted user credentials
|
|
const encryptedAccessToken = await tokenVault.encrypt(rawAccessToken);
|
|
const encryptedRefreshToken = await tokenVault.encrypt(rawRefreshToken);
|
|
|
|
const user = await userRepo.upsertUserWithTokens({
|
|
vkUserId: '12345678',
|
|
firstName: 'Alice',
|
|
lastName: 'Organizer',
|
|
encryptedAccessToken,
|
|
encryptedRefreshToken,
|
|
expiresIn: 3600, // Valid for 1 hour
|
|
});
|
|
organizerId = user.id;
|
|
});
|
|
|
|
it('selects SERVICE token by default for public operations (least privilege)', async () => {
|
|
const auth = await resolver.resolveAuthContext({
|
|
method: 'wall.getById',
|
|
resource: { ownerId: '-100', postId: '1' },
|
|
});
|
|
|
|
expect(auth.type).toBe('SERVICE');
|
|
expect(auth.token).toBe('vk_service_token_secret_12345');
|
|
});
|
|
|
|
it('selects USER token when preferredMode is explicitly set to USER', async () => {
|
|
const auth = await resolver.resolveAuthContext({
|
|
organizerId,
|
|
preferredMode: 'USER',
|
|
method: 'wall.getById',
|
|
});
|
|
|
|
expect(auth.type).toBe('USER');
|
|
expect(auth.token).toBe(rawAccessToken);
|
|
});
|
|
|
|
it('throws VkReauthenticationRequiredError when USER token is requested but organizer is unauthenticated', async () => {
|
|
await expect(
|
|
resolver.resolveAuthContext({
|
|
preferredMode: 'USER',
|
|
method: 'wall.getById',
|
|
})
|
|
).rejects.toThrow(VkReauthenticationRequiredError);
|
|
});
|
|
|
|
it('throws VkReauthenticationRequiredError when organizer credentials do not exist in database', async () => {
|
|
await expect(
|
|
resolver.resolveAuthContext({
|
|
organizerId: 'usr_non_existent_organizer',
|
|
preferredMode: 'USER',
|
|
method: 'wall.getById',
|
|
})
|
|
).rejects.toThrow(VkReauthenticationRequiredError);
|
|
});
|
|
|
|
it('resolves fallback USER token for controlled privacy/permission errors', async () => {
|
|
const auth = await resolver.resolveUserFallbackContext(organizerId);
|
|
|
|
expect(auth.type).toBe('USER');
|
|
expect(auth.token).toBe(rawAccessToken);
|
|
});
|
|
});
|