diff --git a/eslint.config.js b/eslint.config.js index 9e95a0a..d422cb0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -21,6 +21,6 @@ export default [ } }, { - ignores: ['build/', '.svelte-kit/', 'dist/', 'src/lib/googleMaps.js'] + ignores: ['build/', '.svelte-kit/', 'dist/', 'src/lib/googleMaps.js', 'coverage'] } ]; diff --git a/src/lib/obaSdk.js b/src/lib/obaSdk.js index ab13781..05de54a 100644 --- a/src/lib/obaSdk.js +++ b/src/lib/obaSdk.js @@ -9,8 +9,16 @@ const oba = new onebusaway({ }); export function handleOBAResponse(response, entityName) { + if (!response) { + throw error(500, `Unable to fetch ${entityName}.`); + } + + if (typeof response.code === 'undefined') { + throw error(500, `Unable to fetch ${entityName}.`); + } + if (response.code !== 200) { - return error(500, `Unable to fetch ${entityName}.`); + throw error(500, `Unable to fetch ${entityName}.`); } return json(response); diff --git a/src/tests/lib/obaSdk.test.js b/src/tests/lib/obaSdk.test.js new file mode 100644 index 0000000..9474cde --- /dev/null +++ b/src/tests/lib/obaSdk.test.js @@ -0,0 +1,140 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import onebusaway from 'onebusaway-sdk'; +import { handleOBAResponse } from '$lib/obaSdk'; +import { error, json } from '@sveltejs/kit'; + +// Mock the onebusaway-sdk +vi.mock('onebusaway-sdk'); + +// Mock @sveltejs/kit error and json functions +vi.mock('@sveltejs/kit', () => ({ + error: vi.fn((status, message) => { + throw new Error(message); + }), + json: vi.fn((data) => ({ + status: 200, + body: data + })) +})); + +// Mock environment variables +vi.mock('$env/static/public', () => ({ + PUBLIC_OBA_SERVER_URL: 'https://test-api.example.com' +})); + +vi.mock('$env/static/private', () => ({ + PRIVATE_OBA_API_KEY: 'test-api-key' +})); + +describe('OneBusAway Client', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('OBA Client Initialization', () => { + it('should initialize with correct configuration', () => { + new onebusaway({ + baseURL: 'https://test-api.example.com', + apiKey: 'test-api-key' + }); + + expect(onebusaway).toHaveBeenCalledWith({ + baseURL: 'https://test-api.example.com', + apiKey: 'test-api-key' + }); + }); + }); + + describe('handleOBAResponse', () => { + it('should return JSON response when status code is 200', () => { + const mockResponse = { + code: 200, + data: { + stops: [] + } + }; + + const result = handleOBAResponse(mockResponse, 'stops'); + expect(json).toHaveBeenCalledWith(mockResponse); + expect(result.status).toBe(200); + expect(result.body).toEqual(mockResponse); + }); + + it('should throw error when status code is not 200', () => { + const mockResponse = { + code: 404, + data: null + }; + + expect(() => handleOBAResponse(mockResponse, 'stops')).toThrow(/Unable to fetch stops/); + expect(error).toHaveBeenCalledWith(500, 'Unable to fetch stops.'); + }); + + it('should handle undefined response gracefully', () => { + expect(() => handleOBAResponse(undefined, 'stops')).toThrow(/Unable to fetch stops/); + expect(error).toHaveBeenCalledWith(500, 'Unable to fetch stops.'); + }); + + it('should handle null response gracefully', () => { + expect(() => handleOBAResponse(null, 'stops')).toThrow(/Unable to fetch stops/); + expect(error).toHaveBeenCalledWith(500, 'Unable to fetch stops.'); + }); + + it('should handle response with missing code gracefully', () => { + const mockResponse = { + data: { + stops: [] + } + }; + + expect(() => handleOBAResponse(mockResponse, 'stops')).toThrow(/Unable to fetch stops/); + expect(error).toHaveBeenCalledWith(500, 'Unable to fetch stops.'); + }); + }); +}); + +describe('OBA Client Integration', () => { + let oba; + + beforeEach(() => { + vi.clearAllMocks(); + oba = new onebusaway({ + baseURL: 'https://test-api.example.com', + apiKey: 'test-api-key' + }); + }); + + it('should handle successful API responses', async () => { + const mockApiResponse = { + code: 200, + data: { + stops: [{ id: 1, name: 'Test Stop' }] + } + }; + + // Mock a successful API call + oba.stops = vi.fn().mockResolvedValue(mockApiResponse); + + const response = await oba.stops(); + const result = handleOBAResponse(response, 'stops'); + + expect(json).toHaveBeenCalledWith(mockApiResponse); + expect(result.status).toBe(200); + expect(result.body).toEqual(mockApiResponse); + }); + + it('should handle failed API responses', async () => { + const mockApiResponse = { + code: 500, + data: null + }; + + // Mock a failed API call + oba.stops = vi.fn().mockResolvedValue(mockApiResponse); + + const response = await oba.stops(); + + expect(() => handleOBAResponse(response, 'stops')).toThrow(/Unable to fetch stops/); + expect(error).toHaveBeenCalledWith(500, 'Unable to fetch stops.'); + }); +}); diff --git a/src/tests/lib/urls.test.js b/src/tests/lib/urls.test.js new file mode 100644 index 0000000..ae71f6c --- /dev/null +++ b/src/tests/lib/urls.test.js @@ -0,0 +1,74 @@ +import { describe, it, expect } from 'vitest'; +import { buildURL } from '$lib/urls'; + +describe('buildURL', () => { + it('should build a basic URL with query parameters', () => { + const result = buildURL('http://example.com', 'api/data', { key: 'value', page: '1' }); + expect(result).toBe('http://example.com/api/data?key=value&page=1'); + }); + + it('should handle trailing slashes in baseURL', () => { + const result = buildURL('http://example.com/', 'api/data', { key: 'value' }); + expect(result).toBe('http://example.com/api/data?key=value'); + }); + + it('should handle leading slashes in path', () => { + const result = buildURL('http://example.com', '/api/data', { key: 'value' }); + expect(result).toBe('http://example.com/api/data?key=value'); + }); + + it('should handle both trailing and leading slashes', () => { + const result = buildURL('http://example.com/', '/api/data', { key: 'value' }); + expect(result).toBe('http://example.com/api/data?key=value'); + }); + + it('should handle multiple trailing slashes in baseURL', () => { + const result = buildURL('http://example.com///', 'api/data', { key: 'value' }); + expect(result).toBe('http://example.com/api/data?key=value'); + }); + + it('should handle multiple leading slashes in path', () => { + const result = buildURL('http://example.com', '///api/data', { key: 'value' }); + expect(result).toBe('http://example.com/api/data?key=value'); + }); + + it('should handle empty query parameters', () => { + const result = buildURL('http://example.com', 'api/data', {}); + expect(result).toBe('http://example.com/api/data?'); + }); + + it('should encode query parameter values', () => { + const result = buildURL('http://example.com', 'api/data', { + key: 'value with spaces', + special: '!@#$%' + }); + expect(result).toBe( + 'http://example.com/api/data?key=value+with+spaces&special=%21%40%23%24%25' + ); + }); + + it('should convert undefined values to "undefined" string', () => { + const result = buildURL('http://example.com', 'api/data', { key: undefined, value: 'test' }); + expect(result).toBe('http://example.com/api/data?key=undefined&value=test'); + }); + + it('should convert null values to "null" string', () => { + const result = buildURL('http://example.com', 'api/data', { key: null, value: 'test' }); + expect(result).toBe('http://example.com/api/data?key=null&value=test'); + }); + + it('should convert array to comma-separated string', () => { + const result = buildURL('http://example.com', 'api/data', { items: ['a', 'b', 'c'] }); + expect(result).toBe('http://example.com/api/data?items=a%2Cb%2Cc'); + }); + + it('should handle boolean values', () => { + const result = buildURL('http://example.com', 'api/data', { isActive: true, isDeleted: false }); + expect(result).toBe('http://example.com/api/data?isActive=true&isDeleted=false'); + }); + + it('should handle number values', () => { + const result = buildURL('http://example.com', 'api/data', { count: 42, price: 19.99 }); + expect(result).toBe('http://example.com/api/data?count=42&price=19.99'); + }); +}); diff --git a/src/tests/lib/utils.test.js b/src/tests/lib/utils.test.js new file mode 100644 index 0000000..a778a90 --- /dev/null +++ b/src/tests/lib/utils.test.js @@ -0,0 +1,107 @@ +import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; +import { debounce } from '$lib/utils'; + +describe('debounce', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should call the function only once after the wait time', async () => { + const mockFn = vi.fn(); + const debouncedFn = debounce(mockFn, 100); + + debouncedFn(); + expect(mockFn).not.toBeCalled(); + + vi.advanceTimersByTime(50); + expect(mockFn).not.toBeCalled(); + + vi.advanceTimersByTime(50); + expect(mockFn).toBeCalledTimes(1); + }); + + it('should reset the timer when called again before wait time', () => { + const mockFn = vi.fn(); + const debouncedFn = debounce(mockFn, 100); + + debouncedFn(); + vi.advanceTimersByTime(50); + debouncedFn(); + vi.advanceTimersByTime(50); + expect(mockFn).not.toBeCalled(); + + vi.advanceTimersByTime(50); + expect(mockFn).toBeCalledTimes(1); + }); + + it('should pass arguments to the debounced function', () => { + const mockFn = vi.fn(); + const debouncedFn = debounce(mockFn, 100); + + debouncedFn('test', 123); + vi.advanceTimersByTime(100); + + expect(mockFn).toBeCalledWith('test', 123); + }); + + it('should pass the latest arguments when called multiple times', () => { + const mockFn = vi.fn(); + const debouncedFn = debounce(mockFn, 100); + + debouncedFn('first', 1); + vi.advanceTimersByTime(50); + + debouncedFn('second', 2); + vi.advanceTimersByTime(100); + + expect(mockFn).toBeCalledTimes(1); + expect(mockFn).toBeCalledWith('second', 2); + }); + + it('should maintain correct context (this binding)', () => { + const context = { + value: 'test', + method: vi.fn(function () { + return this.value; + }) + }; + + const debouncedMethod = debounce(context.method, 100); + context.debouncedMethod = debouncedMethod; + + context.debouncedMethod(); + vi.advanceTimersByTime(100); + + expect(context.method).toBeCalledTimes(1); + expect(context.method.mock.results[0].value).toBe('test'); + }); + + it('should handle zero wait time', () => { + const mockFn = vi.fn(); + const debouncedFn = debounce(mockFn, 0); + + debouncedFn(); + vi.advanceTimersByTime(0); + + expect(mockFn).toBeCalledTimes(1); + }); + + it('should handle multiple rapid calls', () => { + const mockFn = vi.fn(); + const debouncedFn = debounce(mockFn, 100); + + debouncedFn(); + debouncedFn(); + debouncedFn(); + debouncedFn(); + debouncedFn(); + + vi.advanceTimersByTime(100); + + expect(mockFn).toBeCalledTimes(1); + }); +});