mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
154 lines
5.3 KiB
TypeScript
154 lines
5.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { ServiceAccountImpersonationProvider } from './sa-impersonation-provider.js';
|
|
import type { MCPServerConfig } from '../config/config.js';
|
|
|
|
const mockRequest = vi.fn();
|
|
const mockGetClient = vi.fn(() => ({
|
|
request: mockRequest,
|
|
}));
|
|
|
|
// Mock the google-auth-library to use a shared mock function
|
|
vi.mock('google-auth-library', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('google-auth-library')>();
|
|
return {
|
|
...actual,
|
|
GoogleAuth: vi.fn().mockImplementation(() => ({
|
|
getClient: mockGetClient,
|
|
})),
|
|
};
|
|
});
|
|
|
|
const defaultSAConfig: MCPServerConfig = {
|
|
url: 'https://my-iap-service.run.app',
|
|
targetAudience: 'my-audience',
|
|
targetServiceAccount: 'my-sa',
|
|
};
|
|
|
|
describe('ServiceAccountImpersonationProvider', () => {
|
|
beforeEach(() => {
|
|
// Reset mocks before each test
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should throw an error if no URL is provided', () => {
|
|
const config: MCPServerConfig = {};
|
|
expect(() => new ServiceAccountImpersonationProvider(config)).toThrow(
|
|
'A url or httpUrl must be provided for the Service Account Impersonation provider',
|
|
);
|
|
});
|
|
|
|
it('should throw an error if no targetAudience is provided', () => {
|
|
const config: MCPServerConfig = {
|
|
url: 'https://my-iap-service.run.app',
|
|
};
|
|
expect(() => new ServiceAccountImpersonationProvider(config)).toThrow(
|
|
'targetAudience must be provided for the Service Account Impersonation provider',
|
|
);
|
|
});
|
|
|
|
it('should throw an error if no targetSA is provided', () => {
|
|
const config: MCPServerConfig = {
|
|
url: 'https://my-iap-service.run.app',
|
|
targetAudience: 'my-audience',
|
|
};
|
|
expect(() => new ServiceAccountImpersonationProvider(config)).toThrow(
|
|
'targetServiceAccount must be provided for the Service Account Impersonation provider',
|
|
);
|
|
});
|
|
|
|
it('should correctly get tokens for a valid config', async () => {
|
|
const mockToken = 'mock-id-token-123';
|
|
mockRequest.mockResolvedValue({ data: { token: mockToken } });
|
|
|
|
const provider = new ServiceAccountImpersonationProvider(defaultSAConfig);
|
|
const tokens = await provider.tokens();
|
|
|
|
expect(tokens).toBeDefined();
|
|
expect(tokens?.access_token).toBe(mockToken);
|
|
expect(tokens?.token_type).toBe('Bearer');
|
|
});
|
|
|
|
it('should return undefined if token acquisition fails', async () => {
|
|
mockRequest.mockResolvedValue({ data: { token: null } });
|
|
|
|
const provider = new ServiceAccountImpersonationProvider(defaultSAConfig);
|
|
const tokens = await provider.tokens();
|
|
|
|
expect(tokens).toBeUndefined();
|
|
});
|
|
|
|
it('should make a request with the correct parameters', async () => {
|
|
mockRequest.mockResolvedValue({ data: { token: 'test-token' } });
|
|
|
|
const provider = new ServiceAccountImpersonationProvider(defaultSAConfig);
|
|
await provider.tokens();
|
|
|
|
expect(mockRequest).toHaveBeenCalledWith({
|
|
url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-sa:generateIdToken',
|
|
method: 'POST',
|
|
data: {
|
|
audience: 'my-audience',
|
|
includeEmail: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should return a cached token if it is not expired', async () => {
|
|
const provider = new ServiceAccountImpersonationProvider(defaultSAConfig);
|
|
vi.useFakeTimers();
|
|
|
|
// jwt payload with exp set to 1 hour from now
|
|
const payload = { exp: Math.floor(Date.now() / 1000) + 3600 };
|
|
const jwt = `header.${Buffer.from(JSON.stringify(payload)).toString('base64')}.signature`;
|
|
mockRequest.mockResolvedValue({ data: { token: jwt } });
|
|
|
|
const firstTokens = await provider.tokens();
|
|
expect(firstTokens?.access_token).toBe(jwt);
|
|
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
|
|
// Advance time by 30 minutes
|
|
vi.advanceTimersByTime(1800 * 1000);
|
|
|
|
// Seturn cached token
|
|
const secondTokens = await provider.tokens();
|
|
expect(secondTokens).toBe(firstTokens);
|
|
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should fetch a new token if the cached token is expired (using fake timers)', async () => {
|
|
const provider = new ServiceAccountImpersonationProvider(defaultSAConfig);
|
|
vi.useFakeTimers();
|
|
|
|
// Get and cache a token that expires in 1 second
|
|
const expiredPayload = { exp: Math.floor(Date.now() / 1000) + 1 };
|
|
const expiredJwt = `header.${Buffer.from(JSON.stringify(expiredPayload)).toString('base64')}.signature`;
|
|
|
|
mockRequest.mockResolvedValue({ data: { token: expiredJwt } });
|
|
const firstTokens = await provider.tokens();
|
|
expect(firstTokens?.access_token).toBe(expiredJwt);
|
|
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
|
|
// Prepare the mock for the *next* call
|
|
const newPayload = { exp: Math.floor(Date.now() / 1000) + 3600 };
|
|
const newJwt = `header.${Buffer.from(JSON.stringify(newPayload)).toString('base64')}.signature`;
|
|
mockRequest.mockResolvedValue({ data: { token: newJwt } });
|
|
|
|
vi.advanceTimersByTime(1001);
|
|
|
|
const newTokens = await provider.tokens();
|
|
expect(newTokens?.access_token).toBe(newJwt);
|
|
expect(newTokens?.access_token).not.toBe(expiredJwt);
|
|
expect(mockRequest).toHaveBeenCalledTimes(2); // Confirms a new fetch
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
});
|