change semantics

Update README deployment steps and add deploy:secrets script to package.json

update local inference script and README

update lockfile

reconfigure package scripts for development

update test execution

pass server tests

Update README with revised Bun commands and workspace details

remove pnpm package manager designator

create bun server
This commit is contained in:
geoffsee
2025-06-02 18:41:16 -04:00
committed by Geoff Seemueller
parent 1055cda2f1
commit 497eb22ad8
218 changed files with 1273 additions and 4987 deletions

View File

@@ -0,0 +1,164 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getSnapshot } from 'mobx-state-tree';
import AssetService from '../AssetService.ts';
// 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');
});
});
});

View 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.ts';
import OpenAI from 'openai';
import ChatSdk from '../../lib/chat-sdk.ts';
import Message from '../../models/Message.ts';
import { SUPPORTED_MODELS } from '@open-gsio/ai/supported-models';
import handleStreamData from '../../lib/handleStreamData.ts';
// 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',
SERVER_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 SERVER_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,
SERVER_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({
SERVER_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
});
});
});
});

View File

@@ -0,0 +1,150 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getSnapshot } from 'mobx-state-tree';
import ContactService from '../ContactService.ts';
import ContactRecord from '../../models/ContactRecord.ts';
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');
});
});
});

View File

@@ -0,0 +1,203 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getSnapshot } from 'mobx-state-tree';
import FeedbackService from '../FeedbackService.ts';
import FeedbackRecord from '../../models/FeedbackRecord.ts';
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');
});
});
});

View File

@@ -0,0 +1,136 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import MetricsService from '../MetricsService.ts';
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');
});
});
});

View File

@@ -0,0 +1,220 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getSnapshot, Instance } from 'mobx-state-tree';
import TransactionService from '../TransactionService.ts';
// 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' });
});
});
});