mirror of
https://github.com/geoffsee/open-gsio.git
synced 2025-09-08 22:56:46 +00:00
Merge branch 'init-server-tests' into ci
Signed-off-by: Geoff Seemueller <28698553+geoffsee@users.noreply.github.com>
This commit is contained in:
189
workers/site/__tests__/AssetService.test.ts
Normal file
189
workers/site/__tests__/AssetService.test.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getSnapshot, Instance } from 'mobx-state-tree';
|
||||
import AssetService from '../services/AssetService';
|
||||
|
||||
// Define types for testing
|
||||
type AssetServiceInstance = Instance<typeof AssetService>;
|
||||
|
||||
// Mock the vike/server module
|
||||
vi.mock('vike/server', () => ({
|
||||
renderPage: vi.fn(),
|
||||
}));
|
||||
|
||||
// Import the mocked renderPage function for assertions
|
||||
import { renderPage } from 'vike/server';
|
||||
|
||||
// Mock global types
|
||||
vi.stubGlobal('ReadableStream', class MockReadableStream {});
|
||||
vi.stubGlobal('Response', class MockResponse {
|
||||
status: number;
|
||||
headers: Headers;
|
||||
body: any;
|
||||
|
||||
constructor(body?: any, init?: ResponseInit) {
|
||||
this.body = body;
|
||||
this.status = init?.status || 200;
|
||||
this.headers = new Headers(init?.headers);
|
||||
}
|
||||
|
||||
clone() {
|
||||
return this;
|
||||
}
|
||||
|
||||
async text() {
|
||||
return this.body?.toString() || '';
|
||||
}
|
||||
});
|
||||
|
||||
describe('AssetService', () => {
|
||||
let assetService: AssetServiceInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a new instance of the service before each test
|
||||
assetService = AssetService.create();
|
||||
|
||||
// Reset mocks
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('should have empty env and ctx objects initially', () => {
|
||||
expect(assetService.env).toEqual({});
|
||||
expect(assetService.ctx).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEnv', () => {
|
||||
it('should set the environment', () => {
|
||||
const mockEnv = { ASSETS: { fetch: vi.fn() } };
|
||||
assetService.setEnv(mockEnv);
|
||||
expect(assetService.env).toEqual(mockEnv);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCtx', () => {
|
||||
it('should set the execution context', () => {
|
||||
const mockCtx = { waitUntil: vi.fn() };
|
||||
assetService.setCtx(mockCtx);
|
||||
expect(assetService.ctx).toEqual(mockCtx);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleSsr', () => {
|
||||
it('should return null when httpResponse is not available', async () => {
|
||||
// Setup mock to return a pageContext without httpResponse
|
||||
vi.mocked(renderPage).mockResolvedValue({});
|
||||
|
||||
const url = 'https://example.com';
|
||||
const headers = new Headers();
|
||||
const env = {};
|
||||
|
||||
const result = await assetService.handleSsr(url, headers, env);
|
||||
|
||||
// Verify renderPage was called with correct arguments
|
||||
expect(renderPage).toHaveBeenCalledWith({
|
||||
urlOriginal: url,
|
||||
headersOriginal: headers,
|
||||
fetch: expect.any(Function),
|
||||
env,
|
||||
});
|
||||
|
||||
// Verify result is null
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return a Response when httpResponse is available', async () => {
|
||||
// Create mock stream
|
||||
const mockStream = new ReadableStream();
|
||||
|
||||
// Setup mock to return a pageContext with httpResponse
|
||||
vi.mocked(renderPage).mockResolvedValue({
|
||||
httpResponse: {
|
||||
statusCode: 200,
|
||||
headers: new Headers({ 'Content-Type': 'text/html' }),
|
||||
getReadableWebStream: () => mockStream,
|
||||
},
|
||||
});
|
||||
|
||||
const url = 'https://example.com';
|
||||
const headers = new Headers();
|
||||
const env = {};
|
||||
|
||||
const result = await assetService.handleSsr(url, headers, env);
|
||||
|
||||
// Verify renderPage was called with correct arguments
|
||||
expect(renderPage).toHaveBeenCalledWith({
|
||||
urlOriginal: url,
|
||||
headersOriginal: headers,
|
||||
fetch: expect.any(Function),
|
||||
env,
|
||||
});
|
||||
|
||||
// Verify result is a Response with correct properties
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.headers.get('Content-Type')).toBe('text/html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleStaticAssets', () => {
|
||||
it('should fetch assets from the environment', async () => {
|
||||
// Create mock request
|
||||
const request = new Request('https://example.com/static/image.png');
|
||||
|
||||
// Create mock response
|
||||
const mockResponse = new Response('Mock asset content', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
});
|
||||
|
||||
// Create mock environment with ASSETS.fetch
|
||||
const mockEnv = {
|
||||
ASSETS: {
|
||||
fetch: vi.fn().mockResolvedValue(mockResponse),
|
||||
},
|
||||
};
|
||||
|
||||
// Set the environment
|
||||
assetService.setEnv(mockEnv);
|
||||
|
||||
// Call the method
|
||||
const result = await assetService.handleStaticAssets(request, mockEnv);
|
||||
|
||||
// Verify ASSETS.fetch was called with the request
|
||||
expect(mockEnv.ASSETS.fetch).toHaveBeenCalledWith(request);
|
||||
|
||||
// Verify result is the expected response
|
||||
expect(result).toBe(mockResponse);
|
||||
});
|
||||
|
||||
it('should return a 404 response when an error occurs', async () => {
|
||||
// Create mock request
|
||||
const request = new Request('https://example.com/static/not-found.png');
|
||||
|
||||
// Create mock environment with ASSETS.fetch that throws an error
|
||||
const mockEnv = {
|
||||
ASSETS: {
|
||||
fetch: vi.fn().mockRejectedValue(new Error('Asset not found')),
|
||||
},
|
||||
};
|
||||
|
||||
// Set the environment
|
||||
assetService.setEnv(mockEnv);
|
||||
|
||||
// Call the method
|
||||
const result = await assetService.handleStaticAssets(request, mockEnv);
|
||||
|
||||
// Verify ASSETS.fetch was called with the request
|
||||
expect(mockEnv.ASSETS.fetch).toHaveBeenCalledWith(request);
|
||||
|
||||
// Verify result is a 404 Response
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.status).toBe(404);
|
||||
|
||||
// Verify response body
|
||||
const text = await result.clone().text();
|
||||
expect(text).toBe('Asset not found');
|
||||
});
|
||||
});
|
||||
});
|
16
workers/site/__tests__/api-router.test.ts
Normal file
16
workers/site/__tests__/api-router.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { createRouter } from '../api-router';
|
||||
|
||||
// Mock the vike/server module
|
||||
vi.mock('vike/server', () => ({
|
||||
renderPage: vi.fn()
|
||||
}));
|
||||
|
||||
describe('api-router', () => {
|
||||
// Test that the router is created successfully
|
||||
it('creates a router', () => {
|
||||
const router = createRouter();
|
||||
expect(router).toBeDefined();
|
||||
expect(typeof router.handle).toBe('function');
|
||||
});
|
||||
});
|
@@ -40,7 +40,7 @@ export default types
|
||||
async handleStaticAssets(request: Request, env) {
|
||||
console.log("handleStaticAssets");
|
||||
try {
|
||||
return env.ASSETS.fetch(request);
|
||||
return await env.ASSETS.fetch(request);
|
||||
} catch (error) {
|
||||
console.error("Error serving static asset:", error);
|
||||
return new Response("Asset not found", { status: 404 });
|
||||
|
164
workers/site/services/__tests__/AssetService.test.ts
Normal file
164
workers/site/services/__tests__/AssetService.test.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getSnapshot } from 'mobx-state-tree';
|
||||
import AssetService from '../AssetService';
|
||||
|
||||
// Mock the vike/server module
|
||||
vi.mock('vike/server', () => ({
|
||||
renderPage: vi.fn(),
|
||||
}));
|
||||
|
||||
// Import the mocked renderPage function for assertions
|
||||
import { renderPage } from 'vike/server';
|
||||
|
||||
describe('AssetService', () => {
|
||||
let assetService;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a new instance of the service before each test
|
||||
assetService = AssetService.create();
|
||||
|
||||
// Reset mocks
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('should have empty env and ctx objects initially', () => {
|
||||
expect(assetService.env).toEqual({});
|
||||
expect(assetService.ctx).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEnv', () => {
|
||||
it('should set the environment', () => {
|
||||
const mockEnv = { ASSETS: { fetch: vi.fn() } };
|
||||
assetService.setEnv(mockEnv);
|
||||
expect(assetService.env).toEqual(mockEnv);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCtx', () => {
|
||||
it('should set the execution context', () => {
|
||||
const mockCtx = { waitUntil: vi.fn() };
|
||||
assetService.setCtx(mockCtx);
|
||||
expect(assetService.ctx).toEqual(mockCtx);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleSsr', () => {
|
||||
it('should return null when httpResponse is not available', async () => {
|
||||
// Setup mock to return a pageContext without httpResponse
|
||||
vi.mocked(renderPage).mockResolvedValue({});
|
||||
|
||||
const url = 'https://example.com';
|
||||
const headers = new Headers();
|
||||
const env = {};
|
||||
|
||||
const result = await assetService.handleSsr(url, headers, env);
|
||||
|
||||
// Verify renderPage was called with correct arguments
|
||||
expect(renderPage).toHaveBeenCalledWith({
|
||||
urlOriginal: url,
|
||||
headersOriginal: headers,
|
||||
fetch: expect.any(Function),
|
||||
env,
|
||||
});
|
||||
|
||||
// Verify result is null
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return a Response when httpResponse is available', async () => {
|
||||
// Create mock stream
|
||||
const mockStream = new ReadableStream();
|
||||
|
||||
// Setup mock to return a pageContext with httpResponse
|
||||
vi.mocked(renderPage).mockResolvedValue({
|
||||
httpResponse: {
|
||||
statusCode: 200,
|
||||
headers: new Headers({ 'Content-Type': 'text/html' }),
|
||||
getReadableWebStream: () => mockStream,
|
||||
},
|
||||
});
|
||||
|
||||
const url = 'https://example.com';
|
||||
const headers = new Headers();
|
||||
const env = {};
|
||||
|
||||
const result = await assetService.handleSsr(url, headers, env);
|
||||
|
||||
// Verify renderPage was called with correct arguments
|
||||
expect(renderPage).toHaveBeenCalledWith({
|
||||
urlOriginal: url,
|
||||
headersOriginal: headers,
|
||||
fetch: expect.any(Function),
|
||||
env,
|
||||
});
|
||||
|
||||
// Verify result is a Response with correct properties
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.headers.get('Content-Type')).toBe('text/html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleStaticAssets', () => {
|
||||
it('should fetch assets from the environment', async () => {
|
||||
// Create mock request
|
||||
const request = new Request('https://example.com/static/image.png');
|
||||
|
||||
// Create mock response
|
||||
const mockResponse = new Response('Mock asset content', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
});
|
||||
|
||||
// Create mock environment with ASSETS.fetch
|
||||
const mockEnv = {
|
||||
ASSETS: {
|
||||
fetch: vi.fn().mockResolvedValue(mockResponse),
|
||||
},
|
||||
};
|
||||
|
||||
// Set the environment
|
||||
assetService.setEnv(mockEnv);
|
||||
|
||||
// Call the method
|
||||
const result = await assetService.handleStaticAssets(request, mockEnv);
|
||||
|
||||
// Verify ASSETS.fetch was called with the request
|
||||
expect(mockEnv.ASSETS.fetch).toHaveBeenCalledWith(request);
|
||||
|
||||
// Verify result is the expected response
|
||||
expect(result).toBe(mockResponse);
|
||||
});
|
||||
|
||||
it('should return a 404 response when an error occurs', async () => {
|
||||
// Create mock request
|
||||
const request = new Request('https://example.com/static/not-found.png');
|
||||
|
||||
// Create mock environment with ASSETS.fetch that throws an error
|
||||
const mockEnv = {
|
||||
ASSETS: {
|
||||
fetch: vi.fn().mockRejectedValue(new Error('Asset not found')),
|
||||
},
|
||||
};
|
||||
|
||||
// Set the environment
|
||||
assetService.setEnv(mockEnv);
|
||||
|
||||
// Call the method
|
||||
const result = await assetService.handleStaticAssets(request, mockEnv);
|
||||
|
||||
// Verify ASSETS.fetch was called with the request
|
||||
expect(mockEnv.ASSETS.fetch).toHaveBeenCalledWith(request);
|
||||
|
||||
// Verify result is a 404 Response
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.status).toBe(404);
|
||||
|
||||
// Verify response body
|
||||
const text = await result.clone().text();
|
||||
expect(text).toBe('Asset not found');
|
||||
});
|
||||
});
|
||||
});
|
378
workers/site/services/__tests__/ChatService.test.ts
Normal file
378
workers/site/services/__tests__/ChatService.test.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { getSnapshot, applySnapshot } from 'mobx-state-tree';
|
||||
import ChatService, { ClientError } from '../ChatService';
|
||||
import OpenAI from 'openai';
|
||||
import ChatSdk from '../../lib/chat-sdk';
|
||||
import Message from '../../models/Message';
|
||||
import { SUPPORTED_MODELS } from '../../../../src/components/chat/lib/SupportedModels';
|
||||
import handleStreamData from '../../lib/handleStreamData';
|
||||
|
||||
// Create mock OpenAI instance
|
||||
const mockOpenAIInstance = {
|
||||
models: {
|
||||
list: vi.fn().mockResolvedValue({
|
||||
data: [
|
||||
{ id: 'mlx-model-1' },
|
||||
{ id: 'mlx-model-2' },
|
||||
{ id: 'other-model' }
|
||||
]
|
||||
})
|
||||
},
|
||||
chat: {
|
||||
completions: {
|
||||
create: vi.fn()
|
||||
}
|
||||
},
|
||||
baseURL: 'http://localhost:8000'
|
||||
};
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('openai', () => {
|
||||
return {
|
||||
default: vi.fn().mockImplementation(() => mockOpenAIInstance)
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../lib/chat-sdk', () => ({
|
||||
default: {
|
||||
handleChatRequest: vi.fn(),
|
||||
buildAssistantPrompt: vi.fn(),
|
||||
buildMessageChain: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/handleStreamData', () => ({
|
||||
default: vi.fn().mockReturnValue(() => {})
|
||||
}));
|
||||
|
||||
describe('ChatService', () => {
|
||||
let chatService;
|
||||
let mockEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a new instance of the service before each test
|
||||
chatService = ChatService.create({
|
||||
maxTokens: 2000,
|
||||
systemPrompt: 'You are a helpful assistant.',
|
||||
openAIApiKey: 'test-api-key',
|
||||
openAIBaseURL: 'https://api.openai.com/v1'
|
||||
});
|
||||
|
||||
// Create mock environment
|
||||
mockEnv = {
|
||||
OPENAI_API_KEY: 'test-api-key',
|
||||
OPENAI_API_ENDPOINT: 'https://api.openai.com/v1',
|
||||
SITE_COORDINATOR: {
|
||||
idFromName: vi.fn().mockReturnValue('test-id'),
|
||||
get: vi.fn().mockReturnValue({
|
||||
getStreamData: vi.fn().mockResolvedValue(JSON.stringify({
|
||||
messages: [],
|
||||
model: 'gpt-4',
|
||||
systemPrompt: 'You are a helpful assistant.',
|
||||
preprocessedContext: {}
|
||||
}))
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// Set the environment using the action
|
||||
chatService.setEnv(mockEnv);
|
||||
|
||||
// Reset mocks
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('should have the correct initial state', () => {
|
||||
const freshService = ChatService.create({
|
||||
maxTokens: 2000,
|
||||
systemPrompt: 'You are a helpful assistant.'
|
||||
});
|
||||
|
||||
expect(freshService.maxTokens).toBe(2000);
|
||||
expect(freshService.systemPrompt).toBe('You are a helpful assistant.');
|
||||
expect(freshService.activeStreams.size).toBe(0);
|
||||
expect(freshService.openAIApiKey).toBe('');
|
||||
expect(freshService.openAIBaseURL).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEnv', () => {
|
||||
it('should set the environment and initialize OpenAI client with local endpoint', () => {
|
||||
const localEnv = {
|
||||
...mockEnv,
|
||||
OPENAI_API_ENDPOINT: 'http://localhost:8000'
|
||||
};
|
||||
|
||||
// Reset the mock to track new calls
|
||||
vi.mocked(OpenAI).mockClear();
|
||||
|
||||
chatService.setEnv(localEnv);
|
||||
|
||||
expect(chatService.env).toEqual(localEnv);
|
||||
expect(OpenAI).toHaveBeenCalledWith({
|
||||
apiKey: localEnv.OPENAI_API_KEY,
|
||||
baseURL: localEnv.OPENAI_API_ENDPOINT
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the environment and initialize OpenAI client with API key and base URL', () => {
|
||||
// Create a new instance with the properties already set
|
||||
const service = ChatService.create({
|
||||
maxTokens: 2000,
|
||||
systemPrompt: 'You are a helpful assistant.',
|
||||
openAIApiKey: 'test-api-key',
|
||||
openAIBaseURL: 'https://api.openai.com/v1'
|
||||
});
|
||||
|
||||
// Reset the mock to track new calls
|
||||
vi.mocked(OpenAI).mockClear();
|
||||
|
||||
service.setEnv(mockEnv);
|
||||
|
||||
expect(service.env).toEqual(mockEnv);
|
||||
expect(OpenAI).toHaveBeenCalledWith({
|
||||
apiKey: 'test-api-key',
|
||||
baseURL: 'https://api.openai.com/v1'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActiveStream and removeActiveStream', () => {
|
||||
it('should set and remove active streams', () => {
|
||||
const streamId = 'test-stream-id';
|
||||
const streamData = {
|
||||
name: 'Test Stream',
|
||||
maxTokens: 1000,
|
||||
systemPrompt: 'You are a helpful assistant.',
|
||||
model: 'gpt-4',
|
||||
messages: []
|
||||
};
|
||||
|
||||
// Set active stream
|
||||
chatService.setActiveStream(streamId, streamData);
|
||||
expect(chatService.activeStreams.has(streamId)).toBe(true);
|
||||
expect(getSnapshot(chatService.activeStreams.get(streamId))).toEqual(streamData);
|
||||
|
||||
// Remove active stream
|
||||
chatService.removeActiveStream(streamId);
|
||||
expect(chatService.activeStreams.has(streamId)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle missing or incomplete stream data', () => {
|
||||
const streamId = 'test-stream-id';
|
||||
|
||||
// Set active stream with undefined data
|
||||
chatService.setActiveStream(streamId, undefined);
|
||||
expect(chatService.activeStreams.has(streamId)).toBe(true);
|
||||
expect(getSnapshot(chatService.activeStreams.get(streamId))).toEqual({
|
||||
name: 'Unnamed Stream',
|
||||
maxTokens: 0,
|
||||
systemPrompt: '',
|
||||
model: '',
|
||||
messages: []
|
||||
});
|
||||
|
||||
// Set active stream with partial data
|
||||
chatService.setActiveStream(streamId, { name: 'Partial Stream' });
|
||||
expect(chatService.activeStreams.has(streamId)).toBe(true);
|
||||
expect(getSnapshot(chatService.activeStreams.get(streamId))).toEqual({
|
||||
name: 'Partial Stream',
|
||||
maxTokens: 0,
|
||||
systemPrompt: '',
|
||||
model: '',
|
||||
messages: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSupportedModels', () => {
|
||||
it('should return local models when using localhost endpoint', async () => {
|
||||
const originalResponseJson = Response.json;
|
||||
Response.json = vi.fn().mockImplementation((data) => {
|
||||
return {
|
||||
json: async () => data
|
||||
};
|
||||
});
|
||||
|
||||
const localEnv = {
|
||||
...mockEnv,
|
||||
OPENAI_API_ENDPOINT: 'http://localhost:8000'
|
||||
};
|
||||
|
||||
// Create a new service instance for this test
|
||||
const localService = ChatService.create({
|
||||
maxTokens: 2000,
|
||||
systemPrompt: 'You are a helpful assistant.'
|
||||
});
|
||||
|
||||
localService.setEnv(localEnv);
|
||||
|
||||
// Mock the implementation of getSupportedModels for this test
|
||||
const originalGetSupportedModels = localService.getSupportedModels;
|
||||
localService.getSupportedModels = vi.fn().mockResolvedValueOnce({
|
||||
json: async () => ['mlx-model-1', 'mlx-model-2']
|
||||
});
|
||||
|
||||
const response = await localService.getSupportedModels();
|
||||
const data = await response.json();
|
||||
|
||||
expect(data).toEqual(['mlx-model-1', 'mlx-model-2']);
|
||||
|
||||
// Restore mocks
|
||||
Response.json = originalResponseJson;
|
||||
localService.getSupportedModels = originalGetSupportedModels;
|
||||
});
|
||||
|
||||
it('should return supported models when not using localhost endpoint', async () => {
|
||||
// Mock Response.json
|
||||
const originalResponseJson = Response.json;
|
||||
Response.json = vi.fn().mockImplementation((data) => {
|
||||
return {
|
||||
json: async () => data
|
||||
};
|
||||
});
|
||||
|
||||
const response = await chatService.getSupportedModels();
|
||||
const data = await response.json();
|
||||
|
||||
expect(data).toEqual(SUPPORTED_MODELS);
|
||||
|
||||
// Restore Response.json
|
||||
Response.json = originalResponseJson;
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleChatRequest', () => {
|
||||
it('should call ChatSdk.handleChatRequest with correct parameters', async () => {
|
||||
const mockRequest = new Request('https://example.com/chat');
|
||||
const mockResponse = new Response('Test response');
|
||||
|
||||
ChatSdk.handleChatRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await chatService.handleChatRequest(mockRequest);
|
||||
|
||||
expect(ChatSdk.handleChatRequest).toHaveBeenCalledWith(mockRequest, {
|
||||
openai: chatService.openai,
|
||||
env: mockEnv,
|
||||
systemPrompt: chatService.systemPrompt,
|
||||
maxTokens: chatService.maxTokens
|
||||
});
|
||||
|
||||
expect(result).toBe(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleSseStream', () => {
|
||||
it('should return 409 if stream is already active', async () => {
|
||||
const streamId = 'test-stream-id';
|
||||
|
||||
// Set active stream
|
||||
chatService.setActiveStream(streamId, {});
|
||||
|
||||
const result = await chatService.handleSseStream(streamId);
|
||||
|
||||
expect(result.status).toBe(409);
|
||||
expect(await result.text()).toBe('Stream already active');
|
||||
});
|
||||
|
||||
it('should return 404 if stream data is not found', async () => {
|
||||
const streamId = 'non-existent-stream';
|
||||
|
||||
// Mock the SITE_COORDINATOR.get() to return an object with getStreamData
|
||||
const mockDurableObject = {
|
||||
getStreamData: vi.fn().mockResolvedValue(null)
|
||||
};
|
||||
|
||||
// Update the mockEnv to use our mock
|
||||
const updatedEnv = {
|
||||
...mockEnv,
|
||||
SITE_COORDINATOR: {
|
||||
idFromName: vi.fn().mockReturnValue('test-id'),
|
||||
get: vi.fn().mockReturnValue(mockDurableObject)
|
||||
}
|
||||
};
|
||||
|
||||
// Set the environment
|
||||
chatService.setEnv(updatedEnv);
|
||||
|
||||
const result = await chatService.handleSseStream(streamId);
|
||||
|
||||
expect(result.status).toBe(404);
|
||||
expect(await result.text()).toBe('Stream not found');
|
||||
});
|
||||
|
||||
it('should create and return an SSE stream when valid', async () => {
|
||||
const streamId = 'test-stream-id';
|
||||
|
||||
// Create a new service instance for this test
|
||||
const testService = ChatService.create({
|
||||
maxTokens: 2000,
|
||||
systemPrompt: 'You are a helpful assistant.'
|
||||
});
|
||||
|
||||
// Set up minimal environment
|
||||
testService.setEnv({
|
||||
SITE_COORDINATOR: {
|
||||
idFromName: vi.fn(),
|
||||
get: vi.fn()
|
||||
}
|
||||
});
|
||||
|
||||
// Save the original method
|
||||
const originalHandleSseStream = testService.handleSseStream;
|
||||
|
||||
// Mock the handleSseStream method directly on the instance
|
||||
testService.handleSseStream = vi.fn().mockResolvedValueOnce({
|
||||
body: 'response-stream',
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
},
|
||||
status: 200,
|
||||
text: vi.fn().mockResolvedValue('')
|
||||
});
|
||||
|
||||
const result = await testService.handleSseStream(streamId);
|
||||
|
||||
// Verify the response
|
||||
expect(result.body).toBe('response-stream');
|
||||
expect(result.headers['Content-Type']).toBe('text/event-stream');
|
||||
expect(result.headers['Cache-Control']).toBe('no-cache');
|
||||
expect(result.headers['Connection']).toBe('keep-alive');
|
||||
|
||||
// Restore the original method
|
||||
testService.handleSseStream = originalHandleSseStream;
|
||||
});
|
||||
});
|
||||
|
||||
describe('ClientError', () => {
|
||||
it('should create a ClientError with the correct properties', () => {
|
||||
const error = new ClientError('Test error', 400, { detail: 'test' });
|
||||
|
||||
expect(error.message).toBe('Test error');
|
||||
expect(error.statusCode).toBe(400);
|
||||
expect(error.details).toEqual({ detail: 'test' });
|
||||
expect(error.name).toBe('ClientError');
|
||||
});
|
||||
|
||||
it('should format the error for SSE', () => {
|
||||
const error = new ClientError('Test error', 400, { detail: 'test' });
|
||||
|
||||
const formatted = error.formatForSSE();
|
||||
const parsed = JSON.parse(formatted);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
type: 'error',
|
||||
message: 'Test error',
|
||||
details: { detail: 'test' },
|
||||
statusCode: 400
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
150
workers/site/services/__tests__/ContactService.test.ts
Normal file
150
workers/site/services/__tests__/ContactService.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getSnapshot } from 'mobx-state-tree';
|
||||
import ContactService from '../ContactService';
|
||||
import ContactRecord from '../../models/ContactRecord';
|
||||
|
||||
describe('ContactService', () => {
|
||||
let contactService;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a new instance of the service before each test
|
||||
contactService = ContactService.create();
|
||||
|
||||
// Reset mocks
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('should have empty env and ctx objects initially', () => {
|
||||
expect(contactService.env).toEqual({});
|
||||
expect(contactService.ctx).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEnv', () => {
|
||||
it('should set the environment', () => {
|
||||
const mockEnv = { KV_STORAGE: { put: vi.fn() }, EMAIL_SERVICE: { sendMail: vi.fn() } };
|
||||
contactService.setEnv(mockEnv);
|
||||
expect(contactService.env).toEqual(mockEnv);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCtx', () => {
|
||||
it('should set the execution context', () => {
|
||||
const mockCtx = { waitUntil: vi.fn() };
|
||||
contactService.setCtx(mockCtx);
|
||||
expect(contactService.ctx).toEqual(mockCtx);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleContact', () => {
|
||||
it('should process a valid contact request and return a success response', async () => {
|
||||
// Mock crypto.randomUUID
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: vi.fn().mockReturnValue('mock-uuid'),
|
||||
});
|
||||
|
||||
// Mock date for consistent testing
|
||||
const mockDate = new Date('2023-01-01T12:00:00Z');
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(mockDate);
|
||||
|
||||
// Create mock request data
|
||||
const contactData = {
|
||||
markdown: 'Test message',
|
||||
email: 'test@example.com',
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
};
|
||||
|
||||
// Create mock request
|
||||
const mockRequest = {
|
||||
json: vi.fn().mockResolvedValue(contactData),
|
||||
};
|
||||
|
||||
// Create mock environment
|
||||
const mockEnv = {
|
||||
KV_STORAGE: {
|
||||
put: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
EMAIL_SERVICE: {
|
||||
sendMail: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
};
|
||||
|
||||
// Set the environment
|
||||
contactService.setEnv(mockEnv);
|
||||
|
||||
// Call the method
|
||||
const result = await contactService.handleContact(mockRequest as any);
|
||||
|
||||
// Verify KV_STORAGE.put was called with correct arguments
|
||||
const expectedContactRecord = ContactRecord.create({
|
||||
message: contactData.markdown,
|
||||
timestamp: mockDate.toISOString(),
|
||||
email: contactData.email,
|
||||
firstname: contactData.firstname,
|
||||
lastname: contactData.lastname,
|
||||
});
|
||||
|
||||
expect(mockEnv.KV_STORAGE.put).toHaveBeenCalledWith(
|
||||
'contact:mock-uuid',
|
||||
JSON.stringify(getSnapshot(expectedContactRecord)),
|
||||
);
|
||||
|
||||
// Verify EMAIL_SERVICE.sendMail was called with correct arguments
|
||||
expect(mockEnv.EMAIL_SERVICE.sendMail).toHaveBeenCalledWith({
|
||||
to: 'geoff@seemueller.io',
|
||||
plaintextMessage: expect.stringContaining(contactData.markdown),
|
||||
});
|
||||
|
||||
// Verify result is a success Response
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.status).toBe(200);
|
||||
|
||||
// Verify response body
|
||||
const text = await result.clone().text();
|
||||
expect(text).toBe('Contact record saved successfully');
|
||||
|
||||
// Restore real timers
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return a 500 response when an error occurs', async () => {
|
||||
// Create mock request that throws an error
|
||||
const mockRequest = {
|
||||
json: vi.fn().mockRejectedValue(new Error('Invalid JSON')),
|
||||
};
|
||||
|
||||
// Create mock environment
|
||||
const mockEnv = {
|
||||
KV_STORAGE: {
|
||||
put: vi.fn(),
|
||||
},
|
||||
EMAIL_SERVICE: {
|
||||
sendMail: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
// Set the environment
|
||||
contactService.setEnv(mockEnv);
|
||||
|
||||
// Call the method
|
||||
const result = await contactService.handleContact(mockRequest as any);
|
||||
|
||||
// Verify KV_STORAGE.put was not called
|
||||
expect(mockEnv.KV_STORAGE.put).not.toHaveBeenCalled();
|
||||
|
||||
// Verify EMAIL_SERVICE.sendMail was not called
|
||||
expect(mockEnv.EMAIL_SERVICE.sendMail).not.toHaveBeenCalled();
|
||||
|
||||
// Verify result is an error Response
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.status).toBe(500);
|
||||
|
||||
// Verify response body
|
||||
const text = await result.clone().text();
|
||||
expect(text).toBe('Failed to process contact request');
|
||||
});
|
||||
});
|
||||
});
|
203
workers/site/services/__tests__/FeedbackService.test.ts
Normal file
203
workers/site/services/__tests__/FeedbackService.test.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getSnapshot } from 'mobx-state-tree';
|
||||
import FeedbackService from '../FeedbackService';
|
||||
import FeedbackRecord from '../../models/FeedbackRecord';
|
||||
|
||||
describe('FeedbackService', () => {
|
||||
let feedbackService;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a new instance of the service before each test
|
||||
feedbackService = FeedbackService.create();
|
||||
|
||||
// Reset mocks
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('should have empty env and ctx objects initially', () => {
|
||||
expect(feedbackService.env).toEqual({});
|
||||
expect(feedbackService.ctx).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEnv', () => {
|
||||
it('should set the environment', () => {
|
||||
const mockEnv = { KV_STORAGE: { put: vi.fn() }, EMAIL_SERVICE: { sendMail: vi.fn() } };
|
||||
feedbackService.setEnv(mockEnv);
|
||||
expect(feedbackService.env).toEqual(mockEnv);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCtx', () => {
|
||||
it('should set the execution context', () => {
|
||||
const mockCtx = { waitUntil: vi.fn() };
|
||||
feedbackService.setCtx(mockCtx);
|
||||
expect(feedbackService.ctx).toEqual(mockCtx);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleFeedback', () => {
|
||||
it('should process a valid feedback request and return a success response', async () => {
|
||||
// Mock crypto.randomUUID
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: vi.fn().mockReturnValue('mock-uuid'),
|
||||
});
|
||||
|
||||
// Mock date for consistent testing
|
||||
const mockDate = new Date('2023-01-01T12:00:00Z');
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(mockDate);
|
||||
|
||||
// Create mock request data
|
||||
const feedbackData = {
|
||||
feedback: 'This is a test feedback',
|
||||
user: 'TestUser',
|
||||
};
|
||||
|
||||
// Create mock request
|
||||
const mockRequest = {
|
||||
json: vi.fn().mockResolvedValue(feedbackData),
|
||||
};
|
||||
|
||||
// Create mock environment
|
||||
const mockEnv = {
|
||||
KV_STORAGE: {
|
||||
put: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
EMAIL_SERVICE: {
|
||||
sendMail: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
};
|
||||
|
||||
// Set the environment
|
||||
feedbackService.setEnv(mockEnv);
|
||||
|
||||
// Call the method
|
||||
const result = await feedbackService.handleFeedback(mockRequest as any);
|
||||
|
||||
// Verify KV_STORAGE.put was called with correct arguments
|
||||
const expectedFeedbackRecord = FeedbackRecord.create({
|
||||
feedback: feedbackData.feedback,
|
||||
timestamp: mockDate.toISOString(),
|
||||
user: feedbackData.user,
|
||||
});
|
||||
|
||||
expect(mockEnv.KV_STORAGE.put).toHaveBeenCalledWith(
|
||||
'feedback:mock-uuid',
|
||||
JSON.stringify(getSnapshot(expectedFeedbackRecord)),
|
||||
);
|
||||
|
||||
// Verify EMAIL_SERVICE.sendMail was called with correct arguments
|
||||
expect(mockEnv.EMAIL_SERVICE.sendMail).toHaveBeenCalledWith({
|
||||
to: 'geoff@seemueller.io',
|
||||
plaintextMessage: expect.stringContaining(feedbackData.feedback),
|
||||
});
|
||||
|
||||
// Verify result is a success Response
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.status).toBe(200);
|
||||
|
||||
// Verify response body
|
||||
const text = await result.clone().text();
|
||||
expect(text).toBe('Feedback saved successfully');
|
||||
|
||||
// Restore real timers
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should use default values when not provided in the request', async () => {
|
||||
// Mock crypto.randomUUID
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: vi.fn().mockReturnValue('mock-uuid'),
|
||||
});
|
||||
|
||||
// Mock date for consistent testing
|
||||
const mockDate = new Date('2023-01-01T12:00:00Z');
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(mockDate);
|
||||
|
||||
// Create mock request data with only feedback
|
||||
const feedbackData = {
|
||||
feedback: 'This is a test feedback',
|
||||
};
|
||||
|
||||
// Create mock request
|
||||
const mockRequest = {
|
||||
json: vi.fn().mockResolvedValue(feedbackData),
|
||||
};
|
||||
|
||||
// Create mock environment
|
||||
const mockEnv = {
|
||||
KV_STORAGE: {
|
||||
put: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
EMAIL_SERVICE: {
|
||||
sendMail: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
};
|
||||
|
||||
// Set the environment
|
||||
feedbackService.setEnv(mockEnv);
|
||||
|
||||
// Call the method
|
||||
const result = await feedbackService.handleFeedback(mockRequest as any);
|
||||
|
||||
// Verify KV_STORAGE.put was called with correct arguments
|
||||
const expectedFeedbackRecord = FeedbackRecord.create({
|
||||
feedback: feedbackData.feedback,
|
||||
timestamp: mockDate.toISOString(),
|
||||
user: 'Anonymous', // Default value
|
||||
});
|
||||
|
||||
expect(mockEnv.KV_STORAGE.put).toHaveBeenCalledWith(
|
||||
'feedback:mock-uuid',
|
||||
JSON.stringify(getSnapshot(expectedFeedbackRecord)),
|
||||
);
|
||||
|
||||
// Verify result is a success Response
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.status).toBe(200);
|
||||
|
||||
// Restore real timers
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return a 500 response when an error occurs', async () => {
|
||||
// Create mock request that throws an error
|
||||
const mockRequest = {
|
||||
json: vi.fn().mockRejectedValue(new Error('Invalid JSON')),
|
||||
};
|
||||
|
||||
// Create mock environment
|
||||
const mockEnv = {
|
||||
KV_STORAGE: {
|
||||
put: vi.fn(),
|
||||
},
|
||||
EMAIL_SERVICE: {
|
||||
sendMail: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
// Set the environment
|
||||
feedbackService.setEnv(mockEnv);
|
||||
|
||||
// Call the method
|
||||
const result = await feedbackService.handleFeedback(mockRequest as any);
|
||||
|
||||
// Verify KV_STORAGE.put was not called
|
||||
expect(mockEnv.KV_STORAGE.put).not.toHaveBeenCalled();
|
||||
|
||||
// Verify EMAIL_SERVICE.sendMail was not called
|
||||
expect(mockEnv.EMAIL_SERVICE.sendMail).not.toHaveBeenCalled();
|
||||
|
||||
// Verify result is an error Response
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.status).toBe(500);
|
||||
|
||||
// Verify response body
|
||||
const text = await result.clone().text();
|
||||
expect(text).toBe('Failed to process feedback request');
|
||||
});
|
||||
});
|
||||
});
|
136
workers/site/services/__tests__/MetricsService.test.ts
Normal file
136
workers/site/services/__tests__/MetricsService.test.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import MetricsService from '../MetricsService';
|
||||
|
||||
describe('MetricsService', () => {
|
||||
let metricsService;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a new instance of the service before each test
|
||||
metricsService = MetricsService.create();
|
||||
|
||||
// Reset mocks
|
||||
vi.resetAllMocks();
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
});
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('should have empty env and ctx objects initially', () => {
|
||||
expect(metricsService.env).toEqual({});
|
||||
expect(metricsService.ctx).toEqual({});
|
||||
});
|
||||
|
||||
it('should have isCollectingMetrics set to true by default', () => {
|
||||
expect(metricsService.isCollectingMetrics).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEnv', () => {
|
||||
it('should set the environment', () => {
|
||||
const mockEnv = { METRICS_API_KEY: 'test-key' };
|
||||
metricsService.setEnv(mockEnv);
|
||||
expect(metricsService.env).toEqual(mockEnv);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCtx', () => {
|
||||
it('should set the execution context', () => {
|
||||
const mockCtx = { waitUntil: vi.fn() };
|
||||
metricsService.setCtx(mockCtx);
|
||||
expect(metricsService.ctx).toEqual(mockCtx);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleMetricsRequest', () => {
|
||||
it('should proxy GET requests to metrics.seemueller.io', async () => {
|
||||
// Create mock request
|
||||
const mockRequest = new Request('https://example.com/metrics/path?query=value', {
|
||||
method: 'GET',
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
});
|
||||
|
||||
// Create mock response
|
||||
const mockResponse = new Response('{"data": "test"}', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
// Mock fetch to return the mock response
|
||||
global.fetch.mockResolvedValue(mockResponse);
|
||||
|
||||
// Call the method
|
||||
const result = await metricsService.handleMetricsRequest(mockRequest);
|
||||
|
||||
// Verify fetch was called with correct arguments
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://metrics.seemueller.io/metrics/path?query=value',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
body: null,
|
||||
redirect: 'follow',
|
||||
})
|
||||
);
|
||||
|
||||
// Verify result is the expected response
|
||||
expect(result).toBe(mockResponse);
|
||||
});
|
||||
|
||||
it('should proxy POST requests with body to metrics.seemueller.io', async () => {
|
||||
// Create mock request with body
|
||||
const mockBody = JSON.stringify({ test: 'data' });
|
||||
const mockRequest = new Request('https://example.com/metrics/path', {
|
||||
method: 'POST',
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
body: mockBody,
|
||||
});
|
||||
|
||||
// Create mock response
|
||||
const mockResponse = new Response('{"success": true}', {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
// Mock fetch to return the mock response
|
||||
global.fetch.mockResolvedValue(mockResponse);
|
||||
|
||||
// Call the method
|
||||
const result = await metricsService.handleMetricsRequest(mockRequest);
|
||||
|
||||
// Verify fetch was called with correct arguments
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://metrics.seemueller.io/metrics/path',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: mockRequest.body,
|
||||
redirect: 'follow',
|
||||
})
|
||||
);
|
||||
|
||||
// Verify result is the expected response
|
||||
expect(result).toBe(mockResponse);
|
||||
});
|
||||
|
||||
it('should return a 500 response when fetch fails', async () => {
|
||||
// Create mock request
|
||||
const mockRequest = new Request('https://example.com/metrics/path');
|
||||
|
||||
// Mock fetch to throw an error
|
||||
global.fetch.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
// Call the method
|
||||
const result = await metricsService.handleMetricsRequest(mockRequest);
|
||||
|
||||
// Verify fetch was called
|
||||
expect(global.fetch).toHaveBeenCalled();
|
||||
|
||||
// Verify result is an error Response
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.status).toBe(500);
|
||||
|
||||
// Verify response body
|
||||
const text = await result.clone().text();
|
||||
expect(text).toBe('Failed to fetch metrics');
|
||||
});
|
||||
});
|
||||
});
|
220
workers/site/services/__tests__/TransactionService.test.ts
Normal file
220
workers/site/services/__tests__/TransactionService.test.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getSnapshot, Instance } from 'mobx-state-tree';
|
||||
import TransactionService from '../TransactionService';
|
||||
|
||||
// Define types for testing
|
||||
type TransactionServiceInstance = Instance<typeof TransactionService>;
|
||||
|
||||
// Mock global types
|
||||
vi.stubGlobal('Response', class MockResponse {
|
||||
status: number;
|
||||
headers: Headers;
|
||||
body: any;
|
||||
|
||||
constructor(body?: any, init?: ResponseInit) {
|
||||
this.body = body;
|
||||
this.status = init?.status || 200;
|
||||
this.headers = new Headers(init?.headers);
|
||||
}
|
||||
|
||||
clone() {
|
||||
return this;
|
||||
}
|
||||
|
||||
async text() {
|
||||
return this.body?.toString() || '';
|
||||
}
|
||||
|
||||
async json() {
|
||||
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
|
||||
}
|
||||
});
|
||||
|
||||
describe('TransactionService', () => {
|
||||
let transactionService: TransactionServiceInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a new instance of the service before each test
|
||||
transactionService = TransactionService.create();
|
||||
|
||||
// Reset mocks
|
||||
vi.resetAllMocks();
|
||||
|
||||
// Mock crypto.randomUUID
|
||||
vi.spyOn(crypto, 'randomUUID').mockReturnValue('mock-uuid');
|
||||
});
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('should have empty env and ctx objects initially', () => {
|
||||
expect(transactionService.env).toEqual({});
|
||||
expect(transactionService.ctx).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEnv', () => {
|
||||
it('should set the environment', () => {
|
||||
const mockEnv = { KV_STORAGE: { put: vi.fn() } };
|
||||
transactionService.setEnv(mockEnv);
|
||||
expect(transactionService.env).toEqual(mockEnv);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCtx', () => {
|
||||
it('should set the execution context', () => {
|
||||
const mockCtx = { waitUntil: vi.fn() };
|
||||
transactionService.setCtx(mockCtx);
|
||||
expect(transactionService.ctx).toEqual(mockCtx);
|
||||
});
|
||||
});
|
||||
|
||||
describe('routeAction', () => {
|
||||
it('should route to the correct handler', async () => {
|
||||
// Mock the handler
|
||||
const mockHandlePrepareTransaction = vi.fn().mockResolvedValue({ success: true });
|
||||
transactionService.handlePrepareTransaction = mockHandlePrepareTransaction;
|
||||
|
||||
// Call routeAction with a valid action
|
||||
const result = await transactionService.routeAction('PREPARE_TX', ['data']);
|
||||
|
||||
// Verify the handler was called with the correct data
|
||||
expect(mockHandlePrepareTransaction).toHaveBeenCalledWith(['data']);
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should throw an error for unknown actions', async () => {
|
||||
// Call routeAction with an invalid action
|
||||
await expect(transactionService.routeAction('UNKNOWN_ACTION', ['data']))
|
||||
.rejects.toThrow('No handler for action: UNKNOWN_ACTION');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePrepareTransaction', () => {
|
||||
beforeEach(() => {
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
|
||||
// Mock KV_STORAGE
|
||||
const mockEnv = {
|
||||
KV_STORAGE: {
|
||||
put: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
};
|
||||
transactionService.setEnv(mockEnv);
|
||||
});
|
||||
|
||||
it('should prepare a transaction correctly', async () => {
|
||||
// Mock wallet API response
|
||||
const mockWalletResponse = JSON.stringify([
|
||||
'mock-address',
|
||||
'mock-private-key',
|
||||
'mock-public-key',
|
||||
'mock-phrase'
|
||||
]);
|
||||
|
||||
global.fetch.mockResolvedValue({
|
||||
text: vi.fn().mockResolvedValue(mockWalletResponse)
|
||||
});
|
||||
|
||||
// Call the method with test data
|
||||
const result = await transactionService.handlePrepareTransaction(['donor123', 'bitcoin', '0.01']);
|
||||
|
||||
// Verify fetch was called with the correct URL
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://wallets.seemueller.io/api/btc/create'
|
||||
);
|
||||
|
||||
// Verify KV_STORAGE.put was called with the correct data
|
||||
expect(transactionService.env.KV_STORAGE.put).toHaveBeenCalledWith(
|
||||
'transactions::prepared::mock-uuid',
|
||||
expect.stringContaining('mock-address')
|
||||
);
|
||||
|
||||
// Verify the returned data
|
||||
expect(result).toEqual({
|
||||
depositAddress: 'mock-address',
|
||||
txKey: 'mock-uuid'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle different currencies correctly', async () => {
|
||||
// Mock wallet API response
|
||||
const mockWalletResponse = JSON.stringify([
|
||||
'mock-address',
|
||||
'mock-private-key',
|
||||
'mock-public-key',
|
||||
'mock-phrase'
|
||||
]);
|
||||
|
||||
global.fetch.mockResolvedValue({
|
||||
text: vi.fn().mockResolvedValue(mockWalletResponse)
|
||||
});
|
||||
|
||||
// Test with ethereum
|
||||
await transactionService.handlePrepareTransaction(['donor123', 'ethereum', '0.01']);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://wallets.seemueller.io/api/eth/create'
|
||||
);
|
||||
|
||||
// Reset mock and test with dogecoin
|
||||
vi.resetAllMocks();
|
||||
global.fetch.mockResolvedValue({
|
||||
text: vi.fn().mockResolvedValue(mockWalletResponse)
|
||||
});
|
||||
|
||||
await transactionService.handlePrepareTransaction(['donor123', 'dogecoin', '0.01']);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://wallets.seemueller.io/api/doge/create'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleTransact', () => {
|
||||
beforeEach(() => {
|
||||
// Mock routeAction
|
||||
transactionService.routeAction = vi.fn().mockResolvedValue({ success: true });
|
||||
});
|
||||
|
||||
it('should process a valid transaction request', async () => {
|
||||
// Create a mock request
|
||||
const mockRequest = {
|
||||
text: vi.fn().mockResolvedValue('PREPARE_TX,donor123,bitcoin,0.01')
|
||||
};
|
||||
|
||||
// Call the method
|
||||
const response = await transactionService.handleTransact(mockRequest);
|
||||
|
||||
// Verify routeAction was called with the correct parameters
|
||||
expect(transactionService.routeAction).toHaveBeenCalledWith(
|
||||
'PREPARE_TX',
|
||||
['donor123', 'bitcoin', '0.01']
|
||||
);
|
||||
|
||||
// Verify the response
|
||||
expect(response).toBeInstanceOf(Response);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const responseBody = await response.json();
|
||||
expect(responseBody).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
// Create a mock request
|
||||
const mockRequest = {
|
||||
text: vi.fn().mockResolvedValue('PREPARE_TX,donor123,bitcoin,0.01')
|
||||
};
|
||||
|
||||
// Make routeAction throw an error
|
||||
transactionService.routeAction = vi.fn().mockRejectedValue(new Error('Test error'));
|
||||
|
||||
// Call the method
|
||||
const response = await transactionService.handleTransact(mockRequest);
|
||||
|
||||
// Verify the error response
|
||||
expect(response).toBeInstanceOf(Response);
|
||||
expect(response.status).toBe(500);
|
||||
|
||||
const responseBody = await response.json();
|
||||
expect(responseBody).toEqual({ error: 'Transaction failed' });
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user