website refactor
This commit is contained in:
102
apps/website/lib/adapters/MediaProxyAdapter.test.ts
Normal file
102
apps/website/lib/adapters/MediaProxyAdapter.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { proxyMediaRequest, getMediaContentType, getMediaCacheControl } from './MediaProxyAdapter';
|
||||
|
||||
// Mock fetch globally
|
||||
const mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
|
||||
describe('MediaProxyAdapter', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('proxyMediaRequest', () => {
|
||||
it('should successfully proxy media and return ArrayBuffer', async () => {
|
||||
const mockBuffer = new ArrayBuffer(8);
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
arrayBuffer: vi.fn().mockResolvedValue(mockBuffer),
|
||||
};
|
||||
|
||||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await proxyMediaRequest('/media/avatar/123');
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBe(mockBuffer);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3000/media/avatar/123',
|
||||
{ method: 'GET' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 404 errors', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
};
|
||||
|
||||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await proxyMediaRequest('/media/avatar/999');
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error.type).toBe('notFound');
|
||||
expect(error.message).toContain('Media not found');
|
||||
});
|
||||
|
||||
it('should handle other HTTP errors', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
};
|
||||
|
||||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await proxyMediaRequest('/media/avatar/123');
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error.type).toBe('serverError');
|
||||
expect(error.message).toContain('HTTP 500');
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const result = await proxyMediaRequest('/media/avatar/123');
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const error = result.getError();
|
||||
expect(error.type).toBe('networkError');
|
||||
expect(error.message).toContain('Failed to fetch media');
|
||||
});
|
||||
|
||||
it('should use custom API base URL from environment', () => {
|
||||
process.env.API_BASE_URL = 'https://api.example.com';
|
||||
|
||||
// Just verify the function exists and can be called
|
||||
expect(typeof proxyMediaRequest).toBe('function');
|
||||
|
||||
// Reset
|
||||
delete process.env.API_BASE_URL;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMediaContentType', () => {
|
||||
it('should return image/png for all media paths', () => {
|
||||
expect(getMediaContentType('/media/avatar/123')).toBe('image/png');
|
||||
expect(getMediaContentType('/media/teams/456/logo')).toBe('image/png');
|
||||
expect(getMediaContentType('/media/leagues/789/cover')).toBe('image/png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMediaCacheControl', () => {
|
||||
it('should return public cache control with max-age', () => {
|
||||
expect(getMediaCacheControl()).toBe('public, max-age=3600');
|
||||
});
|
||||
});
|
||||
});
|
||||
67
apps/website/lib/adapters/MediaProxyAdapter.ts
Normal file
67
apps/website/lib/adapters/MediaProxyAdapter.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* MediaProxyAdapter
|
||||
*
|
||||
* Handles direct HTTP proxy operations for media assets.
|
||||
* This is a special case where direct fetch is needed for binary responses.
|
||||
*/
|
||||
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
|
||||
export type MediaProxyError =
|
||||
| { type: 'notFound'; message: string }
|
||||
| { type: 'serverError'; message: string }
|
||||
| { type: 'networkError'; message: string };
|
||||
|
||||
/**
|
||||
* Proxy media request to backend API
|
||||
*
|
||||
* @param mediaPath - The API path to fetch media from (e.g., "/media/avatar/123")
|
||||
* @returns Result with ArrayBuffer on success, or error on failure
|
||||
*/
|
||||
export async function proxyMediaRequest(
|
||||
mediaPath: string
|
||||
): Promise<Result<ArrayBuffer, MediaProxyError>> {
|
||||
try {
|
||||
const baseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
|
||||
const response = await fetch(`${baseUrl}${mediaPath}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return Result.err({
|
||||
type: 'notFound',
|
||||
message: `Media not found: ${mediaPath}`
|
||||
});
|
||||
}
|
||||
return Result.err({
|
||||
type: 'serverError',
|
||||
message: `HTTP ${response.status}: ${response.statusText}`
|
||||
});
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
return Result.ok(buffer);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return Result.err({
|
||||
type: 'networkError',
|
||||
message: `Failed to fetch media: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content type for media path
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function getMediaContentType(mediaPath: string): string {
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache control header value
|
||||
*/
|
||||
export function getMediaCacheControl(): string {
|
||||
return 'public, max-age=3600';
|
||||
}
|
||||
Reference in New Issue
Block a user