Skip to content

Add test coverage incrementally #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions backend/catalog/jest.config.js
Original file line number Diff line number Diff line change
@@ -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
}
}
};
142 changes: 142 additions & 0 deletions backend/catalog/src/__tests__/handlers.test.ts
Original file line number Diff line number Diff line change
@@ -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<Request>;
let mockResponse: Partial<Response>;

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();
});
});
});
61 changes: 61 additions & 0 deletions backend/catalog/src/__tests__/movies.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
50 changes: 50 additions & 0 deletions backend/catalog/src/handlers.ts
Original file line number Diff line number Diff line change
@@ -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' });
}
};
67 changes: 14 additions & 53 deletions backend/catalog/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,28 @@
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' });
}
});

// Start server
app.listen(port, () => {
console.log(`Catalog service running on port ${port}`);
});
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) {
app.listen(port, () => {
console.log(`Catalog service running on port ${port}`);
});
}

// Export app for testing
export default app;
4 changes: 2 additions & 2 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gitpod Flix</title>
<title>Company Flix</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
</html>
4 changes: 2 additions & 2 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function Home() {
<Hero />
<div className="mt-8 space-y-8">
<MovieRow title="Trending Now" movies={movies.trending} />
<MovieRow title="Popular on Gitpod Flix" movies={movies.popular} />
<MovieRow title="Popular on Company Flix" movies={movies.popular} />
<MovieRow title="Sci-Fi & Fantasy" movies={movies.scifi} />
</div>
</div>
Expand All @@ -59,4 +59,4 @@ function App() {
return <RouterProvider router={router} />
}

export default App
export default App