Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"moby": true
}
},
"ghcr.io/devcontainers-extra/features/renovate-cli:2": {}
},
"forwardPorts": [
3000,
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ coverage/
.pgsql/

services/database/database.sqlite

# Renovate debug files
renovate_debug.txt
renovate_output.txt
8 changes: 8 additions & 0 deletions backend/catalog/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testTimeout: 5000,
forceExit: true,
verbose: true,
bail: true
};
2,384 changes: 2,384 additions & 0 deletions backend/catalog/jest_debug.txt

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion backend/catalog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"dev": "nodemon --exec ts-node src/index.ts",
"build": "tsc",
"test": "jest",
"setup": "./scripts/setup.sh"
"setup": "./scripts/setup.sh",
"renovate": "if [ \"$npm_config_live\" ]; then DRY_RUN_FLAG=\"\"; echo 'LIVE MODE: Will create actual pull requests'; else DRY_RUN_FLAG=\"--dry-run=full\"; echo 'DRY RUN MODE: No pull requests will be created (use --live to create PRs)'; fi && RENOVATE_TOKEN=$GH_CLI_TOKEN LOG_LEVEL=debug renovate --platform=github $DRY_RUN_FLAG gitpod-samples/gitpodflix-demo > renovate_debug.txt 2>&1 && echo 'Renovate scan complete! Check renovate_debug.txt for full output' && if [ \"$npm_config_filter\" ]; then echo 'Filtering results for:' $npm_config_filter && grep -i -A 20 -B 5 \"$npm_config_filter\" renovate_debug.txt; else echo 'Use --filter=<package> to filter results for specific dependency'; fi",
"renovate:jest": "echo 'Creating Jest v30 pull request...' && echo '{\"extends\":[],\"enabledManagers\":[\"npm\"],\"includePaths\":[\"backend/catalog/package.json\"],\"packageRules\":[{\"matchPackageNames\":[\"jest\",\"@types/jest\"],\"enabled\":true}],\"prConcurrentLimit\":0,\"prHourlyLimit\":0,\"branchConcurrentLimit\":0}' > renovate-jest-only.json && RENOVATE_TOKEN=$GH_CLI_TOKEN RENOVATE_CONFIG_FILE=renovate-jest-only.json renovate --platform=github gitpod-samples/gitpodflix-demo && rm renovate-jest-only.json"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move commands to actual configuration file

},
"dependencies": {
"express": "^4.18.2",
Expand Down
118 changes: 118 additions & 0 deletions backend/catalog/src/__tests__/deprecated-matchers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// This test file uses Jest v29 deprecated matcher syntax that will break in v30
describe('Jest v29 Deprecated Matchers Demo', () => {
describe('Mock function matchers that will break in Jest v30', () => {
it('uses toBeCalled instead of toHaveBeenCalled', () => {
const mockFn = jest.fn();
mockFn('test');

// This will break in Jest v30 - should be toHaveBeenCalled()
expect(mockFn).toBeCalled();
});

it('uses toBeCalledTimes instead of toHaveBeenCalledTimes', () => {
const mockFn = jest.fn();
mockFn('first');
mockFn('second');

// This will break in Jest v30 - should be toHaveBeenCalledTimes()
expect(mockFn).toBeCalledTimes(2);
});

it('uses toBeCalledWith instead of toHaveBeenCalledWith', () => {
const mockFn = jest.fn();
mockFn('test-arg');

// This will break in Jest v30 - should be toHaveBeenCalledWith()
expect(mockFn).toBeCalledWith('test-arg');
});

it('uses lastCalledWith instead of toHaveBeenLastCalledWith', () => {
const mockFn = jest.fn();
mockFn('first');
mockFn('last');

// This will break in Jest v30 - should be toHaveBeenLastCalledWith()
expect(mockFn).lastCalledWith('last');
});

it('uses nthCalledWith instead of toHaveBeenNthCalledWith', () => {
const mockFn = jest.fn();
mockFn('first');
mockFn('second');

// This will break in Jest v30 - should be toHaveBeenNthCalledWith()
expect(mockFn).nthCalledWith(1, 'first');
expect(mockFn).nthCalledWith(2, 'second');
});
});

describe('Return value matchers that will break in Jest v30', () => {
it('uses toReturn instead of toHaveReturned', () => {
const mockFn = jest.fn().mockReturnValue('result');
mockFn();

// This will break in Jest v30 - should be toHaveReturned()
expect(mockFn).toReturn();
});

it('uses toReturnTimes instead of toHaveReturnedTimes', () => {
const mockFn = jest.fn().mockReturnValue('result');
mockFn();
mockFn();

// This will break in Jest v30 - should be toHaveReturnedTimes()
expect(mockFn).toReturnTimes(2);
});

it('uses toReturnWith instead of toHaveReturnedWith', () => {
const mockFn = jest.fn().mockReturnValue('specific-result');
mockFn();

// This will break in Jest v30 - should be toHaveReturnedWith()
expect(mockFn).toReturnWith('specific-result');
});

it('uses lastReturnedWith instead of toHaveLastReturnedWith', () => {
const mockFn = jest.fn();
mockFn.mockReturnValueOnce('first');
mockFn.mockReturnValueOnce('last');
mockFn();
mockFn();

// This will break in Jest v30 - should be toHaveLastReturnedWith()
expect(mockFn).lastReturnedWith('last');
});

it('uses nthReturnedWith instead of toHaveNthReturnedWith', () => {
const mockFn = jest.fn();
mockFn.mockReturnValueOnce('first');
mockFn.mockReturnValueOnce('second');
mockFn();
mockFn();

// This will break in Jest v30 - should be toHaveNthReturnedWith()
expect(mockFn).nthReturnedWith(1, 'first');
expect(mockFn).nthReturnedWith(2, 'second');
});
});

describe('Error matchers that will break in Jest v30', () => {
it('uses toThrowError instead of toThrow', () => {
const errorFn = () => {
throw new Error('Test error');
};

// This will break in Jest v30 - should be toThrow()
expect(errorFn).toThrowError('Test error');
});

it('uses toThrowError with no message instead of toThrow', () => {
const errorFn = () => {
throw new Error('Any error');
};

// This will break in Jest v30 - should be toThrow()
expect(errorFn).toThrowError();
});
});
});
44 changes: 44 additions & 0 deletions backend/catalog/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { formatMovieTitle, calculateAverageRating } from '../utils/movieUtils';

describe('Movie Utilities', () => {
describe('formatMovieTitle', () => {
it('should format title with proper capitalization', () => {
expect(formatMovieTitle('the dark knight')).toBe('The Dark Knight');
expect(formatMovieTitle('PULP FICTION')).toBe('Pulp Fiction');
expect(formatMovieTitle('fight club')).toBe('Fight Club');
});

it('should handle empty strings', () => {
expect(formatMovieTitle('')).toBe('');
});

it('should handle single words', () => {
expect(formatMovieTitle('matrix')).toBe('Matrix');
expect(formatMovieTitle('MATRIX')).toBe('Matrix');
});
});

describe('calculateAverageRating', () => {
it('should calculate correct average rating', () => {
const movies = [
{ title: 'Movie 1', description: 'Desc 1', release_year: 2000, rating: 8.0, image_url: 'url1' },
{ title: 'Movie 2', description: 'Desc 2', release_year: 2001, rating: 9.0, image_url: 'url2' },
{ title: 'Movie 3', description: 'Desc 3', release_year: 2002, rating: 7.0, image_url: 'url3' }
];

expect(calculateAverageRating(movies)).toBe(8.0);
});

it('should return 0 for empty array', () => {
expect(calculateAverageRating([])).toBe(0);
});

it('should handle single movie', () => {
const movies = [
{ title: 'Solo Movie', description: 'Desc', release_year: 2000, rating: 7.5, image_url: 'url' }
];

expect(calculateAverageRating(movies)).toBe(7.5);
});
});
});
45 changes: 45 additions & 0 deletions backend/catalog/src/services/catalogService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Pool } from 'pg';
import { Movie } from '../utils/movieUtils';

