test(geschichte): add Testcontainers integration test and fix V58 author FK
The end-to-end test creates a DRAFT, verifies it is hidden from a READ_ALL reader (list and getById), publishes it, verifies the reader sees it, then deletes it and confirms the join rows go with it but the linked Person remains. Also corrects the V58 author FK to reference the actual users table (not app_users). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ CREATE TABLE geschichten (
|
|||||||
title VARCHAR(255) NOT NULL,
|
title VARCHAR(255) NOT NULL,
|
||||||
body TEXT,
|
body TEXT,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
author_id UUID REFERENCES app_users (id) ON DELETE SET NULL,
|
author_id UUID REFERENCES users (id) ON DELETE SET NULL,
|
||||||
created_at TIMESTAMP NOT NULL,
|
created_at TIMESTAMP NOT NULL,
|
||||||
updated_at TIMESTAMP NOT NULL,
|
updated_at TIMESTAMP NOT NULL,
|
||||||
published_at TIMESTAMP
|
published_at TIMESTAMP
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package org.raddatz.familienarchiv.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||||
|
import org.raddatz.familienarchiv.dto.GeschichteUpdateDTO;
|
||||||
|
import org.raddatz.familienarchiv.model.AppUser;
|
||||||
|
import org.raddatz.familienarchiv.model.Geschichte;
|
||||||
|
import org.raddatz.familienarchiv.model.GeschichteStatus;
|
||||||
|
import org.raddatz.familienarchiv.model.Person;
|
||||||
|
import org.raddatz.familienarchiv.repository.AppUserRepository;
|
||||||
|
import org.raddatz.familienarchiv.repository.GeschichteRepository;
|
||||||
|
import org.raddatz.familienarchiv.repository.PersonRepository;
|
||||||
|
import org.raddatz.familienarchiv.security.Permission;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@Import(PostgresContainerConfig.class)
|
||||||
|
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||||
|
class GeschichteServiceIntegrationTest {
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
S3Client s3Client;
|
||||||
|
|
||||||
|
@Autowired GeschichteService geschichteService;
|
||||||
|
@Autowired GeschichteRepository geschichteRepository;
|
||||||
|
@Autowired PersonRepository personRepository;
|
||||||
|
@Autowired AppUserRepository appUserRepository;
|
||||||
|
|
||||||
|
AppUser writer;
|
||||||
|
AppUser reader;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void seed() {
|
||||||
|
writer = appUserRepository.save(AppUser.builder()
|
||||||
|
.email("writer-int@test")
|
||||||
|
.password("hash")
|
||||||
|
.build());
|
||||||
|
reader = appUserRepository.save(AppUser.builder()
|
||||||
|
.email("reader-int@test")
|
||||||
|
.password("hash")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void clear() {
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void create_then_publish_then_read_then_delete_full_lifecycle() {
|
||||||
|
// Create as writer
|
||||||
|
authenticateAs(writer, Permission.BLOG_WRITE);
|
||||||
|
|
||||||
|
Person franz = personRepository.save(Person.builder().firstName("Franz").lastName("Raddatz").build());
|
||||||
|
|
||||||
|
GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
|
||||||
|
dto.setTitle("Erinnerung an Opa Franz");
|
||||||
|
dto.setBody("<p>Ich erinnere mich, wie er <strong>jeden Sonntag</strong> sang.</p>"
|
||||||
|
+ "<script>alert('xss')</script>");
|
||||||
|
dto.setPersonIds(List.of(franz.getId()));
|
||||||
|
|
||||||
|
Geschichte created = geschichteService.create(dto);
|
||||||
|
|
||||||
|
assertThat(created.getId()).isNotNull();
|
||||||
|
assertThat(created.getStatus()).isEqualTo(GeschichteStatus.DRAFT);
|
||||||
|
assertThat(created.getBody())
|
||||||
|
.contains("<strong>jeden Sonntag</strong>")
|
||||||
|
.doesNotContain("<script>");
|
||||||
|
|
||||||
|
// Reader cannot see DRAFT in list
|
||||||
|
authenticateAs(reader, Permission.READ_ALL);
|
||||||
|
assertThat(geschichteService.list(null, null, null, 50)).isEmpty();
|
||||||
|
|
||||||
|
// Reader cannot fetch DRAFT by id (404 via GESCHICHTE_NOT_FOUND)
|
||||||
|
UUID draftId = created.getId();
|
||||||
|
org.assertj.core.api.Assertions.assertThatThrownBy(() -> geschichteService.getById(draftId))
|
||||||
|
.hasMessageContaining("not found");
|
||||||
|
|
||||||
|
// Publish as writer
|
||||||
|
authenticateAs(writer, Permission.BLOG_WRITE);
|
||||||
|
GeschichteUpdateDTO publishDto = new GeschichteUpdateDTO();
|
||||||
|
publishDto.setStatus(GeschichteStatus.PUBLISHED);
|
||||||
|
Geschichte publishedGesch = geschichteService.update(draftId, publishDto);
|
||||||
|
assertThat(publishedGesch.getPublishedAt()).isNotNull();
|
||||||
|
|
||||||
|
// Reader can now see and fetch it
|
||||||
|
authenticateAs(reader, Permission.READ_ALL);
|
||||||
|
assertThat(geschichteService.list(null, null, null, 50)).hasSize(1);
|
||||||
|
assertThat(geschichteService.list(null, franz.getId(), null, 50)).hasSize(1);
|
||||||
|
Geschichte fetched = geschichteService.getById(draftId);
|
||||||
|
assertThat(fetched.getTitle()).isEqualTo("Erinnerung an Opa Franz");
|
||||||
|
assertThat(fetched.getPersons()).extracting(Person::getId).containsExactly(franz.getId());
|
||||||
|
|
||||||
|
// Delete as writer; join rows go with it
|
||||||
|
authenticateAs(writer, Permission.BLOG_WRITE);
|
||||||
|
geschichteService.delete(draftId);
|
||||||
|
assertThat(geschichteRepository.findById(draftId)).isEmpty();
|
||||||
|
|
||||||
|
// The Person itself is untouched (cascade only flows from Geschichte to join table)
|
||||||
|
assertThat(personRepository.findById(franz.getId())).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void authenticateAs(AppUser user, Permission... permissions) {
|
||||||
|
var authorities = java.util.Arrays.stream(permissions)
|
||||||
|
.map(p -> new SimpleGrantedAuthority(p.name()))
|
||||||
|
.toList();
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(
|
||||||
|
new UsernamePasswordAuthenticationToken(user.getEmail(), null, authorities));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user