Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 additions & 1 deletion mflix/README-JAVA-SPRING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
cbullinger marked this conversation as resolved.
Outdated

## 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/)
Expand Down
4 changes: 4 additions & 0 deletions mflix/README-JAVASCRIPT-EXPRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
cbullinger marked this conversation as resolved.
Outdated

## Prerequisites

- **Node.js 22** or higher
Expand Down
4 changes: 4 additions & 0 deletions mflix/README-PYTHON-FASTAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
cbullinger marked this conversation as resolved.
Outdated

## Prerequisites

- **Python 3.10** to **Python 3.13**
Expand Down
15 changes: 15 additions & 0 deletions mflix/client/app/components/FilterBar/FilterBar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 10 additions & 2 deletions mflix/client/app/components/FilterBar/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -137,14 +140,19 @@ export default function FilterBar({
<label className={styles.filterLabel}>Year</label>
<input
type="number"
className={styles.filterInput}
placeholder="e.g. 2020"
className={`${styles.filterInput} ${filters.year && filters.year > 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 && (
<span className={styles.yearWarning}>
Dataset only contains movies up to {MAX_DATASET_YEAR}
Comment thread
dacharyc marked this conversation as resolved.
Outdated
</span>
)}
</div>

<div className={styles.filterGroup}>
Expand Down
13 changes: 11 additions & 2 deletions mflix/client/app/components/MovieCard/MovieCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,9 +57,9 @@ export default function MovieCard({ movie, isSelected = false, onSelectionChange
)}

<div className={movieStyles.moviePoster}>
{movie.poster ? (
{isValidPosterUrl(movie.poster) ? (
<Image
src={movie.poster}
src={movie.poster!}
alt={`${movie.title} poster`}
fill
sizes="(max-width: 480px) 100vw, (max-width: 768px) 50vw, 280px"
Expand Down
11 changes: 10 additions & 1 deletion mflix/client/app/movie/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import { Movie } from '@/types/movie';
import { ROUTES } from '@/lib/constants';
import pageStyles from './page.module.css';

/**
* 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 MovieDetailsPageProps {
params: Promise<{
id: string;
Expand Down Expand Up @@ -202,7 +211,7 @@ export default function MovieDetailsPage({ params }: MovieDetailsPageProps) {
) : (
<div className={pageStyles.movieDetails}>
<div className={pageStyles.posterSection}>
{movie.poster ? (
{isValidPosterUrl(movie.poster) ? (
<div className={pageStyles.posterContainer}>
<Image
src={movie.poster!}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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;
Expand Down Expand Up @@ -85,7 +86,27 @@ public ResponseEntity<SuccessResponse<List<Movie>>> 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)
Expand All @@ -97,16 +118,22 @@ public ResponseEntity<SuccessResponse<List<Movie>>> getAllMovies(
.sortBy(sortBy)
.sortOrder(sortOrder)
.build();

List<Movie> 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<List<Movie>> response = SuccessResponse.<List<Movie>>builder()
.success(true)
.message("Found " + movies.size() + " movies")
.message(message)
.data(movies)
.timestamp(Instant.now().toString())
.build();

return ResponseEntity.ok(response);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Movie> 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<Movie> 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
Expand Down
35 changes: 32 additions & 3 deletions mflix/server/js-express/src/controllers/movieController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,32 @@ export async function getAllMovies(req: Request, res: Response): Promise<void> {
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;
Comment thread
dacharyc marked this conversation as resolved.
Outdated
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) {
Comment thread
dacharyc marked this conversation as resolved.
Outdated
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
Expand Down Expand Up @@ -131,8 +154,14 @@ export async function getAllMovies(req: Request, res: Response): Promise<void> {
.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));
}

/**
Expand Down
43 changes: 41 additions & 2 deletions mflix/server/js-express/tests/controllers/movieController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -195,14 +195,53 @@ 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(
testMovies,
"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", () => {
Expand Down
Loading