From 6ab2edd2d6fd295e1089767d4333b825bfce7c7f Mon Sep 17 00:00:00 2001 From: Lou Bichard Date: Fri, 16 May 2025 13:36:10 +0000 Subject: [PATCH 1/4] Update website title from GitpodFlix to Company Flix --- frontend/index.html | 4 ++-- frontend/src/App.jsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 15cbad8..ee64ca1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,10 +3,10 @@ - Gitpod Flix + Company Flix
- \ No newline at end of file + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0f57099..c3ff599 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -36,7 +36,7 @@ function Home() {
- +
@@ -59,4 +59,4 @@ function App() { return } -export default App \ No newline at end of file +export default App From 7e7a27489465158fa3ef5dd889d28ac7d5d388a4 Mon Sep 17 00:00:00 2001 From: Lou Bichard Date: Fri, 16 May 2025 14:17:05 +0000 Subject: [PATCH 2/4] Add whitespace for readability --- backend/catalog/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/catalog/src/index.ts b/backend/catalog/src/index.ts index 3d711b1..4ede270 100644 --- a/backend/catalog/src/index.ts +++ b/backend/catalog/src/index.ts @@ -64,4 +64,4 @@ app.post('/api/movies/clear', async (req, res) => { // Start server app.listen(port, () => { console.log(`Catalog service running on port ${port}`); -}); \ No newline at end of file +}); From 95ce8ea6ffb3814331d084840824776eff53e64f Mon Sep 17 00:00:00 2001 From: Lou Bichard Date: Fri, 16 May 2025 14:27:10 +0000 Subject: [PATCH 3/4] Add first test for GET /api/movies endpoint --- backend/catalog/jest.config.js | 17 ++++++ backend/catalog/src/__tests__/movies.test.ts | 61 ++++++++++++++++++++ backend/catalog/src/index.ts | 13 +++-- 3 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 backend/catalog/jest.config.js create mode 100644 backend/catalog/src/__tests__/movies.test.ts diff --git a/backend/catalog/jest.config.js b/backend/catalog/jest.config.js new file mode 100644 index 0000000..7dfc992 --- /dev/null +++ b/backend/catalog/jest.config.js @@ -0,0 +1,17 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/**/__tests__/**' + ], + coverageThreshold: { + global: { + statements: 50, + branches: 50, + functions: 50, + lines: 50 + } + } +}; diff --git a/backend/catalog/src/__tests__/movies.test.ts b/backend/catalog/src/__tests__/movies.test.ts new file mode 100644 index 0000000..6220bd3 --- /dev/null +++ b/backend/catalog/src/__tests__/movies.test.ts @@ -0,0 +1,61 @@ +import { Pool } from 'pg'; +import express from 'express'; + +// Mock the pg Pool +jest.mock('pg', () => { + const mPool = { + query: jest.fn(), + }; + return { Pool: jest.fn(() => mPool) }; +}); + +describe('Movies API', () => { + let pool: any; + + beforeEach(() => { + // Reset modules to ensure clean tests + jest.resetModules(); + + // Get the mocked pool instance + pool = new Pool(); + + // Clear all mocks before each test + jest.clearAllMocks(); + }); + + it('GET /api/movies should return movies sorted by rating', async () => { + // Mock data + const mockMovies = [ + { id: 1, title: 'Movie 1', rating: 9.5 }, + { id: 2, title: 'Movie 2', rating: 8.5 } + ]; + + // Setup the mock implementation for this test + pool.query.mockResolvedValueOnce({ rows: mockMovies }); + + // Create mock request and response objects + const req = {} as express.Request; + const res = { + json: jest.fn(), + status: jest.fn().mockReturnThis() + } as unknown as express.Response; + + // Import the route handler function + const getMoviesHandler = async (req: express.Request, res: express.Response) => { + try { + const result = await pool.query('SELECT * FROM movies ORDER BY rating DESC'); + res.json(result.rows); + } catch (err) { + console.error('Error fetching movies:', err); + res.status(500).json({ error: 'Internal server error' }); + } + }; + + // Call the handler directly + await getMoviesHandler(req, res); + + // Assertions + expect(pool.query).toHaveBeenCalledWith('SELECT * FROM movies ORDER BY rating DESC'); + expect(res.json).toHaveBeenCalledWith(mockMovies); + }); +}); diff --git a/backend/catalog/src/index.ts b/backend/catalog/src/index.ts index 4ede270..fa682f2 100644 --- a/backend/catalog/src/index.ts +++ b/backend/catalog/src/index.ts @@ -61,7 +61,12 @@ app.post('/api/movies/clear', async (req, res) => { } }); -// Start server -app.listen(port, () => { - console.log(`Catalog service running on port ${port}`); -}); +// Only start the server if this file is run directly (not imported for testing) +if (require.main === module) { + app.listen(port, () => { + console.log(`Catalog service running on port ${port}`); + }); +} + +// Export app for testing +export default app; From d6568e5257373739c51e3fd587338f344c8a0dc3 Mon Sep 17 00:00:00 2001 From: Lou Bichard Date: Fri, 16 May 2025 14:37:55 +0000 Subject: [PATCH 4/4] Refactor API handlers and add comprehensive test coverage --- .../catalog/src/__tests__/handlers.test.ts | 142 ++++++++++++++++++ backend/catalog/src/handlers.ts | 50 ++++++ backend/catalog/src/index.ts | 52 +------ 3 files changed, 196 insertions(+), 48 deletions(-) create mode 100644 backend/catalog/src/__tests__/handlers.test.ts create mode 100644 backend/catalog/src/handlers.ts diff --git a/backend/catalog/src/__tests__/handlers.test.ts b/backend/catalog/src/__tests__/handlers.test.ts new file mode 100644 index 0000000..4f77511 --- /dev/null +++ b/backend/catalog/src/__tests__/handlers.test.ts @@ -0,0 +1,142 @@ +import { Request, Response } from 'express'; +import { Pool } from 'pg'; +import { getMovies, seedMovies, clearMovies } from '../handlers'; + +// Mock the pg Pool +jest.mock('pg', () => { + const mPool = { + query: jest.fn(), + }; + return { Pool: jest.fn(() => mPool) }; +}); + +describe('API Handlers', () => { + let pool: any; + let mockRequest: Partial; + let mockResponse: Partial; + + beforeEach(() => { + // Get the mocked pool instance + pool = new Pool(); + + // Setup mock request and response + mockRequest = {}; + mockResponse = { + json: jest.fn(), + status: jest.fn().mockReturnThis() + }; + + // Clear all mocks before each test + jest.clearAllMocks(); + }); + + describe('getMovies', () => { + it('should return movies sorted by rating', async () => { + // Mock data + const mockMovies = [ + { id: 1, title: 'Movie 1', rating: 9.5 }, + { id: 2, title: 'Movie 2', rating: 8.5 } + ]; + + // Setup the mock implementation for this test + pool.query.mockResolvedValueOnce({ rows: mockMovies }); + + // Call the handler + await getMovies(mockRequest as Request, mockResponse as Response); + + // Assertions + expect(pool.query).toHaveBeenCalledWith('SELECT * FROM movies ORDER BY rating DESC'); + expect(mockResponse.json).toHaveBeenCalledWith(mockMovies); + }); + + it('should handle database errors', async () => { + // Setup the mock implementation to throw an error + const dbError = new Error('Database connection failed'); + pool.query.mockRejectedValueOnce(dbError); + + // Spy on console.error + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + // Call the handler + await getMovies(mockRequest as Request, mockResponse as Response); + + // Assertions + expect(pool.query).toHaveBeenCalledWith('SELECT * FROM movies ORDER BY rating DESC'); + expect(consoleErrorSpy).toHaveBeenCalledWith('Error fetching movies:', dbError); + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Internal server error' }); + + // Restore console.error + consoleErrorSpy.mockRestore(); + }); + }); + + describe('seedMovies', () => { + it('should seed the database with movies', async () => { + // Setup the mock implementation for this test + pool.query.mockResolvedValueOnce({}); + + // Call the handler + await seedMovies(mockRequest as Request, mockResponse as Response); + + // Assertions + expect(pool.query).toHaveBeenCalledWith(expect.stringContaining('TRUNCATE TABLE movies')); + expect(pool.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO movies')); + expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Database seeded successfully' }); + }); + + it('should handle database errors', async () => { + // Setup the mock implementation to throw an error + const dbError = new Error('Database connection failed'); + pool.query.mockRejectedValueOnce(dbError); + + // Spy on console.error + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + // Call the handler + await seedMovies(mockRequest as Request, mockResponse as Response); + + // Assertions + expect(consoleErrorSpy).toHaveBeenCalledWith('Error seeding database:', dbError); + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Internal server error' }); + + // Restore console.error + consoleErrorSpy.mockRestore(); + }); + }); + + describe('clearMovies', () => { + it('should clear all movies from the database', async () => { + // Setup the mock implementation for this test + pool.query.mockResolvedValueOnce({}); + + // Call the handler + await clearMovies(mockRequest as Request, mockResponse as Response); + + // Assertions + expect(pool.query).toHaveBeenCalledWith('TRUNCATE TABLE movies'); + expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Database cleared successfully' }); + }); + + it('should handle database errors', async () => { + // Setup the mock implementation to throw an error + const dbError = new Error('Database connection failed'); + pool.query.mockRejectedValueOnce(dbError); + + // Spy on console.error + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + // Call the handler + await clearMovies(mockRequest as Request, mockResponse as Response); + + // Assertions + expect(consoleErrorSpy).toHaveBeenCalledWith('Error clearing database:', dbError); + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Internal server error' }); + + // Restore console.error + consoleErrorSpy.mockRestore(); + }); + }); +}); diff --git a/backend/catalog/src/handlers.ts b/backend/catalog/src/handlers.ts new file mode 100644 index 0000000..90e6ca4 --- /dev/null +++ b/backend/catalog/src/handlers.ts @@ -0,0 +1,50 @@ +import { Request, Response } from 'express'; +import { Pool } from 'pg'; + +// Database configuration +const pool = new Pool({ + user: process.env.DB_USER || 'gitpod', + host: process.env.DB_HOST || 'localhost', + database: process.env.DB_NAME || 'gitpodflix', + password: process.env.DB_PASSWORD || 'gitpod', + port: parseInt(process.env.DB_PORT || '5432'), +}); + +export const getMovies = async (req: Request, res: Response) => { + try { + const result = await pool.query('SELECT * FROM movies ORDER BY rating DESC'); + res.json(result.rows); + } catch (err) { + console.error('Error fetching movies:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const seedMovies = async (req: Request, res: Response) => { + try { + // Execute the seed script + await pool.query(` + TRUNCATE TABLE movies; + INSERT INTO movies (title, description, release_year, rating, image_url) VALUES + ('The Shawshank Redemption', 'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.', 1994, 9.3, 'https://m.media-amazon.com/images/M/MV5BNDE3ODcxYzMtY2YzZC00NmNlLWJiNDMtZDViZWM2MzIxZDYwXkEyXkFqcGdeQXVyNjAwNDUxODI@._V1_.jpg'), + ('The Godfather', 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.', 1972, 9.2, 'https://m.media-amazon.com/images/M/MV5BM2MyNjYxNmUtYTAwNi00MTYxLWJmNWYtYzZlODY3ZTk3OTFlXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_.jpg'), + ('The Dark Knight', 'When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.', 2008, 9.0, 'https://m.media-amazon.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_.jpg'), + ('Pulp Fiction', 'The lives of two mob hitmen, a boxer, a gangster and his wife, and a pair of diner bandits intertwine in four tales of violence and redemption.', 1994, 8.9, 'https://m.media-amazon.com/images/M/MV5BNGNhMDIzZTUtNTBlZi00MTRlLWFjM2ItYzViMjE3YzI5MjljXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_.jpg'), + ('Fight Club', 'An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.', 1999, 8.8, 'https://m.media-amazon.com/images/M/MV5BNDIzNDU0YzEtYzE5Ni00ZjlkLTk5ZjgtNjM3NWE4YzA3Nzk3XkEyXkFqcGdeQXVyMjUzOTY1NTc@._V1_.jpg') + `); + res.json({ message: 'Database seeded successfully' }); + } catch (err) { + console.error('Error seeding database:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const clearMovies = async (req: Request, res: Response) => { + try { + await pool.query('TRUNCATE TABLE movies'); + res.json({ message: 'Database cleared successfully' }); + } catch (err) { + console.error('Error clearing database:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/backend/catalog/src/index.ts b/backend/catalog/src/index.ts index fa682f2..7a1ff5f 100644 --- a/backend/catalog/src/index.ts +++ b/backend/catalog/src/index.ts @@ -1,65 +1,21 @@ import express from 'express'; import cors from 'cors'; -import { Pool } from 'pg'; import dotenv from 'dotenv'; +import { getMovies, seedMovies, clearMovies } from './handlers'; dotenv.config(); const app = express(); const port = process.env.PORT || 3001; -// Database configuration -const pool = new Pool({ - user: process.env.DB_USER || 'gitpod', - host: process.env.DB_HOST || 'localhost', - database: process.env.DB_NAME || 'gitpodflix', - password: process.env.DB_PASSWORD || 'gitpod', - port: parseInt(process.env.DB_PORT || '5432'), -}); - // Middleware app.use(cors()); app.use(express.json()); // Routes -app.get('/api/movies', async (req, res) => { - try { - const result = await pool.query('SELECT * FROM movies ORDER BY rating DESC'); - res.json(result.rows); - } catch (err) { - console.error('Error fetching movies:', err); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -app.post('/api/movies/seed', async (req, res) => { - try { - // Execute the seed script - await pool.query(` - TRUNCATE TABLE movies; - INSERT INTO movies (title, description, release_year, rating, image_url) VALUES - ('The Shawshank Redemption', 'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.', 1994, 9.3, 'https://m.media-amazon.com/images/M/MV5BNDE3ODcxYzMtY2YzZC00NmNlLWJiNDMtZDViZWM2MzIxZDYwXkEyXkFqcGdeQXVyNjAwNDUxODI@._V1_.jpg'), - ('The Godfather', 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.', 1972, 9.2, 'https://m.media-amazon.com/images/M/MV5BM2MyNjYxNmUtYTAwNi00MTYxLWJmNWYtYzZlODY3ZTk3OTFlXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_.jpg'), - ('The Dark Knight', 'When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.', 2008, 9.0, 'https://m.media-amazon.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_.jpg'), - ('Pulp Fiction', 'The lives of two mob hitmen, a boxer, a gangster and his wife, and a pair of diner bandits intertwine in four tales of violence and redemption.', 1994, 8.9, 'https://m.media-amazon.com/images/M/MV5BNGNhMDIzZTUtNTBlZi00MTRlLWFjM2ItYzViMjE3YzI5MjljXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_.jpg'), - ('Fight Club', 'An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.', 1999, 8.8, 'https://m.media-amazon.com/images/M/MV5BNDIzNDU0YzEtYzE5Ni00ZjlkLTk5ZjgtNjM3NWE4YzA3Nzk3XkEyXkFqcGdeQXVyMjUzOTY1NTc@._V1_.jpg') - `); - res.json({ message: 'Database seeded successfully' }); - } catch (err) { - console.error('Error seeding database:', err); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -app.post('/api/movies/clear', async (req, res) => { - try { - await pool.query('TRUNCATE TABLE movies'); - res.json({ message: 'Database cleared successfully' }); - } catch (err) { - console.error('Error clearing database:', err); - res.status(500).json({ error: 'Internal server error' }); - } -}); +app.get('/api/movies', getMovies); +app.post('/api/movies/seed', seedMovies); +app.post('/api/movies/clear', clearMovies); // Only start the server if this file is run directly (not imported for testing) if (require.main === module) {