diff --git a/workers/site/services/__tests__/ContactService.test.ts b/workers/site/services/__tests__/ContactService.test.ts new file mode 100644 index 0000000..b37d195 --- /dev/null +++ b/workers/site/services/__tests__/ContactService.test.ts @@ -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'); + }); + }); +}); diff --git a/workers/site/services/__tests__/FeedbackService.test.ts b/workers/site/services/__tests__/FeedbackService.test.ts new file mode 100644 index 0000000..8821c7b --- /dev/null +++ b/workers/site/services/__tests__/FeedbackService.test.ts @@ -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'); + }); + }); +}); diff --git a/workers/site/services/__tests__/MetricsService.test.ts b/workers/site/services/__tests__/MetricsService.test.ts new file mode 100644 index 0000000..9f60285 --- /dev/null +++ b/workers/site/services/__tests__/MetricsService.test.ts @@ -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'); + }); + }); +});