Fix app smokescreen and ImageController integration tests for use with Postgres.

This commit is contained in:
Jesse Brault 2025-12-26 23:26:51 -06:00
parent b9e7ccedce
commit 2642f6100e
6 changed files with 154 additions and 147 deletions

View File

@ -83,10 +83,10 @@ dependencies {
runtimeOnly 'org.apache.xmlgraphics:batik-all:1.19'
// Custom testing
testRuntimeOnly 'com.h2database:h2'
testImplementation 'org.testcontainers:testcontainers:1.21.4'
testImplementation 'org.testcontainers:junit-jupiter:1.21.4'
testImplementation "org.testcontainers:minio:1.21.4"
testImplementation 'org.testcontainers:postgresql:1.21.4'
testImplementation 'org.testcontainers:minio:1.21.4'
testFixturesImplementation 'org.hamcrest:hamcrest:3.0'
}

View File

@ -0,0 +1,19 @@
package app.mealsmadeeasy.api;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.testcontainers.containers.PostgreSQLContainer;
public class IntegrationTestsExtension implements BeforeAllCallback {
@Override
public void beforeAll(ExtensionContext context) {
final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("pgvector/pgvector:pg18-trixie")
.withDatabaseName("meals_made_easy_api");
postgres.start();
System.setProperty("spring.datasource.url", postgres.getJdbcUrl());
System.setProperty("spring.datasource.username", postgres.getUsername());
System.setProperty("spring.datasource.password", postgres.getPassword());
}
}

View File

