diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateReplaceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateReplaceTests.java new file mode 100644 index 000000000..3f4fab96b --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateReplaceTests.java @@ -0,0 +1,309 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.core.ReplaceOptions.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Query.*; + +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.bson.BsonInt64; +import org.bson.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.reactivestreams.Publisher; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.test.util.Client; +import org.springframework.data.mongodb.test.util.MongoClientExtension; + +import com.mongodb.client.model.Filters; +import com.mongodb.client.result.UpdateResult; +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoCollection; + +/** + * @author Christoph Strobl + */ +@ExtendWith(MongoClientExtension.class) +public class ReactiveMongoTemplateReplaceTests { + + static final String DB_NAME = "mongo-template-replace-tests"; + static final String RESTAURANT_COLLECTION = "restaurant"; + + static @Client MongoClient client; + private ReactiveMongoTemplate template; + + @BeforeEach + void beforeEach() { + + template = new ReactiveMongoTemplate(client, DB_NAME); + template.setEntityLifecycleEventsEnabled(false); + + initTestData(); + } + + @AfterEach() + void afterEach() { + clearTestData(); + } + + @Test // GH-4462 + void replacesExistingDocument() { + + Mono result = template.replace(query(where("name").is("Central Perk Cafe")), + new Restaurant("Central Pork Cafe", "Manhattan")); + + result.as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it.getMatchedCount()).isEqualTo(1); + assertThat(it.getModifiedCount()).isEqualTo(1); + }).verifyComplete(); + + retrieve(collection -> collection.find(Filters.eq("_id", 1)).first()).as(StepVerifier::create) + .consumeNextWith(document -> { + assertThat(document).containsEntry("r-name", "Central Pork Cafe"); + }).verifyComplete(); + } + + @Test // GH-4462 + void replacesFirstOnMoreThanOneMatch() { + + Mono result = template.replace(query(where("violations").exists(true)), + new Restaurant("Central Pork Cafe", "Manhattan")); + + result.as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it.getMatchedCount()).isEqualTo(1); + assertThat(it.getModifiedCount()).isEqualTo(1); + }).verifyComplete(); + + retrieve(collection -> collection.find(Filters.eq("_id", 2)).first()).as(StepVerifier::create) + .consumeNextWith(document -> { + assertThat(document).containsEntry("r-name", "Central Pork Cafe"); + }).verifyComplete(); + } + + @Test // GH-4462 + void replacesExistingDocumentWithRawDoc() { + + Mono result = template.replace(query(where("r-name").is("Central Perk Cafe")), + Document.parse("{ 'r-name' : 'Central Pork Cafe', 'Borough' : 'Manhattan' }"), + template.getCollectionName(Restaurant.class)); + + result.as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it.getMatchedCount()).isEqualTo(1); + assertThat(it.getModifiedCount()).isEqualTo(1); + }).verifyComplete(); + + retrieve(collection -> collection.find(Filters.eq("_id", 1)).first()).as(StepVerifier::create) + .consumeNextWith(document -> { + assertThat(document).containsEntry("r-name", "Central Pork Cafe"); + }).verifyComplete(); + } + + @Test // GH-4462 + void replacesExistingDocumentWithRawDocMappingQueryAgainstDomainType() { + + Mono result = template.replace(query(where("name").is("Central Perk Cafe")), Restaurant.class, + Document.parse("{ 'r-name' : 'Central Pork Cafe', 'Borough' : 'Manhattan' }"), ReplaceOptions.none()); + + result.as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it.getMatchedCount()).isEqualTo(1); + assertThat(it.getModifiedCount()).isEqualTo(1); + }).verifyComplete(); + + retrieve(collection -> collection.find(Filters.eq("_id", 1)).first()).as(StepVerifier::create) + .consumeNextWith(document -> { + assertThat(document).containsEntry("r-name", "Central Pork Cafe"); + }).verifyComplete(); + } + + @Test // GH-4462 + void replacesExistingDocumentWithMatchingId() { + + Mono result = template.replace(query(where("name").is("Central Perk Cafe")), + new Restaurant(1L, "Central Pork Cafe", "Manhattan", 0)); + + result.as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it.getMatchedCount()).isEqualTo(1); + assertThat(it.getModifiedCount()).isEqualTo(1); + }).verifyComplete(); + + retrieve(collection -> collection.find(Filters.eq("_id", 1)).first()).as(StepVerifier::create) + .consumeNextWith(document -> { + assertThat(document).containsEntry("r-name", "Central Pork Cafe"); + }).verifyComplete(); + } + + @Test // GH-4462 + void replacesExistingDocumentWithNewIdThrowsDataIntegrityViolationException() { + + template.replace(query(where("name").is("Central Perk Cafe")), + new Restaurant(4L, "Central Pork Cafe", "Manhattan", 0)) + .as(StepVerifier::create) + .expectError(DataIntegrityViolationException.class) + .verify(); + } + + @Test // GH-4462 + void doesNothingIfNoMatchFoundAndUpsertSetToFalse/* by default */() { + + Mono result = template.replace(query(where("name").is("Pizza Rat's Pizzaria")), + new Restaurant(null, "Pizza Rat's Pizzaria", "Manhattan", 8)); + + result.as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it.getMatchedCount()).isEqualTo(0); + assertThat(it.getModifiedCount()).isEqualTo(0); + }).verifyComplete(); + + retrieve(collection -> collection.find(Filters.eq("r-name", "Pizza Rat's Pizzaria")).first()) + .as(StepVerifier::create).verifyComplete(); + } + + @Test // GH-4462 + void insertsIfNoMatchFoundAndUpsertSetToTrue() { + + Mono result = template.replace(query(where("name").is("Pizza Rat's Pizzaria")), + new Restaurant(4L, "Pizza Rat's Pizzaria", "Manhattan", 8), replaceOptions().upsert()); + + result.as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it.getMatchedCount()).isEqualTo(0); + assertThat(it.getModifiedCount()).isEqualTo(0); + assertThat(it.getUpsertedId()).isEqualTo(new BsonInt64(4L)); + }).verifyComplete(); + + retrieve(collection -> collection.find(Filters.eq("_id", 4)).first()).as(StepVerifier::create) + .consumeNextWith(document -> { + assertThat(document).containsEntry("r-name", "Pizza Rat's Pizzaria"); + }); + } + + void initTestData() { + + List testData = Stream.of( // + "{ '_id' : 1, 'r-name' : 'Central Perk Cafe', 'Borough' : 'Manhattan' }", + "{ '_id' : 2, 'r-name' : 'Rock A Feller Bar and Grill', 'Borough' : 'Queens', 'violations' : 2 }", + "{ '_id' : 3, 'r-name' : 'Empire State Pub', 'Borough' : 'Brooklyn', 'violations' : 0 }") // + .map(Document::parse).collect(Collectors.toList()); + + doInCollection(collection -> collection.insertMany(testData)); + } + + void clearTestData() { + doInCollection(collection -> collection.deleteMany(new Document())); + } + + void doInCollection(Function, Publisher> fkt) { + retrieve(collection -> Mono.from(fkt.apply(collection))).then().as(StepVerifier::create).verifyComplete(); + } + + Mono retrieve(Function, Publisher> fkt) { + return Mono.from(fkt.apply(client.getDatabase(DB_NAME).getCollection(RESTAURANT_COLLECTION))); + } + + @org.springframework.data.mongodb.core.mapping.Document(RESTAURANT_COLLECTION) + static class Restaurant { + + Long id; + + @Field("r-name") String name; + String borough; + Integer violations; + + Restaurant() {} + + Restaurant(String name, String borough) { + + this.name = name; + this.borough = borough; + } + + Restaurant(Long id, String name, String borough, Integer violations) { + + this.id = id; + this.name = name; + this.borough = borough; + this.violations = violations; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getRName() { + return name; + } + + public void setRName(String rName) { + this.name = rName; + } + + public String getBorough() { + return borough; + } + + public void setBorough(String borough) { + this.borough = borough; + } + + public int getViolations() { + return violations; + } + + public void setViolations(int violations) { + this.violations = violations; + } + + @Override + public String toString() { + return "Restaurant{" + "id=" + id + ", name='" + name + '\'' + ", borough='" + borough + '\'' + ", violations=" + + violations + '}'; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Restaurant that = (Restaurant) o; + return violations == that.violations && Objects.equals(id, that.id) && Objects.equals(name, that.name) + && Objects.equals(borough, that.borough); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, borough, violations); + } + } + +}