From 69e56f5289ff335c06d957a75f08d4ed1086b7f1 Mon Sep 17 00:00:00 2001 From: Vitaliy Baschlykoff Date: Wed, 15 Apr 2026 16:18:14 +1000 Subject: [PATCH 1/4] Replace java.util.Date with Instant and LocalDate for MongoDB datetime fields --- .../samplemflix/config/MongoConfig.java | 6 ++++++ .../com/mongodb/samplemflix/model/Movie.java | 19 ++++++++++++++----- .../model/dto/MovieWithCommentsResult.java | 16 +++++++++++----- .../samplemflix/service/MovieServiceImpl.java | 4 ++-- .../controller/MovieControllerTest.java | 6 +++--- 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java index 815c248..c654a81 100644 --- a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java +++ b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/config/MongoConfig.java @@ -9,6 +9,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; import org.springframework.lang.NonNull; @@ -119,4 +120,9 @@ public MongoDatabase mongoDatabase() { return client.getDatabase(databaseName); } + + @Bean + public MongoCustomConversions customConversions() { + return MongoCustomConversions.create(MongoCustomConversions.MongoConverterConfigurationAdapter::useNativeDriverJavaTimeCodecs); + } } diff --git a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java index 770a5cb..d5f1fdf 100644 --- a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java +++ b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/model/Movie.java @@ -1,7 +1,8 @@ package com.mongodb.samplemflix.model; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Date; +import java.time.Instant; +import java.time.LocalDate; import java.util.List; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -111,9 +112,14 @@ private Fields() { private String fullplot; /** - * Release date. + * Release date as a calendar date (no time-of-day or time zone). + * + *

A movie release date is a "date only" concept (e.g. "1999-03-31"), not a specific + * moment in time. We use {@link LocalDate} because it represents exactly that: a date + * without time-of-day or time zone information. Spring Data MongoDB maps this via the + * Jsr310 {@code LocalDateCodec}. */ - private Date released; + private LocalDate released; /** * Runtime in minutes. @@ -270,9 +276,12 @@ public static class Tomatoes { private String production; /** - * Last updated date. + * Timestamp of the last update to Tomatoes ratings. + * + *

Stored as BSON DateTime in MongoDB. Uses {@link Instant} for an immutable, + * UTC-only representation of this point-in-time event. */ - private Date lastUpdated; + private Instant lastUpdated; /** * Nested class for viewer ratings. diff --git a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieWithCommentsResult.java b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieWithCommentsResult.java index 25c3d05..37c4f8d 100644 --- a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieWithCommentsResult.java +++ b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/model/dto/MovieWithCommentsResult.java @@ -1,7 +1,7 @@ package com.mongodb.samplemflix.model.dto; import com.fasterxml.jackson.annotation.JsonInclude; -import java.util.Date; +import java.time.Instant; import java.util.List; import lombok.Builder; @@ -61,9 +61,12 @@ public record MovieWithCommentsResult ( Integer totalComments, /** - * Date of the most recent comment. + * Timestamp of the most recent comment as a UTC instant. + * + *

Uses {@link Instant} for an immutable, unambiguous UTC representation. + * BSON DateTime values are converted via {@code Date.toInstant()}. */ - Date mostRecentCommentDate) { + Instant mostRecentCommentDate) { /** * Nested record for comment information. @@ -91,8 +94,11 @@ public record CommentInfo ( String text, /** - * Comment date. + * Comment timestamp as a UTC instant. + * + *

Stored as BSON DateTime in MongoDB. Uses {@link Instant} for immutability + * and unambiguous UTC semantics. */ - Date date) {} + Instant date) {} } 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 5ce4188..9aec741 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 @@ -574,7 +574,7 @@ private MovieWithCommentsResult mapToMovieWithCommentsResult(Document doc) { .name(commentDoc.getString("name")) .email(commentDoc.getString("email")) .text(commentDoc.getString("text")) - .date(commentDoc.getDate("date")) + .date(commentDoc.get("date") != null ? commentDoc.getDate("date").toInstant() : null) .build()) .collect(Collectors.toList()); } @@ -598,7 +598,7 @@ private MovieWithCommentsResult mapToMovieWithCommentsResult(Document doc) { .imdbRating(imdbRating) .recentComments(recentComments) .totalComments(doc.getInteger("totalComments")) - .mostRecentCommentDate(doc.getDate("mostRecentCommentDate")) + .mostRecentCommentDate(doc.getDate("mostRecentCommentDate") != null ? doc.getDate("mostRecentCommentDate").toInstant() : null) .build(); } 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..4eacf86 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 @@ -22,8 +22,8 @@ import com.mongodb.samplemflix.model.dto.UpdateMovieRequest; import com.mongodb.samplemflix.model.dto.VectorSearchResult; import com.mongodb.samplemflix.service.MovieService; +import java.time.Instant; import java.util.Arrays; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -343,7 +343,7 @@ void testGetMoviesWithMostComments_Success() throws Exception { .name("John Doe") .email("john@example.com") .text("Great movie!") - .date(new Date()) + .date(Instant.now()) .build(); MovieWithCommentsResult result = MovieWithCommentsResult.builder() @@ -356,7 +356,7 @@ void testGetMoviesWithMostComments_Success() throws Exception { .imdbRating(8.5) .recentComments(Arrays.asList(comment)) .totalComments(5) - .mostRecentCommentDate(new Date()) + .mostRecentCommentDate(Instant.now()) .build(); when(movieService.getMoviesWithMostRecentComments(anyInt(), isNull())).thenReturn(Arrays.asList(result)); From 304a2585e31e0630fda2ba4865cdbd2341cd09b2 Mon Sep 17 00:00:00 2001 From: Vitaliy Baschlykoff Date: Thu, 16 Apr 2026 10:57:51 +1000 Subject: [PATCH 2/4] Test that BSON DateTime at midnight UTC is read as the correct LocalDate without date shift --- .../MongoDBSearchIntegrationTest.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBSearchIntegrationTest.java b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBSearchIntegrationTest.java index d1b65cb..a1e48f0 100644 --- a/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBSearchIntegrationTest.java +++ b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBSearchIntegrationTest.java @@ -1,5 +1,6 @@ package com.mongodb.samplemflix.integration; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -7,11 +8,15 @@ import com.mongodb.client.MongoCollection; import com.mongodb.samplemflix.model.Movie; import com.mongodb.samplemflix.service.MovieService; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.TimeZone; +import org.bson.BsonDateTime; import org.bson.Document; +import org.bson.types.ObjectId; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; @@ -20,6 +25,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; import org.springframework.test.context.ActiveProfiles; /** @@ -223,6 +230,58 @@ void testSearchMoviesByPlot_WithPagination() { } } + // ==================== RELEASED FIELD ROUND-TRIP TESTS ==================== + + @Test + @DisplayName("Should read BSON DateTime at midnight UTC as the correct LocalDate without date shift") + void testReleasedFieldRoundTrip_NoDateShift() { + if (!isSearchEnabled()) { + System.out.println("Skipping test - Search not enabled"); + return; + } + + // The test movies were inserted with known BSON DateTime values at midnight UTC: + // "Test Space Adventure" -> 2024-01-01T00:00:00Z (1704067200000) + // "Test Mystery Movie" -> 2024-03-31T00:00:00Z (1711843200000) + // "Test Adventure Quest" -> 2024-12-31T00:00:00Z (1735603200000) + // A JVM timezone west of UTC could shift these dates backward by one day + // (e.g. 2024-01-01 -> 2023-12-31). Cycle through multiple timezones to + // verify the native LocalDateCodec always interprets BSON DateTime in UTC. + + List timezones = List.of( + "America/New_York", + "America/Los_Angeles", + "Asia/Tokyo", + "Europe/London", + "Pacific/Auckland" + ); + + TimeZone originalTz = TimeZone.getDefault(); + try { + for (String zoneId : timezones) { + TimeZone.setDefault(TimeZone.getTimeZone(zoneId)); + + Movie spaceAdventure = findTestMovieByTitle("Test Space Adventure"); + assertEquals(LocalDate.of(2024, 1, 1), spaceAdventure.getReleased(), + "2024-01-01T00:00:00Z shifted in " + zoneId); + + Movie mystery = findTestMovieByTitle("Test Mystery Movie"); + assertEquals(LocalDate.of(2024, 3, 31), mystery.getReleased(), + "2024-03-31T00:00:00Z shifted in " + zoneId); + + Movie adventureQuest = findTestMovieByTitle("Test Adventure Quest"); + assertEquals(LocalDate.of(2024, 12, 31), adventureQuest.getReleased(), + "2024-12-31T00:00:00Z shifted in " + zoneId); + } + } finally { + TimeZone.setDefault(originalTz); + } + } + + private Movie findTestMovieByTitle(String title) { + return mongoTemplate.findOne(new Query(Criteria.where("title").is(title)), Movie.class); + } + // ==================== HELPER METHODS ==================== private boolean isSearchEnabled() { @@ -239,16 +298,19 @@ private void createTestMovies() { new Document() .append("title", "Test Space Adventure") .append("year", 2024) + .append("released", new BsonDateTime(1704067200000L)) // 1st of Jan 2024 in UTC .append("plot", "An epic space adventure across the galaxy") .append("genres", Arrays.asList("Sci-Fi", "Adventure")), new Document() .append("title", "Test Mystery Movie") .append("year", 2024) + .append("released", new BsonDateTime(1711843200000L)) // 31 of March 2024 in UTC .append("plot", "A detective solves a mysterious crime") .append("genres", Arrays.asList("Mystery", "Thriller")), new Document() .append("title", "Test Adventure Quest") .append("year", 2024) + .append("released", new BsonDateTime(1735603200000L)) // 31 December 2024 in UTC .append("plot", "Heroes embark on a dangerous adventure") .append("genres", Arrays.asList("Adventure", "Fantasy")) ); From 686cb2c5e5b2f418e42d7d43a78fbc814f70a3ea Mon Sep 17 00:00:00 2001 From: Vitaliy Baschlykoff Date: Thu, 16 Apr 2026 12:45:02 +1000 Subject: [PATCH 3/4] Add integration test to verify LocalDate is not shifted when read and also improve intergration tests setup --- mflix/server/java-spring/pom.xml | 15 ++++ .../MongoDBSearchIntegrationTest.java | 86 +++++-------------- .../MongoDBTestContainersConfig.java | 42 +++++++++ .../mongodb/samplemflix/integration/README.md | 82 +++++++++--------- .../resources/application-test.properties | 2 +- 5 files changed, 122 insertions(+), 105 deletions(-) create mode 100644 mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBTestContainersConfig.java diff --git a/mflix/server/java-spring/pom.xml b/mflix/server/java-spring/pom.xml index 43f1546..f0872d0 100644 --- a/mflix/server/java-spring/pom.xml +++ b/mflix/server/java-spring/pom.xml @@ -26,6 +26,7 @@ 1.13.0 1.17.8 1.11.0-beta19 + 1.21.4 @@ -88,6 +89,20 @@ test + + + org.testcontainers + junit-jupiter + test + + + + + org.testcontainers + mongodb + test + + com.fasterxml.jackson.core diff --git a/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBSearchIntegrationTest.java b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBSearchIntegrationTest.java index a1e48f0..b5e759a 100644 --- a/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBSearchIntegrationTest.java +++ b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBSearchIntegrationTest.java @@ -14,9 +14,9 @@ import java.util.Collections; import java.util.List; import java.util.TimeZone; +import lombok.extern.slf4j.Slf4j; import org.bson.BsonDateTime; import org.bson.Document; -import org.bson.types.ObjectId; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; @@ -32,22 +32,19 @@ /** * Integration tests for MongoDB Search functionality. * - *

These tests verify the MongoDB Search endpoints work correctly with a real MongoDB Atlas instance. - * The tests require: - *

+ *

These tests verify the MongoDB Search endpoints work correctly with a real + * MongoDB instance. By default, a MongoDBAtlasLocalContainer (Docker) is started + * automatically, providing a local Atlas environment with Search support. * - *

Note: These tests are disabled by default and should only be run against a test Atlas cluster. - * To enable, set the environment variable ENABLE_SEARCH_TESTS=true + *

To run against an external MongoDB Atlas cluster instead, set the + * {@code MONGODB_URI} environment variable. When set, no container is started. */ -@SpringBootTest +@SpringBootTest(classes = MongoDBTestContainersConfig.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @ActiveProfiles("test") @DisplayName("MongoDB Search Integration Tests") -class MongoDBSearchIntegrationTest { +@Slf4j +class MongoDBSearchIntegrationTest { @Autowired private MovieService movieService; @@ -64,13 +61,7 @@ class MongoDBSearchIntegrationTest { @BeforeAll void setUp() throws Exception { - // Skip tests if not running against Atlas - if (!isSearchEnabled()) { - System.out.println("Skipping MongoDB Search tests - ENABLE_SEARCH_TESTS not set"); - return; - } - - System.out.println("Setting up MongoDB Search integration tests..."); + log.info("Setting up MongoDB Search integration tests..."); // Create test data createTestMovies(); @@ -83,23 +74,19 @@ void setUp() throws Exception { // Wait a bit for the newly created documents to be indexed // MongoDB Search indexes documents asynchronously - System.out.println("Waiting for test documents to be indexed..."); + log.info("Waiting for test documents to be indexed..."); try { Thread.sleep(10000); // Wait 10 seconds for indexing } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - System.out.println("MongoDB Search index is ready for testing"); + log.info("MongoDB Search index is ready for testing"); } @AfterAll void tearDown() { - if (!isSearchEnabled()) { - return; - } - - System.out.println("Cleaning up MongoDB Search test data..."); + log.info("Cleaning up MongoDB Search test data..."); // Clean up test movies if (!testMovieIds.isEmpty()) { @@ -116,11 +103,6 @@ void tearDown() { @Test @DisplayName("Should search movies by plot using MongoDB Search") void testSearchMoviesByPlot_Success() { - if (!isSearchEnabled()) { - System.out.println("Skipping test - Search not enabled"); - return; - } - // Act com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest = com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() @@ -145,11 +127,6 @@ void testSearchMoviesByPlot_Success() { @Test @DisplayName("Should return empty list when no movies match search query") void testSearchMoviesByPlot_NoResults() { - if (!isSearchEnabled()) { - System.out.println("Skipping test - Search not enabled"); - return; - } - // Act - search for something that definitely doesn't exist com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest = com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() @@ -168,11 +145,6 @@ void testSearchMoviesByPlot_NoResults() { @Test @DisplayName("Should respect limit parameter in search") void testSearchMoviesByPlot_WithLimit() { - if (!isSearchEnabled()) { - System.out.println("Skipping test - Search not enabled"); - return; - } - // Act com.mongodb.samplemflix.model.dto.MovieSearchRequest searchRequest = com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() @@ -191,11 +163,6 @@ void testSearchMoviesByPlot_WithLimit() { @Test @DisplayName("Should support pagination with skip parameter") void testSearchMoviesByPlot_WithPagination() { - if (!isSearchEnabled()) { - System.out.println("Skipping test - Search not enabled"); - return; - } - // Act - Get first page com.mongodb.samplemflix.model.dto.MovieSearchRequest firstPageRequest = com.mongodb.samplemflix.model.dto.MovieSearchRequest.builder() @@ -235,11 +202,6 @@ void testSearchMoviesByPlot_WithPagination() { @Test @DisplayName("Should read BSON DateTime at midnight UTC as the correct LocalDate without date shift") void testReleasedFieldRoundTrip_NoDateShift() { - if (!isSearchEnabled()) { - System.out.println("Skipping test - Search not enabled"); - return; - } - // The test movies were inserted with known BSON DateTime values at midnight UTC: // "Test Space Adventure" -> 2024-01-01T00:00:00Z (1704067200000) // "Test Mystery Movie" -> 2024-03-31T00:00:00Z (1711843200000) @@ -284,13 +246,9 @@ private Movie findTestMovieByTitle(String title) { // ==================== HELPER METHODS ==================== - private boolean isSearchEnabled() { - String enabled = System.getenv("ENABLE_SEARCH_TESTS"); - return "true".equalsIgnoreCase(enabled); - } private void createTestMovies() { - System.out.println("Creating test movies..."); + log.info("Creating test movies..."); MongoCollection collection = mongoTemplate.getCollection("movies"); @@ -320,11 +278,11 @@ private void createTestMovies() { testMovieIds.add(movie.getObjectId("_id").toHexString()); }); - System.out.println("Created " + testMovieIds.size() + " test movies"); + log.info("Created {} test movies", testMovieIds.size()); } private void createSearchIndex() throws Exception { - System.out.println("Creating Search index..."); + log.info("Creating Search index..."); MongoCollection collection = mongoTemplate.getCollection("movies"); @@ -336,7 +294,7 @@ private void createSearchIndex() throws Exception { .anyMatch(idx -> SEARCH_INDEX_NAME.equals(idx.getString("name"))); if (indexExists) { - System.out.println("Search index already exists"); + log.info("Search index already exists"); return; } @@ -371,15 +329,15 @@ private void createSearchIndex() throws Exception { try { mongoTemplate.getDb().runCommand(createIndexCommand); - System.out.println("Search index creation initiated"); + log.info("Search index creation initiated"); } catch (Exception e) { - System.err.println("Error creating search index: " + e.getMessage()); + log.error("Error creating search index: {}", e.getMessage()); throw e; } } private void waitForSearchIndexReady() throws Exception { - System.out.println("Waiting for search index to be ready..."); + log.info("Waiting for search index to be ready..."); MongoCollection collection = mongoTemplate.getCollection("movies"); long startTime = System.currentTimeMillis(); @@ -396,10 +354,10 @@ private void waitForSearchIndexReady() throws Exception { if (searchIndex != null) { String status = searchIndex.getString("status"); - System.out.println("Index status: " + status); + log.info("Index status: {}", status); if ("READY".equals(status)) { - System.out.println("Search index is ready!"); + log.info("Search index is ready!"); return; } } diff --git a/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBTestContainersConfig.java b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBTestContainersConfig.java new file mode 100644 index 0000000..a2b4fb4 --- /dev/null +++ b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/MongoDBTestContainersConfig.java @@ -0,0 +1,42 @@ +package com.mongodb.samplemflix.integration; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.lang.NonNull; +import org.springframework.test.context.DynamicPropertyRegistrar; +import org.testcontainers.mongodb.MongoDBAtlasLocalContainer; + +@TestConfiguration +public class MongoDBTestContainersConfig { + + // Note: @ServiceConnection cannot be used here because MongoDBAtlasLocalContainer + // extends GenericContainer, not MongoDBContainer. Spring Boot's auto-detection + // only recognizes the latter (fixed in Spring Boot v4). We use initMethod instead so Spring manages the + // full lifecycle (start on init, close/stop on context shutdown via AutoCloseable). + + @Bean(initMethod = "start") + @Conditional(MongoUriMissingCondition.class) + public MongoDBAtlasLocalContainer mongoDbContainer() { + return new MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:8"); + } + + @Bean + @Conditional(MongoUriMissingCondition.class) + public DynamicPropertyRegistrar mongoDbProperties(MongoDBAtlasLocalContainer mongoDBContainer) { + return (registry) -> { + registry.add("spring.data.mongodb.uri", mongoDBContainer::getConnectionString); + }; + } + + static class MongoUriMissingCondition implements Condition { + @Override + public boolean matches(ConditionContext context, @NonNull AnnotatedTypeMetadata metadata) { + String uri = context.getEnvironment().getProperty("spring.data.mongodb.uri"); + return uri == null || uri.isBlank(); + } + } +} diff --git a/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md index ceae7b0..dddce6d 100644 --- a/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md +++ b/mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/integration/README.md @@ -4,36 +4,41 @@ This directory contains integration tests for MongoDB Search functionality. ## Overview -The `MongoDBSearchIntegrationTest` class tests the MongoDB Search endpoints with a real MongoDB Atlas instance. These tests verify that: +The `MongoDBSearchIntegrationTest` class tests the MongoDB Search endpoints with a real MongoDB instance. These tests verify that: 1. The MongoDB Search index is created correctly 2. The index becomes ready for use (using polling) 3. The `/search` endpoint returns correct results 4. Pagination works correctly 5. Empty results are handled properly +6. BSON DateTime values at midnight UTC round-trip correctly to `LocalDate` without date shift across JVM timezones ## Requirements -These tests require: +- **Docker** must be running (for the local Atlas container) +- Alternatively, set `MONGODB_URI` to use an external MongoDB Atlas cluster instead of Docker -- **MongoDB Atlas cluster** (not local MongoDB or Testcontainers) -- **MongoDB Search capability** enabled on the cluster -- **MONGODB_URI** environment variable pointing to your Atlas cluster -- **ENABLE_SEARCH_TESTS=true** environment variable to enable the tests +By default, tests spin up a `MongoDBAtlasLocalContainer` via Testcontainers, which provides a local Atlas environment with full Search support. No external cluster or special environment variables are needed. + +If neither Docker nor `MONGODB_URI` is available, the tests will fail. ## Running the Tests -### Enable the Tests +### Default (local Atlas container via Docker) -By default, these tests are **disabled** to prevent accidental runs against production databases. To enable them: +Just run the tests — Docker handles the rest: ```bash -export ENABLE_SEARCH_TESTS=true +# Run all integration tests +./mvnw test -Dtest=MongoDBSearchIntegrationTest + +# Run a specific test +./mvnw test -Dtest=MongoDBSearchIntegrationTest#testSearchMoviesByPlot_Success ``` -### Set MongoDB URI +### External Atlas Cluster (optional) -Make sure your `MONGODB_URI` environment variable points to a MongoDB Atlas cluster: +To run against an external MongoDB Atlas cluster instead of Docker, set the `MONGODB_URI` environment variable: ```bash export MONGODB_URI="mongodb+srv://username:password@cluster.mongodb.net/sample_mflix?retryWrites=true&w=majority" @@ -45,19 +50,18 @@ Or use a `.env` file in the `server/java-spring` directory: MONGODB_URI="mongodb+srv://username:password@cluster.mongodb.net/sample_mflix?retryWrites=true&w=majority" ``` -### Run the Tests +When `MONGODB_URI` is set, no Docker container is started. -```bash -# Run all integration tests -./mvnw test -Dtest=MongoDBSearchIntegrationTest +## How the Tests Work -# Run a specific test -./mvnw test -Dtest=MongoDBSearchIntegrationTest#testSearchMoviesByPlot_Success -``` +### Test Configuration -## How the Tests Work +`MongoDBTestContainersConfig` is a `@TestConfiguration` that conditionally starts a `MongoDBAtlasLocalContainer`: -### 1. Index Creation and Polling +- If `spring.data.mongodb.uri` is empty or absent, it starts a local Atlas container and registers its connection string +- If `spring.data.mongodb.uri` is already set (via `MONGODB_URI` env var), no container is started + +### Index Creation and Polling The tests use a `@BeforeAll` method to: @@ -66,18 +70,21 @@ The tests use a `@BeforeAll` method to: 3. Poll the index status every 5 seconds until it's "READY" 4. Wait up to 120 seconds (2 minutes) for the index to be ready 5. Throw an exception if the index doesn't become ready in time +6. Wait an additional 10 seconds for asynchronous document indexing -### 2. Test Data Setup +### Test Data Setup -The tests create temporary test movies with known plot content: +The tests create temporary test movies with known plot content and `released` dates stored as BSON DateTime at midnight UTC: -- "An epic space adventure across the galaxy" -- "A detective solves a mysterious crime" -- "Heroes embark on a dangerous adventure" +| Title | Plot | Released (UTC) | +|---|---|---| +| Test Space Adventure | An epic space adventure across the galaxy | 2024-01-01 | +| Test Mystery Movie | A detective solves a mysterious crime | 2024-03-31 | +| Test Adventure Quest | Heroes embark on a dangerous adventure | 2024-12-31 | -These movies are used to verify search functionality. +These movies are used to verify both search functionality and correct `LocalDate` round-tripping of the `released` field. -### 3. Test Cleanup +### Test Cleanup The `@AfterAll` method removes the test movies after all tests complete. The search index is **not** deleted because: @@ -99,22 +106,17 @@ Verifies that the `limit` parameter correctly limits the number of results. ### testSearchMoviesByPlot_WithPagination Verifies that the `skip` parameter works for pagination and returns different results on different pages. -## Troubleshooting - -### Tests are Skipped - -If you see "Skipping Search tests - ENABLE_SEARCH_TESTS not set", make sure you've set the environment variable: +### testReleasedFieldRoundTrip_NoDateShift +Verifies that BSON DateTime values at midnight UTC are read as the correct `LocalDate` without date shift. The test temporarily switches the JVM default timezone through `America/New_York`, `America/Los_Angeles`, `Asia/Tokyo`, `Europe/London`, and `Pacific/Auckland` to ensure the native `LocalDateCodec` always interprets BSON DateTime in UTC. -```bash -export ENABLE_SEARCH_TESTS=true -``` +## Troubleshooting ### Index Creation Timeout If the tests fail with "Search index did not become ready within 120 seconds": -1. Check that your cluster has MongoDB Search enabled -2. Verify you're using a MongoDB Atlas cluster (not local MongoDB) +1. Ensure Docker is running and has enough resources +2. If using an external cluster, check that it has MongoDB Search enabled 3. Check the Atlas UI to see if the index is being created 4. Increase `MAX_INDEX_WAIT_SECONDS` if needed @@ -122,9 +124,9 @@ If the tests fail with "Search index did not become ready within 120 seconds": If you get connection errors: -1. Verify your `MONGODB_URI` is correct -2. Check that your IP address is whitelisted in Atlas -3. Verify your database user credentials are correct +1. Verify Docker is running (`docker ps`) +2. If using an external Atlas cluster, verify your `MONGODB_URI` is correct +3. Check that your IP address is whitelisted in Atlas (for external clusters) ## Notes diff --git a/mflix/server/java-spring/src/test/resources/application-test.properties b/mflix/server/java-spring/src/test/resources/application-test.properties index bb3103e..6360d0f 100644 --- a/mflix/server/java-spring/src/test/resources/application-test.properties +++ b/mflix/server/java-spring/src/test/resources/application-test.properties @@ -3,7 +3,7 @@ # MongoDB Configuration # For MongoDB Search tests, use the same MONGODB_URI as the main application -spring.data.mongodb.uri=${MONGODB_URI} +spring.data.mongodb.uri=${MONGODB_URI:} spring.data.mongodb.database=sample_mflix # Server Configuration From 7e5d3b89d80a9bb2ade9b17b15f7cb202cfdee75 Mon Sep 17 00:00:00 2001 From: Vitaliy Baschlykoff Date: Fri, 17 Apr 2026 09:32:31 +1000 Subject: [PATCH 4/4] Bump spring boot version for related test containers version increase to be compatible with the latest Docker release --- mflix/server/java-spring/pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mflix/server/java-spring/pom.xml b/mflix/server/java-spring/pom.xml index f0872d0..a814870 100644 --- a/mflix/server/java-spring/pom.xml +++ b/mflix/server/java-spring/pom.xml @@ -8,7 +8,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.7 + 3.5.13 @@ -26,7 +26,6 @@ 1.13.0 1.17.8 1.11.0-beta19 - 1.21.4