Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
58 changes: 32 additions & 26 deletions .github/scripts/generate-test-summary-jest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ set -e
# Shows breakdown by test type (unit vs integration)
# Usage: ./generate-test-summary-jest.sh <unit-json> <integration-json>

# 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() {
Expand Down Expand Up @@ -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)

Expand All @@ -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
14 changes: 12 additions & 2 deletions .github/workflows/run-express-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ 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 })"

- name: Set up Node.js
uses: actions/setup-node@v4
with:
Expand All @@ -54,13 +64,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
Expand Down
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 **2016**. Searching for movies from 2017 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/)
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 **2016**. Searching for movies from 2017 or later will return no results. This is a limitation of the sample dataset, not the application.

## 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 **2016**. Searching for movies from 2017 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**
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
44 changes: 39 additions & 5 deletions mflix/client/app/components/FilterBar/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -39,6 +39,8 @@ export default function FilterBar({
const [filters, setFilters] = useState<MovieFilterParams>(initialFilters);
const [genres, setGenres] = useState<string[]>([]);
const [isLoadingGenres, setIsLoadingGenres] = useState(true);
const [maxDatasetYear, setMaxDatasetYear] = useState<number | null>(null);
const [minDatasetYear, setMinDatasetYear] = useState<number | null>(null);

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

// Fetch year bounds from the API on mount
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

Love this approach - seems much cleaner.

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)) {
Expand Down Expand Up @@ -134,16 +158,26 @@ export default function FilterBar({
</div>

<div className={styles.filterGroup}>
{maxDatasetYear && filters.year && filters.year > maxDatasetYear && (
<span className={styles.yearWarning}>
Dataset only contains movies up to {maxDatasetYear}
</span>
)}
{minDatasetYear && filters.year && filters.year < minDatasetYear && (
<span className={styles.yearWarning}>
Dataset only contains movies from {minDatasetYear} onwards
</span>
)}
<label className={styles.filterLabel}>Year</label>
<input
type="number"
className={styles.filterInput}
placeholder="e.g. 2020"
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}
/>
</div>

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
21 changes: 19 additions & 2 deletions mflix/client/app/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
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
Expand Up @@ -85,7 +85,7 @@ 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) {

MovieSearchQuery query = MovieSearchQuery.builder()
.q(q)
.genre(genre)
Expand All @@ -97,16 +97,18 @@ public ResponseEntity<SuccessResponse<List<Movie>>> getAllMovies(
.sortBy(sortBy)
.sortOrder(sortOrder)
.build();

List<Movie> movies = movieService.getAllMovies(query);


String message = "Found " + movies.size() + " movies";

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
Loading