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
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,26 @@ public ResponseEntity<SuccessResponse<List<Movie>>> getAllMovies(

return ResponseEntity.ok(response);
}


@Operation(
summary = "Get all distinct genres",
description = "Retrieve a list of all unique genre values from the movies collection. " +
"Demonstrates the distinct() operation. Returns genres sorted alphabetically."
)
@GetMapping("/genres")
public ResponseEntity<SuccessResponse<List<String>>> getDistinctGenres() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we add tests for this to the test target? Looks like we have both Controller and Service tests there where we may want to consider adding this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

added

List<String> genres = movieService.getDistinctGenres();

SuccessResponse<List<String>> response = SuccessResponse.<List<String>>builder()
.success(true)
.message("Found " + genres.size() + " distinct genres")
.data(genres)
.timestamp(Instant.now().toString())
.build();

return ResponseEntity.ok(response);
}

@Operation(
summary = "Get a single movie by ID",
description = "Retrieve a single movie by its MongoDB ObjectId."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ public interface MovieService {

List<Movie> getAllMovies(MovieSearchQuery query);

/**
* Gets all distinct genre values from the movies collection.
* Demonstrates the distinct() operation.
*
* @return List of unique genre strings, sorted alphabetically
*/
List<String> getDistinctGenres();

Movie getMovieById(String id);

Movie createMovie(CreateMovieRequest request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,25 @@ public List<Movie> getAllMovies(MovieSearchQuery query) {

return mongoTemplate.find(mongoQuery, Movie.class);
}


@Override
public List<String> getDistinctGenres() {
// Use MongoTemplate's findDistinct to get all unique values from the genres array field
// MongoDB automatically flattens array fields when using distinct()
List<String> genres = mongoTemplate.findDistinct(
new Query(),
Movie.Fields.GENRES,
Movie.class,
String.class
);

// Filter out null/empty values and sort alphabetically
return genres.stream()
.filter(genre -> genre != null && !genre.isEmpty())
.sorted(String::compareTo)
.collect(Collectors.toList());
}

@Override
public Movie getMovieById(String id) {
if (!ObjectId.isValid(id)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -936,4 +936,38 @@ void testDeleteMoviesBatch_EmptyFilter() throws Exception {
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.deletedCount").value(0));
}

// ==================== GET DISTINCT GENRES TESTS ====================

@Test
@DisplayName("GET /api/movies/genres - Should return list of distinct genres")
void testGetDistinctGenres_Success() throws Exception {
// Arrange
List<String> genres = Arrays.asList("Action", "Comedy", "Drama", "Horror", "Sci-Fi");
when(movieService.getDistinctGenres()).thenReturn(genres);

// Act & Assert
mockMvc.perform(get("/api/movies/genres"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data", hasSize(5)))
.andExpect(jsonPath("$.data[0]").value("Action"))
.andExpect(jsonPath("$.data[1]").value("Comedy"))
.andExpect(jsonPath("$.data[2]").value("Drama"));
}

@Test
@DisplayName("GET /api/movies/genres - Should return empty list when no genres exist")
void testGetDistinctGenres_EmptyList() throws Exception {
// Arrange
when(movieService.getDistinctGenres()).thenReturn(Arrays.asList());

// Act & Assert
mockMvc.perform(get("/api/movies/genres"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.data", hasSize(0)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -801,4 +801,82 @@ void testFindSimilarMovies_MovieNotFound() {
// Act & Assert
assertThrows(ResourceNotFoundException.class, () -> movieService.findSimilarMovies(movieId, 10));
}

// ==================== GET DISTINCT GENRES TESTS ====================

@Test
@DisplayName("Should get distinct genres successfully")
void testGetDistinctGenres_Success() {
// Arrange
List<String> expectedGenres = Arrays.asList("Action", "Comedy", "Drama", "Horror", "Sci-Fi");
when(mongoTemplate.findDistinct(any(Query.class), eq("genres"), eq(Movie.class), eq(String.class)))
.thenReturn(expectedGenres);

// Act
List<String> result = movieService.getDistinctGenres();

// Assert
assertNotNull(result);
assertEquals(5, result.size());
assertEquals("Action", result.get(0));
assertEquals("Comedy", result.get(1));
verify(mongoTemplate).findDistinct(any(Query.class), eq("genres"), eq(Movie.class), eq(String.class));
}

@Test
@DisplayName("Should return empty list when no genres exist")
void testGetDistinctGenres_EmptyList() {
// Arrange
when(mongoTemplate.findDistinct(any(Query.class), eq("genres"), eq(Movie.class), eq(String.class)))
.thenReturn(Arrays.asList());

// Act
List<String> result = movieService.getDistinctGenres();

// Assert
assertNotNull(result);
assertEquals(0, result.size());
verify(mongoTemplate).findDistinct(any(Query.class), eq("genres"), eq(Movie.class), eq(String.class));
}

@Test
@DisplayName("Should filter out null and empty genres")
void testGetDistinctGenres_FiltersNullAndEmpty() {
// Arrange
List<String> genresWithNulls = new ArrayList<>(Arrays.asList("Action", null, "", "Drama", "Comedy"));
when(mongoTemplate.findDistinct(any(Query.class), eq("genres"), eq(Movie.class), eq(String.class)))
.thenReturn(genresWithNulls);

// Act
List<String> result = movieService.getDistinctGenres();

// Assert
assertNotNull(result);
// The service should filter out null and empty values
assertEquals(3, result.size());
assertTrue(result.contains("Action"));
assertTrue(result.contains("Drama"));
assertTrue(result.contains("Comedy"));
assertFalse(result.contains(null));
assertFalse(result.contains(""));
}

@Test
@DisplayName("Should return genres sorted alphabetically")
void testGetDistinctGenres_SortedAlphabetically() {
// Arrange
List<String> unsortedGenres = Arrays.asList("Drama", "Action", "Comedy");
when(mongoTemplate.findDistinct(any(Query.class), eq("genres"), eq(Movie.class), eq(String.class)))
.thenReturn(unsortedGenres);

// Act
List<String> result = movieService.getDistinctGenres();

// Assert
assertNotNull(result);
assertEquals(3, result.size());
assertEquals("Action", result.get(0));
assertEquals("Comedy", result.get(1));
assertEquals("Drama", result.get(2));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,53 @@ describeSearch("MongoDB Search Integration Tests", () => {
expect(response.body.error).toBeDefined();
});
});

describe("GET /api/movies/genres", () => {
test("should return list of distinct genres", async () => {
const response = await request(app)
.get("/api/movies/genres")
.expect(200);

expect(response.body.success).toBe(true);
expect(response.body.data).toBeDefined();
expect(Array.isArray(response.body.data)).toBe(true);
expect(response.body.data.length).toBeGreaterThan(0);

// Verify genres are strings
response.body.data.forEach((genre: any) => {
expect(typeof genre).toBe("string");
expect(genre.length).toBeGreaterThan(0);
});
});

test("should return genres sorted alphabetically", async () => {
const response = await request(app)
.get("/api/movies/genres")
.expect(200);

expect(response.body.success).toBe(true);
const genres = response.body.data;

// Verify alphabetical sorting
for (let i = 0; i < genres.length - 1; i++) {
expect(genres[i].localeCompare(genres[i + 1])).toBeLessThanOrEqual(0);
}
});

test("should include common genres like Action, Drama, Comedy", async () => {
const response = await request(app)
.get("/api/movies/genres")
.expect(200);

expect(response.body.success).toBe(true);
const genres = response.body.data;

// The sample_mflix dataset should contain these common genres
expect(genres).toContain("Action");
expect(genres).toContain("Drama");
expect(genres).toContain("Comedy");
});
});
});


Expand Down
39 changes: 39 additions & 0 deletions mflix/server/python-fastapi/src/routers/movies.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
Search movies using MongoDB Vector Search to enable semantic search capabilities over
the plot field.

- GET /api/movies/genres :
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same Q here - since this is a new route, should we add a test for it in the tests dir?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

added

Retrieve all distinct genre values from the movies collection.
Demonstrates the distinct() operation.

- GET /api/movies/{id} :
Retrieve a single movie by its ID.

Expand Down Expand Up @@ -427,6 +431,41 @@ async def vector_search_movies(
detail=f"Error performing vector search: {str(e)}"
)

"""
GET /api/movies/genres

Retrieve all distinct genre values from the movies collection.
Demonstrates the distinct() operation.

Returns:
SuccessResponse[List[str]]: A response object containing the list of unique genres, sorted alphabetically.
"""

@router.get("/genres",
response_model=SuccessResponse[List[str]],
status_code=200,
summary="Retrieve all distinct genres from the movies collection.")
async def get_distinct_genres():
movies_collection = get_collection("movies")

try:
# Use distinct() to get all unique values from the genres array field
# MongoDB automatically flattens array fields when using distinct()
genres = await movies_collection.distinct("genres")
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Database error occurred: {str(e)}"
)

# Filter out null/empty values and sort alphabetically
valid_genres = sorted([
genre for genre in genres
if isinstance(genre, str) and len(genre) > 0
])

return create_success_response(valid_genres, f"Found {len(valid_genres)} distinct genres")

"""
GET /api/movies/{id}
Retrieve a single movie by its ID.
Expand Down
79 changes: 79 additions & 0 deletions mflix/server/python-fastapi/tests/test_movie_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,3 +1091,82 @@ async def test_aggregate_directors_empty_results(self, mock_execute_aggregation)
# Assertions
assert result.success is True
assert len(result.data) == 0


@pytest.mark.unit
@pytest.mark.asyncio
class TestGetDistinctGenres:
"""Tests for GET /api/movies/genres endpoint."""

@patch('src.routers.movies.get_collection')
async def test_get_distinct_genres_success(self, mock_get_collection):
"""Should return list of distinct genres sorted alphabetically."""
# Setup mock
mock_collection = AsyncMock()
mock_collection.distinct.return_value = ["Drama", "Action", "Comedy", "Horror", "Sci-Fi"]
mock_get_collection.return_value = mock_collection

# Call the route handler
from src.routers.movies import get_distinct_genres
result = await get_distinct_genres()

# Assertions
assert result.success is True
assert len(result.data) == 5
# Verify alphabetical sorting
assert result.data == ["Action", "Comedy", "Drama", "Horror", "Sci-Fi"]
mock_collection.distinct.assert_called_once_with("genres")

@patch('src.routers.movies.get_collection')
async def test_get_distinct_genres_empty_list(self, mock_get_collection):
"""Should return empty list when no genres exist."""
# Setup mock
mock_collection = AsyncMock()
mock_collection.distinct.return_value = []
mock_get_collection.return_value = mock_collection

# Call the route handler
from src.routers.movies import get_distinct_genres
result = await get_distinct_genres()

# Assertions
assert result.success is True
assert len(result.data) == 0

@patch('src.routers.movies.get_collection')
async def test_get_distinct_genres_filters_null_and_empty(self, mock_get_collection):
"""Should filter out null and empty genre values."""
# Setup mock
mock_collection = AsyncMock()
mock_collection.distinct.return_value = ["Action", None, "", "Drama", "Comedy"]
mock_get_collection.return_value = mock_collection

# Call the route handler
from src.routers.movies import get_distinct_genres
result = await get_distinct_genres()

# Assertions
assert result.success is True
assert len(result.data) == 3
assert "Action" in result.data
assert "Drama" in result.data
assert "Comedy" in result.data
assert None not in result.data
assert "" not in result.data

@patch('src.routers.movies.get_collection')
async def test_get_distinct_genres_database_error(self, mock_get_collection):
"""Should handle database errors gracefully."""
# Setup mock to raise exception
mock_collection = AsyncMock()
mock_collection.distinct.side_effect = Exception("Database connection failed")
mock_get_collection.return_value = mock_collection

# Call the route handler
from src.routers.movies import get_distinct_genres
with pytest.raises(HTTPException) as exc_info:
await get_distinct_genres()

# Assertions
assert exc_info.value.status_code == 500
assert "Database error" in str(exc_info.value.detail)