@ -1,12 +1,14 @@
package app.mealsmadeeasy.api;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MealsMadeEasyApiApplicationTests {
@ExtendWith(IntegrationTestsExtension.class)
public class MealsMadeEasyApiApplicationTests {
@Test
void contextLoads() {}
public void contextLoads() {}
}

View File

@ -1,5 +1,6 @@
package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.IntegrationTestsExtension;
import app.mealsmadeeasy.api.auth.AuthService;
import app.mealsmadeeasy.api.auth.LoginException;
import app.mealsmadeeasy.api.image.body.ImageUpdateInfoBody;
@ -9,12 +10,12 @@ import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserCreateException;
import app.mealsmadeeasy.api.user.UserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.mock.web.MockMultipartFile;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
@ -27,7 +28,7 @@ import tools.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.UUID;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.empty;
@ -38,10 +39,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Testcontainers
@SpringBootTest
@AutoConfigureMockMvc
@ExtendWith(IntegrationTestsExtension.class)
public class ImageControllerTests {
private static final String USER_FILENAME = "HAL9000.svg";
@Container
private static final MinIOContainer container = new MinIOContainer(
DockerImageName.parse("minio/minio:latest")
@ -54,7 +54,7 @@ public class ImageControllerTests {
registry.add("app.mealsmadeeasy.api.minio.secretKey", container::getPassword);
}
private static InputStream getHal9000() {
private static InputStream getHal9000InputStream() {
return ImageControllerTests.class.getResourceAsStream("HAL9000.svg");
}
@ -73,65 +73,73 @@ public class ImageControllerTests {
@Autowired
private ObjectMapper objectMapper;
private User createTestUser(String username) {
private static final String TEST_PASSWORD = "test";
private static final long HAL_SIZE = 27881L;
private static String makeUserFilename() {
return UUID.randomUUID() + ".svg";
}
private User seedUser() {
final String uuid = UUID.randomUUID().toString();
try {
return this.userService.createUser(username, username + "@test.com", "test");
return this.userService.createUser(uuid, uuid + "@test.com", TEST_PASSWORD);
} catch (UserCreateException e) {
throw new RuntimeException(e);
}
}
private Image createHal9000(User owner) throws ImageException, IOException {
try (final InputStream hal9000 = getHal9000()) {
private Image seedImage(User owner, ImageCreateInfoSpec spec) {
try {
return this.imageService.create(
owner,
USER_FILENAME,
hal9000,
27881L,
new ImageCreateInfoSpec()
makeUserFilename(),
getHal9000InputStream(),
HAL_SIZE,
spec
);
} catch (IOException | ImageException e) {
throw new RuntimeException(e);
}
}
private String getAccessToken(String username) {
private Image seedImage(User owner) {
return this.seedImage(owner, new ImageCreateInfoSpec());
}
private static String getImageUrl(User owner, Image image) {
return "/images/" + owner.getUsername() + "/" + image.getUserFilename();
}
private String getAccessToken(User user) {
try {
return this.authService.login(username, "test").getAccessToken().getToken();
return this.authService.login(user.getUsername(), TEST_PASSWORD).getAccessToken().getToken();
} catch (LoginException e) {
throw new RuntimeException(e);
}
}
private Image makePublic(Image image, User modifier) {
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setPublic(true);
return this.imageService.update(image, modifier, spec);
}
private Image addViewer(Image image, User modifier, User viewerToAdd) {
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setViewersToAdd(Set.of(viewerToAdd));
return this.imageService.update(image, modifier, spec);
}
@Test
@DirtiesContext
public void getImageNoPrincipal() throws Exception {
final User owner = this.createTestUser("imageOwner");
final Image image = this.createHal9000(owner);
this.makePublic(image, owner);
try (final InputStream hal9000 = getHal9000()) {
public void getPublicImageNoPrincipal() throws Exception {
final User owner = this.seedUser();
final ImageCreateInfoSpec spec = new ImageCreateInfoSpec();
spec.setPublic(true);
final Image image = this.seedImage(owner, spec);
// Assert bytes the same and proper mime type
try (final InputStream hal9000 = getHal9000InputStream()) {
final byte[] halBytes = hal9000.readAllBytes();
this.mockMvc.perform(get("/images/imageOwner/HAL9000.svg"))
this.mockMvc.perform(get(getImageUrl(owner, image)))
.andExpect(status().isOk())
.andExpect(content().contentType("image/svg+xml"))
.andExpect(content().bytes(halBytes));
}
}
private void doGetImageTestWithViewer(String accessToken) throws Exception {
try (final InputStream hal9000 = getHal9000()) {
private void doGetImageTestWithAccessToken(User owner, Image image, String accessToken) throws Exception {
try (final InputStream hal9000 = getHal9000InputStream()) {
final byte[] halBytes = hal9000.readAllBytes();
this.mockMvc.perform(get("/images/imageOwner/HAL9000.svg")
this.mockMvc.perform(get(getImageUrl(owner, image))
.header("Authorization", "Bearer " + accessToken))
.andExpect(status().isOk())
.andExpect(content().contentType("image/svg+xml"))
@ -140,88 +148,73 @@ public class ImageControllerTests {
}
@Test
@DirtiesContext
public void getImageWithOwner() throws Exception {
final User owner = this.createTestUser("imageOwner");
this.createHal9000(owner);
final String accessToken = this.getAccessToken(owner.getUsername());
this.doGetImageTestWithViewer(accessToken);
public void getImageForOwner() throws Exception {
final User owner = this.seedUser();
final Image image = this.seedImage(owner);
this.doGetImageTestWithAccessToken(owner, image, this.getAccessToken(owner));
}
@Test
@DirtiesContext
public void getImageWithViewer() throws Exception {
final User owner = this.createTestUser("imageOwner");
final User viewer = this.createTestUser("viewer");
final Image image = this.createHal9000(owner);
this.addViewer(image, owner, viewer);
final String accessToken = this.getAccessToken(viewer.getUsername());
this.doGetImageTestWithViewer(accessToken);
public void getImageForViewer() throws Exception {
final User owner = this.seedUser();
final User viewer = this.seedUser();
final Image image = this.seedImage(owner);
// add viewer
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setViewersToAdd(Set.of(viewer));
this.imageService.update(image, owner, spec);
this.doGetImageTestWithAccessToken(owner, image, this.getAccessToken(viewer));
}
@Test
@DirtiesContext
public void getNonPublicImageNoPrincipalForbidden() throws Exception {
final User owner = this.createTestUser("imageOwner");
this.createHal9000(owner);
this.mockMvc.perform(
get("/images/imageOwner/HAL9000.svg")
).andExpect(status().isForbidden());
final User owner = this.seedUser();
final Image image = this.seedImage(owner);
this.mockMvc.perform(get(getImageUrl(owner, image)))
.andExpect(status().isForbidden());
}
@Test
@DirtiesContext
public void getNonPublicImageWithPrincipalForbidden() throws Exception {
final User owner = this.createTestUser("imageOwner");
final User viewer = this.createTestUser("viewer");
this.createHal9000(owner);
final String accessToken = this.getAccessToken(viewer.getUsername());
final User owner = this.seedUser();
final Image image = this.seedImage(owner);
final User nonViewer = this.seedUser();
final String nonViewerToken = this.getAccessToken(nonViewer);
this.mockMvc.perform(
get("/images/imageOwner/HAL9000.svg")
.header("Authorization", "Bearer " + accessToken)
get(getImageUrl(owner, image))
.header("Authorization", "Bearer " + nonViewerToken)
).andExpect(status().isForbidden());
}
@Test
@DirtiesContext
public void getImageWithViewersNoPrincipalForbidden() throws Exception {
final User owner = this.createTestUser("imageOwner");
final User viewer = this.createTestUser("viewer");
final Image image = this.createHal9000(owner);
this.addViewer(image, owner, viewer);
this.mockMvc.perform(
get("/images/imageOwner/HAL9000.svg")
).andExpect(status().isForbidden());
final User owner = this.seedUser();
final Image image = this.seedImage(owner);
final User viewer = this.seedUser();
// add viewer
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setViewersToAdd(Set.of(viewer));
this.imageService.update(image, owner, spec);
this.mockMvc.perform(get(getImageUrl(owner, image))).andExpect(status().isForbidden());
}
@Test
@DirtiesContext
public void getImageWithViewersWrongViewerForbidden() throws Exception {
final User owner = this.createTestUser("imageOwner");
final User viewer = this.createTestUser("viewer");
final User wrongViewer = this.createTestUser("wrongViewer");
final Image image = this.createHal9000(owner);
this.addViewer(image, owner, viewer);
final String accessToken = this.getAccessToken(wrongViewer.getUsername());
this.mockMvc.perform(
get("/images/imageOwner/HAL9000.svg")
.header("Authorization", "Bearer " + accessToken)
).andExpect(status().isForbidden());
}
@Test
@DirtiesContext
public void putImage() throws Exception {
final User owner = this.createTestUser("imageOwner");
final String accessToken = this.getAccessToken(owner.getUsername());
try (final InputStream hal9000 = getHal9000()) {
final User owner = this.seedUser();
final String accessToken = this.getAccessToken(owner);
try (final InputStream hal9000 = getHal9000InputStream()) {
final String userFilename = makeUserFilename();
final MockMultipartFile mockMultipartFile = new MockMultipartFile(
"image", "HAL9000.svg", "image/svg+xml", hal9000
"image", userFilename, "image/svg+xml", hal9000
);
this.mockMvc.perform(
multipart("/images")
.file(mockMultipartFile)
.param("filename", "HAL9000.svg")
.param("filename", userFilename)
.param("alt", "HAL 9000")
.param("caption", "HAL 9000, from 2001: A Space Odyssey")
.param("isPublic", "true")
@ -234,31 +227,26 @@ public class ImageControllerTests {
.andExpect(status().isCreated())
.andExpect(jsonPath("$.created").exists())
.andExpect(jsonPath("$.modified").value(nullValue()))
.andExpect(jsonPath("$.filename").value(USER_FILENAME))
.andExpect(jsonPath("$.filename").value(userFilename))
.andExpect(jsonPath("$.mimeType").value("image/svg+xml"))
.andExpect(jsonPath("$.alt").value("HAL 9000"))
.andExpect(jsonPath("$.caption").value("HAL 9000, from 2001: A Space Odyssey"))
.andExpect(jsonPath("$.isPublic").value(true))
.andExpect(jsonPath("$.owner.username").value("imageOwner"))
.andExpect(jsonPath("$.owner.username").value(owner.getUsername()))
.andExpect(jsonPath("$.owner.id").value(owner.getId()))
.andExpect(jsonPath("$.viewers").value(empty()));
}
}
private String prepUpdate() throws ImageException, IOException {
final User owner = this.createTestUser("imageOwner");
this.createHal9000(owner);
return this.getAccessToken(owner.getUsername());
}
@Test
@DirtiesContext
public void updateAlt() throws Exception {
final String accessToken = this.prepUpdate();
final User owner = this.seedUser();
final Image image = this.seedImage(owner);
final String accessToken = this.getAccessToken(owner);
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
body.setAlt("HAL 9000");
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
post(getImageUrl(owner, image))
.contentType(MediaType.APPLICATION_JSON)
.content(this.objectMapper.writeValueAsString(body))
.header("Authorization", "Bearer " + accessToken)
@ -269,13 +257,14 @@ public class ImageControllerTests {
}
@Test
@DirtiesContext
public void updateCaption() throws Exception {
final String accessToken = this.prepUpdate();
final User owner = this.seedUser();
final Image image = this.seedImage(owner);
final String accessToken = this.getAccessToken(owner);
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
body.setCaption("HAL 9000 from 2001: A Space Odyssey");
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
post(getImageUrl(owner, image))
.contentType(MediaType.APPLICATION_JSON)
.content(this.objectMapper.writeValueAsString(body))
.header("Authorization", "Bearer " + accessToken)
@ -286,13 +275,14 @@ public class ImageControllerTests {
}
@Test
@DirtiesContext
public void updateIsPublic() throws Exception {
final String accessToken = this.prepUpdate();
final User owner = this.seedUser();
final Image image = this.seedImage(owner);
final String accessToken = this.getAccessToken(owner);
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
body.setPublic(true);
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
post(getImageUrl(owner, image))
.contentType(MediaType.APPLICATION_JSON)
.content(this.objectMapper.writeValueAsString(body))
.header("Authorization", "Bearer " + accessToken)
@ -303,16 +293,18 @@ public class ImageControllerTests {
}
@Test
@DirtiesContext
public void addViewers() throws Exception {
final String accessToken = this.prepUpdate();
final User owner = this.seedUser();
final User viewerToAdd = this.seedUser();
final Image image = this.seedImage(owner);
final String accessToken = this.getAccessToken(owner);
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
final Set<String> viewerUsernames = Set.of(this.createTestUser("imageViewer")).stream()
.map(User::getUsername)
.collect(Collectors.toSet());
final Set<String> viewerUsernames = Set.of(viewerToAdd.getUsername());
body.setViewersToAdd(viewerUsernames);
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
post(getImageUrl(owner, image))
.contentType(MediaType.APPLICATION_JSON)
.content(this.objectMapper.writeValueAsString(body))
.header("Authorization", "Bearer " + accessToken)
@ -320,15 +312,15 @@ public class ImageControllerTests {
.andExpect(status().isOk())
.andExpect(jsonPath("$.modified").value(notNullValue()))
.andExpect(jsonPath("$.viewers").value(not(empty())))
.andExpect(jsonPath("$.viewers[0].username").value("imageViewer"));
.andExpect(jsonPath("$.viewers[0].username").value(viewerToAdd.getUsername()));
}
private record OwnerViewerImage(User owner, User viewer, Image image) {}
private OwnerViewerImage prepOwnerViewerImage() throws ImageException, IOException {
final User owner = this.createTestUser("imageOwner");
final User viewer = this.createTestUser("imageViewer");
final Image image = this.createHal9000(owner);
private OwnerViewerImage prepOwnerViewerImage() {
final User owner = this.seedUser();
final User viewer = this.seedUser();
final Image image = this.seedImage(owner);
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setViewersToAdd(Set.of(viewer));
this.imageService.update(image, owner, spec);
@ -336,14 +328,15 @@ public class ImageControllerTests {
}
@Test
@DirtiesContext
public void removeViewers() throws Exception {
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
final String accessToken = this.getAccessToken(ownerViewerImage.owner().getUsername());
final String accessToken = this.getAccessToken(ownerViewerImage.owner());
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
body.setViewersToRemove(Set.of(ownerViewerImage.viewer().getUsername()));
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
post(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
.contentType(MediaType.APPLICATION_JSON)
.content(this.objectMapper.writeValueAsString(body))
.header("Authorization", "Bearer " + accessToken)
@ -354,14 +347,13 @@ public class ImageControllerTests {
}
@Test
@DirtiesContext
public void clearAllViewers() throws Exception {
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
final String accessToken = this.getAccessToken(ownerViewerImage.owner().getUsername());
final String accessToken = this.getAccessToken(ownerViewerImage.owner());
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
body.setClearAllViewers(true);
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
post(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
.contentType(MediaType.APPLICATION_JSON)
.content(this.objectMapper.writeValueAsString(body))
.header("Authorization", "Bearer " + accessToken)
@ -372,14 +364,13 @@ public class ImageControllerTests {
}
@Test
@DirtiesContext
public void updateInfoByViewerForbidden() throws Exception {
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
final String accessToken = this.getAccessToken(ownerViewerImage.viewer().getUsername()); // viewer
final String accessToken = this.getAccessToken(ownerViewerImage.viewer()); // viewer
final ImageUpdateInfoBody body = new ImageUpdateInfoBody();
this.mockMvc.perform(
post("/images/imageOwner/HAL9000.svg")
.contentType(MediaType.APPLICATION_JSON )
post(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
.contentType(MediaType.APPLICATION_JSON)
.content(this.objectMapper.writeValueAsString(body))
.header("Authorization", "Bearer " + accessToken)
)
@ -389,13 +380,12 @@ public class ImageControllerTests {
}
@Test
@DirtiesContext
public void deleteImageWithOwner() throws Exception {
final User owner = this.createTestUser("imageOwner");
final Image image = this.createHal9000(owner);
final String accessToken = this.getAccessToken(owner.getUsername());
final User owner = this.seedUser();
final Image image = this.seedImage(owner);
final String accessToken = this.getAccessToken(owner);
this.mockMvc.perform(
delete("/images/imageOwner/HAL9000.svg")
delete(getImageUrl(owner, image))
.header("Authorization", "Bearer " + accessToken)
)
.andExpect(status().isNoContent());
@ -403,12 +393,11 @@ public class ImageControllerTests {
}
@Test
@DirtiesContext
public void deleteImageByViewerForbidden() throws Exception {
final OwnerViewerImage ownerViewerImage = this.prepOwnerViewerImage();
final String accessToken = this.getAccessToken(ownerViewerImage.viewer().getUsername());
final String accessToken = this.getAccessToken(ownerViewerImage.viewer());
this.mockMvc.perform(
delete("/images/imageOwner/HAL9000.svg")
delete(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
.header("Authorization", "Bearer " + accessToken)
)
.andExpect(status().isForbidden());

View File

@ -1,7 +1,3 @@
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa
app.mealsmadeeasy.api.baseUrl=http://localhost:8080
app.mealsmadeeasy.api.security.access-token-lifetime=60
app.mealsmadeeasy.api.security.refresh-token-lifetime=120

View File

@ -0,0 +1 @@
CREATE UNIQUE INDEX image_owner_id_user_filename ON image (owner_id, user_filename);