Skip to content

Commit dfdff26

Browse files
authored
Merge pull request #80 from mongodb/docsp-57250-movie-year
DOCSP-57250: Reflect dataset limitation around movie year (no data 2016+)
2 parents ce1e9b2 + 62d9c02 commit dfdff26

16 files changed

Lines changed: 175 additions & 58 deletions

File tree

.github/scripts/generate-test-summary-jest.sh

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ set -e
55
# Shows breakdown by test type (unit vs integration)
66
# Usage: ./generate-test-summary-jest.sh <unit-json> <integration-json>
77

8+
# Guard: skip if GITHUB_STEP_SUMMARY is not set
9+
if [ -z "$GITHUB_STEP_SUMMARY" ]; then
10+
echo "Warning: GITHUB_STEP_SUMMARY not set, skipping summary generation"
11+
exit 0
12+
fi
13+
814
UNIT_JSON="${1:-}"
915
INTEGRATION_JSON="${2:-}"
1016

11-
echo "## Test Results" >> $GITHUB_STEP_SUMMARY
12-
echo "" >> $GITHUB_STEP_SUMMARY
17+
echo "## Test Results" >> "$GITHUB_STEP_SUMMARY"
18+
echo "" >> "$GITHUB_STEP_SUMMARY"
1319

1420
# Function to parse Jest JSON file
1521
parse_json() {
@@ -55,37 +61,37 @@ total_failed=$((unit_failed + int_failed))
5561
total_skipped=$((unit_skipped + int_skipped))
5662

5763
# Display detailed breakdown
58-
echo "### Summary by Test Type" >> $GITHUB_STEP_SUMMARY
59-
echo "" >> $GITHUB_STEP_SUMMARY
60-
echo "| Test Type | Passed | Failed | Skipped | Total |" >> $GITHUB_STEP_SUMMARY
61-
echo "|-----------|--------|--------|---------|-------|" >> $GITHUB_STEP_SUMMARY
64+
echo "### Summary by Test Type" >> "$GITHUB_STEP_SUMMARY"
65+
echo "" >> "$GITHUB_STEP_SUMMARY"
66+
echo "| Test Type | Passed | Failed | Skipped | Total |" >> "$GITHUB_STEP_SUMMARY"
67+
echo "|-----------|--------|--------|---------|-------|" >> "$GITHUB_STEP_SUMMARY"
6268

6369
if [ -f "$UNIT_JSON" ]; then
64-
echo "| 🔧 Unit Tests | $unit_passed | $unit_failed | $unit_skipped | $unit_tests |" >> $GITHUB_STEP_SUMMARY
70+
echo "| 🔧 Unit Tests | $unit_passed | $unit_failed | $unit_skipped | $unit_tests |" >> "$GITHUB_STEP_SUMMARY"
6571
fi
6672

6773
if [ -f "$INTEGRATION_JSON" ]; then
68-
echo "| 🔗 Integration Tests | $int_passed | $int_failed | $int_skipped | $int_tests |" >> $GITHUB_STEP_SUMMARY
74+
echo "| 🔗 Integration Tests | $int_passed | $int_failed | $int_skipped | $int_tests |" >> "$GITHUB_STEP_SUMMARY"
6975
fi
7076

71-
echo "| **Total** | **$total_passed** | **$total_failed** | **$total_skipped** | **$total_tests** |" >> $GITHUB_STEP_SUMMARY
72-
echo "" >> $GITHUB_STEP_SUMMARY
77+
echo "| **Total** | **$total_passed** | **$total_failed** | **$total_skipped** | **$total_tests** |" >> "$GITHUB_STEP_SUMMARY"
78+
echo "" >> "$GITHUB_STEP_SUMMARY"
7379

7480
# Overall status
75-
echo "### Overall Status" >> $GITHUB_STEP_SUMMARY
76-
echo "" >> $GITHUB_STEP_SUMMARY
77-
echo "| Status | Count |" >> $GITHUB_STEP_SUMMARY
78-
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
79-
echo "| ✅ Passed | $total_passed |" >> $GITHUB_STEP_SUMMARY
80-
echo "| ❌ Failed | $total_failed |" >> $GITHUB_STEP_SUMMARY
81-
echo "| ⏭️ Skipped | $total_skipped |" >> $GITHUB_STEP_SUMMARY
82-
echo "| **Total** | **$total_tests** |" >> $GITHUB_STEP_SUMMARY
83-
echo "" >> $GITHUB_STEP_SUMMARY
81+
echo "### Overall Status" >> "$GITHUB_STEP_SUMMARY"
82+
echo "" >> "$GITHUB_STEP_SUMMARY"
83+
echo "| Status | Count |" >> "$GITHUB_STEP_SUMMARY"
84+
echo "|--------|-------|" >> "$GITHUB_STEP_SUMMARY"
85+
echo "| ✅ Passed | $total_passed |" >> "$GITHUB_STEP_SUMMARY"
86+
echo "| ❌ Failed | $total_failed |" >> "$GITHUB_STEP_SUMMARY"
87+
echo "| ⏭️ Skipped | $total_skipped |" >> "$GITHUB_STEP_SUMMARY"
88+
echo "| **Total** | **$total_tests** |" >> "$GITHUB_STEP_SUMMARY"
89+
echo "" >> "$GITHUB_STEP_SUMMARY"
8490

8591
# List failed tests if any
8692
if [ $total_failed -gt 0 ]; then
87-
echo "### ❌ Failed Tests" >> $GITHUB_STEP_SUMMARY
88-
echo "" >> $GITHUB_STEP_SUMMARY
93+
echo "### ❌ Failed Tests" >> "$GITHUB_STEP_SUMMARY"
94+
echo "" >> "$GITHUB_STEP_SUMMARY"
8995

9096
failed_tests_file=$(mktemp)
9197

@@ -107,16 +113,16 @@ if [ $total_failed -gt 0 ]; then
107113

108114
if [ -s "$failed_tests_file" ]; then
109115
while IFS= read -r test; do
110-
echo "- \`$test\`" >> $GITHUB_STEP_SUMMARY
116+
echo "- \`$test\`" >> "$GITHUB_STEP_SUMMARY"
111117
done < "$failed_tests_file"
112118
else
113-
echo "_Unable to parse individual test names_" >> $GITHUB_STEP_SUMMARY
119+
echo "_Unable to parse individual test names_" >> "$GITHUB_STEP_SUMMARY"
114120
fi
115121

116-
echo "" >> $GITHUB_STEP_SUMMARY
117-
echo "❌ **Tests failed!**" >> $GITHUB_STEP_SUMMARY
122+
echo "" >> "$GITHUB_STEP_SUMMARY"
123+
echo "❌ **Tests failed!**" >> "$GITHUB_STEP_SUMMARY"
118124
rm -f "$failed_tests_file"
119125
exit 1
120126
else
121-
echo "✅ **All tests passed!**" >> $GITHUB_STEP_SUMMARY
127+
echo "✅ **All tests passed!**" >> "$GITHUB_STEP_SUMMARY"
122128
fi

.github/workflows/run-express-tests.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ jobs:
4141
- name: Add sample data to database
4242
run: mongorestore --archive=sampledata.archive --port=27017
4343

44+
- name: Install MongoDB Shell (mongosh)
45+
run: |
46+
curl https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz -o mongosh.tgz
47+
tar -xzf mongosh.tgz
48+
sudo cp mongosh-2.3.8-linux-x64/bin/* /usr/local/bin/
49+
50+
- name: Create indexes for aggregation performance
51+
run: |
52+
mongosh "mongodb://localhost:27017/sample_mflix?directConnection=true" --eval "db.comments.createIndex({ movie_id: 1 })"
53+
4454
- name: Set up Node.js
4555
uses: actions/setup-node@v4
4656
with:
@@ -54,13 +64,13 @@ jobs:
5464
working-directory: mflix/server/js-express
5565
run: npm run test:unit -- --json --outputFile=test-results-unit.json || true
5666
env:
57-
MONGODB_URI: mongodb://localhost:27017/sample_mflix
67+
MONGODB_URI: mongodb://localhost:27017/sample_mflix?directConnection=true
5868

5969
- name: Run integration tests
6070
working-directory: mflix/server/js-express
6171
run: npm run test:integration -- --json --outputFile=test-results-integration.json || true
6272
env:
63-
MONGODB_URI: mongodb://localhost:27017/sample_mflix
73+
MONGODB_URI: mongodb://localhost:27017/sample_mflix?directConnection=true
6474
ENABLE_SEARCH_TESTS: true
6575
# Note: Vector search tests will be skipped without VOYAGE_API_KEY
6676
# Run these tests locally with a valid API key

mflix/README-JAVA-SPRING.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ This is a full-stack movie browsing application built with Java Spring Boot and
1414
└── mvnw
1515
```
1616

17+
## Data Limitations
18+
19+
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.
20+
1721
## Prerequisites
1822

1923
- **Java 21** or higher
2024
- **Node.js 20** or higher
2125
- **MongoDB Atlas cluster or local deployment** with the `sample_mflix` dataset loaded
22-
- [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/)
26+
- [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/)
2327
- **Maven** (included via Maven Wrapper)
2428
- **Voyage AI API key** (For MongoDB Vector Search)
2529
- [Get a Voyage AI API key](https://www.voyageai.com/)

mflix/README-JAVASCRIPT-EXPRESS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ This is a full-stack movie browsing application built with Express.js and Next.j
1414
└── tsconfig.json
1515
```
1616

17+
## Data Limitations
18+
19+
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.
20+
1721
## Prerequisites
1822

1923
- **Node.js 22** or higher

mflix/README-PYTHON-FASTAPI.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ This is a full-stack movie browsing application built with Python FastAPI and Ne
1717
└── requirements.txt
1818
```
1919

20+
## Data Limitations
21+
22+
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.
23+
2024
## Prerequisites
2125

2226
- **Python 3.10** to **Python 3.13**

mflix/client/app/components/FilterBar/FilterBar.module.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@
9393
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
9494
}
9595

96+
.inputWarning {
97+
border-color: #f59e0b;
98+
}
99+
100+
.inputWarning:focus {
101+
border-color: #f59e0b;
102+
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.15);
103+
}
104+
105+
.yearWarning {
106+
font-size: 0.7rem;
107+
color: #b45309;
108+
margin-top: 0.25rem;
109+
}
110+
96111
.ratingGroup {
97112
display: flex;
98113
align-items: center;

mflix/client/app/components/FilterBar/FilterBar.tsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState, useCallback, useEffect, useRef } from 'react';
44
import styles from './FilterBar.module.css';
5-
import { fetchGenres, type MovieFilterParams } from '@/lib/api';
5+
import { fetchGenres, fetchYearBounds, type MovieFilterParams } from '@/lib/api';
66

77
const SORT_OPTIONS = [
88
{ value: 'title', label: 'Title' },
@@ -39,6 +39,8 @@ export default function FilterBar({
3939
const [filters, setFilters] = useState<MovieFilterParams>(initialFilters);
4040
const [genres, setGenres] = useState<string[]>([]);
4141
const [isLoadingGenres, setIsLoadingGenres] = useState(true);
42+
const [maxDatasetYear, setMaxDatasetYear] = useState<number | null>(null);
43+
const [minDatasetYear, setMinDatasetYear] = useState<number | null>(null);
4244

4345
// Track previous initialFilters to detect changes
4446
const prevInitialFiltersRef = useRef<MovieFilterParams>(initialFilters);
@@ -54,6 +56,28 @@ export default function FilterBar({
5456
loadGenres();
5557
}, []);
5658

59+
// Fetch year bounds from the API on mount
60+
useEffect(() => {
61+
async function loadYearBounds() {
62+
console.log('FilterBar: Fetching year bounds...');
63+
const result = await fetchYearBounds();
64+
console.log('FilterBar: Year bounds result:', result);
65+
if (result.success) {
66+
if (result.maxYear) {
67+
console.log('FilterBar: Setting maxDatasetYear to', result.maxYear);
68+
setMaxDatasetYear(result.maxYear);
69+
}
70+
if (result.minYear) {
71+
console.log('FilterBar: Setting minDatasetYear to', result.minYear);
72+
setMinDatasetYear(result.minYear);
73+
}
74+
} else {
75+
console.warn('FilterBar: Failed to fetch year bounds:', result.error);
76+
}
77+
}
78+
loadYearBounds();
79+
}, []);
80+
5781
// Sync internal state when initialFilters changes (e.g. from URL navigation)
5882
useEffect(() => {
5983
if (!areFiltersEqual(prevInitialFiltersRef.current, initialFilters)) {
@@ -134,16 +158,26 @@ export default function FilterBar({
134158
</div>
135159

136160
<div className={styles.filterGroup}>
161+
{maxDatasetYear && filters.year && filters.year > maxDatasetYear && (
162+
<span className={styles.yearWarning}>
163+
Dataset only contains movies up to {maxDatasetYear}
164+
</span>
165+
)}
166+
{minDatasetYear && filters.year && filters.year < minDatasetYear && (
167+
<span className={styles.yearWarning}>
168+
Dataset only contains movies from {minDatasetYear} onwards
169+
</span>
170+
)}
137171
<label className={styles.filterLabel}>Year</label>
138172
<input
139173
type="number"
140-
className={styles.filterInput}
141-
placeholder="e.g. 2020"
174+
className={`${styles.filterInput} ${(maxDatasetYear && filters.year && filters.year > maxDatasetYear) || (minDatasetYear && filters.year && filters.year < minDatasetYear) ? styles.inputWarning : ''}`}
175+
placeholder="e.g. 2010"
142176
value={filters.year || ''}
143177
onChange={(e) => handleFilterChange('year', e.target.value ? parseInt(e.target.value) : undefined)}
144178
disabled={isLoading}
145-
min={1900}
146-
max={2030}
179+
min={minDatasetYear || undefined}
180+
max={maxDatasetYear || undefined}
147181
/>
148182
</div>
149183

mflix/client/app/components/MovieCard/MovieCard.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ import React from "react";
1414
* such as image error handling and selection checkbox.
1515
*/
1616

17+
/**
18+
* Validates that a poster URL is valid for Next.js Image component.
19+
* Must be an absolute URL (http/https) or a relative path starting with /
20+
*/
21+
const isValidPosterUrl = (url: string | undefined): boolean => {
22+
if (!url || typeof url !== 'string') return false;
23+
return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/');
24+
};
25+
1726
interface MovieCardProps {
1827
movie: Movie;
1928
isSelected?: boolean;
@@ -48,9 +57,9 @@ export default function MovieCard({ movie, isSelected = false, onSelectionChange
4857
)}
4958

5059
<div className={movieStyles.moviePoster}>
51-
{movie.poster ? (
60+
{isValidPosterUrl(movie.poster) ? (
5261
<Image
53-
src={movie.poster}
62+
src={movie.poster!}
5463
alt={`${movie.title} poster`}
5564
fill
5665
sizes="(max-width: 480px) 100vw, (max-width: 768px) 50vw, 280px"

mflix/client/app/lib/api.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -585,13 +585,30 @@ export async function fetchMoviesByYear(): Promise<{ success: boolean; error?: s
585585
error: 'Request timed out after 15 seconds'
586586
};
587587
}
588-
return {
589-
success: false,
588+
return {
589+
success: false,
590590
error: error instanceof Error ? error.message : 'Network error occurred while fetching movies by year'
591591
};
592592
}
593593
}
594594

595+
/**
596+
* Fetch the min and max year bounds from the available movie data.
597+
* This allows dynamic detection of the dataset's year range.
598+
*/
599+
export async function fetchYearBounds(): Promise<{ success: boolean; minYear?: number; maxYear?: number; error?: string }> {
600+
const result = await fetchMoviesByYear();
601+
if (!result.success || !result.data || result.data.length === 0) {
602+
return { success: false, error: result.error || 'No year data available' };
603+
}
604+
const years = result.data.map(stat => stat.year);
605+
return {
606+
success: true,
607+
minYear: Math.min(...years),
608+
maxYear: Math.max(...years)
609+
};
610+
}
611+
595612
/**
596613
* Fetch directors with most movies and their statistics
597614
*/

mflix/client/app/movie/[id]/page.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ import { Movie } from '@/types/movie';
1111
import { ROUTES } from '@/lib/constants';
1212
import pageStyles from './page.module.css';
1313

14+
/**
15+
* Validates that a poster URL is valid for Next.js Image component.
16+
* Must be an absolute URL (http/https) or a relative path starting with /
17+
*/
18+
const isValidPosterUrl = (url: string | undefined): boolean => {
19+
if (!url || typeof url !== 'string') return false;
20+
return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/');
21+
};
22+
1423
interface MovieDetailsPageProps {
1524
params: Promise<{
1625
id: string;
@@ -202,7 +211,7 @@ export default function MovieDetailsPage({ params }: MovieDetailsPageProps) {
202211
) : (
203212
<div className={pageStyles.movieDetails}>
204213
<div className={pageStyles.posterSection}>
205-
{movie.poster ? (
214+
{isValidPosterUrl(movie.poster) ? (
206215
<div className={pageStyles.posterContainer}>
207216
<Image
208217
src={movie.poster!}

0 commit comments

Comments
 (0)