Skip to content

Commit 110c23f

Browse files
authored
Merge pull request #36 from gitpod-samples/renovate-jest-demo
Add tests and renovate configuration for renovate demo
2 parents 6d5bf4a + feae5ac commit 110c23f

File tree

11 files changed

+494
-2
lines changed

11 files changed

+494
-2
lines changed

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"ghcr.io/devcontainers/features/docker-in-docker:2": {
1919
"version": "latest",
2020
"moby": true
21-
}
21+
},
22+
"ghcr.io/devcontainers-extra/features/renovate-cli:2": {}
2223
},
2324
"forwardPorts": [
2425
3000,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Catalog Service Tests
2+
3+
on:
4+
pull_request:
5+
branches: [ main ]
6+
paths:
7+
- 'backend/catalog/**'
8+
- '.github/workflows/catalog-tests.yml'
9+
push:
10+
branches: [ main ]
11+
paths:
12+
- 'backend/catalog/**'
13+
- '.github/workflows/catalog-tests.yml'
14+
15+
jobs:
16+
test:
17+
runs-on: ubuntu-latest
18+
19+
defaults:
20+
run:
21+
working-directory: backend/catalog
22+
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
27+
- name: Setup Node.js
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version: '20'
31+
cache: 'npm'
32+
cache-dependency-path: backend/catalog/package-lock.json
33+
34+
- name: Install dependencies
35+
run: npm install
36+
37+
- name: Run tests
38+
run: npm run test

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,8 @@ coverage/
3737
.pgsql/
3838

3939
services/database/database.sqlite
40+
41+
# Renovate debug files
42+
renovate_debug.txt
43+
renovate_output.txt
44+
jest_debug.txt

backend/catalog/jest.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
testTimeout: 5000,
5+
forceExit: true,
6+
verbose: true,
7+
bail: true
8+
};
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
describe('Jest v29 Deprecated Matchers Demo', () => {
2+
describe('Mock function matchers that will break in Jest v30', () => {
3+
it('uses toBeCalled instead of toHaveBeenCalled', () => {
4+
const mockFn = jest.fn();
5+
mockFn('test');
6+
7+
expect(mockFn).toBeCalled();
8+
});
9+
10+
it('uses toBeCalledTimes instead of toHaveBeenCalledTimes', () => {
11+
const mockFn = jest.fn();
12+
mockFn('first');
13+
mockFn('second');
14+
15+
expect(mockFn).toBeCalledTimes(2);
16+
});
17+
18+
it('uses toBeCalledWith instead of toHaveBeenCalledWith', () => {
19+
const mockFn = jest.fn();
20+
mockFn('test-arg');
21+
22+
expect(mockFn).toBeCalledWith('test-arg');
23+
});
24+
25+
it('uses lastCalledWith instead of toHaveBeenLastCalledWith', () => {
26+
const mockFn = jest.fn();
27+
mockFn('first');
28+
mockFn('last');
29+
30+
expect(mockFn).lastCalledWith('last');
31+
});
32+
33+
it('uses nthCalledWith instead of toHaveBeenNthCalledWith', () => {
34+
const mockFn = jest.fn();
35+
mockFn('first');
36+
mockFn('second');
37+
38+
expect(mockFn).nthCalledWith(1, 'first');
39+
expect(mockFn).nthCalledWith(2, 'second');
40+
});
41+
});
42+
43+
describe('Return value matchers that will break in Jest v30', () => {
44+
it('uses toReturn instead of toHaveReturned', () => {
45+
const mockFn = jest.fn().mockReturnValue('result');
46+
mockFn();
47+
48+
expect(mockFn).toReturn();
49+
});
50+
51+
it('uses toReturnTimes instead of toHaveReturnedTimes', () => {
52+
const mockFn = jest.fn().mockReturnValue('result');
53+
mockFn();
54+
mockFn();
55+
56+
expect(mockFn).toReturnTimes(2);
57+
});
58+
59+
it('uses toReturnWith instead of toHaveReturnedWith', () => {
60+
const mockFn = jest.fn().mockReturnValue('specific-result');
61+
mockFn();
62+
63+
expect(mockFn).toReturnWith('specific-result');
64+
});
65+
66+
it('uses lastReturnedWith instead of toHaveLastReturnedWith', () => {
67+
const mockFn = jest.fn();
68+
mockFn.mockReturnValueOnce('first');
69+
mockFn.mockReturnValueOnce('last');
70+
mockFn();
71+
mockFn();
72+
73+
expect(mockFn).lastReturnedWith('last');
74+
});
75+
76+
it('uses nthReturnedWith instead of toHaveNthReturnedWith', () => {
77+
const mockFn = jest.fn();
78+
mockFn.mockReturnValueOnce('first');
79+
mockFn.mockReturnValueOnce('second');
80+
mockFn();
81+
mockFn();
82+
83+
expect(mockFn).nthReturnedWith(1, 'first');
84+
expect(mockFn).nthReturnedWith(2, 'second');
85+
});
86+
});
87+
88+
describe('Error matchers that will break in Jest v30', () => {
89+
it('uses toThrowError instead of toThrow', () => {
90+
const errorFn = () => {
91+
throw new Error('Test error');
92+
};
93+
94+
expect(errorFn).toThrowError('Test error');
95+
});
96+
97+
it('uses toThrowError with no message instead of toThrow', () => {
98+
const errorFn = () => {
99+
throw new Error('Any error');
100+
};
101+
102+
expect(errorFn).toThrowError();
103+
});
104+
});
105+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { formatMovieTitle, calculateAverageRating } from '../utils/movieUtils';
2+
3+
describe('Movie Utilities', () => {
4+
describe('formatMovieTitle', () => {
5+
it('should format title with proper capitalization', () => {
6+
expect(formatMovieTitle('the dark knight')).toBe('The Dark Knight');
7+
expect(formatMovieTitle('PULP FICTION')).toBe('Pulp Fiction');
8+
expect(formatMovieTitle('fight club')).toBe('Fight Club');
9+
});
10+
11+
it('should handle empty strings', () => {
12+
expect(formatMovieTitle('')).toBe('');
13+
});
14+
15+
it('should handle single words', () => {
16+
expect(formatMovieTitle('matrix')).toBe('Matrix');
17+
expect(formatMovieTitle('MATRIX')).toBe('Matrix');
18+
});
19+
});
20+
21+
describe('calculateAverageRating', () => {
22+
it('should calculate correct average rating', () => {
23+
const movies = [
24+
{ title: 'Movie 1', description: 'Desc 1', release_year: 2000, rating: 8.0, image_url: 'url1' },
25+
{ title: 'Movie 2', description: 'Desc 2', release_year: 2001, rating: 9.0, image_url: 'url2' },
26+
{ title: 'Movie 3', description: 'Desc 3', release_year: 2002, rating: 7.0, image_url: 'url3' }
27+
];
28+
29+
expect(calculateAverageRating(movies)).toBe(8.0);
30+
});
31+
32+
it('should return 0 for empty array', () => {
33+
expect(calculateAverageRating([])).toBe(0);
34+
});
35+
36+
it('should handle single movie', () => {
37+
const movies = [
38+
{ title: 'Solo Movie', description: 'Desc', release_year: 2000, rating: 7.5, image_url: 'url' }
39+
];
40+
41+
expect(calculateAverageRating(movies)).toBe(7.5);
42+
});
43+
});
44+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Pool } from 'pg';
2+
import { Movie } from '../utils/movieUtils';
3+
4+
export class CatalogService {
5+
private pool: Pool;
6+
7+
constructor(pool: Pool) {
8+
this.pool = pool;
9+
}
10+
11+
async getAllMovies(): Promise<Movie[]> {
12+
const result = await this.pool.query('SELECT * FROM movies ORDER BY rating DESC');
13+
return result.rows;
14+
}
15+
16+
async getMovieById(id: number): Promise<Movie | null> {
17+
const result = await this.pool.query('SELECT * FROM movies WHERE id = $1', [id]);
18+
return result.rows[0] || null;
19+
}
20+
21+
async searchMovies(query: string): Promise<Movie[]> {
22+
const searchQuery = `%${query.toLowerCase()}%`;
23+
const result = await this.pool.query(
24+
'SELECT * FROM movies WHERE LOWER(title) LIKE $1 OR LOWER(description) LIKE $1',
25+
[searchQuery]
26+
);
27+
return result.rows;
28+
}
29+
30+
async getTopRatedMovies(limit: number = 10): Promise<Movie[]> {
31+
const result = await this.pool.query(
32+
'SELECT * FROM movies ORDER BY rating DESC LIMIT $1',
33+
[limit]
34+
);
35+
return result.rows;
36+
}
37+
38+
async getMoviesByYear(year: number): Promise<Movie[]> {
39+
const result = await this.pool.query(
40+
'SELECT * FROM movies WHERE release_year = $1 ORDER BY rating DESC',
41+
[year]
42+
);
43+
return result.rows;
44+
}
45+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
export interface ProcessingResult<T> {
2+
data: T[];
3+
processed: number;
4+
errors: string[];
5+
}
6+
7+
export function processMovieData<T>(
8+
items: T[],
9+
processor: (item: T) => T | null
10+
): ProcessingResult<T> {
11+
const result: ProcessingResult<T> = {
12+
data: [],
13+
processed: 0,
14+
errors: []
15+
};
16+
17+
for (const item of items) {
18+
try {
19+
const processed = processor(item);
20+
if (processed !== null) {
21+
result.data.push(processed);
22+
result.processed++;
23+
}
24+
} catch (error) {
25+
result.errors.push(error instanceof Error ? error.message : 'Unknown error');
26+
}
27+
}
28+
29+
return result;
30+
}
31+
32+
export function batchProcess<T, R>(
33+
items: T[],
34+
batchSize: number,
35+
processor: (batch: T[]) => Promise<R[]>
36+
): Promise<R[]> {
37+
const batches: T[][] = [];
38+
39+
for (let i = 0; i < items.length; i += batchSize) {
40+
batches.push(items.slice(i, i + batchSize));
41+
}
42+
43+
return Promise.all(batches.map(processor)).then(results =>
44+
results.flat()
45+
);
46+
}
47+
48+
export function sanitizeInput(input: string): string {
49+
return input
50+
.trim()
51+
.replace(/<[^>]*>/g, '')
52+
.replace(/script/gi, '')
53+
.substring(0, 1000);
54+
}
55+
56+
export function parseRating(rating: string | number): number {
57+
if (typeof rating === 'number') {
58+
return Math.max(0, Math.min(10, rating));
59+
}
60+
61+
const parsed = parseFloat(rating);
62+
if (isNaN(parsed)) {
63+
return 0;
64+
}
65+
66+
return Math.max(0, Math.min(10, parsed));
67+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
export interface Movie {
2+
id?: number;
3+
title: string;
4+
description: string;
5+
release_year: number;
6+
rating: number;
7+
image_url: string;
8+
}
9+
10+
export function validateMovie(movie: Partial<Movie>): string[] {
11+
const errors: string[] = [];
12+
13+
if (!movie.title || movie.title.trim().length === 0) {
14+
errors.push('Title is required');
15+
}
16+
17+
if (!movie.description || movie.description.trim().length === 0) {
18+
errors.push('Description is required');
19+
}
20+
21+
if (!movie.release_year || movie.release_year < 1900 || movie.release_year > new Date().getFullYear()) {
22+
errors.push('Release year must be between 1900 and current year');
23+
}
24+
25+
if (!movie.rating || movie.rating < 0 || movie.rating > 10) {
26+
errors.push('Rating must be between 0 and 10');
27+
}
28+
29+
if (!movie.image_url || !isValidUrl(movie.image_url)) {
30+
errors.push('Valid image URL is required');
31+
}
32+
33+
return errors;
34+
}
35+
36+
export function isValidUrl(url: string): boolean {
37+
try {
38+
new URL(url);
39+
return true;
40+
} catch {
41+
return false;
42+
}
43+
}
44+
45+
export function formatMovieTitle(title: string): string {
46+
return title
47+
.split(' ')
48+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
49+
.join(' ');
50+
}
51+
52+
export function calculateAverageRating(movies: Movie[]): number {
53+
if (movies.length === 0) return 0;
54+
55+
const sum = movies.reduce((acc, movie) => acc + movie.rating, 0);
56+
return Math.round((sum / movies.length) * 10) / 10;
57+
}
58+
59+
export function filterMoviesByDecade(movies: Movie[], decade: number): Movie[] {
60+
const startYear = decade;
61+
const endYear = decade + 9;
62+
63+
return movies.filter(movie =>
64+
movie.release_year >= startYear && movie.release_year <= endYear
65+
);
66+
}

0 commit comments

Comments
 (0)