export class CatalogService {
private pool: Pool;

constructor(pool: Pool) {
this.pool = pool;
}

async getAllMovies(): Promise<Movie[]> {
const result = await this.pool.query('SELECT * FROM movies ORDER BY rating DESC');
return result.rows;
}

async getMovieById(id: number): Promise<Movie | null> {
const result = await this.pool.query('SELECT * FROM movies WHERE id = $1', [id]);
return result.rows[0] || null;
}

async searchMovies(query: string): Promise<Movie[]> {
const searchQuery = `%${query.toLowerCase()}%`;
const result = await this.pool.query(
'SELECT * FROM movies WHERE LOWER(title) LIKE $1 OR LOWER(description) LIKE $1',
[searchQuery]
);
return result.rows;
}

async getTopRatedMovies(limit: number = 10): Promise<Movie[]> {
const result = await this.pool.query(
'SELECT * FROM movies ORDER BY rating DESC LIMIT $1',
[limit]
);
return result.rows;
}

async getMoviesByYear(year: number): Promise<Movie[]> {
const result = await this.pool.query(
'SELECT * FROM movies WHERE release_year = $1 ORDER BY rating DESC',
[year]
);
return result.rows;
}
}
67 changes: 67 additions & 0 deletions backend/catalog/src/utils/dataProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export interface ProcessingResult<T> {
data: T[];
processed: number;
errors: string[];
}

