diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 837a9de..bdeed0e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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, diff --git a/.github/workflows/catalog-tests.yml b/.github/workflows/catalog-tests.yml new file mode 100644 index 0000000..7825b51 --- /dev/null +++ b/.github/workflows/catalog-tests.yml @@ -0,0 +1,38 @@ +name: Catalog Service Tests + +on: + pull_request: + branches: [ main ] + paths: + - 'backend/catalog/**' + - '.github/workflows/catalog-tests.yml' + push: + branches: [ main ] + paths: + - 'backend/catalog/**' + - '.github/workflows/catalog-tests.yml' + +jobs: + test: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: backend/catalog + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: backend/catalog/package-lock.json + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm run test diff --git a/.gitignore b/.gitignore index a411b9e..5a869c5 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,8 @@ coverage/ .pgsql/ services/database/database.sqlite + +# Renovate debug files +renovate_debug.txt +renovate_output.txt +jest_debug.txt diff --git a/backend/catalog/jest.config.js b/backend/catalog/jest.config.js new file mode 100644 index 0000000..306ec97 --- /dev/null +++ b/backend/catalog/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testTimeout: 5000, + forceExit: true, + verbose: true, + bail: true +}; diff --git a/backend/catalog/src/__tests__/deprecated-matchers.test.ts b/backend/catalog/src/__tests__/deprecated-matchers.test.ts new file mode 100644 index 0000000..b0f3374 --- /dev/null +++ b/backend/catalog/src/__tests__/deprecated-matchers.test.ts @@ -0,0 +1,105 @@ +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'); + + expect(mockFn).toBeCalled(); + }); + + it('uses toBeCalledTimes instead of toHaveBeenCalledTimes', () => { + const mockFn = jest.fn(); + mockFn('first'); + mockFn('second'); + + expect(mockFn).toBeCalledTimes(2); + }); + + it('uses toBeCalledWith instead of toHaveBeenCalledWith', () => { + const mockFn = jest.fn(); + mockFn('test-arg'); + + expect(mockFn).toBeCalledWith('test-arg'); + }); + + it('uses lastCalledWith instead of toHaveBeenLastCalledWith', () => { + const mockFn = jest.fn(); + mockFn('first'); + mockFn('last'); + + expect(mockFn).lastCalledWith('last'); + }); + + it('uses nthCalledWith instead of toHaveBeenNthCalledWith', () => { + const mockFn = jest.fn(); + mockFn('first'); + mockFn('second'); + + 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(); + + expect(mockFn).toReturn(); + }); + + it('uses toReturnTimes instead of toHaveReturnedTimes', () => { + const mockFn = jest.fn().mockReturnValue('result'); + mockFn(); + mockFn(); + + expect(mockFn).toReturnTimes(2); + }); + + it('uses toReturnWith instead of toHaveReturnedWith', () => { + const mockFn = jest.fn().mockReturnValue('specific-result'); + mockFn(); + + expect(mockFn).toReturnWith('specific-result'); + }); + + it('uses lastReturnedWith instead of toHaveLastReturnedWith', () => { + const mockFn = jest.fn(); + mockFn.mockReturnValueOnce('first'); + mockFn.mockReturnValueOnce('last'); + mockFn(); + mockFn(); + + expect(mockFn).lastReturnedWith('last'); + }); + + it('uses nthReturnedWith instead of toHaveNthReturnedWith', () => { + const mockFn = jest.fn(); + mockFn.mockReturnValueOnce('first'); + mockFn.mockReturnValueOnce('second'); + mockFn(); + mockFn(); + + 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'); + }; + + expect(errorFn).toThrowError('Test error'); + }); + + it('uses toThrowError with no message instead of toThrow', () => { + const errorFn = () => { + throw new Error('Any error'); + }; + + expect(errorFn).toThrowError(); + }); + }); +}); diff --git a/backend/catalog/src/__tests__/utils.test.ts b/backend/catalog/src/__tests__/utils.test.ts new file mode 100644 index 0000000..e75a488 --- /dev/null +++ b/backend/catalog/src/__tests__/utils.test.ts @@ -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); + }); + }); +}); diff --git a/backend/catalog/src/services/catalogService.ts b/backend/catalog/src/services/catalogService.ts new file mode 100644 index 0000000..8b1e1cb --- /dev/null +++ b/backend/catalog/src/services/catalogService.ts @@ -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 { + const result = await this.pool.query('SELECT * FROM movies ORDER BY rating DESC'); + return result.rows; + } + + async getMovieById(id: number): Promise { + const result = await this.pool.query('SELECT * FROM movies WHERE id = $1', [id]); + return result.rows[0] || null; + } + + async searchMovies(query: string): Promise { + 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 { + const result = await this.pool.query( + 'SELECT * FROM movies ORDER BY rating DESC LIMIT $1', + [limit] + ); + return result.rows; + } + + async getMoviesByYear(year: number): Promise { + const result = await this.pool.query( + 'SELECT * FROM movies WHERE release_year = $1 ORDER BY rating DESC', + [year] + ); + return result.rows; + } +} diff --git a/backend/catalog/src/utils/dataProcessor.ts b/backend/catalog/src/utils/dataProcessor.ts new file mode 100644 index 0000000..1fd38a3 --- /dev/null +++ b/backend/catalog/src/utils/dataProcessor.ts @@ -0,0 +1,67 @@ +export interface ProcessingResult { + data: T[]; + processed: number; + errors: string[]; +} + +export function processMovieData( + items: T[], + processor: (item: T) => T | null +): ProcessingResult { + const result: ProcessingResult = { + 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( + items: T[], + batchSize: number, + processor: (batch: T[]) => Promise +): Promise { + 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)); +} diff --git a/backend/catalog/src/utils/movieUtils.ts b/backend/catalog/src/utils/movieUtils.ts new file mode 100644 index 0000000..032b584 --- /dev/null +++ b/backend/catalog/src/utils/movieUtils.ts @@ -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): 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 + ); +} diff --git a/demos/fixing-renovate-dependencies.md b/demos/fixing-renovate-dependencies.md new file mode 100644 index 0000000..b2694a5 --- /dev/null +++ b/demos/fixing-renovate-dependencies.md @@ -0,0 +1,94 @@ +# Fixing Renovate Dependencies Demo + +This demo shows how to use Renovate to create pull requests for dependency updates and then use AI assistance to resolve any breaking changes. + +## Prerequisites + +- Access to a Gitpod environment with this repository +- GitHub CLI token configured (`GH_CLI_TOKEN` environment variable) +- Renovate CLI is pre-installed in the devcontainer + +## Steps to Replicate + +### 1. Create a Renovate Pull Request + +The Renovate CLI is available in this environment and can be used to scan for dependency updates. The repository includes a `renovate.json` configuration that focuses on Jest dependencies. + +To create a pull request for dependency updates, you can run: + +```bash +# Set the GitHub token for Renovate +export RENOVATE_TOKEN=$GH_CLI_TOKEN + +# Run Renovate to scan and create pull requests +renovate --platform=github gitpod-samples/gitpodflix-demo +``` + +This will: +- Use the existing `renovate.json` configuration +- Scan for dependency updates (currently configured for Jest) +- Create pull requests for available updates +- Target upgrades that may introduce breaking changes + +### 2. Review the Pull Request + +After the command completes, check the GitHub repository for newly created pull requests. The PRs will contain: +- Updated dependencies +- Changelog information about breaking changes +- Details about what needs to be addressed + +### 3. Resolve Breaking Changes with AI + +You have several options to get AI assistance for resolving the breaking changes: + +#### Option A: Using GitHub CLI +```bash +# Get PR details and diff +gh pr view --json body,title,files +gh pr diff + +# Use this information to prompt your AI assistant +``` + +#### Option B: Manual Context Gathering +1. Copy the PR description and diff manually +2. Include relevant test files that might be affected +3. Construct a prompt asking for help with dependency migration + +#### Option C: Direct File Analysis +1. Review the failing tests after merging the PR +2. Copy error messages and affected code +3. Ask AI to help fix compatibility issues + +## Example AI Prompt + +``` +I have a dependency upgrade that's causing test failures due to breaking changes. Here are the failing tests and error messages: + +[Include test file contents and error messages] + +Please help me update the code to be compatible with the new version. +``` + +## Renovate Configuration + +The repository includes a `renovate.json` file that can be customized to target specific dependencies or change update behavior. You can modify this configuration to: +- Target different packages +- Change update frequency +- Adjust PR creation settings +- Enable/disable specific dependency types + +## Expected Outcomes + +- Pull requests with dependency updates +- Understanding of how to use AI to resolve breaking changes +- Updated code compatible with new dependency versions +- Successful test suite execution after fixes + +## Notes for Sales Engineers + +- This demonstrates real-world dependency management scenarios +- Shows how AI can assist with breaking changes during upgrades +- Highlights the importance of having good test coverage +- Illustrates the collaborative workflow between automated tools and AI assistance +- Renovate CLI provides flexibility for different scanning and update strategies diff --git a/renovate.json b/renovate.json index 7190a60..883ce55 100644 --- a/renovate.json +++ b/renovate.json @@ -1,3 +1,22 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json" + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [], + "enabledManagers": ["npm"], + "includePaths": ["backend/catalog/package.json"], + "packageRules": [ + { + "matchPackageNames": ["jest", "@types/jest"], + "enabled": true, + "recreateClosed": true + }, + { + "matchPackageNames": ["*"], + "excludePackageNames": ["jest", "@types/jest"], + "enabled": false + } + ], + "recreateClosed": true, + "prConcurrentLimit": 0, + "prHourlyLimit": 0, + "branchConcurrentLimit": 0 }