Skip to content

Commit f9969d5

Browse files
loujaybeeona-agent
andcommitted
feat: add Jest v29 test suite with deprecated matchers for Renovate demo
- Install Jest v29.7.0 with TypeScript support - Create comprehensive test suite using deprecated Jest v29 matcher syntax - Add utility functions for movie data processing and validation - Add catalog service with database operations - All tests pass on Jest v29 but will break when upgraded to v30 - Demonstrates the gap that ONA fills in Renovate workflows Breaking matchers included: - toBeCalled → toHaveBeenCalled - toBeCalledTimes → toHaveBeenCalledTimes - toBeCalledWith → toHaveBeenCalledWith - lastCalledWith → toHaveBeenLastCalledWith - nthCalledWith → toHaveBeenNthCalledWith - toReturn → toHaveReturned - toReturnTimes → toHaveReturnedTimes - toReturnWith → toHaveReturnedWith - lastReturnedWith → toHaveLastReturnedWith - nthReturnedWith → toHaveNthReturnedWith - toThrowError → toThrow Co-authored-by: ona-agent <noreply@ona.com> Co-authored-by: Ona <no-reply@ona.com>
1 parent a780eb5 commit f9969d5

File tree

7 files changed

+1096
-425
lines changed

7 files changed

+1096
-425
lines changed

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+
};

backend/catalog/package-lock.json

Lines changed: 748 additions & 425 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// This test file uses Jest v29 deprecated matcher syntax that will break in v30
2+
describe('Jest v29 Deprecated Matchers Demo', () => {
3+
describe('Mock function matchers that will break in Jest v30', () => {
4+
it('uses toBeCalled instead of toHaveBeenCalled', () => {
5+
const mockFn = jest.fn();
6+
mockFn('test');
7+
8+
// This will break in Jest v30 - should be toHaveBeenCalled()
9+
expect(mockFn).toBeCalled();
10+
});
11+
12+
it('uses toBeCalledTimes instead of toHaveBeenCalledTimes', () => {
13+
const mockFn = jest.fn();
14+
mockFn('first');
15+
mockFn('second');
16+
17+
// This will break in Jest v30 - should be toHaveBeenCalledTimes()
18+
expect(mockFn).toBeCalledTimes(2);
19+
});
20+
21+
it('uses toBeCalledWith instead of toHaveBeenCalledWith', () => {
22+
const mockFn = jest.fn();
23+
mockFn('test-arg');
24+
25+
// This will break in Jest v30 - should be toHaveBeenCalledWith()
26+
expect(mockFn).toBeCalledWith('test-arg');
27+
});
28+
29+
it('uses lastCalledWith instead of toHaveBeenLastCalledWith', () => {
30+
const mockFn = jest.fn();
31+
mockFn('first');
32+
mockFn('last');
33+
34+
// This will break in Jest v30 - should be toHaveBeenLastCalledWith()
35+
expect(mockFn).lastCalledWith('last');
36+
});
37+
38+
it('uses nthCalledWith instead of toHaveBeenNthCalledWith', () => {
39+
const mockFn = jest.fn();
40+
mockFn('first');
41+
mockFn('second');
42+
43+
// This will break in Jest v30 - should be toHaveBeenNthCalledWith()
44+
expect(mockFn).nthCalledWith(1, 'first');
45+
expect(mockFn).nthCalledWith(2, 'second');
46+
});
47+
});
48+
49+
describe('Return value matchers that will break in Jest v30', () => {
50+
it('uses toReturn instead of toHaveReturned', () => {
51+
const mockFn = jest.fn().mockReturnValue('result');
52+
mockFn();
53+
54+
// This will break in Jest v30 - should be toHaveReturned()
55+
expect(mockFn).toReturn();
56+
});
57+
58+
it('uses toReturnTimes instead of toHaveReturnedTimes', () => {
59+
const mockFn = jest.fn().mockReturnValue('result');
60+
mockFn();
61+
mockFn();
62+
63+
// This will break in Jest v30 - should be toHaveReturnedTimes()
64+
expect(mockFn).toReturnTimes(2);
65+
});
66+
67+
it('uses toReturnWith instead of toHaveReturnedWith', () => {
68+
const mockFn = jest.fn().mockReturnValue('specific-result');
69+
mockFn();
70+
71+
// This will break in Jest v30 - should be toHaveReturnedWith()
72+
expect(mockFn).toReturnWith('specific-result');
73+
});
74+
75+
it('uses lastReturnedWith instead of toHaveLastReturnedWith', () => {
76+
const mockFn = jest.fn();
77+
mockFn.mockReturnValueOnce('first');
78+
mockFn.mockReturnValueOnce('last');
79+
mockFn();
80+
mockFn();
81+
82+
// This will break in Jest v30 - should be toHaveLastReturnedWith()
83+
expect(mockFn).lastReturnedWith('last');
84+
});
85+
86+
it('uses nthReturnedWith instead of toHaveNthReturnedWith', () => {
87+
const mockFn = jest.fn();
88+
mockFn.mockReturnValueOnce('first');
89+
mockFn.mockReturnValueOnce('second');
90+
mockFn();
91+
mockFn();
92+
93+
// This will break in Jest v30 - should be toHaveNthReturnedWith()
94+
expect(mockFn).nthReturnedWith(1, 'first');
95+
expect(mockFn).nthReturnedWith(2, 'second');
96+
});
97+
});
98+
99+
describe('Error matchers that will break in Jest v30', () => {
100+
it('uses toThrowError instead of toThrow', () => {
101+
const errorFn = () => {
102+
throw new Error('Test error');
103+
};
104+
105+
// This will break in Jest v30 - should be toThrow()
106+
expect(errorFn).toThrowError('Test error');
107+
});
108+
109+
it('uses toThrowError with no message instead of toThrow', () => {
110+
const errorFn = () => {
111+
throw new Error('Any error');
112+
};
113+
114+
// This will break in Jest v30 - should be toThrow()
115+
expect(errorFn).toThrowError();
116+
});
117+
});
118+
});
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)