export function processMovieData<T>(
items: T[],
processor: (item: T) => T | null
): ProcessingResult<T> {
const result: ProcessingResult<T> = {
data: [],
processed: 0,
errors: []
};

for (const item of items) {
try {
const processed = processor(item);
if (processed !== null) {
result.data.push(processed);
result.processed++;
}
} catch (error) {
result.errors.push(error instanceof Error ? error.message : 'Unknown error');
}
}

return result;
}

export function batchProcess<T, R>(
items: T[],
batchSize: number,
processor: (batch: T[]) => Promise<R[]>
): Promise<R[]> {
const batches: T[][] = [];

for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}

return Promise.all(batches.map(processor)).then(results =>
results.flat()
);
}

export function sanitizeInput(input: string): string {
return input
.trim()
.replace(/<[^>]*>/g, '')
.replace(/script/gi, '')
.substring(0, 1000);
}

export function parseRating(rating: string | number): number {
if (typeof rating === 'number') {
return Math.max(0, Math.min(10, rating));
}

const parsed = parseFloat(rating);
if (isNaN(parsed)) {
return 0;
}

return Math.max(0, Math.min(10, parsed));
}
66 changes: 66 additions & 0 deletions backend/catalog/src/utils/movieUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
export interface Movie {
id?: number;
title: string;
description: string;
release_year: number;
rating: number;
image_url: string;
}

export function validateMovie(movie: Partial<Movie>): string[] {
const errors: string[] = [];

if (!movie.title || movie.title.trim().length === 0) {
errors.push('Title is required');
}

if (!movie.description || movie.description.trim().length === 0) {
errors.push('Description is required');
}

if (!movie.release_year || movie.release_year < 1900 || movie.release_year > new Date().getFullYear()) {
errors.push('Release year must be between 1900 and current year');
}

if (!movie.rating || movie.rating < 0 || movie.rating > 10) {
errors.push('Rating must be between 0 and 10');
}

if (!movie.image_url || !isValidUrl(movie.image_url)) {
errors.push('Valid image URL is required');
}

return errors;
}

export function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}

export function formatMovieTitle(title: string): string {
return title
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
}

export function calculateAverageRating(movies: Movie[]): number {
if (movies.length === 0) return 0;

const sum = movies.reduce((acc, movie) => acc + movie.rating, 0);
return Math.round((sum / movies.length) * 10) / 10;
}

export function filterMoviesByDecade(movies: Movie[], decade: number): Movie[] {
const startYear = decade;
const endYear = decade + 9;

return movies.filter(movie =>
movie.release_year >= startYear && movie.release_year <= endYear
);
}
Loading