From 1e9dc15c2453eb4f7aee91aa81a939a5e0fe3edb Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Wed, 28 Jan 2026 12:38:50 -0500 Subject: [PATCH 1/7] Acknowledge year limitation of sample dataset --- mflix/README-JAVA-SPRING.md | 6 +- mflix/README-JAVASCRIPT-EXPRESS.md | 4 ++ mflix/README-PYTHON-FASTAPI.md | 4 ++ .../components/FilterBar/FilterBar.module.css | 15 +++++ .../app/components/FilterBar/FilterBar.tsx | 12 +++- .../app/components/MovieCard/MovieCard.tsx | 13 +++- mflix/client/app/movie/[id]/page.tsx | 11 +++- .../controller/MovieControllerImpl.java | 37 +++++++++-- .../controller/MovieControllerTest.java | 41 ++++++++++++ .../src/controllers/movieController.ts | 35 ++++++++++- .../tests/controllers/movieController.test.ts | 43 ++++++++++++- .../python-fastapi/src/routers/movies.py | 24 ++++++- .../python-fastapi/tests/test_movie_routes.py | 62 ++++++++++++++++++- 13 files changed, 288 insertions(+), 19 deletions(-) diff --git a/mflix/README-JAVA-SPRING.md b/mflix/README-JAVA-SPRING.md index d73a717..523d749 100644 --- a/mflix/README-JAVA-SPRING.md +++ b/mflix/README-JAVA-SPRING.md @@ -14,12 +14,16 @@ This is a full-stack movie browsing application built with Java Spring Boot and └── mvnw ``` +## Data Limitations + +The `sample_mflix` dataset contains movies released up to **2015**. Searching for movies from 2016 or later will return no results. This is a limitation of the sample dataset, not the application. + ## Prerequisites - **Java 21** or higher - **Node.js 20** or higher - **MongoDB Atlas cluster or local deployment** with the `sample_mflix` dataset loaded - - [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/) + - [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/) - **Maven** (included via Maven Wrapper) - **Voyage AI API key** (For MongoDB Vector Search) - [Get a Voyage AI API key](https://www.voyageai.com/) diff --git a/mflix/README-JAVASCRIPT-EXPRESS.md b/mflix/README-JAVASCRIPT-EXPRESS.md index 130bf5f..0ecb764 100644 --- a/mflix/README-JAVASCRIPT-EXPRESS.md +++ b/mflix/README-JAVASCRIPT-EXPRESS.md @@ -14,6 +14,10 @@ This is a full-stack movie browsing application built with Express.js and Next.j └── tsconfig.json ``` +## Data Limitations + +The `sample_mflix` dataset contains movies released up to **2015**. Searching for movies from 2016 or later will return no results. This is a limitation of the sample dataset, not the application. + ## Prerequisites - **Node.js 22** or higher diff --git a/mflix/README-PYTHON-FASTAPI.md b/mflix/README-PYTHON-FASTAPI.md index f3be9de..1934680 100644 --- a/mflix/README-PYTHON-FASTAPI.md +++ b/mflix/README-PYTHON-FASTAPI.md @@ -17,6 +17,10 @@ This is a full-stack movie browsing application built with Python FastAPI and Ne └── requirements.txt ``` +## Data Limitations + +The `sample_mflix` dataset contains movies released up to **2015**. Searching for movies from 2016 or later will return no results. This is a limitation of the sample dataset, not the application. + ## Prerequisites - **Python 3.10** to **Python 3.13** diff --git a/mflix/client/app/components/FilterBar/FilterBar.module.css b/mflix/client/app/components/FilterBar/FilterBar.module.css index ee17f25..990b3a9 100644 --- a/mflix/client/app/components/FilterBar/FilterBar.module.css +++ b/mflix/client/app/components/FilterBar/FilterBar.module.css @@ -93,6 +93,21 @@ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15); } +.inputWarning { + border-color: #f59e0b; +} + +.inputWarning:focus { + border-color: #f59e0b; + box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.15); +} + +.yearWarning { + font-size: 0.7rem; + color: #b45309; + margin-top: 0.25rem; +} + .ratingGroup { display: flex; align-items: center; diff --git a/mflix/client/app/components/FilterBar/FilterBar.tsx b/mflix/client/app/components/FilterBar/FilterBar.tsx index 324840d..0711c7c 100644 --- a/mflix/client/app/components/FilterBar/FilterBar.tsx +++ b/mflix/client/app/components/FilterBar/FilterBar.tsx @@ -10,6 +10,9 @@ const SORT_OPTIONS = [ { value: 'imdb.rating', label: 'IMDB Rating' }, ]; +// The sample_mflix dataset only contains movies up to 2015 +const MAX_DATASET_YEAR = 2015; + interface FilterBarProps { onFilterChange: (filters: MovieFilterParams) => void; isLoading?: boolean; @@ -137,14 +140,19 @@ export default function FilterBar({ MAX_DATASET_YEAR ? styles.inputWarning : ''}`} + placeholder="e.g. 2010" value={filters.year || ''} onChange={(e) => handleFilterChange('year', e.target.value ? parseInt(e.target.value) : undefined)} disabled={isLoading} min={1900} max={2030} /> + {filters.year && filters.year > MAX_DATASET_YEAR && ( + + Dataset only contains movies up to {MAX_DATASET_YEAR} + + )}
diff --git a/mflix/client/app/components/MovieCard/MovieCard.tsx b/mflix/client/app/components/MovieCard/MovieCard.tsx index 7fa702c..04afb91 100644 --- a/mflix/client/app/components/MovieCard/MovieCard.tsx +++ b/mflix/client/app/components/MovieCard/MovieCard.tsx @@ -14,6 +14,15 @@ import React from "react"; * such as image error handling and selection checkbox. */ +/** + * Validates that a poster URL is valid for Next.js Image component. + * Must be an absolute URL (http/https) or a relative path starting with / + */ +const isValidPosterUrl = (url: string | undefined): boolean => { + if (!url || typeof url !== 'string') return false; + return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/'); +}; + interface MovieCardProps { movie: Movie; isSelected?: boolean; @@ -48,9 +57,9 @@ export default function MovieCard({ movie, isSelected = false, onSelectionChange )}
- {movie.poster ? ( + {isValidPosterUrl(movie.poster) ? ( {`${movie.title} { + if (!url || typeof url !== 'string') return false; + return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/'); +}; + interface MovieDetailsPageProps { params: Promise<{ id: string; @@ -202,7 +211,7 @@ export default function MovieDetailsPage({ params }: MovieDetailsPageProps) { ) : (
- {movie.poster ? ( + {isValidPosterUrl(movie.poster) ? (
>> getAllMovies( @RequestParam(defaultValue = "title") String sortBy, @Parameter(description = "Sort order: 'asc' or 'desc' (default: asc)") @RequestParam(defaultValue = "asc") String sortOrder) { - + + // The sample_mflix dataset only contains movies up to 2015 + final int MAX_DATASET_YEAR = 2015; + final int MIN_VALID_YEAR = 1800; + String yearWarning = null; + + // Validate year if provided + if (year != null) { + if (year < MIN_VALID_YEAR) { + throw new ValidationException( + String.format("Invalid year: %d. Year must be %d or later.", year, MIN_VALID_YEAR) + ); + } + if (year > MAX_DATASET_YEAR) { + yearWarning = String.format( + "Note: The sample_mflix dataset only contains movies up to %d. Your search for year %d may return no results.", + MAX_DATASET_YEAR, year + ); + } + } + MovieSearchQuery query = MovieSearchQuery.builder() .q(q) .genre(genre) @@ -97,16 +118,22 @@ public ResponseEntity>> getAllMovies( .sortBy(sortBy) .sortOrder(sortOrder) .build(); - + List movies = movieService.getAllMovies(query); - + + // Build response message, including year warning if applicable + String message = "Found " + movies.size() + " movies"; + if (yearWarning != null) { + message = message + ". " + yearWarning; + } + SuccessResponse> response = SuccessResponse.>builder() .success(true) - .message("Found " + movies.size() + " movies") + .message(message) .data(movies) .timestamp(Instant.now().toString()) .build(); - + return ResponseEntity.ok(response); } diff --git a/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java index 1add6ed..be1a7ec 100644 --- a/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java +++ b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java @@ -125,6 +125,47 @@ void testGetAllMovies_WithQueryParams() throws Exception { .andExpect(jsonPath("$.data").isArray()); } + @Test + @DisplayName("GET /api/movies - Should return 400 for year before 1800") + void testGetAllMovies_InvalidYearBefore1800() throws Exception { + // Act & Assert + mockMvc.perform(get("/api/movies") + .param("year", "1700")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")); + } + + @Test + @DisplayName("GET /api/movies - Should include warning for year after 2015") + void testGetAllMovies_YearAfter2015IncludesWarning() throws Exception { + // Arrange + List movies = Arrays.asList(); + when(movieService.getAllMovies(any(MovieSearchQuery.class))).thenReturn(movies); + + // Act & Assert + mockMvc.perform(get("/api/movies") + .param("year", "2020")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message", containsString("2015"))); + } + + @Test + @DisplayName("GET /api/movies - Should not include warning for year 2015 or earlier") + void testGetAllMovies_Year2015NoWarning() throws Exception { + // Arrange + List movies = Arrays.asList(testMovie); + when(movieService.getAllMovies(any(MovieSearchQuery.class))).thenReturn(movies); + + // Act & Assert + mockMvc.perform(get("/api/movies") + .param("year", "2015")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message", not(containsString("sample_mflix")))); + } + // ==================== GET MOVIE BY ID TESTS ==================== @Test diff --git a/mflix/server/js-express/src/controllers/movieController.ts b/mflix/server/js-express/src/controllers/movieController.ts index 78437ea..294612c 100644 --- a/mflix/server/js-express/src/controllers/movieController.ts +++ b/mflix/server/js-express/src/controllers/movieController.ts @@ -87,9 +87,32 @@ export async function getAllMovies(req: Request, res: Response): Promise { filter.genres = { $regex: new RegExp(genre, "i") }; } - // Year filtering + // Year filtering and validation + // The sample_mflix dataset only contains movies up to 2015 + const MAX_DATASET_YEAR = 2015; + const MIN_VALID_YEAR = 1800; + let yearWarning: string | undefined; + if (year) { - filter.year = parseInt(year); + const yearNum = parseInt(year); + + // Validate year is within reasonable bounds + if (yearNum < MIN_VALID_YEAR) { + res.status(400).json( + createErrorResponse( + `Invalid year: ${yearNum}. Year must be ${MIN_VALID_YEAR} or later.`, + "INVALID_YEAR" + ) + ); + return; + } + + // Warn if searching for years beyond the dataset's range + if (yearNum > MAX_DATASET_YEAR) { + yearWarning = `Note: The sample_mflix dataset only contains movies up to ${MAX_DATASET_YEAR}. Your search for year ${yearNum} may return no results.`; + } + + filter.year = yearNum; } // Rating range filtering @@ -131,8 +154,14 @@ export async function getAllMovies(req: Request, res: Response): Promise { .skip(skipNum) .toArray(); + // Build response message, including year warning if applicable + let message = `Found ${movies.length} movies`; + if (yearWarning) { + message = `${message}. ${yearWarning}`; + } + // Return successful response - res.json(createSuccessResponse(movies, `Found ${movies.length} movies`)); + res.json(createSuccessResponse(movies, message)); } /** diff --git a/mflix/server/js-express/tests/controllers/movieController.test.ts b/mflix/server/js-express/tests/controllers/movieController.test.ts index f6d5b2e..a77c44e 100644 --- a/mflix/server/js-express/tests/controllers/movieController.test.ts +++ b/mflix/server/js-express/tests/controllers/movieController.test.ts @@ -183,7 +183,7 @@ describe("Movie Controller Tests", () => { const testMovies = [{ _id: TEST_MOVIE_ID, title: "Action Movie" }]; mockRequest.query = { genre: "Action", - year: "2024", + year: "2010", minRating: "7.0", limit: "10", sortBy: "year", @@ -195,7 +195,7 @@ describe("Movie Controller Tests", () => { expect(mockFind).toHaveBeenCalledWith({ genres: { $regex: new RegExp("Action", "i") }, - year: 2024, + year: 2010, "imdb.rating": { $gte: 7.0 }, }); expect(mockCreateSuccessResponse).toHaveBeenCalledWith( @@ -203,6 +203,45 @@ describe("Movie Controller Tests", () => { "Found 1 movies" ); }); + + it("should return 400 for year before 1800", async () => { + mockRequest.query = { year: "1700" }; + + await getAllMovies(mockRequest as Request, mockResponse as Response); + + expect(mockStatus).toHaveBeenCalledWith(400); + expect(mockCreateErrorResponse).toHaveBeenCalledWith( + "Invalid year: 1700. Year must be 1800 or later.", + "INVALID_YEAR" + ); + }); + + it("should include warning message for year after 2015", async () => { + mockRequest.query = { year: "2020" }; + mockToArray.mockResolvedValue([]); + + await getAllMovies(mockRequest as Request, mockResponse as Response); + + expect(mockFind).toHaveBeenCalledWith({ year: 2020 }); + expect(mockCreateSuccessResponse).toHaveBeenCalledWith( + [], + "Found 0 movies. Note: The sample_mflix dataset only contains movies up to 2015. Your search for year 2020 may return no results." + ); + }); + + it("should not include warning message for year 2015 or earlier", async () => { + const testMovies = [{ _id: TEST_MOVIE_ID, title: "Old Movie" }]; + mockRequest.query = { year: "2015" }; + mockToArray.mockResolvedValue(testMovies); + + await getAllMovies(mockRequest as Request, mockResponse as Response); + + expect(mockFind).toHaveBeenCalledWith({ year: 2015 }); + expect(mockCreateSuccessResponse).toHaveBeenCalledWith( + testMovies, + "Found 1 movies" + ); + }); }); describe("getMovieById", () => { diff --git a/mflix/server/python-fastapi/src/routers/movies.py b/mflix/server/python-fastapi/src/routers/movies.py index bb30c32..76843de 100644 --- a/mflix/server/python-fastapi/src/routers/movies.py +++ b/mflix/server/python-fastapi/src/routers/movies.py @@ -546,6 +546,11 @@ async def get_all_movies( sort_by:str = Query(default="title", alias="sortBy"), sort_order:str = Query(default="asc", alias="sortOrder") ): + # The sample_mflix dataset only contains movies up to 2015 + MAX_DATASET_YEAR = 2015 + MIN_VALID_YEAR = 1800 + year_warning = None + movies_collection = get_collection("movies") filter_dict = {} if q: @@ -554,7 +559,16 @@ async def get_all_movies( filter_dict["title"] = {"$regex": title, "$options": "i"} if genre: filter_dict["genres"] = {"$regex": genre, "$options": "i"} - if year: + if isinstance(year, int): + # Validate year is within reasonable bounds + if year < MIN_VALID_YEAR: + raise HTTPException( + status_code=400, + detail=f"Invalid year: {year}. Year must be {MIN_VALID_YEAR} or later." + ) + # Warn if searching for years beyond the dataset's range + if year > MAX_DATASET_YEAR: + year_warning = f"Note: The sample_mflix dataset only contains movies up to {MAX_DATASET_YEAR}. Your search for year {year} may return no results." filter_dict["year"] = year if min_rating is not None or max_rating is not None: rating_filter = {} @@ -593,8 +607,14 @@ async def get_all_movies( movie["year"] = None movies.append(movie) + + # Build response message, including year warning if applicable + message = f"Found {len(movies)} movies." + if year_warning: + message = f"{message} {year_warning}" + # Return the results wrapped in a SuccessResponse - return create_success_response(movies, f"Found {len(movies)} movies.") + return create_success_response(movies, message) """ POST /api/movies/ diff --git a/mflix/server/python-fastapi/tests/test_movie_routes.py b/mflix/server/python-fastapi/tests/test_movie_routes.py index 5d74779..58862c4 100644 --- a/mflix/server/python-fastapi/tests/test_movie_routes.py +++ b/mflix/server/python-fastapi/tests/test_movie_routes.py @@ -394,13 +394,73 @@ async def test_get_all_movies_database_error(self, mock_get_collection): # Call the route handler from src.routers.movies import get_all_movies - with pytest.raises(HTTPException) as e: + with pytest.raises(HTTPException) as e: await get_all_movies() # Assertions assert e.value.status_code == 500 assert "error" in str(e.value.detail.lower()) + async def test_get_all_movies_invalid_year_before_1800(self): + """Should return 400 error when year is before 1800.""" + from src.routers.movies import get_all_movies + + with pytest.raises(HTTPException) as e: + await get_all_movies(year=1700) + + assert e.value.status_code == 400 + assert "1800" in str(e.value.detail) + assert "1700" in str(e.value.detail) + + @patch('src.routers.movies.get_collection') + async def test_get_all_movies_year_after_2015_includes_warning(self, mock_get_collection): + """Should include warning message when searching for year after 2015.""" + # Setup mock with proper cursor chaining + mock_collection = MagicMock() + mock_cursor = MagicMock() + + mock_cursor.sort.return_value = mock_cursor + mock_cursor.skip.return_value = mock_cursor + mock_cursor.limit.return_value = mock_cursor + mock_cursor.__aiter__.return_value = iter([]) + + mock_collection.find.return_value = mock_cursor + mock_get_collection.return_value = mock_collection + + from src.routers.movies import get_all_movies + result = await get_all_movies(year=2020) + + # Assertions + assert result.success is True + assert "2015" in result.message + assert "2020" in result.message + assert "sample_mflix" in result.message.lower() + + @patch('src.routers.movies.get_collection') + async def test_get_all_movies_year_2015_no_warning(self, mock_get_collection): + """Should not include warning message when searching for year 2015 or earlier.""" + # Setup mock with proper cursor chaining + mock_collection = MagicMock() + mock_cursor = MagicMock() + + mock_cursor.sort.return_value = mock_cursor + mock_cursor.skip.return_value = mock_cursor + mock_cursor.limit.return_value = mock_cursor + mock_cursor.__aiter__.return_value = iter([ + {"_id": ObjectId(TEST_MOVIE_ID), "title": "Old Movie", "year": 2015} + ]) + + mock_collection.find.return_value = mock_cursor + mock_get_collection.return_value = mock_collection + + from src.routers.movies import get_all_movies + result = await get_all_movies(year=2015) + + # Assertions + assert result.success is True + assert "sample_mflix" not in result.message.lower() + assert "Found 1 movies" in result.message + @pytest.mark.unit @pytest.mark.asyncio From 556d9e50058f27f80904f59ea7be4b3fc9aac9a8 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Fri, 30 Jan 2026 09:26:50 -0500 Subject: [PATCH 2/7] Refactor year validation to use dynamic detection from database - Remove hardcoded year bounds (1800-2030) from all three backends - Backend aggregations now only validate year field is numeric (: number) - Add fetchYearBounds() on client to dynamically detect min/max years from data - Move dataset warning above Year field label in FilterBar - Update tests to reflect removed year bound validation Backends modified: Java Spring, Express, Python FastAPI Client modified: FilterBar.tsx, api.ts --- .../app/components/FilterBar/FilterBar.tsx | 50 ++++++++++++---- mflix/client/app/lib/api.ts | 21 ++++++- .../controller/MovieControllerImpl.java | 25 -------- .../samplemflix/service/MovieServiceImpl.java | 6 +- .../controller/MovieControllerTest.java | 41 ------------- .../src/controllers/movieController.ts | 41 ++----------- .../tests/controllers/movieController.test.ts | 38 ------------ .../python-fastapi/src/routers/movies.py | 26 ++------ .../python-fastapi/tests/test_movie_routes.py | 60 ------------------- 9 files changed, 70 insertions(+), 238 deletions(-) diff --git a/mflix/client/app/components/FilterBar/FilterBar.tsx b/mflix/client/app/components/FilterBar/FilterBar.tsx index 0711c7c..8f6b45d 100644 --- a/mflix/client/app/components/FilterBar/FilterBar.tsx +++ b/mflix/client/app/components/FilterBar/FilterBar.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useRef } from 'react'; import styles from './FilterBar.module.css'; -import { fetchGenres, type MovieFilterParams } from '@/lib/api'; +import { fetchGenres, fetchYearBounds, type MovieFilterParams } from '@/lib/api'; const SORT_OPTIONS = [ { value: 'title', label: 'Title' }, @@ -10,9 +10,6 @@ const SORT_OPTIONS = [ { value: 'imdb.rating', label: 'IMDB Rating' }, ]; -// The sample_mflix dataset only contains movies up to 2015 -const MAX_DATASET_YEAR = 2015; - interface FilterBarProps { onFilterChange: (filters: MovieFilterParams) => void; isLoading?: boolean; @@ -42,6 +39,8 @@ export default function FilterBar({ const [filters, setFilters] = useState(initialFilters); const [genres, setGenres] = useState([]); const [isLoadingGenres, setIsLoadingGenres] = useState(true); + const [maxDatasetYear, setMaxDatasetYear] = useState(null); + const [minDatasetYear, setMinDatasetYear] = useState(null); // Track previous initialFilters to detect changes const prevInitialFiltersRef = useRef(initialFilters); @@ -57,6 +56,28 @@ export default function FilterBar({ loadGenres(); }, []); + // Fetch year bounds from the API on mount + useEffect(() => { + async function loadYearBounds() { + console.log('FilterBar: Fetching year bounds...'); + const result = await fetchYearBounds(); + console.log('FilterBar: Year bounds result:', result); + if (result.success) { + if (result.maxYear) { + console.log('FilterBar: Setting maxDatasetYear to', result.maxYear); + setMaxDatasetYear(result.maxYear); + } + if (result.minYear) { + console.log('FilterBar: Setting minDatasetYear to', result.minYear); + setMinDatasetYear(result.minYear); + } + } else { + console.warn('FilterBar: Failed to fetch year bounds:', result.error); + } + } + loadYearBounds(); + }, []); + // Sync internal state when initialFilters changes (e.g. from URL navigation) useEffect(() => { if (!areFiltersEqual(prevInitialFiltersRef.current, initialFilters)) { @@ -137,22 +158,27 @@ export default function FilterBar({
+ {maxDatasetYear && filters.year && filters.year > maxDatasetYear && ( + + Dataset only contains movies up to {maxDatasetYear} + + )} + {minDatasetYear && filters.year && filters.year < minDatasetYear && ( + + Dataset only contains movies from {minDatasetYear} onwards + + )} MAX_DATASET_YEAR ? styles.inputWarning : ''}`} + className={`${styles.filterInput} ${(maxDatasetYear && filters.year && filters.year > maxDatasetYear) || (minDatasetYear && filters.year && filters.year < minDatasetYear) ? styles.inputWarning : ''}`} placeholder="e.g. 2010" value={filters.year || ''} onChange={(e) => handleFilterChange('year', e.target.value ? parseInt(e.target.value) : undefined)} disabled={isLoading} - min={1900} - max={2030} + min={minDatasetYear || undefined} + max={maxDatasetYear || undefined} /> - {filters.year && filters.year > MAX_DATASET_YEAR && ( - - Dataset only contains movies up to {MAX_DATASET_YEAR} - - )}
diff --git a/mflix/client/app/lib/api.ts b/mflix/client/app/lib/api.ts index 7ac8976..9fe4375 100644 --- a/mflix/client/app/lib/api.ts +++ b/mflix/client/app/lib/api.ts @@ -585,13 +585,30 @@ export async function fetchMoviesByYear(): Promise<{ success: boolean; error?: s error: 'Request timed out after 15 seconds' }; } - return { - success: false, + return { + success: false, error: error instanceof Error ? error.message : 'Network error occurred while fetching movies by year' }; } } +/** + * Fetch the min and max year bounds from the available movie data. + * This allows dynamic detection of the dataset's year range. + */ +export async function fetchYearBounds(): Promise<{ success: boolean; minYear?: number; maxYear?: number; error?: string }> { + const result = await fetchMoviesByYear(); + if (!result.success || !result.data || result.data.length === 0) { + return { success: false, error: result.error || 'No year data available' }; + } + const years = result.data.map(stat => stat.year); + return { + success: true, + minYear: Math.min(...years), + maxYear: Math.max(...years) + }; +} + /** * Fetch directors with most movies and their statistics */ diff --git a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java index 22d0c26..519f13b 100644 --- a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java +++ b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java @@ -1,6 +1,5 @@ package com.mongodb.samplemflix.controller; -import com.mongodb.samplemflix.exception.ValidationException; import com.mongodb.samplemflix.model.Movie; import com.mongodb.samplemflix.model.dto.BatchInsertResponse; import com.mongodb.samplemflix.model.dto.BatchUpdateResponse; @@ -87,26 +86,6 @@ public ResponseEntity>> getAllMovies( @Parameter(description = "Sort order: 'asc' or 'desc' (default: asc)") @RequestParam(defaultValue = "asc") String sortOrder) { - // The sample_mflix dataset only contains movies up to 2015 - final int MAX_DATASET_YEAR = 2015; - final int MIN_VALID_YEAR = 1800; - String yearWarning = null; - - // Validate year if provided - if (year != null) { - if (year < MIN_VALID_YEAR) { - throw new ValidationException( - String.format("Invalid year: %d. Year must be %d or later.", year, MIN_VALID_YEAR) - ); - } - if (year > MAX_DATASET_YEAR) { - yearWarning = String.format( - "Note: The sample_mflix dataset only contains movies up to %d. Your search for year %d may return no results.", - MAX_DATASET_YEAR, year - ); - } - } - MovieSearchQuery query = MovieSearchQuery.builder() .q(q) .genre(genre) @@ -121,11 +100,7 @@ public ResponseEntity>> getAllMovies( List movies = movieService.getAllMovies(query); - // Build response message, including year warning if applicable String message = "Found " + movies.size() + " movies"; - if (yearWarning != null) { - message = message + ". " + yearWarning; - } SuccessResponse> response = SuccessResponse.>builder() .success(true) diff --git a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index d293b23..3ccb9d5 100644 --- a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -376,7 +376,7 @@ public List getMoviesWithMostRecentComments(Integer lim int resultLimit = Math.clamp(limit != null ? limit : 10, 1, 50); // Build match criteria - Criteria matchCriteria = Criteria.where(Movie.Fields.YEAR).type(16).gte(1800).lte(2030); + Criteria matchCriteria = Criteria.where(Movie.Fields.YEAR).type(16); // Add movie ID filter if provided if (movieId != null && !movieId.trim().isEmpty()) { @@ -461,7 +461,7 @@ public List getMoviesByYearWithStats() { Aggregation aggregation = Aggregation.newAggregation( // STAGE 1: Match movies with valid year data Aggregation.match( - Criteria.where(Movie.Fields.YEAR).type(16).gte(1800).lte(2030) + Criteria.where(Movie.Fields.YEAR).type(16) ), // STAGE 2: Group by year and calculate statistics @@ -512,7 +512,7 @@ public List getDirectorsWithMostMovies(Integer limit) // STAGE 1: Match movies with directors and valid year Aggregation.match( Criteria.where(Movie.Fields.DIRECTORS).exists(true).ne(null).ne(List.of()) - .and(Movie.Fields.YEAR).type(16).gte(1800).lte(2030) + .and(Movie.Fields.YEAR).type(16) ), // STAGE 2: Unwind directors array diff --git a/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java index be1a7ec..1add6ed 100644 --- a/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java +++ b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java @@ -125,47 +125,6 @@ void testGetAllMovies_WithQueryParams() throws Exception { .andExpect(jsonPath("$.data").isArray()); } - @Test - @DisplayName("GET /api/movies - Should return 400 for year before 1800") - void testGetAllMovies_InvalidYearBefore1800() throws Exception { - // Act & Assert - mockMvc.perform(get("/api/movies") - .param("year", "1700")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.success").value(false)) - .andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR")); - } - - @Test - @DisplayName("GET /api/movies - Should include warning for year after 2015") - void testGetAllMovies_YearAfter2015IncludesWarning() throws Exception { - // Arrange - List movies = Arrays.asList(); - when(movieService.getAllMovies(any(MovieSearchQuery.class))).thenReturn(movies); - - // Act & Assert - mockMvc.perform(get("/api/movies") - .param("year", "2020")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message", containsString("2015"))); - } - - @Test - @DisplayName("GET /api/movies - Should not include warning for year 2015 or earlier") - void testGetAllMovies_Year2015NoWarning() throws Exception { - // Arrange - List movies = Arrays.asList(testMovie); - when(movieService.getAllMovies(any(MovieSearchQuery.class))).thenReturn(movies); - - // Act & Assert - mockMvc.perform(get("/api/movies") - .param("year", "2015")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message", not(containsString("sample_mflix")))); - } - // ==================== GET MOVIE BY ID TESTS ==================== @Test diff --git a/mflix/server/js-express/src/controllers/movieController.ts b/mflix/server/js-express/src/controllers/movieController.ts index 294612c..26801e4 100644 --- a/mflix/server/js-express/src/controllers/movieController.ts +++ b/mflix/server/js-express/src/controllers/movieController.ts @@ -87,32 +87,9 @@ export async function getAllMovies(req: Request, res: Response): Promise { filter.genres = { $regex: new RegExp(genre, "i") }; } - // Year filtering and validation - // The sample_mflix dataset only contains movies up to 2015 - const MAX_DATASET_YEAR = 2015; - const MIN_VALID_YEAR = 1800; - let yearWarning: string | undefined; - + // Year filtering if (year) { - const yearNum = parseInt(year); - - // Validate year is within reasonable bounds - if (yearNum < MIN_VALID_YEAR) { - res.status(400).json( - createErrorResponse( - `Invalid year: ${yearNum}. Year must be ${MIN_VALID_YEAR} or later.`, - "INVALID_YEAR" - ) - ); - return; - } - - // Warn if searching for years beyond the dataset's range - if (yearNum > MAX_DATASET_YEAR) { - yearWarning = `Note: The sample_mflix dataset only contains movies up to ${MAX_DATASET_YEAR}. Your search for year ${yearNum} may return no results.`; - } - - filter.year = yearNum; + filter.year = parseInt(year); } // Rating range filtering @@ -154,14 +131,8 @@ export async function getAllMovies(req: Request, res: Response): Promise { .skip(skipNum) .toArray(); - // Build response message, including year warning if applicable - let message = `Found ${movies.length} movies`; - if (yearWarning) { - message = `${message}. ${yearWarning}`; - } - // Return successful response - res.json(createSuccessResponse(movies, message)); + res.json(createSuccessResponse(movies, `Found ${movies.length} movies`)); } /** @@ -996,7 +967,7 @@ export async function getMoviesWithMostRecentComments( // the collection { $match: { - year: { $type: "number", $gte: 1800, $lte: 2030 }, + year: { $type: "number" }, }, }, ]; @@ -1134,7 +1105,7 @@ export async function getMoviesByYearWithStats( // STAGE 1: Data quality filter { $match: { - year: { $type: "number", $gte: 1800, $lte: 2030 }, + year: { $type: "number" }, }, }, // STAGE 2: Group by year and calculate statistics @@ -1238,7 +1209,7 @@ export async function getDirectorsWithMostMovies( { $match: { directors: { $exists: true, $ne: null, $not: { $eq: [] } }, - year: { $type: "number", $gte: 1800, $lte: 2030 }, + year: { $type: "number" }, }, }, // STAGE 2: Unwind directors array diff --git a/mflix/server/js-express/tests/controllers/movieController.test.ts b/mflix/server/js-express/tests/controllers/movieController.test.ts index a77c44e..433994b 100644 --- a/mflix/server/js-express/tests/controllers/movieController.test.ts +++ b/mflix/server/js-express/tests/controllers/movieController.test.ts @@ -204,44 +204,6 @@ describe("Movie Controller Tests", () => { ); }); - it("should return 400 for year before 1800", async () => { - mockRequest.query = { year: "1700" }; - - await getAllMovies(mockRequest as Request, mockResponse as Response); - - expect(mockStatus).toHaveBeenCalledWith(400); - expect(mockCreateErrorResponse).toHaveBeenCalledWith( - "Invalid year: 1700. Year must be 1800 or later.", - "INVALID_YEAR" - ); - }); - - it("should include warning message for year after 2015", async () => { - mockRequest.query = { year: "2020" }; - mockToArray.mockResolvedValue([]); - - await getAllMovies(mockRequest as Request, mockResponse as Response); - - expect(mockFind).toHaveBeenCalledWith({ year: 2020 }); - expect(mockCreateSuccessResponse).toHaveBeenCalledWith( - [], - "Found 0 movies. Note: The sample_mflix dataset only contains movies up to 2015. Your search for year 2020 may return no results." - ); - }); - - it("should not include warning message for year 2015 or earlier", async () => { - const testMovies = [{ _id: TEST_MOVIE_ID, title: "Old Movie" }]; - mockRequest.query = { year: "2015" }; - mockToArray.mockResolvedValue(testMovies); - - await getAllMovies(mockRequest as Request, mockResponse as Response); - - expect(mockFind).toHaveBeenCalledWith({ year: 2015 }); - expect(mockCreateSuccessResponse).toHaveBeenCalledWith( - testMovies, - "Found 1 movies" - ); - }); }); describe("getMovieById", () => { diff --git a/mflix/server/python-fastapi/src/routers/movies.py b/mflix/server/python-fastapi/src/routers/movies.py index 76843de..1da9b6a 100644 --- a/mflix/server/python-fastapi/src/routers/movies.py +++ b/mflix/server/python-fastapi/src/routers/movies.py @@ -546,11 +546,6 @@ async def get_all_movies( sort_by:str = Query(default="title", alias="sortBy"), sort_order:str = Query(default="asc", alias="sortOrder") ): - # The sample_mflix dataset only contains movies up to 2015 - MAX_DATASET_YEAR = 2015 - MIN_VALID_YEAR = 1800 - year_warning = None - movies_collection = get_collection("movies") filter_dict = {} if q: @@ -560,15 +555,6 @@ async def get_all_movies( if genre: filter_dict["genres"] = {"$regex": genre, "$options": "i"} if isinstance(year, int): - # Validate year is within reasonable bounds - if year < MIN_VALID_YEAR: - raise HTTPException( - status_code=400, - detail=f"Invalid year: {year}. Year must be {MIN_VALID_YEAR} or later." - ) - # Warn if searching for years beyond the dataset's range - if year > MAX_DATASET_YEAR: - year_warning = f"Note: The sample_mflix dataset only contains movies up to {MAX_DATASET_YEAR}. Your search for year {year} may return no results." filter_dict["year"] = year if min_rating is not None or max_rating is not None: rating_filter = {} @@ -608,12 +594,8 @@ async def get_all_movies( movies.append(movie) - # Build response message, including year warning if applicable - message = f"Found {len(movies)} movies." - if year_warning: - message = f"{message} {year_warning}" - # Return the results wrapped in a SuccessResponse + message = f"Found {len(movies)} movies." return create_success_response(movies, message) """ @@ -1034,7 +1016,7 @@ async def aggregate_movies_recent_commented( # Filter movies to only those with valid year data { "$match": { - "year": {"$type": "number", "$gte": 1800, "$lte": 2030} + "year": {"$type": "number"} } } ] @@ -1180,7 +1162,7 @@ async def aggregate_movies_by_year(): # Tip: Filter early to reduce dataset size and improve performance { "$match": { - "year": {"$type": "number", "$gte": 1800, "$lte": 2030} + "year": {"$type": "number"} } }, @@ -1309,7 +1291,7 @@ async def aggregate_directors_most_movies( { "$match": { "directors": {"$exists": True, "$ne": None, "$ne": []}, # Has directors array - "year": {"$type": "number", "$gte": 1800, "$lte": 2030} # Valid year range + "year": {"$type": "number"} # Valid year (numeric) } }, diff --git a/mflix/server/python-fastapi/tests/test_movie_routes.py b/mflix/server/python-fastapi/tests/test_movie_routes.py index 58862c4..8bb335d 100644 --- a/mflix/server/python-fastapi/tests/test_movie_routes.py +++ b/mflix/server/python-fastapi/tests/test_movie_routes.py @@ -401,66 +401,6 @@ async def test_get_all_movies_database_error(self, mock_get_collection): assert e.value.status_code == 500 assert "error" in str(e.value.detail.lower()) - async def test_get_all_movies_invalid_year_before_1800(self): - """Should return 400 error when year is before 1800.""" - from src.routers.movies import get_all_movies - - with pytest.raises(HTTPException) as e: - await get_all_movies(year=1700) - - assert e.value.status_code == 400 - assert "1800" in str(e.value.detail) - assert "1700" in str(e.value.detail) - - @patch('src.routers.movies.get_collection') - async def test_get_all_movies_year_after_2015_includes_warning(self, mock_get_collection): - """Should include warning message when searching for year after 2015.""" - # Setup mock with proper cursor chaining - mock_collection = MagicMock() - mock_cursor = MagicMock() - - mock_cursor.sort.return_value = mock_cursor - mock_cursor.skip.return_value = mock_cursor - mock_cursor.limit.return_value = mock_cursor - mock_cursor.__aiter__.return_value = iter([]) - - mock_collection.find.return_value = mock_cursor - mock_get_collection.return_value = mock_collection - - from src.routers.movies import get_all_movies - result = await get_all_movies(year=2020) - - # Assertions - assert result.success is True - assert "2015" in result.message - assert "2020" in result.message - assert "sample_mflix" in result.message.lower() - - @patch('src.routers.movies.get_collection') - async def test_get_all_movies_year_2015_no_warning(self, mock_get_collection): - """Should not include warning message when searching for year 2015 or earlier.""" - # Setup mock with proper cursor chaining - mock_collection = MagicMock() - mock_cursor = MagicMock() - - mock_cursor.sort.return_value = mock_cursor - mock_cursor.skip.return_value = mock_cursor - mock_cursor.limit.return_value = mock_cursor - mock_cursor.__aiter__.return_value = iter([ - {"_id": ObjectId(TEST_MOVIE_ID), "title": "Old Movie", "year": 2015} - ]) - - mock_collection.find.return_value = mock_cursor - mock_get_collection.return_value = mock_collection - - from src.routers.movies import get_all_movies - result = await get_all_movies(year=2015) - - # Assertions - assert result.success is True - assert "sample_mflix" not in result.message.lower() - assert "Found 1 movies" in result.message - @pytest.mark.unit @pytest.mark.asyncio From 9be645df93245290851e18200eefbe91796b46c1 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Fri, 30 Jan 2026 10:26:41 -0500 Subject: [PATCH 3/7] Fix Express CI tests: add directConnection=true to MongoDB URI The Atlas CLI local deployment creates a replica set (myLocalRs1) that advertises hostname 'mylocalrs1'. Without directConnection=true, the MongoDB driver attempts server discovery and fails to resolve this hostname in CI, causing all tests to fail with: MongoServerSelectionError: getaddrinfo EAI_AGAIN mylocalrs1 --- .github/workflows/run-express-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-express-tests.yml b/.github/workflows/run-express-tests.yml index d434da2..90672cf 100644 --- a/.github/workflows/run-express-tests.yml +++ b/.github/workflows/run-express-tests.yml @@ -54,13 +54,13 @@ jobs: working-directory: mflix/server/js-express run: npm run test:unit -- --json --outputFile=test-results-unit.json || true env: - MONGODB_URI: mongodb://localhost:27017/sample_mflix + MONGODB_URI: mongodb://localhost:27017/sample_mflix?directConnection=true - name: Run integration tests working-directory: mflix/server/js-express run: npm run test:integration -- --json --outputFile=test-results-integration.json || true env: - MONGODB_URI: mongodb://localhost:27017/sample_mflix + MONGODB_URI: mongodb://localhost:27017/sample_mflix?directConnection=true ENABLE_SEARCH_TESTS: true # Note: Vector search tests will be skipped without VOYAGE_API_KEY # Run these tests locally with a valid API key From 56a2f3c3754f3b17bc932b82b3101c8e8d540dab Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Fri, 30 Jan 2026 11:04:39 -0500 Subject: [PATCH 4/7] Fix generate-test-summary-jest.sh: quote GITHUB_STEP_SUMMARY variable - Quote all references to $GITHUB_STEP_SUMMARY to prevent bash syntax errors when the variable is empty or unset - Add guard at script start to gracefully skip summary generation if GITHUB_STEP_SUMMARY is not set This fixes the 'Generate Test Summary' step failure in CI where unquoted variable references caused 'echo >> ' syntax errors with set -e enabled. --- .github/scripts/generate-test-summary-jest.sh | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/.github/scripts/generate-test-summary-jest.sh b/.github/scripts/generate-test-summary-jest.sh index 48e86bf..cb40ff7 100644 --- a/.github/scripts/generate-test-summary-jest.sh +++ b/.github/scripts/generate-test-summary-jest.sh @@ -5,11 +5,17 @@ set -e # Shows breakdown by test type (unit vs integration) # Usage: ./generate-test-summary-jest.sh +# Guard: skip if GITHUB_STEP_SUMMARY is not set +if [ -z "$GITHUB_STEP_SUMMARY" ]; then + echo "Warning: GITHUB_STEP_SUMMARY not set, skipping summary generation" + exit 0 +fi + UNIT_JSON="${1:-}" INTEGRATION_JSON="${2:-}" -echo "## Test Results" >> $GITHUB_STEP_SUMMARY -echo "" >> $GITHUB_STEP_SUMMARY +echo "## Test Results" >> "$GITHUB_STEP_SUMMARY" +echo "" >> "$GITHUB_STEP_SUMMARY" # Function to parse Jest JSON file parse_json() { @@ -55,37 +61,37 @@ total_failed=$((unit_failed + int_failed)) total_skipped=$((unit_skipped + int_skipped)) # Display detailed breakdown -echo "### Summary by Test Type" >> $GITHUB_STEP_SUMMARY -echo "" >> $GITHUB_STEP_SUMMARY -echo "| Test Type | Passed | Failed | Skipped | Total |" >> $GITHUB_STEP_SUMMARY -echo "|-----------|--------|--------|---------|-------|" >> $GITHUB_STEP_SUMMARY +echo "### Summary by Test Type" >> "$GITHUB_STEP_SUMMARY" +echo "" >> "$GITHUB_STEP_SUMMARY" +echo "| Test Type | Passed | Failed | Skipped | Total |" >> "$GITHUB_STEP_SUMMARY" +echo "|-----------|--------|--------|---------|-------|" >> "$GITHUB_STEP_SUMMARY" if [ -f "$UNIT_JSON" ]; then - echo "| 🔧 Unit Tests | $unit_passed | $unit_failed | $unit_skipped | $unit_tests |" >> $GITHUB_STEP_SUMMARY + echo "| 🔧 Unit Tests | $unit_passed | $unit_failed | $unit_skipped | $unit_tests |" >> "$GITHUB_STEP_SUMMARY" fi if [ -f "$INTEGRATION_JSON" ]; then - echo "| 🔗 Integration Tests | $int_passed | $int_failed | $int_skipped | $int_tests |" >> $GITHUB_STEP_SUMMARY + echo "| 🔗 Integration Tests | $int_passed | $int_failed | $int_skipped | $int_tests |" >> "$GITHUB_STEP_SUMMARY" fi -echo "| **Total** | **$total_passed** | **$total_failed** | **$total_skipped** | **$total_tests** |" >> $GITHUB_STEP_SUMMARY -echo "" >> $GITHUB_STEP_SUMMARY +echo "| **Total** | **$total_passed** | **$total_failed** | **$total_skipped** | **$total_tests** |" >> "$GITHUB_STEP_SUMMARY" +echo "" >> "$GITHUB_STEP_SUMMARY" # Overall status -echo "### Overall Status" >> $GITHUB_STEP_SUMMARY -echo "" >> $GITHUB_STEP_SUMMARY -echo "| Status | Count |" >> $GITHUB_STEP_SUMMARY -echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY -echo "| ✅ Passed | $total_passed |" >> $GITHUB_STEP_SUMMARY -echo "| ❌ Failed | $total_failed |" >> $GITHUB_STEP_SUMMARY -echo "| ⏭️ Skipped | $total_skipped |" >> $GITHUB_STEP_SUMMARY -echo "| **Total** | **$total_tests** |" >> $GITHUB_STEP_SUMMARY -echo "" >> $GITHUB_STEP_SUMMARY +echo "### Overall Status" >> "$GITHUB_STEP_SUMMARY" +echo "" >> "$GITHUB_STEP_SUMMARY" +echo "| Status | Count |" >> "$GITHUB_STEP_SUMMARY" +echo "|--------|-------|" >> "$GITHUB_STEP_SUMMARY" +echo "| ✅ Passed | $total_passed |" >> "$GITHUB_STEP_SUMMARY" +echo "| ❌ Failed | $total_failed |" >> "$GITHUB_STEP_SUMMARY" +echo "| ⏭️ Skipped | $total_skipped |" >> "$GITHUB_STEP_SUMMARY" +echo "| **Total** | **$total_tests** |" >> "$GITHUB_STEP_SUMMARY" +echo "" >> "$GITHUB_STEP_SUMMARY" # List failed tests if any if [ $total_failed -gt 0 ]; then - echo "### ❌ Failed Tests" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Failed Tests" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" failed_tests_file=$(mktemp) @@ -107,16 +113,16 @@ if [ $total_failed -gt 0 ]; then if [ -s "$failed_tests_file" ]; then while IFS= read -r test; do - echo "- \`$test\`" >> $GITHUB_STEP_SUMMARY + echo "- \`$test\`" >> "$GITHUB_STEP_SUMMARY" done < "$failed_tests_file" else - echo "_Unable to parse individual test names_" >> $GITHUB_STEP_SUMMARY + echo "_Unable to parse individual test names_" >> "$GITHUB_STEP_SUMMARY" fi - echo "" >> $GITHUB_STEP_SUMMARY - echo "❌ **Tests failed!**" >> $GITHUB_STEP_SUMMARY + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "❌ **Tests failed!**" >> "$GITHUB_STEP_SUMMARY" rm -f "$failed_tests_file" exit 1 else - echo "✅ **All tests passed!**" >> $GITHUB_STEP_SUMMARY + echo "✅ **All tests passed!**" >> "$GITHUB_STEP_SUMMARY" fi From 819f84e67004ccd81e72a6f25c5afe02fbc19f7d Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Fri, 30 Jan 2026 11:25:20 -0500 Subject: [PATCH 5/7] Add index creation step to CI for aggregation performance Create index on comments.movie_id after data restore to fix the /reportingByComments aggregation timeout. Without this index, the $lookup operation performs a full collection scan (41K comments x 21K movies) causing 5+ minute response times in CI vs ~700ms locally. --- .github/workflows/run-express-tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/run-express-tests.yml b/.github/workflows/run-express-tests.yml index 90672cf..22f1075 100644 --- a/.github/workflows/run-express-tests.yml +++ b/.github/workflows/run-express-tests.yml @@ -41,6 +41,10 @@ jobs: - name: Add sample data to database run: mongorestore --archive=sampledata.archive --port=27017 + - name: Create indexes for aggregation performance + run: | + mongosh "mongodb://localhost:27017/sample_mflix?directConnection=true" --eval "db.comments.createIndex({ movie_id: 1 })" + - name: Set up Node.js uses: actions/setup-node@v4 with: From 680adf00ec2781caea383c07d5709b8068be35c7 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Fri, 30 Jan 2026 11:30:39 -0500 Subject: [PATCH 6/7] Install mongosh in CI workflow for index creation The previous commit added an index creation step that requires mongosh, but mongosh is not installed on the Ubuntu runner by default. This adds a step to download and install mongosh-2.3.8-linux-x64. --- .github/workflows/run-express-tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/run-express-tests.yml b/.github/workflows/run-express-tests.yml index 22f1075..eb1b2aa 100644 --- a/.github/workflows/run-express-tests.yml +++ b/.github/workflows/run-express-tests.yml @@ -41,6 +41,12 @@ jobs: - name: Add sample data to database run: mongorestore --archive=sampledata.archive --port=27017 + - name: Install MongoDB Shell (mongosh) + run: | + curl https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz -o mongosh.tgz + tar -xzf mongosh.tgz + sudo cp mongosh-2.3.8-linux-x64/bin/* /usr/local/bin/ + - name: Create indexes for aggregation performance run: | mongosh "mongodb://localhost:27017/sample_mflix?directConnection=true" --eval "db.comments.createIndex({ movie_id: 1 })" From 62d9c02830c3abbdd828a131ddc76eca70d43aba Mon Sep 17 00:00:00 2001 From: cory <115956901+cbullinger@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:34:29 -0500 Subject: [PATCH 7/7] Apply suggestions from code review Co-authored-by: Dachary --- mflix/README-JAVA-SPRING.md | 2 +- mflix/README-JAVASCRIPT-EXPRESS.md | 2 +- mflix/README-PYTHON-FASTAPI.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mflix/README-JAVA-SPRING.md b/mflix/README-JAVA-SPRING.md index 523d749..245afc5 100644 --- a/mflix/README-JAVA-SPRING.md +++ b/mflix/README-JAVA-SPRING.md @@ -16,7 +16,7 @@ This is a full-stack movie browsing application built with Java Spring Boot and ## Data Limitations -The `sample_mflix` dataset contains movies released up to **2015**. Searching for movies from 2016 or later will return no results. This is a limitation of the sample dataset, not the application. +The `sample_mflix` dataset contains movies released up to **2016**. Searching for movies from 2017 or later will return no results. This is a limitation of the sample dataset, not the application. ## Prerequisites diff --git a/mflix/README-JAVASCRIPT-EXPRESS.md b/mflix/README-JAVASCRIPT-EXPRESS.md index 0ecb764..f3c531c 100644 --- a/mflix/README-JAVASCRIPT-EXPRESS.md +++ b/mflix/README-JAVASCRIPT-EXPRESS.md @@ -16,7 +16,7 @@ This is a full-stack movie browsing application built with Express.js and Next.j ## Data Limitations -The `sample_mflix` dataset contains movies released up to **2015**. Searching for movies from 2016 or later will return no results. This is a limitation of the sample dataset, not the application. +The `sample_mflix` dataset contains movies released up to **2016**. Searching for movies from 2017 or later will return no results. This is a limitation of the sample dataset, not the application. ## Prerequisites diff --git a/mflix/README-PYTHON-FASTAPI.md b/mflix/README-PYTHON-FASTAPI.md index 1934680..3d79f3b 100644 --- a/mflix/README-PYTHON-FASTAPI.md +++ b/mflix/README-PYTHON-FASTAPI.md @@ -19,7 +19,7 @@ This is a full-stack movie browsing application built with Python FastAPI and Ne ## Data Limitations -The `sample_mflix` dataset contains movies released up to **2015**. Searching for movies from 2016 or later will return no results. This is a limitation of the sample dataset, not the application. +The `sample_mflix` dataset contains movies released up to **2016**. Searching for movies from 2017 or later will return no results. This is a limitation of the sample dataset, not the application. ## Prerequisites