feat(geschichten): blog-like family memory stories (closes #381) #382
@@ -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