feat: isolate apartment portfolios by landlord (#25)

This commit is contained in:
nessar committed 2026-08-17 14:18:15 +02:00
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.");
}
}
}
@@ -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;
@@ -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);
}
@@ -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());
}
}
@@ -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);
@@ -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"));
}
}
@@ -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
@@ -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;
}
@@ -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;
}
}
@@ -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());