feat: isolate apartment portfolios by landlord (#25)
This commit is contained in:
1 parent
e8f98a0b3f
commit
f4acbffc9d
21 files changed
+430
-119
No files matched your search
@@ -1,34 +0,0 @@
|
||||
package fr.nessar.lmnporganizer.backend.config;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.model.Landlord;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LandlordRepository;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "lmnp.data-initializer.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class DataInitializer implements ApplicationRunner {
|
||||
|
||||
private final LandlordRepository landlordRepository;
|
||||
|
||||
public DataInitializer(LandlordRepository landlordRepository) {
|
||||
this.landlordRepository = landlordRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
if (!landlordRepository.existsById("default-landlord")) {
|
||||
Landlord defaultLandlord = new Landlord(
|
||||
"default-landlord",
|
||||
"Bailleur",
|
||||
"LMNP",
|
||||
"bailleur@example.com",
|
||||
"0102030405",
|
||||
"123 Rue de la Gestion, Paris");
|
||||
landlordRepository.save(defaultLandlord);
|
||||
System.out.println("Default landlord profile initialized in database.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-12
@@ -3,7 +3,12 @@ package fr.nessar.lmnporganizer.backend.controller;
|
||||
import fr.nessar.lmnporganizer.backend.dto.ApartmentRequest;
|
||||
import fr.nessar.lmnporganizer.backend.dto.ApartmentResponse;
|
||||
import fr.nessar.lmnporganizer.backend.service.ApartmentService;
|
||||
import fr.nessar.lmnporganizer.backend.service.LandlordProfileService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
@@ -12,34 +17,43 @@ import java.util.List;
|
||||
@RequestMapping("/api/apartments")
|
||||
public class ApartmentController {
|
||||
private final ApartmentService apartmentService;
|
||||
private final LandlordProfileService landlordProfileService;
|
||||
|
||||
public ApartmentController(ApartmentService apartmentService) {
|
||||
public ApartmentController(ApartmentService apartmentService, LandlordProfileService landlordProfileService) {
|
||||
this.apartmentService = apartmentService;
|
||||
this.landlordProfileService = landlordProfileService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<ApartmentResponse> getAllApartments() {
|
||||
return apartmentService.getAll();
|
||||
public List<ApartmentResponse> getAllApartments(@AuthenticationPrincipal OidcUser principal) {
|
||||
return apartmentService.getAll(landlordId(principal));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ApartmentResponse> getApartmentById(@PathVariable String id) {
|
||||
return apartmentService.getById(id);
|
||||
public ResponseEntity<ApartmentResponse> getApartmentById(@AuthenticationPrincipal OidcUser principal,
|
||||
@PathVariable String id) {
|
||||
return ResponseEntity.of(apartmentService.getById(landlordId(principal), id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApartmentResponse> createApartment(@RequestBody ApartmentRequest request) {
|
||||
return apartmentService.create(request);
|
||||
public ResponseEntity<ApartmentResponse> createApartment(@AuthenticationPrincipal OidcUser principal,
|
||||
@Valid @RequestBody ApartmentRequest request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(apartmentService.create(landlordId(principal), request));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<ApartmentResponse> updateApartment(@PathVariable String id,
|
||||
@RequestBody ApartmentRequest request) {
|
||||
return apartmentService.update(id, request);
|
||||
public ResponseEntity<ApartmentResponse> updateApartment(@AuthenticationPrincipal OidcUser principal,
|
||||
@PathVariable String id, @Valid @RequestBody ApartmentRequest request) {
|
||||
return ResponseEntity.of(apartmentService.update(landlordId(principal), id, request));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteApartment(@PathVariable String id) {
|
||||
return apartmentService.delete(id);
|
||||
public ResponseEntity<Void> deleteApartment(@AuthenticationPrincipal OidcUser principal, @PathVariable String id) {
|
||||
return apartmentService.delete(landlordId(principal), id) ? ResponseEntity.noContent().build()
|
||||
: ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
private String landlordId(OidcUser principal) {
|
||||
return landlordProfileService.findOrCreate(principal).getId();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
package fr.nessar.lmnporganizer.backend.dto;
|
||||
|
||||
public record ApartmentRequest(String name, String address, double purchasePrice, String description) {
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record ApartmentRequest(
|
||||
@NotBlank(message = "Le nom est obligatoire.") String name,
|
||||
@NotBlank(message = "L'adresse est obligatoire.") String address,
|
||||
@DecimalMin(value = "1", message = "Le prix d'achat doit être supérieur ou égal à 1.") double purchasePrice,
|
||||
String description) {
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
package fr.nessar.lmnporganizer.backend.model;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
@Document(collection = "landlords")
|
||||
public class Landlord {
|
||||
@Id
|
||||
private String id;
|
||||
@Indexed(unique = true)
|
||||
private String oidcSubject;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
|
||||
+5
@@ -4,8 +4,13 @@ import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ApartmentRepository extends MongoRepository<Apartment, String> {
|
||||
List<Apartment> findByBailleurId(String bailleurId);
|
||||
|
||||
Optional<Apartment> findByIdAndBailleurId(String id, String bailleurId);
|
||||
|
||||
long deleteByIdAndBailleurId(String id, String bailleurId);
|
||||
}
|
||||
+22
-23
@@ -5,11 +5,10 @@ import fr.nessar.lmnporganizer.backend.dto.ApartmentResponse;
|
||||
import fr.nessar.lmnporganizer.backend.mapper.ApartmentMapper;
|
||||
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class ApartmentService {
|
||||
@@ -21,34 +20,34 @@ public class ApartmentService {
|
||||
this.apartmentMapper = apartmentMapper;
|
||||
}
|
||||
|
||||
public List<ApartmentResponse> getAll() {
|
||||
return apartmentMapper.toResponses(apartmentRepository.findAll());
|
||||
public List<ApartmentResponse> getAll(String landlordId) {
|
||||
return apartmentMapper.toResponses(apartmentRepository.findByBailleurId(landlordId));
|
||||
}
|
||||
|
||||
public ResponseEntity<ApartmentResponse> getById(String id) {
|
||||
return apartmentRepository.findById(id).map(apartmentMapper::toResponse).map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
public Optional<ApartmentResponse> getById(String landlordId, String id) {
|
||||
return apartmentRepository.findByIdAndBailleurId(id, landlordId)
|
||||
.map(apartmentMapper::toResponse);
|
||||
}
|
||||
|
||||
public ResponseEntity<ApartmentResponse> create(ApartmentRequest request) {
|
||||
Apartment apartment = apartmentMapper.toEntity(request);
|
||||
apartment.setBailleurId("default-landlord");
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(apartmentMapper.toResponse(apartmentRepository.save(apartment)));
|
||||
public ApartmentResponse create(String landlordId, ApartmentRequest request) {
|
||||
Apartment apartment = apartmentMapper.toEntity(trim(request));
|
||||
apartment.setBailleurId(landlordId);
|
||||
return apartmentMapper.toResponse(apartmentRepository.save(apartment));
|
||||
}
|
||||
|
||||
public ResponseEntity<ApartmentResponse> update(String id, ApartmentRequest request) {
|
||||
return apartmentRepository.findById(id).map(apartment -> {
|
||||
apartmentMapper.update(request, apartment);
|
||||
apartment.setBailleurId("default-landlord");
|
||||
return ResponseEntity.ok(apartmentMapper.toResponse(apartmentRepository.save(apartment)));
|
||||
}).orElse(ResponseEntity.notFound().build());
|
||||
public Optional<ApartmentResponse> update(String landlordId, String id, ApartmentRequest request) {
|
||||
return apartmentRepository.findByIdAndBailleurId(id, landlordId).map(apartment -> {
|
||||
apartmentMapper.update(trim(request), apartment);
|
||||
return apartmentMapper.toResponse(apartmentRepository.save(apartment));
|
||||
});
|
||||
}
|
||||
|
||||
public ResponseEntity<Void> delete(String id) {
|
||||
if (!apartmentRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
apartmentRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
public boolean delete(String landlordId, String id) {
|
||||
return apartmentRepository.deleteByIdAndBailleurId(id, landlordId) == 1;
|
||||
}
|
||||
|
||||
private ApartmentRequest trim(ApartmentRequest request) {
|
||||
return new ApartmentRequest(request.name().trim(), request.address().trim(), request.purchasePrice(),
|
||||
request.description() == null ? "" : request.description().trim());
|
||||
}
|
||||
}
|
||||
+15
-6
@@ -21,15 +21,11 @@ public class LandlordProfileService {
|
||||
}
|
||||
|
||||
public LandlordProfileResponse getProfile(OidcUser principal) {
|
||||
Landlord landlord = landlordRepository.findByOidcSubject(principal.getSubject()).orElseGet(() ->
|
||||
new Landlord(null, principal.getSubject(), claim(principal, "given_name"),
|
||||
claim(principal, "family_name"), principal.getClaimAsString("email"), "", ""));
|
||||
return response(landlord);
|
||||
return response(findOrCreate(principal));
|
||||
}
|
||||
|
||||
public LandlordProfileResponse updateProfile(OidcUser principal, LandlordProfileRequest request) {
|
||||
Landlord landlord = landlordRepository.findByOidcSubject(principal.getSubject())
|
||||
.orElseGet(() -> new Landlord(null, principal.getSubject(), "", "", "", "", ""));
|
||||
Landlord landlord = findOrCreate(principal);
|
||||
String email = request.email().trim();
|
||||
landlordRepository.findByEmail(email)
|
||||
.filter(existing -> !existing.getId().equals(landlord.getId()))
|
||||
@@ -44,6 +40,19 @@ public class LandlordProfileService {
|
||||
return response(save(landlord));
|
||||
}
|
||||
|
||||
public Landlord findOrCreate(OidcUser principal) {
|
||||
return landlordRepository.findByOidcSubject(principal.getSubject()).orElseGet(() -> {
|
||||
Landlord landlord = new Landlord("oidc:" + principal.getSubject(), principal.getSubject(),
|
||||
claim(principal, "given_name"), claim(principal, "family_name"),
|
||||
claim(principal, "email"), "", "");
|
||||
try {
|
||||
return landlordRepository.save(landlord);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
return landlordRepository.findByOidcSubject(principal.getSubject()).orElseThrow(() -> exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Landlord requireCompleteProfileFor(String oidcSubject) {
|
||||
Landlord landlord = landlordRepository.findByOidcSubject(oidcSubject)
|
||||
.orElseThrow(IncompleteLandlordProfileException::new);
|
||||
|
||||
+39
-22
@@ -1,7 +1,10 @@
|
||||
package fr.nessar.lmnporganizer.backend.controller;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.model.Apartment;
|
||||
import fr.nessar.lmnporganizer.backend.model.Landlord;
|
||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LandlordRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
@@ -9,6 +12,7 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -30,6 +34,16 @@ class ApartmentControllerTest {
|
||||
@MockitoBean
|
||||
private ApartmentRepository apartmentRepository;
|
||||
|
||||
@MockitoBean
|
||||
private LandlordRepository landlordRepository;
|
||||
|
||||
@BeforeEach
|
||||
void resolveAuthenticatedLandlord() {
|
||||
when(landlordRepository.findByOidcSubject("subject-1")).thenReturn(Optional.of(
|
||||
new Landlord("landlord-1", "subject-1", "Camille", "Martin", "camille@example.com",
|
||||
"+33600000000", "1 rue de Paris")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGetApartmentsWithoutToken_thenUnauthorized() throws Exception {
|
||||
mockMvc.perform(get("/api/apartments"))
|
||||
@@ -38,10 +52,10 @@ class ApartmentControllerTest {
|
||||
|
||||
@Test
|
||||
void whenGetApartmentsWithToken_thenOk() throws Exception {
|
||||
Apartment apt = new Apartment("1", "default-landlord", "Appartement 1", "Adresse 1", 150000.0, "Desc");
|
||||
when(apartmentRepository.findAll()).thenReturn(List.of(apt));
|
||||
Apartment apt = new Apartment("1", "landlord-1", "Appartement 1", "Adresse 1", 150000.0, "Desc");
|
||||
when(apartmentRepository.findByBailleurId("landlord-1")).thenReturn(List.of(apt));
|
||||
|
||||
mockMvc.perform(get("/api/apartments").with(oidcLogin()))
|
||||
mockMvc.perform(get("/api/apartments").with(asLandlord()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].id").value("1"))
|
||||
.andExpect(jsonPath("$[0].name").value("Appartement 1"));
|
||||
@@ -49,29 +63,29 @@ class ApartmentControllerTest {
|
||||
|
||||
@Test
|
||||
void whenGetApartmentByIdNotFound_thenNotFound() throws Exception {
|
||||
when(apartmentRepository.findById("999")).thenReturn(Optional.empty());
|
||||
when(apartmentRepository.findByIdAndBailleurId("999", "landlord-1")).thenReturn(Optional.empty());
|
||||
|
||||
mockMvc.perform(get("/api/apartments/999").with(oidcLogin()))
|
||||
mockMvc.perform(get("/api/apartments/999").with(asLandlord()))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGetApartmentByIdFound_thenOk() throws Exception {
|
||||
Apartment apt = new Apartment("1", "default-landlord", "Appartement 1", "Adresse 1", 150000.0, "Desc");
|
||||
when(apartmentRepository.findById("1")).thenReturn(Optional.of(apt));
|
||||
Apartment apt = new Apartment("1", "landlord-1", "Appartement 1", "Adresse 1", 150000.0, "Desc");
|
||||
when(apartmentRepository.findByIdAndBailleurId("1", "landlord-1")).thenReturn(Optional.of(apt));
|
||||
|
||||
mockMvc.perform(get("/api/apartments/1").with(oidcLogin()))
|
||||
mockMvc.perform(get("/api/apartments/1").with(asLandlord()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value("1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCreateApartment_thenCreated() throws Exception {
|
||||
Apartment savedApt = new Apartment("1", "default-landlord", "New Apartment", "Address", 200000.0, "Desc");
|
||||
Apartment savedApt = new Apartment("1", "landlord-1", "New Apartment", "Address", 200000.0, "Desc");
|
||||
when(apartmentRepository.save(any(Apartment.class))).thenReturn(savedApt);
|
||||
|
||||
mockMvc.perform(post("/api/apartments")
|
||||
.with(oidcLogin())
|
||||
.with(asLandlord())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(
|
||||
"{\"name\":\"New Apartment\",\"address\":\"Address\",\"purchasePrice\":200000.0,\"description\":\"Desc\"}"))
|
||||
@@ -82,10 +96,10 @@ class ApartmentControllerTest {
|
||||
|
||||
@Test
|
||||
void whenUpdateApartmentNotFound_thenNotFound() throws Exception {
|
||||
when(apartmentRepository.findById("999")).thenReturn(Optional.empty());
|
||||
when(apartmentRepository.findByIdAndBailleurId("999", "landlord-1")).thenReturn(Optional.empty());
|
||||
|
||||
mockMvc.perform(put("/api/apartments/999")
|
||||
.with(oidcLogin())
|
||||
.with(asLandlord())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(
|
||||
"{\"name\":\"New Name\",\"address\":\"New Address\",\"purchasePrice\":120000.0,\"description\":\"New Desc\"}"))
|
||||
@@ -94,14 +108,14 @@ class ApartmentControllerTest {
|
||||
|
||||
@Test
|
||||
void whenUpdateApartmentFound_thenOk() throws Exception {
|
||||
Apartment existing = new Apartment("1", "default-landlord", "Old Name", "Old Address", 100000.0, "Old Desc");
|
||||
Apartment updated = new Apartment("1", "default-landlord", "New Name", "New Address", 120000.0, "New Desc");
|
||||
Apartment existing = new Apartment("1", "landlord-1", "Old Name", "Old Address", 100000.0, "Old Desc");
|
||||
Apartment updated = new Apartment("1", "landlord-1", "New Name", "New Address", 120000.0, "New Desc");
|
||||
|
||||
when(apartmentRepository.findById("1")).thenReturn(Optional.of(existing));
|
||||
when(apartmentRepository.findByIdAndBailleurId("1", "landlord-1")).thenReturn(Optional.of(existing));
|
||||
when(apartmentRepository.save(any(Apartment.class))).thenReturn(updated);
|
||||
|
||||
mockMvc.perform(put("/api/apartments/1")
|
||||
.with(oidcLogin())
|
||||
.with(asLandlord())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(
|
||||
"{\"name\":\"New Name\",\"address\":\"New Address\",\"purchasePrice\":120000.0,\"description\":\"New Desc\"}"))
|
||||
@@ -112,20 +126,23 @@ class ApartmentControllerTest {
|
||||
|
||||
@Test
|
||||
void whenDeleteApartmentNotFound_thenNotFound() throws Exception {
|
||||
when(apartmentRepository.existsById("999")).thenReturn(false);
|
||||
when(apartmentRepository.deleteByIdAndBailleurId("999", "landlord-1")).thenReturn(0L);
|
||||
|
||||
mockMvc.perform(delete("/api/apartments/999").with(oidcLogin()))
|
||||
mockMvc.perform(delete("/api/apartments/999").with(asLandlord()))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenDeleteApartmentFound_thenNoContent() throws Exception {
|
||||
when(apartmentRepository.existsById("1")).thenReturn(true);
|
||||
doNothing().when(apartmentRepository).deleteById("1");
|
||||
when(apartmentRepository.deleteByIdAndBailleurId("1", "landlord-1")).thenReturn(1L);
|
||||
|
||||
mockMvc.perform(delete("/api/apartments/1").with(oidcLogin()))
|
||||
mockMvc.perform(delete("/api/apartments/1").with(asLandlord()))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
verify(apartmentRepository, times(1)).deleteById("1");
|
||||
verify(apartmentRepository).deleteByIdAndBailleurId("1", "landlord-1");
|
||||
}
|
||||
|
||||
private static RequestPostProcessor asLandlord() {
|
||||
return oidcLogin().idToken(token -> token.claim("sub", "subject-1"));
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -37,8 +37,9 @@ class LandlordProfileControllerTest {
|
||||
private LandlordRepository landlordRepository;
|
||||
|
||||
@Test
|
||||
void whenFirstProfileRead_thenOidcClaimsPrefillTheNewProfile() throws Exception {
|
||||
void whenFirstProfileRead_thenOidcClaimsPrefillAndPersistTheNewProfile() throws Exception {
|
||||
when(landlordRepository.findByOidcSubject("subject-1")).thenReturn(Optional.empty());
|
||||
when(landlordRepository.save(any(Landlord.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
mockMvc.perform(get("/api/landlord-profile")
|
||||
.with(oidcLogin().idToken(token -> token
|
||||
@@ -52,7 +53,7 @@ class LandlordProfileControllerTest {
|
||||
.andExpect(jsonPath("$.email").value("camille@example.com"));
|
||||
|
||||
verify(landlordRepository).findByOidcSubject("subject-1");
|
||||
verify(landlordRepository, never()).save(any(Landlord.class));
|
||||
verify(landlordRepository).save(any(Landlord.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+11
-5
@@ -42,17 +42,23 @@ class ApartmentPersistenceIT {
|
||||
}
|
||||
|
||||
@Test
|
||||
void savesAndLoadsAnApartmentWithMongoDB() {
|
||||
Apartment saved = apartmentRepository.save(new Apartment(null, "default-landlord", "Studio République",
|
||||
void savesAndLoadsAnApartmentWithinItsBailleurPortfolio() {
|
||||
Apartment saved = apartmentRepository.save(new Apartment(null, "landlord-1", "Studio République",
|
||||
"12 rue de la République, Lyon", 150000, "Deuxième étage"));
|
||||
apartmentRepository.save(new Apartment(null, "landlord-2", "T2 Croix-Rousse",
|
||||
"4 montée de la Boucle, Lyon", 220000, "Quatrième étage"));
|
||||
|
||||
Apartment apartment = apartmentRepository.findById(saved.getId()).orElseThrow();
|
||||
assertThat(apartment.getBailleurId()).isEqualTo("default-landlord");
|
||||
Apartment apartment = apartmentRepository.findByIdAndBailleurId(saved.getId(), "landlord-1").orElseThrow();
|
||||
assertThat(apartment.getBailleurId()).isEqualTo("landlord-1");
|
||||
assertThat(apartment.getAddress()).isEqualTo("12 rue de la République, Lyon");
|
||||
assertThat(apartmentRepository.findByBailleurId("landlord-1"))
|
||||
.extracting(Apartment::getId)
|
||||
.containsExactly(saved.getId());
|
||||
assertThat(apartmentRepository.findByIdAndBailleurId(saved.getId(), "landlord-2")).isEmpty();
|
||||
}
|
||||
|
||||
private static MongoDBContainer startMongoDb() {
|
||||
if (System.getenv("SPRING_MONGODB_URI") != null) {
|
||||
if (System.getenv("SPRING_DATA_MONGODB_URI") != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
package fr.nessar.lmnporganizer.backend.integration;
|
||||
|
||||
import fr.nessar.lmnporganizer.backend.BackendApplication;
|
||||
import fr.nessar.lmnporganizer.backend.model.Landlord;
|
||||
import fr.nessar.lmnporganizer.backend.repository.ApartmentRepository;
|
||||
import fr.nessar.lmnporganizer.backend.repository.LandlordRepository;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
import org.testcontainers.mongodb.MongoDBContainer;
|
||||
|
||||
import static com.jayway.jsonpath.JsonPath.read;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest(classes = BackendApplication.class, properties = "lmnp.data-initializer.enabled=false")
|
||||
@AutoConfigureMockMvc
|
||||
class ApartmentPortfolioApiIT {
|
||||
|
||||
private static final String VALID_APARTMENT = """
|
||||
{"name":"Studio République","address":"12 rue de la République, Lyon","purchasePrice":150000,"description":"Deuxième étage"}
|
||||
""";
|
||||
private static final String UPDATED_APARTMENT = """
|
||||
{"name":"Studio rénové","address":"12 rue de la République, Lyon","purchasePrice":165000,"description":"Troisième étage"}
|
||||
""";
|
||||
private static final String INVALID_APARTMENT = """
|
||||
{"name":" ","address":"","purchasePrice":0,"description":"Incomplet"}
|
||||
""";
|
||||
private static final MongoDBContainer mongoDb = startMongoDb();
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ApartmentRepository apartmentRepository;
|
||||
|
||||
@Autowired
|
||||
private LandlordRepository landlordRepository;
|
||||
|
||||
@DynamicPropertySource
|
||||
static void configureMongoDb(DynamicPropertyRegistry registry) {
|
||||
if (mongoDb != null) {
|
||||
registry.add("spring.mongodb.uri", () -> mongoDb.getConnectionString() + "/lmnp-apartment-portfolios");
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUpBailleurs() {
|
||||
apartmentRepository.deleteAll();
|
||||
landlordRepository.deleteAll();
|
||||
landlordRepository.save(new Landlord("landlord-alice", "subject-alice", "Alice", "Martin",
|
||||
"alice@example.com", "+33600000000", "1 rue de Paris"));
|
||||
landlordRepository.save(new Landlord("landlord-bob", "subject-bob", "Bob", "Durand",
|
||||
"bob@example.com", "+33600000001", "2 rue de Lyon"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearData() {
|
||||
apartmentRepository.deleteAll();
|
||||
landlordRepository.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void eachBailleurCanManageOnlyTheirOwnApartments() throws Exception {
|
||||
String aliceApartmentId = createApartment(as("subject-alice"), VALID_APARTMENT);
|
||||
String bobApartmentId = createApartment(as("subject-bob"), """
|
||||
{"name":"T2 Croix-Rousse","address":"4 montée de la Boucle, Lyon","purchasePrice":220000,"description":"Quatrième étage"}
|
||||
""");
|
||||
|
||||
mockMvc.perform(get("/api/apartments").with(as("subject-alice")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(1)))
|
||||
.andExpect(jsonPath("$[0].id").value(aliceApartmentId))
|
||||
.andExpect(jsonPath("$[0].bailleurId").doesNotExist());
|
||||
|
||||
mockMvc.perform(get("/api/apartments/{id}", aliceApartmentId).with(as("subject-alice")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.name").value("Studio République"))
|
||||
.andExpect(jsonPath("$.bailleurId").doesNotExist());
|
||||
|
||||
mockMvc.perform(get("/api/apartments/{id}", aliceApartmentId).with(as("subject-bob")))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
mockMvc.perform(put("/api/apartments/{id}", aliceApartmentId)
|
||||
.with(as("subject-bob"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(UPDATED_APARTMENT))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
mockMvc.perform(delete("/api/apartments/{id}", aliceApartmentId).with(as("subject-bob")))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
mockMvc.perform(put("/api/apartments/{id}", aliceApartmentId)
|
||||
.with(as("subject-alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(UPDATED_APARTMENT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.name").value("Studio rénové"))
|
||||
.andExpect(jsonPath("$.bailleurId").doesNotExist());
|
||||
|
||||
mockMvc.perform(get("/api/apartments/{id}", aliceApartmentId).with(as("subject-alice")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.name").value("Studio rénové"));
|
||||
|
||||
mockMvc.perform(delete("/api/apartments/{id}", bobApartmentId).with(as("subject-bob")))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(get("/api/apartments/{id}", bobApartmentId).with(as("subject-bob")))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createRejectsAnInvalidApartment() throws Exception {
|
||||
mockMvc.perform(post("/api/apartments")
|
||||
.with(as("subject-alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(INVALID_APARTMENT))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createNormalizesMissingDescriptionToEmptyString() throws Exception {
|
||||
mockMvc.perform(post("/api/apartments")
|
||||
.with(as("subject-alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"name":"Studio République","address":"12 rue de la République, Lyon","purchasePrice":150000}
|
||||
"""))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.description").value(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateRejectsAnInvalidApartment() throws Exception {
|
||||
String apartmentId = createApartment(as("subject-alice"), VALID_APARTMENT);
|
||||
|
||||
mockMvc.perform(put("/api/apartments/{id}", apartmentId)
|
||||
.with(as("subject-alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(INVALID_APARTMENT))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void firstApartmentRequestCreatesOneBailleurAndLaterRequestsReuseIt() throws Exception {
|
||||
createApartment(asNewBailleur(), VALID_APARTMENT);
|
||||
createApartment(asNewBailleur(), UPDATED_APARTMENT);
|
||||
|
||||
mockMvc.perform(get("/api/landlord-profile").with(asNewBailleur()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.firstName").value("Chloé"))
|
||||
.andExpect(jsonPath("$.lastName").value("Bernard"))
|
||||
.andExpect(jsonPath("$.email").value("chloe@example.com"));
|
||||
|
||||
assertThat(landlordRepository.count()).isEqualTo(3);
|
||||
assertThat(landlordRepository.findByOidcSubject("subject-chloe")).isPresent();
|
||||
}
|
||||
|
||||
private String createApartment(RequestPostProcessor principal, String request) throws Exception {
|
||||
String response = mockMvc.perform(post("/api/apartments")
|
||||
.with(principal)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(request))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.bailleurId").doesNotExist())
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
return read(response, "$.id");
|
||||
}
|
||||
|
||||
private static RequestPostProcessor as(String oidcSubject) {
|
||||
return oidcLogin().idToken(token -> token.claim("sub", oidcSubject));
|
||||
}
|
||||
|
||||
private static RequestPostProcessor asNewBailleur() {
|
||||
return oidcLogin().idToken(token -> token
|
||||
.claim("sub", "subject-chloe")
|
||||
.claim("given_name", "Chloé")
|
||||
.claim("family_name", "Bernard")
|
||||
.claim("email", "chloe@example.com"));
|
||||
}
|
||||
|
||||
private static MongoDBContainer startMongoDb() {
|
||||
if (System.getenv("SPRING_DATA_MONGODB_URI") != null) {
|
||||
return null;
|
||||
}
|
||||
MongoDBContainer container = new MongoDBContainer("mongo:8.0");
|
||||
container.start();
|
||||
return container;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -87,7 +87,7 @@ class RentPaymentPersistenceIT {
|
||||
}
|
||||
|
||||
private static MongoDBContainer startMongoDb() {
|
||||
if (System.getenv("SPRING_MONGODB_URI") != null) {
|
||||
if (System.getenv("SPRING_DATA_MONGODB_URI") != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class ModelCoverageTest {
|
||||
void testLandlordGettersSettersAndConstructors() {
|
||||
Landlord landlord = new Landlord();
|
||||
assertNull(landlord.getId());
|
||||
assertNull(landlord.getOidcSubject());
|
||||
assertNull(landlord.getFirstName());
|
||||
assertNull(landlord.getLastName());
|
||||
assertNull(landlord.getEmail());
|
||||
@@ -21,6 +22,7 @@ class ModelCoverageTest {
|
||||
assertNull(landlord.getAddress());
|
||||
|
||||
landlord.setId("id");
|
||||
landlord.setOidcSubject("subject");
|
||||
landlord.setFirstName("first");
|
||||
landlord.setLastName("last");
|
||||
landlord.setEmail("email");
|
||||
@@ -28,14 +30,16 @@ class ModelCoverageTest {
|
||||
landlord.setAddress("address");
|
||||
|
||||
assertEquals("id", landlord.getId());
|
||||
assertEquals("subject", landlord.getOidcSubject());
|
||||
assertEquals("first", landlord.getFirstName());
|
||||
assertEquals("last", landlord.getLastName());
|
||||
assertEquals("email", landlord.getEmail());
|
||||
assertEquals("phone", landlord.getPhone());
|
||||
assertEquals("address", landlord.getAddress());
|
||||
|
||||
Landlord fullLandlord = new Landlord("id2", "first2", "last2", "email2", "phone2", "address2");
|
||||
Landlord fullLandlord = new Landlord("id2", "subject2", "first2", "last2", "email2", "phone2", "address2");
|
||||
assertEquals("id2", fullLandlord.getId());
|
||||
assertEquals("subject2", fullLandlord.getOidcSubject());
|
||||
assertEquals("first2", fullLandlord.getFirstName());
|
||||
assertEquals("last2", fullLandlord.getLastName());
|
||||
assertEquals("email2", fullLandlord.getEmail());
|
||||
|
||||
Reference in new issue
Block a user