Initial commit: Cloud Instances API
Multi-cloud VM instance database with Cloudflare Workers - Linode, Vultr, AWS connector integration - D1 database with regions, instances, pricing - Query API with filtering, caching, pagination - Cron-based auto-sync (daily + 6-hourly) - Health monitoring endpoint Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
325
tests/vault.test.ts
Normal file
325
tests/vault.test.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { VaultClient, VaultError } from '../src/connectors/vault';
|
||||
import type { VaultSecretResponse } from '../src/types';
|
||||
|
||||
// Mock fetch globally
|
||||
const mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
|
||||
describe('VaultClient', () => {
|
||||
const baseUrl = 'https://vault.anvil.it.com';
|
||||
const token = 'hvs.test-token';
|
||||
let client: VaultClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new VaultClient(baseUrl, token);
|
||||
mockFetch.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with correct baseUrl and token', () => {
|
||||
expect(client).toBeInstanceOf(VaultClient);
|
||||
});
|
||||
|
||||
it('should remove trailing slash from baseUrl', () => {
|
||||
const clientWithSlash = new VaultClient('https://vault.anvil.it.com/', token);
|
||||
expect(clientWithSlash).toBeInstanceOf(VaultClient);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCredentials', () => {
|
||||
const mockSuccessResponse: VaultSecretResponse = {
|
||||
data: {
|
||||
data: {
|
||||
provider: 'linode',
|
||||
api_token: 'test-api-token-123',
|
||||
},
|
||||
metadata: {
|
||||
created_time: '2024-01-21T10:00:00Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should successfully retrieve credentials from Vault', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => mockSuccessResponse,
|
||||
});
|
||||
|
||||
const credentials = await client.getCredentials('linode');
|
||||
|
||||
expect(credentials).toEqual({
|
||||
provider: 'linode',
|
||||
api_token: 'test-api-token-123',
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://vault.anvil.it.com/v1/secret/data/linode',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Vault-Token': token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should use cached credentials on second call', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => mockSuccessResponse,
|
||||
});
|
||||
|
||||
// First call - should fetch from Vault
|
||||
const credentials1 = await client.getCredentials('linode');
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second call - should use cache
|
||||
const credentials2 = await client.getCredentials('linode');
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1); // No additional fetch
|
||||
expect(credentials2).toEqual(credentials1);
|
||||
});
|
||||
|
||||
it('should throw VaultError on 401 Unauthorized', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
json: async () => ({ errors: ['permission denied'] }),
|
||||
});
|
||||
|
||||
await expect(client.getCredentials('linode')).rejects.toThrow(VaultError);
|
||||
});
|
||||
|
||||
it('should throw VaultError on 403 Forbidden', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
json: async () => ({ errors: ['permission denied'] }),
|
||||
});
|
||||
|
||||
await expect(client.getCredentials('linode')).rejects.toThrow(VaultError);
|
||||
});
|
||||
|
||||
it('should throw VaultError on 404 Not Found', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
json: async () => ({}),
|
||||
});
|
||||
|
||||
await expect(client.getCredentials('unknown')).rejects.toThrow(VaultError);
|
||||
});
|
||||
|
||||
it('should throw VaultError on 500 Server Error', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
json: async () => ({ errors: ['internal server error'] }),
|
||||
});
|
||||
|
||||
await expect(client.getCredentials('linode')).rejects.toThrow(VaultError);
|
||||
});
|
||||
|
||||
it('should throw VaultError on timeout', async () => {
|
||||
// Mock fetch to simulate AbortError
|
||||
mockFetch.mockImplementation(() => {
|
||||
const error = new Error('This operation was aborted');
|
||||
error.name = 'AbortError';
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
await expect(client.getCredentials('linode')).rejects.toThrow(VaultError);
|
||||
});
|
||||
|
||||
it('should throw VaultError on invalid response structure', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ invalid: 'structure' }),
|
||||
});
|
||||
|
||||
await expect(client.getCredentials('linode')).rejects.toThrow(VaultError);
|
||||
});
|
||||
|
||||
it('should throw VaultError on missing required fields', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
data: {
|
||||
data: {
|
||||
provider: 'linode',
|
||||
api_token: '', // Empty token
|
||||
},
|
||||
metadata: mockSuccessResponse.data.metadata,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(client.getCredentials('linode')).rejects.toThrow(VaultError);
|
||||
});
|
||||
|
||||
it('should handle network errors gracefully', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(client.getCredentials('linode')).rejects.toThrow(VaultError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache management', () => {
|
||||
it('should clear cache for specific provider', async () => {
|
||||
const mockResponse: VaultSecretResponse = {
|
||||
data: {
|
||||
data: { provider: 'linode', api_token: 'token1' },
|
||||
metadata: {
|
||||
created_time: '2024-01-21T10:00:00Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => mockResponse,
|
||||
});
|
||||
|
||||
// Fetch and cache
|
||||
await client.getCredentials('linode');
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Clear cache
|
||||
client.clearCache('linode');
|
||||
|
||||
// Should fetch again
|
||||
await client.getCredentials('linode');
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should clear all cache', async () => {
|
||||
const mockLinode: VaultSecretResponse = {
|
||||
data: {
|
||||
data: { provider: 'linode', api_token: 'token1' },
|
||||
metadata: {
|
||||
created_time: '2024-01-21T10:00:00Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockVultr: VaultSecretResponse = {
|
||||
data: {
|
||||
data: { provider: 'vultr', api_token: 'token2' },
|
||||
metadata: {
|
||||
created_time: '2024-01-21T10:00:00Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => mockLinode,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => mockVultr,
|
||||
});
|
||||
|
||||
// Cache both providers
|
||||
await client.getCredentials('linode');
|
||||
await client.getCredentials('vultr');
|
||||
|
||||
const statsBefore = client.getCacheStats();
|
||||
expect(statsBefore.size).toBe(2);
|
||||
expect(statsBefore.providers).toContain('linode');
|
||||
expect(statsBefore.providers).toContain('vultr');
|
||||
|
||||
// Clear all cache
|
||||
client.clearCache();
|
||||
|
||||
const statsAfter = client.getCacheStats();
|
||||
expect(statsAfter.size).toBe(0);
|
||||
expect(statsAfter.providers).toEqual([]);
|
||||
});
|
||||
|
||||
it('should provide cache statistics', async () => {
|
||||
const mockResponse: VaultSecretResponse = {
|
||||
data: {
|
||||
data: { provider: 'linode', api_token: 'token1' },
|
||||
metadata: {
|
||||
created_time: '2024-01-21T10:00:00Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => mockResponse,
|
||||
});
|
||||
|
||||
// Initially empty
|
||||
let stats = client.getCacheStats();
|
||||
expect(stats.size).toBe(0);
|
||||
expect(stats.providers).toEqual([]);
|
||||
|
||||
// After caching
|
||||
await client.getCredentials('linode');
|
||||
stats = client.getCacheStats();
|
||||
expect(stats.size).toBe(1);
|
||||
expect(stats.providers).toEqual(['linode']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('VaultError', () => {
|
||||
it('should create error with all properties', () => {
|
||||
const error = new VaultError('Test error', 404, 'linode');
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.name).toBe('VaultError');
|
||||
expect(error.message).toBe('Test error');
|
||||
expect(error.statusCode).toBe(404);
|
||||
expect(error.provider).toBe('linode');
|
||||
});
|
||||
|
||||
it('should create error without optional properties', () => {
|
||||
const error = new VaultError('Test error');
|
||||
|
||||
expect(error.message).toBe('Test error');
|
||||
expect(error.statusCode).toBeUndefined();
|
||||
expect(error.provider).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user