Compare commits

...

5 Commits

Author SHA1 Message Date
Jesse Brault
dce0db0385 Fix integration tests, all passing. 2025-12-27 18:48:10 -06:00
Jesse Brault
cf8ebe984b Fix AuthControllerTests. 2025-12-26 23:32:28 -06:00
Jesse Brault
2642f6100e Fix app smokescreen and ImageController integration tests for use with Postgres. 2025-12-26 23:26:51 -06:00
Jesse Brault
b9e7ccedce Fix tests to compile. 2025-12-26 13:50:18 -06:00
Jesse Brault
1fefeaa1da Change to flyway migrations, many SQL/entity updates. 2025-12-26 13:45:27 -06:00
52 changed files with 824 additions and 713 deletions

View File

@ -49,12 +49,17 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-webmvc' implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-jackson' implementation 'org.springframework.boot:spring-boot-starter-jackson'
implementation 'org.springframework.boot:spring-boot-starter-flyway'
runtimeOnly 'org.postgresql:postgresql' runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testImplementation 'org.springframework.boot:spring-boot-starter-jackson-test' testImplementation 'org.springframework.boot:spring-boot-starter-jackson-test'
testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// flyway
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'
// Custom // Custom
implementation 'io.jsonwebtoken:jjwt-api:0.13.0' implementation 'io.jsonwebtoken:jjwt-api:0.13.0'
implementation 'io.jsonwebtoken:jjwt-jackson:0.13.0' implementation 'io.jsonwebtoken:jjwt-jackson:0.13.0'
@ -78,10 +83,10 @@ dependencies {
runtimeOnly 'org.apache.xmlgraphics:batik-all:1.19' runtimeOnly 'org.apache.xmlgraphics:batik-all:1.19'
// Custom testing // Custom testing
testRuntimeOnly 'com.h2database:h2'
testImplementation 'org.testcontainers:testcontainers:1.21.4' testImplementation 'org.testcontainers:testcontainers:1.21.4'
testImplementation 'org.testcontainers:junit-jupiter: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' 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; package app.mealsmadeeasy.api;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest @SpringBootTest
class MealsMadeEasyApiApplicationTests { @ExtendWith(IntegrationTestsExtension.class)
public class MealsMadeEasyApiApplicationTests {
@Test @Test
void contextLoads() {} public void contextLoads() {}
} }

View File

@ -1,22 +1,23 @@
package app.mealsmadeeasy.api.auth; package app.mealsmadeeasy.api.auth;
import app.mealsmadeeasy.api.IntegrationTestsExtension;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserCreateException; import app.mealsmadeeasy.api.user.UserCreateException;
import app.mealsmadeeasy.api.user.UserService; import app.mealsmadeeasy.api.user.UserService;
import jakarta.servlet.http.Cookie; import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.ObjectMapper;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
@ -27,8 +28,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@SpringBootTest @SpringBootTest
@AutoConfigureMockMvc @AutoConfigureMockMvc
@ExtendWith(IntegrationTestsExtension.class)
public class AuthControllerTests { public class AuthControllerTests {
private static final String TEST_PASSWORD = "test";
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
@ -38,43 +42,38 @@ public class AuthControllerTests {
@Autowired @Autowired
private UserService userService; private UserService userService;
private User createTestUser() { private User seedUser() {
final String uuid = UUID.randomUUID().toString();
try { try {
return this.userService.createUser("test", "test@test.com", "test"); return this.userService.createUser(uuid, uuid + "@test.com", TEST_PASSWORD);
} catch (UserCreateException e) { } catch (UserCreateException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private MockHttpServletRequestBuilder getLoginRequest() { private MockHttpServletRequestBuilder getLoginRequest(String username, String password) {
final Map<String, ?> body = Map.of( final Map<String, Object> body = Map.of(
"username", "test", "username", username,
"password", "test" "password", password
); );
return post("/auth/login") return post("/auth/login")
.content(this.objectMapper.writeValueAsString(body)) .content(this.objectMapper.writeValueAsString(body))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.with(user("test").password("test")); .with(user(username).password(password));
}
@BeforeEach
public void setup() {
final User testUser = this.createTestUser();
System.out.println("Created testUser: " + testUser);
} }
@Test @Test
@DirtiesContext
public void simpleLogin() throws Exception { public void simpleLogin() throws Exception {
this.mockMvc.perform(this.getLoginRequest()) final User user = this.seedUser();
this.mockMvc.perform(this.getLoginRequest(user.getUsername(), TEST_PASSWORD))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("test")) .andExpect(jsonPath("$.username").value(user.getUsername()))
.andExpect(jsonPath("$.accessToken").isString()) .andExpect(jsonPath("$.accessToken").isString())
.andExpect(cookie().exists("refresh-token")); .andExpect(cookie().exists("refresh-token"));
} }
private Cookie getRefreshTokenCookie() throws Exception { private Cookie getRefreshTokenCookie(String username, String password) throws Exception {
final MvcResult loginResult = this.mockMvc.perform(this.getLoginRequest()).andReturn(); final MvcResult loginResult = this.mockMvc.perform(this.getLoginRequest(username, password)).andReturn();
final Cookie refreshTokenCookie = loginResult.getResponse().getCookie("refresh-token"); final Cookie refreshTokenCookie = loginResult.getResponse().getCookie("refresh-token");
if (refreshTokenCookie == null) { if (refreshTokenCookie == null) {
throw new NullPointerException("refreshTokenCookie is null"); throw new NullPointerException("refreshTokenCookie is null");
@ -83,24 +82,24 @@ public class AuthControllerTests {
} }
@Test @Test
@DirtiesContext
public void simpleLogout() throws Exception { public void simpleLogout() throws Exception {
final User user = this.seedUser();
final MockHttpServletRequestBuilder req = post("/auth/logout") final MockHttpServletRequestBuilder req = post("/auth/logout")
.cookie(this.getRefreshTokenCookie()); .cookie(this.getRefreshTokenCookie(user.getUsername(), TEST_PASSWORD));
this.mockMvc.perform(req) this.mockMvc.perform(req)
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(cookie().maxAge("refresh-token", 0)); .andExpect(cookie().maxAge("refresh-token", 0));
} }
@Test @Test
@DirtiesContext
public void simpleRefresh() throws Exception { public void simpleRefresh() throws Exception {
final Cookie firstRefreshTokenCookie = this.getRefreshTokenCookie(); final User user = this.seedUser();
final Cookie firstRefreshTokenCookie = this.getRefreshTokenCookie(user.getUsername(), TEST_PASSWORD);
final MockHttpServletRequestBuilder req = post("/auth/refresh") final MockHttpServletRequestBuilder req = post("/auth/refresh")
.cookie(firstRefreshTokenCookie); .cookie(firstRefreshTokenCookie);
final MvcResult res = this.mockMvc.perform(req) final MvcResult res = this.mockMvc.perform(req)
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("test")) .andExpect(jsonPath("$.username").value(user.getUsername()))
.andExpect(jsonPath("$.accessToken").isString()) .andExpect(jsonPath("$.accessToken").isString())
.andExpect(cookie().exists("refresh-token")) .andExpect(cookie().exists("refresh-token"))
.andReturn(); .andReturn();

View File

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

View File

@ -1,14 +1,15 @@
package app.mealsmadeeasy.api.image; package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.IntegrationTestsExtension;
import app.mealsmadeeasy.api.image.spec.ImageCreateInfoSpec; import app.mealsmadeeasy.api.image.spec.ImageCreateInfoSpec;
import app.mealsmadeeasy.api.image.spec.ImageUpdateInfoSpec; import app.mealsmadeeasy.api.image.spec.ImageUpdateInfoSpec;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserCreateException; import app.mealsmadeeasy.api.user.UserCreateException;
import app.mealsmadeeasy.api.user.UserService; import app.mealsmadeeasy.api.user.UserService;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MinIOContainer; import org.testcontainers.containers.MinIOContainer;
@ -20,6 +21,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import static app.mealsmadeeasy.api.image.ContainsImagesMatcher.containsImages; import static app.mealsmadeeasy.api.image.ContainsImagesMatcher.containsImages;
import static app.mealsmadeeasy.api.user.ContainsUsersMatcher.containsUsers; import static app.mealsmadeeasy.api.user.ContainsUsersMatcher.containsUsers;
@ -30,13 +32,13 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@Testcontainers @Testcontainers
@SpringBootTest @SpringBootTest
@ExtendWith(IntegrationTestsExtension.class)
public class S3ImageServiceTests { public class S3ImageServiceTests {
private static final String USER_FILENAME = "HAL9000.svg";
@Container @Container
private static final MinIOContainer container = new MinIOContainer( private static final MinIOContainer container = new MinIOContainer(
DockerImageName.parse("minio/minio:latest") DockerImageName.parse("minio/minio:latest")
@ -49,7 +51,7 @@ public class S3ImageServiceTests {
registry.add("app.mealsmadeeasy.api.minio.secretKey", container::getPassword); registry.add("app.mealsmadeeasy.api.minio.secretKey", container::getPassword);
} }
private static InputStream getHal9000() { private static InputStream getHal9000InputStream() {
return S3ImageServiceTests.class.getResourceAsStream("HAL9000.svg"); return S3ImageServiceTests.class.getResourceAsStream("HAL9000.svg");
} }
@ -59,26 +61,40 @@ public class S3ImageServiceTests {
@Autowired @Autowired
private ImageService imageService; private ImageService imageService;
private User createTestUser(String username) { private static final String TEST_PASSWORD = "test";
private static final long HAL_LENGTH = 27881L;
private User seedUser() {
final String uuid = UUID.randomUUID().toString();
try { try {
return this.userService.createUser(username, username + "@test.com", "test"); return this.userService.createUser(uuid, uuid + "@test.com", TEST_PASSWORD);
} catch (UserCreateException e) { } catch (UserCreateException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private Image createHal9000(User owner) throws ImageException, IOException { private static String makeUserFilename() {
try (final InputStream hal9000 = getHal9000()) { return UUID.randomUUID() + ".svg";
}
private Image seedImage(User owner, ImageCreateInfoSpec spec) {
try (final InputStream hal9000 = getHal9000InputStream()) {
return this.imageService.create( return this.imageService.create(
owner, owner,
USER_FILENAME, makeUserFilename(),
hal9000, hal9000,
27881L, HAL_LENGTH,
new ImageCreateInfoSpec() spec
); );
} catch (ImageException | IOException e) {
throw new RuntimeException(e);
} }
} }
private Image seedImage(User owner) {
return this.seedImage(owner, new ImageCreateInfoSpec());
}
private Image makePublic(Image image, User modifier) { private Image makePublic(Image image, User modifier) {
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec(); final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setPublic(true); spec.setPublic(true);
@ -89,14 +105,13 @@ public class S3ImageServiceTests {
public void smokeScreen() {} public void smokeScreen() {}
@Test @Test
@DirtiesContext public void simpleCreate() {
public void simpleCreate() throws ImageException, IOException { final User owner = this.seedUser();
final User owner = this.createTestUser("imageOwner"); final Image image = this.seedImage(owner);
final Image image = this.createHal9000(owner);
assertThat(image.getOwner(), isUser(owner)); assertThat(image.getOwner(), isUser(owner));
assertThat(image.getCreated(), is(notNullValue())); assertThat(image.getCreated(), is(notNullValue()));
assertThat(image.getModified(), is(nullValue())); assertThat(image.getModified(), is(nullValue()));
assertThat(image.getUserFilename(), is("HAL9000.svg")); assertThat(image.getUserFilename(), is(notNullValue()));
assertThat(image.getMimeType(), is("image/svg+xml")); assertThat(image.getMimeType(), is("image/svg+xml"));
assertThat(image.getAlt(), is(nullValue())); assertThat(image.getAlt(), is(nullValue()));
assertThat(image.getCaption(), is(nullValue())); assertThat(image.getCaption(), is(nullValue()));
@ -105,54 +120,55 @@ public class S3ImageServiceTests {
} }
@Test @Test
@DirtiesContext public void properlyLoadsContent() throws IOException {
public void loadImageWithOwnerAsViewer() throws ImageException, IOException { final User owner = this.seedUser();
final User owner = this.createTestUser("imageOwner"); final Image image = this.seedImage(owner);
final Image image = this.createHal9000(owner); final InputStream content = assertDoesNotThrow(() -> this.imageService.getImageContent(image, owner));
try (final InputStream stored = //noinspection DataFlowIssue
this.imageService.getImageContent(image, owner)) { final byte[] contentBytes = content.readAllBytes();
final byte[] storedBytes = stored.readAllBytes(); assertThat(contentBytes.length, is((int) HAL_LENGTH));
assertThat(storedBytes.length, is(27881)); content.close();
}
} }
@Test @Test
@DirtiesContext public void loadImageWithOwnerAsViewer() throws IOException {
public void loadPublicImage() throws ImageException, IOException { final User owner = this.seedUser();
final User owner = this.createTestUser("imageOwner"); final Image image = this.seedImage(owner);
Image image = this.createHal9000(owner); final InputStream content = assertDoesNotThrow(() -> this.imageService.getImageContent(image, owner));
image = this.makePublic(image, owner); //noinspection DataFlowIssue
try (final InputStream stored = content.close();
this.imageService.getImageContent(image, null)) {
final byte[] storedBytes = stored.readAllBytes();
assertThat(storedBytes.length, is(27881));
}
} }
@Test @Test
@DirtiesContext public void loadPublicImage() throws IOException {
public void loadImageWithViewer() throws ImageException, IOException { final User owner = this.seedUser();
final User owner = this.createTestUser("imageOwner"); final Image seedImage = this.seedImage(owner);
final User viewer = this.createTestUser("imageViewer"); final Image publicImage = this.makePublic(seedImage, owner);
Image image = this.createHal9000(owner); final InputStream content = assertDoesNotThrow(() -> this.imageService.getImageContent(publicImage, null));
//noinspection DataFlowIssue
content.close();
}
@Test
public void loadImageWithViewer() throws IOException {
final User owner = this.seedUser();
final User viewer = this.seedUser();
Image seedImage = this.seedImage(owner);
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec(); final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setViewersToAdd(Set.of(viewer)); spec.setViewersToAdd(Set.of(viewer));
image = this.imageService.update(image, owner, spec); final Image imageWithViewer = this.imageService.update(seedImage, owner, spec);
try (final InputStream stored = final InputStream content = assertDoesNotThrow(() -> this.imageService.getImageContent(imageWithViewer, viewer));
this.imageService.getImageContent(image, viewer)) { //noinspection DataFlowIssue
final byte[] storedBytes = stored.readAllBytes(); content.close();
assertThat(storedBytes.length, is(27881));
}
} }
@Test @Test
@DirtiesContext public void getImagesOwnedBy() {
public void getImagesOwnedBy() throws ImageException, IOException { final User owner = this.seedUser();
final User owner = this.createTestUser("imageOwner"); final User otherOwner = this.seedUser();
final User otherOwner = this.createTestUser("otherImageOwner"); final Image image0 = this.seedImage(owner);
final Image image0 = this.createHal9000(owner); final Image image1 = this.seedImage(owner);
final Image image1 = this.createHal9000(owner); final Image image2 = this.seedImage(otherOwner);
final Image image2 = this.createHal9000(otherOwner);
final List<Image> ownedImages = this.imageService.getImagesOwnedBy(owner); final List<Image> ownedImages = this.imageService.getImagesOwnedBy(owner);
assertThat(ownedImages.size(), is(2)); assertThat(ownedImages.size(), is(2));
@ -164,10 +180,9 @@ public class S3ImageServiceTests {
} }
@Test @Test
@DirtiesContext public void updateAlt() {
public void updateAlt() throws Exception { final User owner = this.seedUser();
final User owner = this.createTestUser("imageOwner"); Image image = this.seedImage(owner);
Image image = this.createHal9000(owner);
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec(); final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setAlt("HAL 9000"); spec.setAlt("HAL 9000");
image = this.imageService.update(image, owner, spec); image = this.imageService.update(image, owner, spec);
@ -175,10 +190,9 @@ public class S3ImageServiceTests {
} }
@Test @Test
@DirtiesContext public void updateCaption() {
public void updateCaption() throws Exception { final User owner = this.seedUser();
final User owner = this.createTestUser("imageOwner"); Image image = this.seedImage(owner);
Image image = this.createHal9000(owner);
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec(); final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setCaption("HAL 9000 from 2001: A Space Odyssey"); spec.setCaption("HAL 9000 from 2001: A Space Odyssey");
image = this.imageService.update(image, owner, spec); image = this.imageService.update(image, owner, spec);
@ -186,10 +200,9 @@ public class S3ImageServiceTests {
} }
@Test @Test
@DirtiesContext public void updateIsPublic() {
public void updateIsPublic() throws Exception { final User owner = this.seedUser();
final User owner = this.createTestUser("imageOwner"); Image image = this.seedImage(owner);
Image image = this.createHal9000(owner);
final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec(); final ImageUpdateInfoSpec spec = new ImageUpdateInfoSpec();
spec.setPublic(true); spec.setPublic(true);
image = this.imageService.update(image, owner, spec); image = this.imageService.update(image, owner, spec);
@ -203,21 +216,19 @@ public class S3ImageServiceTests {
} }
@Test @Test
@DirtiesContext public void addViewers() {
public void addViewers() throws Exception { final User owner = this.seedUser();
final User owner = this.createTestUser("imageOwner"); final User viewer = this.seedUser();
final User viewer = this.createTestUser("imageViewer"); Image image = this.seedImage(owner);
Image image = this.createHal9000(owner);
image = this.addViewer(image, owner, viewer); image = this.addViewer(image, owner, viewer);
assertThat(image.getViewers(), containsUsers(viewer)); assertThat(image.getViewers(), containsUsers(viewer));
} }
@Test @Test
@DirtiesContext public void removeViewers() {
public void removeViewers() throws Exception { final User owner = this.seedUser();
final User owner = this.createTestUser("imageOwner"); final User viewer = this.seedUser();
final User viewer = this.createTestUser("imageViewer"); Image image = this.seedImage(owner);
Image image = this.createHal9000(owner);
image = this.addViewer(image, owner, viewer); image = this.addViewer(image, owner, viewer);
assertThat(image.getViewers(), containsUsers(viewer)); assertThat(image.getViewers(), containsUsers(viewer));
@ -228,11 +239,10 @@ public class S3ImageServiceTests {
} }
@Test @Test
@DirtiesContext public void clearAllViewers() {
public void clearAllViewers() throws Exception { final User owner = this.seedUser();
final User owner = this.createTestUser("imageOwner"); final User viewer = this.seedUser();
final User viewer = this.createTestUser("imageViewer"); Image image = this.seedImage(owner);
Image image = this.createHal9000(owner);
image = this.addViewer(image, owner, viewer); image = this.addViewer(image, owner, viewer);
assertThat(image.getViewers(), containsUsers(viewer)); assertThat(image.getViewers(), containsUsers(viewer));
@ -243,10 +253,9 @@ public class S3ImageServiceTests {
} }
@Test @Test
@DirtiesContext
public void deleteImage() throws Exception { public void deleteImage() throws Exception {
final User owner = this.createTestUser("imageOwner"); final User owner = this.seedUser();
final Image image = this.createHal9000(owner); final Image image = this.seedImage(owner);
this.imageService.deleteImage(image, owner); this.imageService.deleteImage(image, owner);
assertThrows(ImageException.class, () -> this.imageService.getById(image.getId(), owner)); assertThrows(ImageException.class, () -> this.imageService.getById(image.getId(), owner));
} }

View File

@ -1,5 +1,6 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.IntegrationTestsExtension;
import app.mealsmadeeasy.api.auth.AuthService; import app.mealsmadeeasy.api.auth.AuthService;
import app.mealsmadeeasy.api.auth.LoginDetails; import app.mealsmadeeasy.api.auth.LoginDetails;
import app.mealsmadeeasy.api.auth.LoginException; import app.mealsmadeeasy.api.auth.LoginException;
@ -14,11 +15,11 @@ import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserCreateException; import app.mealsmadeeasy.api.user.UserCreateException;
import app.mealsmadeeasy.api.user.UserService; import app.mealsmadeeasy.api.user.UserService;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@ -29,9 +30,9 @@ import org.testcontainers.utility.DockerImageName;
import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.ObjectMapper;
import java.io.InputStream; import java.io.InputStream;
import java.util.UUID;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.nullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -39,6 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Testcontainers @Testcontainers
@SpringBootTest @SpringBootTest
@AutoConfigureMockMvc @AutoConfigureMockMvc
@ExtendWith(IntegrationTestsExtension.class)
public class RecipeControllerTests { public class RecipeControllerTests {
@Container @Container
@ -78,17 +80,20 @@ public class RecipeControllerTests {
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
private User createTestUser(String username) { private static final String TEST_PASSWORD = "test";
private User seedUser() {
final String uuid = UUID.randomUUID().toString();
try { try {
return this.userService.createUser(username, username + "@test.com", "test"); return this.userService.createUser(uuid, uuid + "@test.com", TEST_PASSWORD);
} catch (UserCreateException e) { } catch (UserCreateException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private Recipe createTestRecipe(User owner, boolean isPublic, String slug) { private Recipe createTestRecipe(User owner, boolean isPublic) {
final RecipeCreateSpec spec = new RecipeCreateSpec(); final RecipeCreateSpec spec = new RecipeCreateSpec();
spec.setSlug(slug); spec.setSlug(UUID.randomUUID().toString());
spec.setTitle("Test Recipe"); spec.setTitle("Test Recipe");
spec.setPreparationTime(10); spec.setPreparationTime(10);
spec.setCookingTime(20); spec.setCookingTime(20);
@ -98,12 +103,8 @@ public class RecipeControllerTests {
return this.recipeService.create(owner, spec); return this.recipeService.create(owner, spec);
} }
private Recipe createTestRecipe(User owner, boolean isPublic) {
return this.createTestRecipe(owner, isPublic, "test-recipe");
}
private String getAccessToken(User user) throws LoginException { private String getAccessToken(User user) throws LoginException {
return this.authService.login(user.getUsername(), "test") return this.authService.login(user.getUsername(), TEST_PASSWORD)
.getAccessToken() .getAccessToken()
.getToken(); .getToken();
} }
@ -112,7 +113,7 @@ public class RecipeControllerTests {
try (final InputStream hal9000 = getHal9000()) { try (final InputStream hal9000 = getHal9000()) {
return this.imageService.create( return this.imageService.create(
owner, owner,
"HAL9000.svg", UUID.randomUUID() + ".svg",
hal9000, hal9000,
27881L, 27881L,
new ImageCreateInfoSpec() new ImageCreateInfoSpec()
@ -123,15 +124,14 @@ public class RecipeControllerTests {
} }
@Test @Test
@DirtiesContext
public void getRecipePageViewByIdPublicRecipeNoPrincipal() throws Exception { public void getRecipePageViewByIdPublicRecipeNoPrincipal() throws Exception {
final User owner = this.createTestUser("owner"); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, true); final Recipe recipe = this.createTestRecipe(owner, true);
this.mockMvc.perform( this.mockMvc.perform(
get("/recipes/{username}/{slug}", recipe.getOwner().getUsername(), recipe.getSlug()) get("/recipes/{username}/{slug}", recipe.getOwner().getUsername(), recipe.getSlug())
) )
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.recipe.id").value(1)) .andExpect(jsonPath("$.recipe.id").value(recipe.getId()))
.andExpect(jsonPath("$.recipe.created").exists()) // TODO: better matching of exact LocalDateTime .andExpect(jsonPath("$.recipe.created").exists()) // TODO: better matching of exact LocalDateTime
.andExpect(jsonPath("$.recipe.modified").doesNotExist()) .andExpect(jsonPath("$.recipe.modified").doesNotExist())
.andExpect(jsonPath("$.recipe.slug").value(recipe.getSlug())) .andExpect(jsonPath("$.recipe.slug").value(recipe.getSlug()))
@ -152,9 +152,8 @@ public class RecipeControllerTests {
} }
@Test @Test
@DirtiesContext
public void getFullRecipeViewIncludeRawText() throws Exception { public void getFullRecipeViewIncludeRawText() throws Exception {
final User owner = this.createTestUser("owner"); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, true); final Recipe recipe = this.createTestRecipe(owner, true);
this.mockMvc.perform( this.mockMvc.perform(
get( get(
@ -168,11 +167,10 @@ public class RecipeControllerTests {
} }
@Test @Test
@DirtiesContext
public void getFullRecipeViewPrincipalIsStarer() throws Exception { public void getFullRecipeViewPrincipalIsStarer() throws Exception {
final User owner = this.createTestUser("owner"); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, false); final Recipe recipe = this.createTestRecipe(owner, false);
this.recipeStarService.create(recipe.getId(), owner.getUsername()); this.recipeStarService.create(recipe.getId(), owner.getId());
final String accessToken = this.getAccessToken(owner); final String accessToken = this.getAccessToken(owner);
this.mockMvc.perform( this.mockMvc.perform(
get("/recipes/{username}/{slug}", recipe.getOwner().getUsername(), recipe.getSlug()) get("/recipes/{username}/{slug}", recipe.getOwner().getUsername(), recipe.getSlug())
@ -183,9 +181,8 @@ public class RecipeControllerTests {
} }
@Test @Test
@DirtiesContext
public void getFullRecipeViewPrincipalIsNotStarer() throws Exception { public void getFullRecipeViewPrincipalIsNotStarer() throws Exception {
final User owner = this.createTestUser("owner"); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, false); final Recipe recipe = this.createTestRecipe(owner, false);
final String accessToken = this.getAccessToken(owner); final String accessToken = this.getAccessToken(owner);
this.mockMvc.perform( this.mockMvc.perform(
@ -197,38 +194,23 @@ public class RecipeControllerTests {
} }
@Test @Test
@DirtiesContext
public void getRecipeInfoViewsNoPrincipal() throws Exception { public void getRecipeInfoViewsNoPrincipal() throws Exception {
final User owner = this.createTestUser("owner"); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, true); final Recipe recipe = this.createTestRecipe(owner, true);
this.mockMvc.perform(get("/recipes")) this.mockMvc.perform(get("/recipes"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.slice.number").value(0)) .andExpect(jsonPath("$.slice.number").value(0))
.andExpect(jsonPath("$.slice.size").value(20)) .andExpect(jsonPath("$.slice.size").value(20))
.andExpect(jsonPath("$.content").isArray()) .andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.content", hasSize(1))) .andExpect(jsonPath("$.content[*].id").value(hasItem(recipe.getId())));
.andExpect(jsonPath("$.content[0].id").value(recipe.getId()))
.andExpect(jsonPath("$.content[0].created").exists()) // TODO: better matching of exact LocalDateTime
.andExpect(jsonPath("$.content[0].modified").doesNotExist())
.andExpect(jsonPath("$.content[0].slug").value(recipe.getSlug()))
.andExpect(jsonPath("$.content[0].title").value(recipe.getTitle()))
.andExpect(jsonPath("$.content[0].preparationTime").value(recipe.getPreparationTime()))
.andExpect(jsonPath("$.content[0].cookingTime").value(recipe.getCookingTime()))
.andExpect(jsonPath("$.content[0].totalTime").value(recipe.getTotalTime()))
.andExpect(jsonPath("$.content[0].owner.id").value(owner.getId()))
.andExpect(jsonPath("$.content[0].owner.username").value(owner.getUsername()))
.andExpect(jsonPath("$.content[0].isPublic").value(true))
.andExpect(jsonPath("$.content[0].starCount").value(0))
.andExpect(jsonPath("$.content[0].mainImage").value(nullValue()));
} }
@Test @Test
@DirtiesContext
public void getRecipeInfoViewsWithPrincipalIncludesPrivate() throws Exception { public void getRecipeInfoViewsWithPrincipalIncludesPrivate() throws Exception {
final User owner = this.createTestUser("owner"); final User owner = this.seedUser();
final Recipe r0 = this.createTestRecipe(owner, true, "r0"); final Recipe r0 = this.createTestRecipe(owner, true);
final Recipe r1 = this.createTestRecipe(owner, true, "r1"); final Recipe r1 = this.createTestRecipe(owner, true);
final Recipe r2 = this.createTestRecipe(owner, false, "r2"); final Recipe r2 = this.createTestRecipe(owner, false);
final LoginDetails loginDetails = this.authService.login(owner.getUsername(), "test"); final LoginDetails loginDetails = this.authService.login(owner.getUsername(), "test");
this.mockMvc.perform( this.mockMvc.perform(
get("/recipes") get("/recipes")
@ -238,7 +220,7 @@ public class RecipeControllerTests {
.andExpect(jsonPath("$.slice.number").value(0)) .andExpect(jsonPath("$.slice.number").value(0))
.andExpect(jsonPath("$.slice.size").value(20)) .andExpect(jsonPath("$.slice.size").value(20))
.andExpect(jsonPath("$.content").isArray()) .andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.content", hasSize(3))); .andExpect(jsonPath("$.content[*].id").value(hasItems(r0.getId(), r1.getId(), r2.getId())));
} }
private String getUpdateBody() { private String getUpdateBody() {
@ -253,9 +235,8 @@ public class RecipeControllerTests {
} }
@Test @Test
@DirtiesContext
public void updateRecipe() throws Exception { public void updateRecipe() throws Exception {
final User owner = this.createTestUser("owner"); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, false); final Recipe recipe = this.createTestRecipe(owner, false);
final String accessToken = this.getAccessToken(owner); final String accessToken = this.getAccessToken(owner);
final String body = this.getUpdateBody(); final String body = this.getUpdateBody();
@ -284,9 +265,8 @@ public class RecipeControllerTests {
} }
@Test @Test
@DirtiesContext
public void updateRecipeReturnsViewWithMainImage() throws Exception { public void updateRecipeReturnsViewWithMainImage() throws Exception {
final User owner = this.createTestUser("owner"); final User owner = this.seedUser();
final Image hal9000 = this.createHal9000(owner); final Image hal9000 = this.createHal9000(owner);
@ -319,26 +299,24 @@ public class RecipeControllerTests {
} }
@Test @Test
@DirtiesContext
public void addStarToRecipe() throws Exception { public void addStarToRecipe() throws Exception {
final User owner = this.createTestUser("recipe-owner"); final User owner = this.seedUser();
final User starer = this.createTestUser("recipe-starer"); final User starer = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, true); final Recipe recipe = this.createTestRecipe(owner, true);
this.mockMvc.perform( this.mockMvc.perform(
post("/recipes/{username}/{slug}/star", recipe.getOwner().getUsername(), recipe.getSlug()) post("/recipes/{username}/{slug}/star", recipe.getOwner().getUsername(), recipe.getSlug())
.header("Authorization", "Bearer " + this.getAccessToken(starer)) .header("Authorization", "Bearer " + this.getAccessToken(starer))
) )
.andExpect(status().isCreated()) .andExpect(status().isCreated())
.andExpect(jsonPath("$.date").exists()); .andExpect(jsonPath("$.timestamp").exists());
} }
@Test @Test
@DirtiesContext
public void getStarForRecipe() throws Exception { public void getStarForRecipe() throws Exception {
final User owner = this.createTestUser("recipe-owner"); final User owner = this.seedUser();
final User starer = this.createTestUser("recipe-starer"); final User starer = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, true); final Recipe recipe = this.createTestRecipe(owner, true);
this.recipeStarService.create(recipe.getId(), starer.getUsername()); this.recipeStarService.create(recipe.getId(), starer.getId());
this.mockMvc.perform( this.mockMvc.perform(
get("/recipes/{username}/{slug}/star", recipe.getOwner().getUsername(), recipe.getSlug()) get("/recipes/{username}/{slug}/star", recipe.getOwner().getUsername(), recipe.getSlug())
.header("Authorization", "Bearer " + this.getAccessToken(starer)) .header("Authorization", "Bearer " + this.getAccessToken(starer))
@ -346,16 +324,15 @@ public class RecipeControllerTests {
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.isStarred").value(true)) .andExpect(jsonPath("$.isStarred").value(true))
.andExpect(jsonPath("$.star").isMap()) .andExpect(jsonPath("$.star").isMap())
.andExpect(jsonPath("$.star.date").exists()); .andExpect(jsonPath("$.star.timestamp").exists());
} }
@Test @Test
@DirtiesContext
public void deleteStarFromRecipe() throws Exception { public void deleteStarFromRecipe() throws Exception {
final User owner = this.createTestUser("recipe-owner"); final User owner = this.seedUser();
final User starer = this.createTestUser("recipe-starer"); final User starer = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, true); final Recipe recipe = this.createTestRecipe(owner, true);
this.recipeStarService.create(recipe.getId(), starer.getUsername()); this.recipeStarService.create(recipe.getId(), starer.getId());
this.mockMvc.perform( this.mockMvc.perform(
delete("/recipes/{username}/{slug}/star", recipe.getOwner().getUsername(), recipe.getSlug()) delete("/recipes/{username}/{slug}/star", recipe.getOwner().getUsername(), recipe.getSlug())
.header("Authorization", "Bearer " + this.getAccessToken(starer)) .header("Authorization", "Bearer " + this.getAccessToken(starer))

View File

@ -1,20 +1,23 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.IntegrationTestsExtension;
import app.mealsmadeeasy.api.user.UserEntity; import app.mealsmadeeasy.api.user.UserEntity;
import app.mealsmadeeasy.api.user.UserRepository; import app.mealsmadeeasy.api.user.UserRepository;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest @SpringBootTest
@ExtendWith(IntegrationTestsExtension.class)
public class RecipeRepositoryTests { public class RecipeRepositoryTests {
@Autowired @Autowired
@ -23,65 +26,55 @@ public class RecipeRepositoryTests {
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
private UserEntity getOwnerUser() { private UserEntity seedUser() {
final UserEntity recipeUser = UserEntity.getDefaultDraft(); final String uuid = UUID.randomUUID().toString();
recipeUser.setUsername("recipeUser"); final UserEntity draft = UserEntity.getDefaultDraft();
recipeUser.setEmail("recipe@user.com"); draft.setUsername(uuid);
recipeUser.setPassword("test"); draft.setEmail(uuid + "@test.com");
return this.userRepository.save(recipeUser); draft.setPassword("test");
} return this.userRepository.save(draft);
private UserEntity getViewerUser() {
final UserEntity viewerUser = UserEntity.getDefaultDraft();
viewerUser.setUsername("recipeViewerUser");
viewerUser.setEmail("recipe-viewer@user.com");
viewerUser.setPassword("test");
return this.userRepository.save(viewerUser);
} }
@Test @Test
@DirtiesContext
public void findsAllPublicRecipes() { public void findsAllPublicRecipes() {
final RecipeEntity publicRecipe = new RecipeEntity(); final RecipeEntity publicRecipe = new RecipeEntity();
publicRecipe.setCreated(LocalDateTime.now()); publicRecipe.setCreated(OffsetDateTime.now());
publicRecipe.setSlug("public-recipe"); publicRecipe.setSlug(UUID.randomUUID().toString());
publicRecipe.setPublic(true); publicRecipe.setPublic(true);
publicRecipe.setOwner(this.getOwnerUser()); publicRecipe.setOwner(this.seedUser());
publicRecipe.setTitle("Public Recipe"); publicRecipe.setTitle("Public Recipe");
publicRecipe.setRawText("Hello, World!"); publicRecipe.setRawText("Hello, World!");
this.recipeRepository.save(publicRecipe); final RecipeEntity savedRecipe = this.recipeRepository.save(publicRecipe);
final List<RecipeEntity> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue(); final List<RecipeEntity> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue();
assertThat(publicRecipes.size()).isEqualTo(1); assertThat(publicRecipes).anyMatch(recipeEntity -> recipeEntity.getId().equals(savedRecipe.getId()));
} }
@Test @Test
@DirtiesContext
public void doesNotFindNonPublicRecipe() { public void doesNotFindNonPublicRecipe() {
final RecipeEntity nonPublicRecipe = new RecipeEntity(); final RecipeEntity nonPublicRecipe = new RecipeEntity();
nonPublicRecipe.setCreated(LocalDateTime.now()); nonPublicRecipe.setCreated(OffsetDateTime.now());
nonPublicRecipe.setSlug("non-public-recipe"); nonPublicRecipe.setSlug(UUID.randomUUID().toString());
nonPublicRecipe.setOwner(this.getOwnerUser()); nonPublicRecipe.setOwner(this.seedUser());
nonPublicRecipe.setTitle("Non-Public Recipe"); nonPublicRecipe.setTitle("Non-Public Recipe");
nonPublicRecipe.setRawText("Hello, World!"); nonPublicRecipe.setRawText("Hello, World!");
this.recipeRepository.save(nonPublicRecipe); final RecipeEntity savedRecipe = this.recipeRepository.save(nonPublicRecipe);
final List<RecipeEntity> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue(); final List<RecipeEntity> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue();
assertThat(publicRecipes.size()).isEqualTo(0); assertThat(publicRecipes).noneMatch(recipeEntity -> recipeEntity.getId().equals(savedRecipe.getId()));
} }
@Test @Test
@DirtiesContext
public void findsAllForViewer() { public void findsAllForViewer() {
final RecipeEntity recipe = new RecipeEntity(); final RecipeEntity recipe = new RecipeEntity();
recipe.setCreated(LocalDateTime.now()); recipe.setCreated(OffsetDateTime.now());
recipe.setSlug("test-recipe"); recipe.setSlug(UUID.randomUUID().toString());
recipe.setOwner(this.getOwnerUser()); recipe.setOwner(this.seedUser());
recipe.setTitle("Test Recipe"); recipe.setTitle("Test Recipe");
recipe.setRawText("Hello, World!"); recipe.setRawText("Hello, World!");
final RecipeEntity saved = this.recipeRepository.save(recipe); final RecipeEntity saved = this.recipeRepository.save(recipe);
final UserEntity viewer = this.getViewerUser(); final UserEntity viewer = this.seedUser();
final Set<UserEntity> viewers = new HashSet<>(recipe.getViewerEntities()); final Set<UserEntity> viewers = new HashSet<>(recipe.getViewerEntities());
viewers.add(viewer); viewers.add(viewer);
saved.setViewers(viewers); saved.setViewers(viewers);
@ -93,17 +86,16 @@ public class RecipeRepositoryTests {
} }
@Test @Test
@DirtiesContext
public void doesNotIncludeNonViewable() { public void doesNotIncludeNonViewable() {
final RecipeEntity recipe = new RecipeEntity(); final RecipeEntity recipe = new RecipeEntity();
recipe.setCreated(LocalDateTime.now()); recipe.setCreated(OffsetDateTime.now());
recipe.setSlug("test-recipe"); recipe.setSlug(UUID.randomUUID().toString());
recipe.setOwner(this.getOwnerUser()); recipe.setOwner(this.seedUser());
recipe.setTitle("Test Recipe"); recipe.setTitle("Test Recipe");
recipe.setRawText("Hello, World!"); recipe.setRawText("Hello, World!");
this.recipeRepository.save(recipe); this.recipeRepository.save(recipe);
final UserEntity viewer = this.getViewerUser(); final UserEntity viewer = this.seedUser();
final List<RecipeEntity> viewable = this.recipeRepository.findAllByViewersContaining(viewer); final List<RecipeEntity> viewable = this.recipeRepository.findAllByViewersContaining(viewer);
assertThat(viewable.size()).isEqualTo(0); assertThat(viewable.size()).isEqualTo(0);
} }

View File

@ -1,5 +1,6 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.IntegrationTestsExtension;
import app.mealsmadeeasy.api.image.ImageException; import app.mealsmadeeasy.api.image.ImageException;
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
@ -11,25 +12,28 @@ import app.mealsmadeeasy.api.user.UserEntity;
import app.mealsmadeeasy.api.user.UserRepository; import app.mealsmadeeasy.api.user.UserRepository;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.test.annotation.DirtiesContext;
import java.util.List; import java.util.List;
import java.util.UUID;
import static app.mealsmadeeasy.api.recipe.ContainsRecipeInfoViewsForRecipesMatcher.containsRecipeInfoViewsForRecipes; import static app.mealsmadeeasy.api.recipe.ContainsRecipeInfoViewsForRecipesMatcher.containsRecipeInfoViewsForRecipes;
import static app.mealsmadeeasy.api.recipe.ContainsRecipesMatcher.containsRecipes; import static app.mealsmadeeasy.api.recipe.ContainsRecipesMatcher.containsRecipes;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
// TODO: test mainImage included // TODO: test mainImage included
// TODO: test prep/cooking/total times included // TODO: test prep/cooking/total times included
@SpringBootTest @SpringBootTest
@ExtendWith(IntegrationTestsExtension.class)
public class RecipeServiceTests { public class RecipeServiceTests {
@Autowired @Autowired
@ -41,25 +45,22 @@ public class RecipeServiceTests {
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
private UserEntity createTestUser(String username) { private UserEntity seedUser() {
final String uuid = UUID.randomUUID().toString();
final UserEntity draft = UserEntity.getDefaultDraft(); final UserEntity draft = UserEntity.getDefaultDraft();
draft.setUsername(username); draft.setUsername(uuid);
draft.setEmail(username + "@test.com"); draft.setEmail(uuid + "@test.com");
draft.setPassword("test"); draft.setPassword("test");
return this.userRepository.save(draft); return this.userRepository.save(draft);
} }
private Recipe createTestRecipe(@Nullable User owner) { private Recipe createTestRecipe(@Nullable User owner) {
return this.createTestRecipe(owner, false, null); return this.createTestRecipe(owner, false);
} }
private Recipe createTestRecipe(@Nullable User owner, boolean isPublic) { private Recipe createTestRecipe(@Nullable User owner, boolean isPublic) {
return this.createTestRecipe(owner, isPublic, null);
}
private Recipe createTestRecipe(@Nullable User owner, boolean isPublic, @Nullable String slug) {
final RecipeCreateSpec spec = new RecipeCreateSpec(); final RecipeCreateSpec spec = new RecipeCreateSpec();
spec.setSlug(slug != null ? slug : "my-recipe"); spec.setSlug(UUID.randomUUID().toString());
spec.setTitle("My Recipe"); spec.setTitle("My Recipe");
spec.setRawText("Hello!"); spec.setRawText("Hello!");
spec.setPublic(isPublic); spec.setPublic(isPublic);
@ -70,9 +71,8 @@ public class RecipeServiceTests {
public void smokeScreen() {} public void smokeScreen() {}
@Test @Test
@DirtiesContext
public void create() { public void create() {
final User user = this.createTestUser("recipeOwner"); final User user = this.seedUser();
final Recipe recipe = this.createTestRecipe(user); final Recipe recipe = this.createTestRecipe(user);
assertThat(recipe.getOwner().getUsername(), is(user.getUsername())); assertThat(recipe.getOwner().getUsername(), is(user.getUsername()));
assertThat(recipe.getTitle(), is("My Recipe")); assertThat(recipe.getTitle(), is("My Recipe"));
@ -80,23 +80,20 @@ public class RecipeServiceTests {
} }
@Test @Test
@DirtiesContext
public void createWithoutOwnerThrowsAccessDenied() { public void createWithoutOwnerThrowsAccessDenied() {
assertThrows(AccessDeniedException.class, () -> this.recipeService.create(null, new RecipeCreateSpec())); assertThrows(AccessDeniedException.class, () -> this.recipeService.create(null, new RecipeCreateSpec()));
} }
@Test @Test
@DirtiesContext
public void getByIdPublicNoViewerDoesNotThrow() { public void getByIdPublicNoViewerDoesNotThrow() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, true); final Recipe recipe = this.createTestRecipe(owner, true);
assertDoesNotThrow(() -> this.recipeService.getById(recipe.getId(), null)); assertDoesNotThrow(() -> this.recipeService.getById(recipe.getId(), null));
} }
@Test @Test
@DirtiesContext
public void getByIdHasCorrectProperties() throws RecipeException { public void getByIdHasCorrectProperties() throws RecipeException {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, true); final Recipe recipe = this.createTestRecipe(owner, true);
final Recipe byId = this.recipeService.getById(recipe.getId(), null); final Recipe byId = this.recipeService.getById(recipe.getId(), null);
assertThat(byId.getId(), is(recipe.getId())); assertThat(byId.getId(), is(recipe.getId()));
@ -107,45 +104,40 @@ public class RecipeServiceTests {
} }
@Test @Test
@DirtiesContext
public void getByIdThrowsWhenNotPublicAndNoViewer() { public void getByIdThrowsWhenNotPublicAndNoViewer() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, false); // not public final Recipe recipe = this.createTestRecipe(owner, false); // not public
assertThrows(AccessDeniedException.class, () -> this.recipeService.getById(recipe.getId(), null)); assertThrows(AccessDeniedException.class, () -> this.recipeService.getById(recipe.getId(), null));
} }
@Test @Test
@DirtiesContext
public void getByIdThrowsWhenNotViewer() { public void getByIdThrowsWhenNotViewer() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final User notViewer = this.createTestUser("notViewer"); final User notViewer = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner); final Recipe recipe = this.createTestRecipe(owner);
assertThrows(AccessDeniedException.class, () -> this.recipeService.getById(recipe.getId(), notViewer)); assertThrows(AccessDeniedException.class, () -> this.recipeService.getById(recipe.getId(), notViewer));
} }
@Test @Test
@DirtiesContext
public void getByIdOkayWhenPublicAndNoViewer() { public void getByIdOkayWhenPublicAndNoViewer() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, true); final Recipe recipe = this.createTestRecipe(owner, true);
assertDoesNotThrow(() -> this.recipeService.getById(recipe.getId(), null)); assertDoesNotThrow(() -> this.recipeService.getById(recipe.getId(), null));
} }
@Test @Test
@DirtiesContext
public void getByIdOkayWhenPublicRecipeWithViewer() { public void getByIdOkayWhenPublicRecipeWithViewer() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final User viewer = this.createTestUser("viewer"); final User viewer = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, true); final Recipe recipe = this.createTestRecipe(owner, true);
assertDoesNotThrow(() -> this.recipeService.getById(recipe.getId(), viewer)); assertDoesNotThrow(() -> this.recipeService.getById(recipe.getId(), viewer));
} }
@Test @Test
@DirtiesContext
public void getByIdOkayWithStarsPublicAndNoViewer() { public void getByIdOkayWithStarsPublicAndNoViewer() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, true); final Recipe recipe = this.createTestRecipe(owner, true);
final RecipeStar star = this.recipeStarService.create(recipe.getId(), owner.getUsername()); final RecipeStar star = this.recipeStarService.create(recipe.getId(), owner.getId());
final Recipe byIdWithStars = assertDoesNotThrow(() -> this.recipeService.getByIdWithStars( final Recipe byIdWithStars = assertDoesNotThrow(() -> this.recipeService.getByIdWithStars(
recipe.getId(), null recipe.getId(), null
)); ));
@ -153,19 +145,17 @@ public class RecipeServiceTests {
} }
@Test @Test
@DirtiesContext
public void getByIdOkayWithStarsThrowsWhenNotViewer() { public void getByIdOkayWithStarsThrowsWhenNotViewer() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final User notViewer = this.createTestUser("notViewer"); final User notViewer = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner); final Recipe recipe = this.createTestRecipe(owner);
assertThrows(AccessDeniedException.class, () -> this.recipeService.getByIdWithStars(recipe.getId(), notViewer)); assertThrows(AccessDeniedException.class, () -> this.recipeService.getByIdWithStars(recipe.getId(), notViewer));
} }
@Test @Test
@DirtiesContext
public void getByIdWithStarsOkayWhenPublicRecipeWithViewer() throws RecipeException, ImageException { public void getByIdWithStarsOkayWhenPublicRecipeWithViewer() throws RecipeException, ImageException {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final User viewer = this.createTestUser("viewer"); final User viewer = this.seedUser();
final Recipe notYetPublicRecipe = this.createTestRecipe(owner); final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(notYetPublicRecipe); final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(notYetPublicRecipe);
updateSpec.setIsPublic(true); updateSpec.setIsPublic(true);
@ -179,45 +169,39 @@ public class RecipeServiceTests {
} }
@Test @Test
@DirtiesContext
public void getByMinimumStarsAllPublic() { public void getByMinimumStarsAllPublic() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final User u0 = this.createTestUser("u0"); final User u0 = this.seedUser();
final User u1 = this.createTestUser("u1"); final User u1 = this.seedUser();
final Recipe r0 = this.createTestRecipe(owner, true, "r0"); final Recipe r0 = this.createTestRecipe(owner, true);
final Recipe r1 = this.createTestRecipe(owner, true, "r1"); final Recipe r1 = this.createTestRecipe(owner, true);
final Recipe r2 = this.createTestRecipe(owner, true, "r2"); final Recipe r2 = this.createTestRecipe(owner, true);
// r0.stars = 0, r1.stars = 1, r2.stars = 2 // r0.stars = 0, r1.stars = 1, r2.stars = 2
this.recipeStarService.create(r1.getId(), u0.getUsername()); this.recipeStarService.create(r1.getId(), u0.getId());
this.recipeStarService.create(r2.getId(), u0.getUsername()); this.recipeStarService.create(r2.getId(), u0.getId());
this.recipeStarService.create(r2.getId(), u1.getUsername()); this.recipeStarService.create(r2.getId(), u1.getId());
final List<Recipe> zeroStars = this.recipeService.getByMinimumStars(0, null); final List<Recipe> zeroStars = this.recipeService.getByMinimumStars(0, null);
final List<Recipe> oneStar = this.recipeService.getByMinimumStars(1, null); final List<Recipe> oneStar = this.recipeService.getByMinimumStars(1, null);
final List<Recipe> twoStars = this.recipeService.getByMinimumStars(2, null); final List<Recipe> twoStars = this.recipeService.getByMinimumStars(2, null);
assertThat(zeroStars.size(), is(3));
assertThat(oneStar.size(), is(2));
assertThat(twoStars.size(), is(1));
assertThat(zeroStars, containsRecipes(r0, r1, r2)); assertThat(zeroStars, containsRecipes(r0, r1, r2));
assertThat(oneStar, containsRecipes(r1, r2)); assertThat(oneStar, containsRecipes(r1, r2));
assertThat(twoStars, containsRecipes(r2)); assertThat(twoStars, containsRecipes(r2));
} }
@Test @Test
@DirtiesContext
public void getByMinimumStarsOnlySomeViewable() throws RecipeException { public void getByMinimumStarsOnlySomeViewable() throws RecipeException {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final User u0 = this.createTestUser("u0"); final User u0 = this.seedUser();
final User u1 = this.createTestUser("u1"); final User u1 = this.seedUser();
final User viewer = this.createTestUser("recipeViewer"); final User viewer = this.seedUser();
Recipe r0 = this.createTestRecipe(owner, false, "r0"); // not public Recipe r0 = this.createTestRecipe(owner, false); // not public
Recipe r1 = this.createTestRecipe(owner, false, "r1"); Recipe r1 = this.createTestRecipe(owner, false);
Recipe r2 = this.createTestRecipe(owner, false, "r2"); Recipe r2 = this.createTestRecipe(owner, false);
for (final User starer : List.of(u0, u1)) { for (final User starer : List.of(u0, u1)) {
r0 = this.recipeService.addViewer(r0.getId(), owner, starer); r0 = this.recipeService.addViewer(r0.getId(), owner, starer);
@ -226,17 +210,17 @@ public class RecipeServiceTests {
} }
// r0.stars = 0, r1.stars = 1, r2.stars = 2 // r0.stars = 0, r1.stars = 1, r2.stars = 2
this.recipeStarService.create(r1.getId(), u0.getUsername()); this.recipeStarService.create(r1.getId(), u0.getId());
this.recipeStarService.create(r2.getId(), u0.getUsername()); this.recipeStarService.create(r2.getId(), u0.getId());
this.recipeStarService.create(r2.getId(), u1.getUsername()); this.recipeStarService.create(r2.getId(), u1.getId());
final List<Recipe> zeroStarsNoneViewable = this.recipeService.getByMinimumStars(0, viewer); final List<Recipe> zeroStarsNoneViewable = this.recipeService.getByMinimumStars(0, viewer);
final List<Recipe> oneStarNoneViewable = this.recipeService.getByMinimumStars(1, viewer); final List<Recipe> oneStarNoneViewable = this.recipeService.getByMinimumStars(1, viewer);
final List<Recipe> twoStarsNoneViewable = this.recipeService.getByMinimumStars(2, viewer); final List<Recipe> twoStarsNoneViewable = this.recipeService.getByMinimumStars(2, viewer);
assertThat(zeroStarsNoneViewable.size(), is(0)); assertThat(zeroStarsNoneViewable, not(containsRecipes(r0, r1, r2)));
assertThat(oneStarNoneViewable.size(), is(0)); assertThat(oneStarNoneViewable, not(containsRecipes(r1, r2)));
assertThat(twoStarsNoneViewable.size(), is(0)); assertThat(twoStarsNoneViewable, not(containsRecipes(r2)));
// Now make them viewable // Now make them viewable
r0 = this.recipeService.addViewer(r0.getId(), owner, viewer); r0 = this.recipeService.addViewer(r0.getId(), owner, viewer);
@ -247,49 +231,40 @@ public class RecipeServiceTests {
final List<Recipe> oneStarViewable = this.recipeService.getByMinimumStars(1, viewer); final List<Recipe> oneStarViewable = this.recipeService.getByMinimumStars(1, viewer);
final List<Recipe> twoStarsViewable = this.recipeService.getByMinimumStars(2, viewer); final List<Recipe> twoStarsViewable = this.recipeService.getByMinimumStars(2, viewer);
assertThat(zeroStarsViewable.size(), is(3));
assertThat(oneStarViewable.size(), is(2));
assertThat(twoStarsViewable.size(), is (1));
assertThat(zeroStarsViewable, containsRecipes(r0, r1, r2)); assertThat(zeroStarsViewable, containsRecipes(r0, r1, r2));
assertThat(oneStarViewable, containsRecipes(r1, r2)); assertThat(oneStarViewable, containsRecipes(r1, r2));
assertThat(twoStarsViewable, containsRecipes(r2)); assertThat(twoStarsViewable, containsRecipes(r2));
} }
@Test @Test
@DirtiesContext
public void getPublicRecipes() { public void getPublicRecipes() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
Recipe r0 = this.createTestRecipe(owner, true, "r0"); Recipe r0 = this.createTestRecipe(owner, true);
Recipe r1 = this.createTestRecipe(owner, true, "r1"); Recipe r1 = this.createTestRecipe(owner, true);
final List<Recipe> publicRecipes = this.recipeService.getPublicRecipes(); final List<Recipe> publicRecipes = this.recipeService.getPublicRecipes();
assertThat(publicRecipes.size(), is(2));
assertThat(publicRecipes, containsRecipes(r0, r1)); assertThat(publicRecipes, containsRecipes(r0, r1));
} }
@Test @Test
@DirtiesContext
public void getRecipeInfoViewsViewableByOwnerWhenPublicAndPrivate() { public void getRecipeInfoViewsViewableByOwnerWhenPublicAndPrivate() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
Recipe r0 = this.createTestRecipe(owner, true, "r0"); Recipe r0 = this.createTestRecipe(owner, true);
Recipe r1 = this.createTestRecipe(owner, false, "r1"); Recipe r1 = this.createTestRecipe(owner, false);
final Slice<RecipeInfoView> viewableInfoViewsSlice = this.recipeService.getInfoViewsViewableBy( final Slice<RecipeInfoView> viewableInfoViewsSlice = this.recipeService.getInfoViewsViewableBy(
Pageable.ofSize(20), Pageable.ofSize(20),
owner owner
); );
final List<RecipeInfoView> viewableInfos = viewableInfoViewsSlice.getContent(); final List<RecipeInfoView> viewableInfos = viewableInfoViewsSlice.getContent();
assertThat(viewableInfos.size(), is(2));
assertThat(viewableInfos, containsRecipeInfoViewsForRecipes(r0, r1)); assertThat(viewableInfos, containsRecipeInfoViewsForRecipes(r0, r1));
} }
@Test @Test
@DirtiesContext
public void getRecipesViewableByUser() throws RecipeException { public void getRecipesViewableByUser() throws RecipeException {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final User viewer = this.createTestUser("recipeViewer"); final User viewer = this.seedUser();
Recipe r0 = this.createTestRecipe(owner); Recipe r0 = this.createTestRecipe(owner);
r0 = this.recipeService.addViewer(r0.getId(), owner, viewer); r0 = this.recipeService.addViewer(r0.getId(), owner, viewer);
@ -299,9 +274,8 @@ public class RecipeServiceTests {
} }
@Test @Test
@DirtiesContext
public void getRecipesOwnedByUser() { public void getRecipesOwnedByUser() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final Recipe r0 = this.createTestRecipe(owner); final Recipe r0 = this.createTestRecipe(owner);
final List<Recipe> ownedRecipes = this.recipeService.getRecipesOwnedBy(owner); final List<Recipe> ownedRecipes = this.recipeService.getRecipesOwnedBy(owner);
assertThat(ownedRecipes.size(), is(1)); assertThat(ownedRecipes.size(), is(1));
@ -309,9 +283,8 @@ public class RecipeServiceTests {
} }
@Test @Test
@DirtiesContext
public void updateRawText() throws RecipeException, ImageException { public void updateRawText() throws RecipeException, ImageException {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final RecipeCreateSpec createSpec = new RecipeCreateSpec(); final RecipeCreateSpec createSpec = new RecipeCreateSpec();
createSpec.setSlug("my-recipe"); createSpec.setSlug("my-recipe");
createSpec.setTitle("My Recipe"); createSpec.setTitle("My Recipe");
@ -330,10 +303,9 @@ public class RecipeServiceTests {
} }
@Test @Test
@DirtiesContext
public void updateRawTextThrowsIfNotOwner() { public void updateRawTextThrowsIfNotOwner() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final User notOwner = this.createTestUser("notOwner"); final User notOwner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner); final Recipe recipe = this.createTestRecipe(owner);
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(); final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec();
updateSpec.setRawText("should fail"); updateSpec.setRawText("should fail");
@ -349,19 +321,17 @@ public class RecipeServiceTests {
} }
@Test @Test
@DirtiesContext
public void deleteRecipe() { public void deleteRecipe() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final Recipe toDelete = this.createTestRecipe(owner); final Recipe toDelete = this.createTestRecipe(owner);
this.recipeService.deleteRecipe(toDelete.getId(), owner); this.recipeService.deleteRecipe(toDelete.getId(), owner);
assertThrows(RecipeException.class, () -> this.recipeService.getById(toDelete.getId(), owner)); assertThrows(RecipeException.class, () -> this.recipeService.getById(toDelete.getId(), owner));
} }
@Test @Test
@DirtiesContext
public void deleteRecipeThrowsIfNotOwner() { public void deleteRecipeThrowsIfNotOwner() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.seedUser();
final User notOwner = this.createTestUser("notOwner"); final User notOwner = this.seedUser();
final Recipe toDelete = this.createTestRecipe(owner); final Recipe toDelete = this.createTestRecipe(owner);
assertThrows(AccessDeniedException.class, () -> this.recipeService.deleteRecipe(toDelete.getId(), notOwner)); assertThrows(AccessDeniedException.class, () -> this.recipeService.deleteRecipe(toDelete.getId(), notOwner));
} }

View File

@ -1,20 +1,23 @@
package app.mealsmadeeasy.api.recipe.star; package app.mealsmadeeasy.api.recipe.star;
import app.mealsmadeeasy.api.IntegrationTestsExtension;
import app.mealsmadeeasy.api.recipe.RecipeEntity; import app.mealsmadeeasy.api.recipe.RecipeEntity;
import app.mealsmadeeasy.api.recipe.RecipeRepository; import app.mealsmadeeasy.api.recipe.RecipeRepository;
import app.mealsmadeeasy.api.user.UserEntity; import app.mealsmadeeasy.api.user.UserEntity;
import app.mealsmadeeasy.api.user.UserRepository; import app.mealsmadeeasy.api.user.UserRepository;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.UUID;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
@SpringBootTest @SpringBootTest
@ExtendWith(IntegrationTestsExtension.class)
public class RecipeStarRepositoryTests { public class RecipeStarRepositoryTests {
@Autowired @Autowired
@ -26,18 +29,19 @@ public class RecipeStarRepositoryTests {
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
private UserEntity getOwnerUser() { private UserEntity seedUser() {
final UserEntity draft = UserEntity.getDefaultDraft(); final UserEntity draft = UserEntity.getDefaultDraft();
draft.setUsername("test-user"); final String uuid = UUID.randomUUID().toString();
draft.setEmail("test-user@test.com"); draft.setUsername(uuid);
draft.setEmail(uuid + "@test.com");
draft.setPassword("test"); draft.setPassword("test");
return this.userRepository.save(draft); return this.userRepository.save(draft);
} }
private RecipeEntity getTestRecipe(UserEntity owner) { private RecipeEntity getTestRecipe(UserEntity owner) {
final RecipeEntity recipeDraft = new RecipeEntity(); final RecipeEntity recipeDraft = new RecipeEntity();
recipeDraft.setCreated(LocalDateTime.now()); recipeDraft.setCreated(OffsetDateTime.now());
recipeDraft.setSlug("test-recipe"); recipeDraft.setSlug(UUID.randomUUID().toString());
recipeDraft.setOwner(owner); recipeDraft.setOwner(owner);
recipeDraft.setTitle("Test Recipe"); recipeDraft.setTitle("Test Recipe");
recipeDraft.setRawText("Hello, World!"); recipeDraft.setRawText("Hello, World!");
@ -45,15 +49,14 @@ public class RecipeStarRepositoryTests {
} }
@Test @Test
@DirtiesContext
public void returnsTrueIfStarer() { public void returnsTrueIfStarer() {
final UserEntity owner = this.getOwnerUser(); final UserEntity owner = this.seedUser();
final RecipeEntity recipe = this.getTestRecipe(owner); final RecipeEntity recipe = this.getTestRecipe(owner);
final RecipeStarEntity starDraft = new RecipeStarEntity(); final RecipeStarEntity starDraft = new RecipeStarEntity();
final RecipeStarId starId = new RecipeStarId(); final RecipeStarId starId = new RecipeStarId();
starId.setRecipeId(recipe.getId()); starId.setRecipeId(recipe.getId());
starId.setOwnerUsername(owner.getUsername()); starId.getOwnerId(owner.getId());
starDraft.setId(starId); starDraft.setId(starId);
this.recipeStarRepository.save(starDraft); this.recipeStarRepository.save(starDraft);
@ -61,22 +64,21 @@ public class RecipeStarRepositoryTests {
this.recipeStarRepository.isStarer( this.recipeStarRepository.isStarer(
recipe.getOwner().getUsername(), recipe.getOwner().getUsername(),
recipe.getSlug(), recipe.getSlug(),
owner.getUsername() owner.getId()
), ),
is(true) is(true)
); );
} }
@Test @Test
@DirtiesContext
public void returnsFalseIfNotStarer() { public void returnsFalseIfNotStarer() {
final UserEntity owner = this.getOwnerUser(); final UserEntity owner = this.seedUser();
final RecipeEntity recipe = this.getTestRecipe(owner); final RecipeEntity recipe = this.getTestRecipe(owner);
assertThat( assertThat(
this.recipeStarRepository.isStarer( this.recipeStarRepository.isStarer(
recipe.getOwner().getUsername(), recipe.getOwner().getUsername(),
recipe.getSlug(), recipe.getSlug(),
owner.getUsername() owner.getId()
), ),
is(false) is(false)
); );

View File

@ -1,5 +1,6 @@
package app.mealsmadeeasy.api.recipe.star; package app.mealsmadeeasy.api.recipe.star;
import app.mealsmadeeasy.api.IntegrationTestsExtension;
import app.mealsmadeeasy.api.recipe.Recipe; import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.recipe.RecipeException; import app.mealsmadeeasy.api.recipe.RecipeException;
import app.mealsmadeeasy.api.recipe.RecipeService; import app.mealsmadeeasy.api.recipe.RecipeService;
@ -9,9 +10,11 @@ import app.mealsmadeeasy.api.user.UserCreateException;
import app.mealsmadeeasy.api.user.UserService; import app.mealsmadeeasy.api.user.UserService;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import java.util.UUID;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.notNullValue;
@ -19,6 +22,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@SpringBootTest @SpringBootTest
@ExtendWith(IntegrationTestsExtension.class)
public class RecipeStarServiceTests { public class RecipeStarServiceTests {
@Autowired @Autowired
@ -30,57 +34,57 @@ public class RecipeStarServiceTests {
@Autowired @Autowired
private RecipeService recipeService; private RecipeService recipeService;
private User getTestUser(String username) { private User seedUser() {
final String uuid = UUID.randomUUID().toString();
try { try {
return this.userService.createUser(username, username + "@test.com", "test"); return this.userService.createUser(uuid, uuid + "@test.com", "test");
} catch (UserCreateException e) { } catch (UserCreateException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private Recipe getTestRecipe(User owner, String slug, boolean isPublic) { private Recipe seedRecipe(User owner) {
final RecipeCreateSpec spec = new RecipeCreateSpec(); final RecipeCreateSpec spec = new RecipeCreateSpec();
spec.setSlug(slug); spec.setSlug(UUID.randomUUID().toString());
spec.setTitle("Test Recipe"); spec.setTitle("Test Recipe");
spec.setRawText("My great recipe has five ingredients."); spec.setRawText("My great recipe has five ingredients.");
spec.setPublic(isPublic); spec.setPublic(true);
return this.recipeService.create(owner, spec); return this.recipeService.create(owner, spec);
} }
@Test @Test
@DirtiesContext
public void createViaUsernameAndSlug() { public void createViaUsernameAndSlug() {
final User owner = this.getTestUser("recipe-owner"); final User owner = this.seedUser();
final User starer = this.getTestUser("recipe-starer"); final User starer = this.seedUser();
final Recipe recipe = this.getTestRecipe(owner, "test-recipe", true); final Recipe recipe = this.seedRecipe(owner);
final RecipeStar star = assertDoesNotThrow(() -> this.recipeStarService.create( final RecipeStar star = assertDoesNotThrow(() -> this.recipeStarService.create(
recipe.getOwner().getUsername(), recipe.getOwner().getUsername(),
recipe.getSlug(), recipe.getSlug(),
starer starer
)); ));
assertThat(star.getDate(), is(notNullValue())); //noinspection DataFlowIssue
assertThat(star.getTimestamp(), is(notNullValue()));
} }
@Test @Test
@DirtiesContext
public void createViaId() { public void createViaId() {
final User owner = this.getTestUser("recipe-owner"); final User owner = this.seedUser();
final User starer = this.getTestUser("recipe-starer"); final User starer = this.seedUser();
final Recipe recipe = this.getTestRecipe(owner, "test-recipe", true); final Recipe recipe = this.seedRecipe(owner);
final RecipeStar star = assertDoesNotThrow(() -> this.recipeStarService.create( final RecipeStar star = assertDoesNotThrow(() -> this.recipeStarService.create(
recipe.getId(), recipe.getId(),
starer.getUsername() starer.getId()
)); ));
assertThat(star.getDate(), is(notNullValue())); //noinspection DataFlowIssue
assertThat(star.getTimestamp(), is(notNullValue()));
} }
@Test @Test
@DirtiesContext
public void find() throws RecipeException { public void find() throws RecipeException {
final User owner = this.getTestUser("recipe-owner"); final User owner = this.seedUser();
final User starer = this.getTestUser("recipe-starer"); final User starer = this.seedUser();
final Recipe recipe = this.getTestRecipe(owner, "test-recipe", true); final Recipe recipe = this.seedRecipe(owner);
this.recipeStarService.create(recipe.getId(), starer.getUsername()); this.recipeStarService.create(recipe.getId(), starer.getId());
final @Nullable RecipeStar star = this.recipeStarService.find( final @Nullable RecipeStar star = this.recipeStarService.find(
recipe.getOwner().getUsername(), recipe.getOwner().getUsername(),
recipe.getSlug(), recipe.getSlug(),
@ -90,12 +94,11 @@ public class RecipeStarServiceTests {
} }
@Test @Test
@DirtiesContext
public void deleteViaUsernameAndSlug() { public void deleteViaUsernameAndSlug() {
final User owner = this.getTestUser("recipe-owner"); final User owner = this.seedUser();
final User starer = this.getTestUser("recipe-starer"); final User starer = this.seedUser();
final Recipe recipe = this.getTestRecipe(owner, "test-recipe", true); final Recipe recipe = this.seedRecipe(owner);
this.recipeStarService.create(recipe.getId(), starer.getUsername()); this.recipeStarService.create(recipe.getId(), starer.getId());
assertDoesNotThrow(() -> this.recipeStarService.delete( assertDoesNotThrow(() -> this.recipeStarService.delete(
recipe.getOwner().getUsername(), recipe.getOwner().getUsername(),
recipe.getSlug(), recipe.getSlug(),

View File

@ -1,19 +1,21 @@
package app.mealsmadeeasy.api.signup; package app.mealsmadeeasy.api.signup;
import app.mealsmadeeasy.api.IntegrationTestsExtension;
import app.mealsmadeeasy.api.user.UserCreateException.Type; import app.mealsmadeeasy.api.user.UserCreateException.Type;
import app.mealsmadeeasy.api.user.UserService; import app.mealsmadeeasy.api.user.UserService;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.ObjectMapper;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@ -22,6 +24,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@SpringBootTest @SpringBootTest
@AutoConfigureMockMvc @AutoConfigureMockMvc
@ExtendWith(IntegrationTestsExtension.class)
public class SignUpControllerTests { public class SignUpControllerTests {
@Autowired @Autowired
@ -41,18 +44,18 @@ public class SignUpControllerTests {
} }
@Test @Test
@DirtiesContext
public void checkUsernameExpectAvailable() throws Exception { public void checkUsernameExpectAvailable() throws Exception {
this.mockMvc.perform(this.getCheckUsernameRequest("isAvailable")) final String username = UUID.randomUUID().toString();
this.mockMvc.perform(this.getCheckUsernameRequest(username))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.isAvailable").value(true)); .andExpect(jsonPath("$.isAvailable").value(true));
} }
@Test @Test
@DirtiesContext
public void checkUsernameExpectNotAvailable() throws Exception { public void checkUsernameExpectNotAvailable() throws Exception {
this.userService.createUser("notAvailable", "not-available@notavailable.com", "test"); final String username = UUID.randomUUID().toString();
this.mockMvc.perform(this.getCheckUsernameRequest("notAvailable")) this.userService.createUser(username, username + "@notavailable.com", "test");
this.mockMvc.perform(this.getCheckUsernameRequest(username))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.isAvailable").value(false)); .andExpect(jsonPath("$.isAvailable").value(false));
} }
@ -66,44 +69,43 @@ public class SignUpControllerTests {
} }
@Test @Test
@DirtiesContext
public void checkEmailExpectAvailable() throws Exception { public void checkEmailExpectAvailable() throws Exception {
this.mockMvc.perform(this.getCheckEmailRequest("available@available.com")) this.mockMvc.perform(this.getCheckEmailRequest(UUID.randomUUID() + "@available.com"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.isAvailable").value(true)); .andExpect(jsonPath("$.isAvailable").value(true));
} }
@Test @Test
@DirtiesContext
public void checkEmailExpectNotAvailable() throws Exception { public void checkEmailExpectNotAvailable() throws Exception {
this.userService.createUser("notAvailable", "not-available@notavailable.com", "test"); final String notAvailable = UUID.randomUUID().toString();
this.mockMvc.perform(this.getCheckEmailRequest("not-available@notavailable.com")) this.userService.createUser(notAvailable, notAvailable + "@notavailable.com", "test");
this.mockMvc.perform(this.getCheckEmailRequest(notAvailable + "@notavailable.com"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.isAvailable").value(false)); .andExpect(jsonPath("$.isAvailable").value(false));
} }
@Test @Test
@DirtiesContext
public void simpleSignUp() throws Exception { public void simpleSignUp() throws Exception {
final SignUpBody body = new SignUpBody(); final SignUpBody body = new SignUpBody();
body.setUsername("newUser"); final String username = UUID.randomUUID().toString();
body.setEmail("new@user.com"); body.setUsername(username);
body.setEmail(username + "@user.com");
body.setPassword("test"); body.setPassword("test");
final MockHttpServletRequestBuilder req = post("/sign-up") final MockHttpServletRequestBuilder req = post("/sign-up")
.content(this.objectMapper.writeValueAsString(body)) .content(this.objectMapper.writeValueAsString(body))
.contentType(MediaType.APPLICATION_JSON); .contentType(MediaType.APPLICATION_JSON);
this.mockMvc.perform(req) this.mockMvc.perform(req)
.andExpect(status().isCreated()) .andExpect(status().isCreated())
.andExpect(jsonPath("$.username").value("newUser")); .andExpect(jsonPath("$.username").value(username));
} }
@Test @Test
@DirtiesContext
public void signUpBadRequestWhenUsernameTaken() throws Exception { public void signUpBadRequestWhenUsernameTaken() throws Exception {
this.userService.createUser("taken", "taken@taken.com", "test"); final String takenUsername = UUID.randomUUID().toString();
this.userService.createUser(takenUsername, takenUsername + "@taken.com", "test");
final SignUpBody body = new SignUpBody(); final SignUpBody body = new SignUpBody();
body.setUsername("taken"); body.setUsername(takenUsername);
body.setEmail("not-taken@taken.com"); // n.b. body.setEmail(UUID.randomUUID() + "@taken.com"); // n.b.: not taken email
body.setPassword("test"); body.setPassword("test");
final MockHttpServletRequestBuilder req = post("/sign-up") final MockHttpServletRequestBuilder req = post("/sign-up")
.content(this.objectMapper.writeValueAsString(body)) .content(this.objectMapper.writeValueAsString(body))
@ -111,16 +113,16 @@ public class SignUpControllerTests {
this.mockMvc.perform(req) this.mockMvc.perform(req)
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error.type").value(Type.USERNAME_TAKEN.toString())) .andExpect(jsonPath("$.error.type").value(Type.USERNAME_TAKEN.toString()))
.andExpect(jsonPath("$.error.message").value(containsString("taken"))); .andExpect(jsonPath("$.error.message").value(containsString(takenUsername)));
} }
@Test @Test
@DirtiesContext
public void signUpBadRequestWhenEmailTaken() throws Exception { public void signUpBadRequestWhenEmailTaken() throws Exception {
this.userService.createUser("taken", "taken@taken.com", "test"); final String takenEmail = UUID.randomUUID() + "@taken.com";
this.userService.createUser(UUID.randomUUID().toString(), takenEmail, "test");
final SignUpBody body = new SignUpBody(); final SignUpBody body = new SignUpBody();
body.setUsername("notTaken"); // n.b. body.setUsername(UUID.randomUUID().toString()); // n.b.: random username
body.setEmail("taken@taken.com"); body.setEmail(takenEmail);
body.setPassword("test"); body.setPassword("test");
final MockHttpServletRequestBuilder req = post("/sign-up") final MockHttpServletRequestBuilder req = post("/sign-up")
.content(this.objectMapper.writeValueAsString(body)) .content(this.objectMapper.writeValueAsString(body))
@ -128,7 +130,7 @@ public class SignUpControllerTests {
this.mockMvc.perform(req) this.mockMvc.perform(req)
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error.type").value(Type.EMAIL_TAKEN.toString())) .andExpect(jsonPath("$.error.type").value(Type.EMAIL_TAKEN.toString()))
.andExpect(jsonPath("$.error.message").value(containsString("taken@taken.com"))); .andExpect(jsonPath("$.error.message").value(containsString(takenEmail)));
} }
} }

View File

@ -1,11 +1,12 @@
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.baseUrl=http://localhost:8080
app.mealsmadeeasy.api.security.access-token-lifetime=60 app.mealsmadeeasy.api.security.access-token-lifetime=60
app.mealsmadeeasy.api.security.refresh-token-lifetime=120 app.mealsmadeeasy.api.security.refresh-token-lifetime=120
app.mealsmadeeasy.api.minio.endpoint=http://localhost:9000 app.mealsmadeeasy.api.minio.endpoint=http://localhost:9000
app.mealsmadeeasy.api.minio.accessKey=minio-root app.mealsmadeeasy.api.minio.accessKey=minio-root
app.mealsmadeeasy.api.minio.secretKey=test0123 app.mealsmadeeasy.api.minio.secretKey=test0123
app.mealsmadeeasy.api.images.bucketName=images app.mealsmadeeasy.api.images.bucketName=images
# Source - https://stackoverflow.com/questions/3164072/large-objects-may-not-be-used-in-auto-commit-mode
# Posted by Iogui, modified by community. See post 'Timeline' for change history
# Retrieved 2025-12-25, License - CC BY-SA 4.0
spring.datasource.hikari.auto-commit=false

View File

@ -1,6 +1,5 @@
package app.mealsmadeeasy.api.auth; package app.mealsmadeeasy.api.auth;
import app.mealsmadeeasy.api.security.AuthToken;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -8,6 +7,8 @@ import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/auth")
public final class AuthController { public final class AuthController {
@ -31,9 +32,9 @@ public final class AuthController {
} }
private ResponseEntity<LoginView> getLoginViewResponseEntity(LoginDetails loginDetails) { private ResponseEntity<LoginView> getLoginViewResponseEntity(LoginDetails loginDetails) {
final AuthToken refreshToken = loginDetails.getRefreshToken(); final RefreshToken refreshToken = loginDetails.getRefreshToken();
final ResponseCookie refreshCookie = getRefreshTokenCookie( final ResponseCookie refreshCookie = getRefreshTokenCookie(
refreshToken.getToken(), refreshToken.getToken().toString(),
refreshToken.getLifetime() refreshToken.getLifetime()
); );
final var loginView = new LoginView( final var loginView = new LoginView(
@ -60,7 +61,7 @@ public final class AuthController {
@PostMapping("/refresh") @PostMapping("/refresh")
public ResponseEntity<LoginView> refresh( public ResponseEntity<LoginView> refresh(
@CookieValue(value = "refresh-token", required = false) @Nullable String oldRefreshToken @CookieValue(value = "refresh-token", required = false) @Nullable UUID oldRefreshToken
) throws LoginException { ) throws LoginException {
final LoginDetails loginDetails = this.authService.refresh(oldRefreshToken); final LoginDetails loginDetails = this.authService.refresh(oldRefreshToken);
return this.getLoginViewResponseEntity(loginDetails); return this.getLoginViewResponseEntity(loginDetails);
@ -68,7 +69,7 @@ public final class AuthController {
@PostMapping("/logout") @PostMapping("/logout")
public ResponseEntity<?> logout( public ResponseEntity<?> logout(
@CookieValue(value = "refresh-token", required = false) @Nullable String refreshToken @CookieValue(value = "refresh-token", required = false) @Nullable UUID refreshToken
) { ) {
if (refreshToken != null) { if (refreshToken != null) {
this.authService.logout(refreshToken); this.authService.logout(refreshToken);

View File

@ -2,8 +2,10 @@ package app.mealsmadeeasy.api.auth;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public interface AuthService { public interface AuthService {
LoginDetails login(String username, String password) throws LoginException; LoginDetails login(String username, String password) throws LoginException;
void logout(String refreshToken); void logout(UUID refreshToken);
LoginDetails refresh(@Nullable String refreshToken) throws LoginException; LoginDetails refresh(@Nullable UUID refreshToken) throws LoginException;
} }

View File

@ -12,7 +12,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -38,9 +38,9 @@ public class AuthServiceImpl implements AuthService {
private RefreshToken createRefreshToken(UserEntity principal) { private RefreshToken createRefreshToken(UserEntity principal) {
final RefreshTokenEntity refreshTokenDraft = new RefreshTokenEntity(); final RefreshTokenEntity refreshTokenDraft = new RefreshTokenEntity();
refreshTokenDraft.setToken(UUID.randomUUID().toString()); refreshTokenDraft.setToken(UUID.randomUUID());
refreshTokenDraft.setIssued(LocalDateTime.now()); refreshTokenDraft.setIssued(OffsetDateTime.now());
refreshTokenDraft.setExpiration(LocalDateTime.now().plusSeconds(this.refreshTokenLifetime)); refreshTokenDraft.setExpiration(OffsetDateTime.now().plusSeconds(this.refreshTokenLifetime));
refreshTokenDraft.setOwner(principal); refreshTokenDraft.setOwner(principal);
return this.refreshTokenRepository.save(refreshTokenDraft); return this.refreshTokenRepository.save(refreshTokenDraft);
} }
@ -64,13 +64,13 @@ public class AuthServiceImpl implements AuthService {
@Override @Override
@Transactional @Transactional
public void logout(String refreshToken) { public void logout(UUID refreshToken) {
this.refreshTokenRepository.findByToken(refreshToken).ifPresent(this.refreshTokenRepository::delete); this.refreshTokenRepository.findByToken(refreshToken).ifPresent(this.refreshTokenRepository::delete);
} }
@Override @Override
@Transactional @Transactional
public LoginDetails refresh(@Nullable String refreshToken) throws LoginException { public LoginDetails refresh(@Nullable UUID refreshToken) throws LoginException {
if (refreshToken == null) { if (refreshToken == null) {
throw new LoginException(LoginExceptionReason.NO_REFRESH_TOKEN, "No refresh token provided."); throw new LoginException(LoginExceptionReason.NO_REFRESH_TOKEN, "No refresh token provided.");
} }
@ -83,7 +83,7 @@ public class AuthServiceImpl implements AuthService {
if (old.isRevoked() || old.isDeleted()) { if (old.isRevoked() || old.isDeleted()) {
throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "Invalid refresh token."); throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "Invalid refresh token.");
} }
if (old.getExpires().isBefore(LocalDateTime.now())) { if (old.getExpires().isBefore(OffsetDateTime.now())) {
throw new LoginException(LoginExceptionReason.EXPIRED_REFRESH_TOKEN, "Refresh token is expired."); throw new LoginException(LoginExceptionReason.EXPIRED_REFRESH_TOKEN, "Refresh token is expired.");
} }

View File

@ -6,9 +6,9 @@ public final class LoginDetails {
private final String username; private final String username;
private final AuthToken accessToken; private final AuthToken accessToken;
private final AuthToken refreshToken; private final RefreshToken refreshToken;
public LoginDetails(String username, AuthToken accessToken, AuthToken refreshToken) { public LoginDetails(String username, AuthToken accessToken, RefreshToken refreshToken) {
this.username = username; this.username = username;
this.accessToken = accessToken; this.accessToken = accessToken;
this.refreshToken = refreshToken; this.refreshToken = refreshToken;
@ -22,7 +22,7 @@ public final class LoginDetails {
return this.accessToken; return this.accessToken;
} }
public AuthToken getRefreshToken() { public RefreshToken getRefreshToken() {
return this.refreshToken; return this.refreshToken;
} }

View File

@ -1,11 +1,13 @@
package app.mealsmadeeasy.api.auth; package app.mealsmadeeasy.api.auth;
import app.mealsmadeeasy.api.security.AuthToken; import java.time.OffsetDateTime;
import java.util.UUID;
import java.time.LocalDateTime; public interface RefreshToken {
UUID getToken();
public interface RefreshToken extends AuthToken { long getLifetime();
LocalDateTime getIssued(); OffsetDateTime getExpires();
OffsetDateTime getIssued();
boolean isRevoked(); boolean isRevoked();
boolean isDeleted(); boolean isDeleted();
} }

View File

@ -3,68 +3,58 @@ package app.mealsmadeeasy.api.auth;
import app.mealsmadeeasy.api.user.UserEntity; import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.UUID;
@Entity(name = "RefreshToken") @Entity(name = "RefreshToken")
@Table(name = "refresh_token")
public class RefreshTokenEntity implements RefreshToken { public class RefreshTokenEntity implements RefreshToken {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @Column(nullable = false)
@Column(nullable = false, updatable = false) private UUID token;
private Long id;
@Column(unique = true, nullable = false)
private String token;
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime issued; private OffsetDateTime issued;
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime expiration; private OffsetDateTime expiration;
@Column(nullable = false) @ManyToOne(optional = false)
private Boolean revoked = false; @JoinColumn(name = "owner_id", nullable = false)
@JoinColumn(nullable = false)
@ManyToOne
private UserEntity owner; private UserEntity owner;
@Column(nullable = false) @Column(nullable = false)
private Boolean deleted = false; private Boolean deleted = false;
public Long getId() { @Column(nullable = false)
return this.id; private Boolean revoked = false;
}
public void setId(Long id) {
this.id = id;
}
@Override @Override
public String getToken() { public UUID getToken() {
return this.token; return this.token;
} }
public void setToken(String token) { public void setToken(UUID token) {
this.token = token; this.token = token;
} }
@Override @Override
public LocalDateTime getIssued() { public OffsetDateTime getIssued() {
return this.issued; return this.issued;
} }
public void setIssued(LocalDateTime issued) { public void setIssued(OffsetDateTime issued) {
this.issued = issued; this.issued = issued;
} }
@Override @Override
public LocalDateTime getExpires() { public OffsetDateTime getExpires() {
return this.expiration; return this.expiration;
} }
public void setExpiration(LocalDateTime expiration) { public void setExpiration(OffsetDateTime expiration) {
this.expiration = expiration; this.expiration = expiration;
} }

View File

@ -6,10 +6,11 @@ import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, Long> { public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, Long> {
Optional<RefreshTokenEntity> findByToken(String token); Optional<RefreshTokenEntity> findByToken(UUID token);
@Modifying @Modifying
@Transactional @Transactional

View File

@ -3,13 +3,13 @@ package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.Set; import java.util.Set;
public interface Image { public interface Image {
Long getId(); Integer getId();
LocalDateTime getCreated(); OffsetDateTime getCreated();
@Nullable LocalDateTime getModified(); @Nullable OffsetDateTime getModified();
String getUserFilename(); String getUserFilename();
String getMimeType(); String getMimeType();
@Nullable String getAlt(); @Nullable String getAlt();

View File

@ -5,22 +5,23 @@ import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*; import jakarta.persistence.*;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@Entity(name = "Image") @Entity(name = "Image")
@Table(name = "image")
public class S3ImageEntity implements Image { public class S3ImageEntity implements Image {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)
private Long id; private Integer id;
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime created = LocalDateTime.now(); private OffsetDateTime created = OffsetDateTime.now();
private LocalDateTime modified; private OffsetDateTime modified;
@Column(nullable = false) @Column(nullable = false)
private String userFilename; private String userFilename;
@ -47,32 +48,37 @@ public class S3ImageEntity implements Image {
private Boolean isPublic = false; private Boolean isPublic = false;
@ManyToMany @ManyToMany
@JoinTable(
name = "image_viewer",
joinColumns = @JoinColumn(name = "image_id"),
inverseJoinColumns = @JoinColumn(name = "viewer_id")
)
private Set<UserEntity> viewers = new HashSet<>(); private Set<UserEntity> viewers = new HashSet<>();
@Override @Override
public Long getId() { public Integer getId() {
return this.id; return this.id;
} }
public void setId(Long id) { public void setId(Integer id) {
this.id = id; this.id = id;
} }
@Override @Override
public LocalDateTime getCreated() { public OffsetDateTime getCreated() {
return this.created; return this.created;
} }
public void setCreated(LocalDateTime created) { public void setCreated(OffsetDateTime created) {
this.created = created; this.created = created;
} }
@Override @Override
public @Nullable LocalDateTime getModified() { public @Nullable OffsetDateTime getModified() {
return this.modified; return this.modified;
} }
public void setModified(LocalDateTime modified) { public void setModified(OffsetDateTime modified) {
this.modified = modified; this.modified = modified;
} }

View File

@ -18,7 +18,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.*; import java.util.*;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -225,7 +225,7 @@ public class S3ImageService implements ImageService {
} }
} }
if (didUpdate) { if (didUpdate) {
entity.setModified(LocalDateTime.now()); entity.setModified(OffsetDateTime.now());
} }
return this.imageRepository.save(entity); return this.imageRepository.save(entity);
} }

View File

@ -4,7 +4,7 @@ import app.mealsmadeeasy.api.image.Image;
import app.mealsmadeeasy.api.user.view.UserInfoView; import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -33,8 +33,8 @@ public class ImageView {
} }
private String url; private String url;
private LocalDateTime created; private OffsetDateTime created;
private @Nullable LocalDateTime modified; private @Nullable OffsetDateTime modified;
private String filename; private String filename;
private String mimeType; private String mimeType;
private @Nullable String alt; private @Nullable String alt;
@ -53,19 +53,19 @@ public class ImageView {
this.url = url; this.url = url;
} }
public LocalDateTime getCreated() { public OffsetDateTime getCreated() {
return this.created; return this.created;
} }
public void setCreated(LocalDateTime created) { public void setCreated(OffsetDateTime created) {
this.created = created; this.created = created;
} }
public @Nullable LocalDateTime getModified() { public @Nullable OffsetDateTime getModified() {
return this.modified; return this.modified;
} }
public void setModified(@Nullable LocalDateTime modified) { public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified; this.modified = modified;
} }

View File

@ -0,0 +1,21 @@
package app.mealsmadeeasy.api.job;
import jakarta.persistence.Column;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
public class JobEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable = false, updatable = false)
private Long id;
@Column(nullable = false)
private String key;
@Column(nullable = false, columnDefinition = "jsonb")
private String payload;
}

View File

@ -6,13 +6,13 @@ import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.Set; import java.util.Set;
public interface Recipe { public interface Recipe {
Long getId(); Integer getId();
LocalDateTime getCreated(); OffsetDateTime getCreated();
@Nullable LocalDateTime getModified(); @Nullable OffsetDateTime getModified();
String getSlug(); String getSlug();
String getTitle(); String getTitle();
@Nullable Integer getPreparationTime(); @Nullable Integer getPreparationTime();

View File

@ -10,7 +10,7 @@ import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*; import jakarta.persistence.*;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -18,14 +18,14 @@ import java.util.Set;
public final class RecipeEntity implements Recipe { public final class RecipeEntity implements Recipe {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)
private Long id; private Integer id;
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime created; private OffsetDateTime created;
private LocalDateTime modified; private OffsetDateTime modified;
@Column(nullable = false, unique = true) @Column(nullable = false, unique = true)
private String slug; private String slug;
@ -57,7 +57,7 @@ public final class RecipeEntity implements Recipe {
private UserEntity owner; private UserEntity owner;
@OneToMany @OneToMany
@JoinColumn(name = "recipeId") @JoinColumn(name = "recipe_id")
private Set<RecipeStarEntity> stars = new HashSet<>(); private Set<RecipeStarEntity> stars = new HashSet<>();
@OneToMany(mappedBy = "recipe") @OneToMany(mappedBy = "recipe")
@ -67,35 +67,41 @@ public final class RecipeEntity implements Recipe {
private Boolean isPublic = false; private Boolean isPublic = false;
@ManyToMany @ManyToMany
@JoinTable(
name = "recipe_viewer",
joinColumns = @JoinColumn(name = "recipe_id"),
inverseJoinColumns = @JoinColumn(name = "viewer_id")
)
private Set<UserEntity> viewers = new HashSet<>(); private Set<UserEntity> viewers = new HashSet<>();
@ManyToOne @ManyToOne
@JoinColumn(name = "main_image_id")
private S3ImageEntity mainImage; private S3ImageEntity mainImage;
@Override @Override
public Long getId() { public Integer getId() {
return this.id; return this.id;
} }
public void setId(Long id) { public void setId(Integer id) {
this.id = id; this.id = id;
} }
@Override @Override
public LocalDateTime getCreated() { public OffsetDateTime getCreated() {
return this.created; return this.created;
} }
public void setCreated(LocalDateTime created) { public void setCreated(OffsetDateTime created) {
this.created = created; this.created = created;
} }
@Override @Override
public @Nullable LocalDateTime getModified() { public @Nullable OffsetDateTime getModified() {
return this.modified; return this.modified;
} }
public void setModified(@Nullable LocalDateTime modified) { public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified; this.modified = modified;
} }

View File

@ -22,7 +22,7 @@ import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -53,7 +53,7 @@ public class RecipeServiceImpl implements RecipeService {
throw new AccessDeniedException("Must be logged in."); throw new AccessDeniedException("Must be logged in.");
} }
final RecipeEntity draft = new RecipeEntity(); final RecipeEntity draft = new RecipeEntity();
draft.setCreated(LocalDateTime.now()); draft.setCreated(OffsetDateTime.now());
draft.setOwner((UserEntity) owner); draft.setOwner((UserEntity) owner);
draft.setSlug(spec.getSlug()); draft.setSlug(spec.getSlug());
draft.setTitle(spec.getTitle()); draft.setTitle(spec.getTitle());
@ -222,7 +222,7 @@ public class RecipeServiceImpl implements RecipeService {
} }
recipe.setMainImage(mainImage); recipe.setMainImage(mainImage);
recipe.setModified(LocalDateTime.now()); recipe.setModified(OffsetDateTime.now());
return this.recipeRepository.save(recipe); return this.recipeRepository.save(recipe);
} }
@ -279,7 +279,7 @@ public class RecipeServiceImpl implements RecipeService {
if (viewer == null) { if (viewer == null) {
return null; return null;
} }
return this.recipeStarRepository.isStarer(username, slug, viewer.getUsername()); return this.recipeStarRepository.isStarer(username, slug, viewer.getId());
} }
@Override @Override

View File

@ -4,12 +4,12 @@ import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
public interface RecipeComment { public interface RecipeComment {
Long getId(); Integer getId();
LocalDateTime getCreated(); OffsetDateTime getCreated();
@Nullable LocalDateTime getModified(); @Nullable OffsetDateTime getModified();
String getRawText(); String getRawText();
User getOwner(); User getOwner();
Recipe getRecipe(); Recipe getRecipe();

View File

@ -4,20 +4,21 @@ import app.mealsmadeeasy.api.recipe.RecipeEntity;
import app.mealsmadeeasy.api.user.UserEntity; import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
@Entity(name = "RecipeComment") @Entity(name = "RecipeComment")
@Table(name = "recipe_comment")
public final class RecipeCommentEntity implements RecipeComment { public final class RecipeCommentEntity implements RecipeComment {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false) @Column(nullable = false)
private Long id; private Integer id;
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)
private LocalDateTime created = LocalDateTime.now(); private OffsetDateTime created = OffsetDateTime.now();
private LocalDateTime modified; private OffsetDateTime modified;
@Lob @Lob
@Basic(fetch = FetchType.LAZY) @Basic(fetch = FetchType.LAZY)
@ -35,29 +36,29 @@ public final class RecipeCommentEntity implements RecipeComment {
@JoinColumn(name = "recipe_id", nullable = false, updatable = false) @JoinColumn(name = "recipe_id", nullable = false, updatable = false)
private RecipeEntity recipe; private RecipeEntity recipe;
public Long getId() { public Integer getId() {
return this.id; return this.id;
} }
public void setId(Long id) { public void setId(Integer id) {
this.id = id; this.id = id;
} }
@Override @Override
public LocalDateTime getCreated() { public OffsetDateTime getCreated() {
return this.created; return this.created;
} }
public void setCreated(LocalDateTime created) { public void setCreated(OffsetDateTime created) {
this.created = created; this.created = created;
} }
@Override @Override
public LocalDateTime getModified() { public OffsetDateTime getModified() {
return this.modified; return this.modified;
} }
public void setModified(LocalDateTime modified) { public void setModified(OffsetDateTime modified) {
this.modified = modified; this.modified = modified;
} }

View File

@ -12,7 +12,7 @@ import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
@ -43,7 +43,7 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
) throws RecipeException { ) throws RecipeException {
requireNonNull(commenter); requireNonNull(commenter);
final RecipeCommentEntity draft = new RecipeCommentEntity(); final RecipeCommentEntity draft = new RecipeCommentEntity();
draft.setCreated(LocalDateTime.now()); draft.setCreated(OffsetDateTime.now());
draft.setRawText(body.getText()); draft.setRawText(body.getText());
draft.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(body.getText())); draft.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(body.getText()));
draft.setOwner((UserEntity) commenter); draft.setOwner((UserEntity) commenter);

View File

@ -3,7 +3,7 @@ package app.mealsmadeeasy.api.recipe.comment;
import app.mealsmadeeasy.api.user.view.UserInfoView; import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
public class RecipeCommentView { public class RecipeCommentView {
@ -21,35 +21,35 @@ public class RecipeCommentView {
return view; return view;
} }
private Long id; private Integer id;
private LocalDateTime created; private OffsetDateTime created;
private @Nullable LocalDateTime modified; private @Nullable OffsetDateTime modified;
private String text; private String text;
private @Nullable String rawText; private @Nullable String rawText;
private UserInfoView owner; private UserInfoView owner;
private Long recipeId; private Integer recipeId;
public Long getId() { public Integer getId() {
return this.id; return this.id;
} }
public void setId(Long id) { public void setId(Integer id) {
this.id = id; this.id = id;
} }
public LocalDateTime getCreated() { public OffsetDateTime getCreated() {
return this.created; return this.created;
} }
public void setCreated(LocalDateTime created) { public void setCreated(OffsetDateTime created) {
this.created = created; this.created = created;
} }
public @Nullable LocalDateTime getModified() { public @Nullable OffsetDateTime getModified() {
return this.modified; return this.modified;
} }
public void setModified(@Nullable LocalDateTime modified) { public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified; this.modified = modified;
} }
@ -77,11 +77,11 @@ public class RecipeCommentView {
this.owner = owner; this.owner = owner;
} }
public Long getRecipeId() { public Integer getRecipeId() {
return this.recipeId; return this.recipeId;
} }
public void setRecipeId(Long recipeId) { public void setRecipeId(Integer recipeId) {
this.recipeId = recipeId; this.recipeId = recipeId;
} }

View File

@ -1,7 +1,7 @@
package app.mealsmadeeasy.api.recipe.star; package app.mealsmadeeasy.api.recipe.star;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
public interface RecipeStar { public interface RecipeStar {
LocalDateTime getDate(); OffsetDateTime getTimestamp();
} }

View File

@ -3,17 +3,19 @@ package app.mealsmadeeasy.api.recipe.star;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.EmbeddedId; import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
@Entity(name = "RecipeStar") @Entity(name = "RecipeStar")
@Table(name = "recipe_star")
public final class RecipeStarEntity implements RecipeStar { public final class RecipeStarEntity implements RecipeStar {
@EmbeddedId @EmbeddedId
private RecipeStarId id; private RecipeStarId id;
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)
private LocalDateTime date = LocalDateTime.now(); private OffsetDateTime timestamp = OffsetDateTime.now();
public RecipeStarId getId() { public RecipeStarId getId() {
return this.id; return this.id;
@ -23,13 +25,12 @@ public final class RecipeStarEntity implements RecipeStar {
this.id = id; this.id = id;
} }
@Override public OffsetDateTime getTimestamp() {
public LocalDateTime getDate() { return this.timestamp;
return this.date;
} }
public void setDate(LocalDateTime date) { public void setTimestamp(OffsetDateTime date) {
this.date = date; this.timestamp = date;
} }
@Override @Override

View File

@ -8,25 +8,25 @@ import java.util.Objects;
@Embeddable @Embeddable
public class RecipeStarId { public class RecipeStarId {
@Column(nullable = false) @Column(name = "owner_id", nullable = false)
private String ownerUsername; private Integer ownerId;
@Column(nullable = false) @Column(name = "recipe_id", nullable = false)
private Long recipeId; private Integer recipeId;
public String getOwnerUsername() { public Integer getOwnerId() {
return this.ownerUsername; return this.ownerId;
} }
public void setOwnerUsername(String ownerUsername) { public void getOwnerId(Integer ownerId) {
this.ownerUsername = ownerUsername; this.ownerId = ownerId;
} }
public Long getRecipeId() { public Integer getRecipeId() {
return this.recipeId; return this.recipeId;
} }
public void setRecipeId(Long recipeId) { public void setRecipeId(Integer recipeId) {
this.recipeId = recipeId; this.recipeId = recipeId;
} }
@ -34,19 +34,19 @@ public class RecipeStarId {
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o instanceof RecipeStarId other) { if (o instanceof RecipeStarId other) {
return this.recipeId.equals(other.recipeId) && this.ownerUsername.equals(other.ownerUsername); return this.recipeId.equals(other.recipeId) && this.ownerId.equals(other.ownerId);
} }
return false; return false;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(this.recipeId, this.ownerUsername); return Objects.hash(this.recipeId, this.ownerId);
} }
@Override @Override
public String toString() { public String toString() {
return "RecipeStarId(" + this.recipeId + ", " + this.ownerUsername + ")"; return "RecipeStarId(" + this.recipeId + ", " + this.ownerId + ")";
} }
} }

View File

@ -9,15 +9,15 @@ import java.util.Optional;
public interface RecipeStarRepository extends JpaRepository<RecipeStarEntity, Long> { public interface RecipeStarRepository extends JpaRepository<RecipeStarEntity, Long> {
@Query("SELECT star FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerUsername = ?2") @Query("SELECT star FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerId = ?2")
Optional<RecipeStarEntity> findByRecipeIdAndOwnerUsername(Long recipeId, String username); Optional<RecipeStarEntity> findByRecipeIdAndOwnerId(Integer recipeId, Integer ownerId);
@Query("SELECT count(rs) > 0 FROM RecipeStar rs, Recipe r WHERE r.owner.username = ?1 AND r.slug = ?2 AND r.id = rs.id.recipeId AND rs.id.ownerUsername = ?3") @Query("SELECT count(rs) > 0 FROM RecipeStar rs, Recipe r WHERE r.owner.username = ?1 AND r.slug = ?2 AND r.id = rs.id.recipeId AND rs.id.ownerId = ?3")
boolean isStarer(String ownerUsername, String slug, String viewerUsername); boolean isStarer(String ownerUsername, String slug, Integer viewerId);
@Modifying @Modifying
@Transactional @Transactional
@Query("DELETE FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerUsername = ?2") @Query("DELETE FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerId = ?2")
void deleteByRecipeIdAndOwnerUsername(Long recipeId, String username); void deleteByRecipeIdAndOwnerId(Integer recipeId, Integer ownerId);
} }

View File

@ -6,11 +6,11 @@ import app.mealsmadeeasy.api.user.User;
import java.util.Optional; import java.util.Optional;
public interface RecipeStarService { public interface RecipeStarService {
RecipeStar create(long recipeId, String ownerUsername); RecipeStar create(Integer recipeId, Integer ownerId);
RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException; RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException;
Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException; Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException;
void delete(long recipeId, String ownerUsername); void delete(Integer recipeId, Integer ownerId);
void delete(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException; void delete(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException;
} }

View File

@ -6,7 +6,7 @@ import app.mealsmadeeasy.api.recipe.RecipeService;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.Optional; import java.util.Optional;
@Service @Service
@ -21,45 +21,45 @@ public class RecipeStarServiceImpl implements RecipeStarService {
} }
@Override @Override
public RecipeStar create(long recipeId, String ownerUsername) { public RecipeStar create(Integer recipeId, Integer ownerId) {
final RecipeStarEntity draft = new RecipeStarEntity(); final RecipeStarEntity draft = new RecipeStarEntity();
final RecipeStarId id = new RecipeStarId(); final RecipeStarId id = new RecipeStarId();
id.setRecipeId(recipeId); id.setRecipeId(recipeId);
id.setOwnerUsername(ownerUsername); id.getOwnerId(ownerId);
draft.setId(id); draft.setId(id);
draft.setDate(LocalDateTime.now()); draft.setTimestamp(OffsetDateTime.now());
return this.recipeStarRepository.save(draft); return this.recipeStarRepository.save(draft);
} }
@Override @Override
public RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException { public RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException {
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer); final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
final Optional<RecipeStarEntity> existing = this.recipeStarRepository.findByRecipeIdAndOwnerUsername( final Optional<RecipeStarEntity> existing = this.recipeStarRepository.findByRecipeIdAndOwnerId(
recipe.getId(), recipe.getId(),
starer.getUsername() starer.getId()
); );
if (existing.isPresent()) { if (existing.isPresent()) {
return existing.get(); return existing.get();
} }
return this.create(recipe.getId(), starer.getUsername()); return this.create(recipe.getId(), starer.getId());
} }
@Override @Override
public Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException { public Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException {
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer); final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
return this.recipeStarRepository.findByRecipeIdAndOwnerUsername(recipe.getId(), starer.getUsername()) return this.recipeStarRepository.findByRecipeIdAndOwnerId(recipe.getId(), starer.getId())
.map(RecipeStar.class::cast); .map(RecipeStar.class::cast);
} }
@Override @Override
public void delete(long recipeId, String ownerUsername) { public void delete(Integer recipeId, Integer ownerId) {
this.recipeStarRepository.deleteByRecipeIdAndOwnerUsername(recipeId, ownerUsername); this.recipeStarRepository.deleteByRecipeIdAndOwnerId(recipeId, ownerId);
} }
@Override @Override
public void delete(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException { public void delete(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException {
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer); final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
this.delete(recipe.getId(), starer.getUsername()); this.delete(recipe.getId(), starer.getId());
} }
} }

View File

@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonInclude.Include;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
public class FullRecipeView { public class FullRecipeView {
@ -41,8 +41,8 @@ public class FullRecipeView {
} }
private long id; private long id;
private LocalDateTime created; private OffsetDateTime created;
private @Nullable LocalDateTime modified; private @Nullable OffsetDateTime modified;
private String slug; private String slug;
private String title; private String title;
private @Nullable Integer preparationTime; private @Nullable Integer preparationTime;
@ -64,19 +64,19 @@ public class FullRecipeView {
this.id = id; this.id = id;
} }
public LocalDateTime getCreated() { public OffsetDateTime getCreated() {
return this.created; return this.created;
} }
public void setCreated(LocalDateTime created) { public void setCreated(OffsetDateTime created) {
this.created = created; this.created = created;
} }
public @Nullable LocalDateTime getModified() { public @Nullable OffsetDateTime getModified() {
return this.modified; return this.modified;
} }
public void setModified(@Nullable LocalDateTime modified) { public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified; this.modified = modified;
} }

View File

@ -5,7 +5,7 @@ import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.user.view.UserInfoView; import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
public final class RecipeInfoView { public final class RecipeInfoView {
@ -26,9 +26,9 @@ public final class RecipeInfoView {
return view; return view;
} }
private long id; private Integer id;
private LocalDateTime created; private OffsetDateTime created;
private LocalDateTime modified; private OffsetDateTime modified;
private String slug; private String slug;
private String title; private String title;
private @Nullable Integer preparationTime; private @Nullable Integer preparationTime;
@ -39,27 +39,27 @@ public final class RecipeInfoView {
private int starCount; private int starCount;
private @Nullable ImageView mainImage; private @Nullable ImageView mainImage;
public long getId() { public Integer getId() {
return this.id; return this.id;
} }
public void setId(long id) { public void setId(Integer id) {
this.id = id; this.id = id;
} }
public LocalDateTime getCreated() { public OffsetDateTime getCreated() {
return this.created; return this.created;
} }
public void setCreated(LocalDateTime created) { public void setCreated(OffsetDateTime created) {
this.created = created; this.created = created;
} }
public LocalDateTime getModified() { public OffsetDateTime getModified() {
return this.modified; return this.modified;
} }
public void setModified(LocalDateTime modified) { public void setModified(OffsetDateTime modified) {
this.modified = modified; this.modified = modified;
} }

View File

@ -6,7 +6,7 @@ import java.util.Set;
public interface User extends UserDetails { public interface User extends UserDetails {
Long getId(); Integer getId();
String getEmail(); String getEmail();
void setEmail(String email); void setEmail(String email);

View File

@ -24,7 +24,7 @@ public final class UserEntity implements User {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false) @Column(nullable = false)
private Long id; private Integer id;
@Column(unique = true, nullable = false) @Column(unique = true, nullable = false)
private String username; private String username;
@ -51,11 +51,11 @@ public final class UserEntity implements User {
private Boolean credentialsExpired; private Boolean credentialsExpired;
@Override @Override
public Long getId() { public Integer getId() {
return this.id; return this.id;
} }
public void setId(Long id) { public void setId(Integer id) {
this.id = id; this.id = id;
} }

View File

@ -2,18 +2,19 @@ package app.mealsmadeeasy.api.user;
import jakarta.persistence.*; import jakarta.persistence.*;
@Entity @Entity(name = "UserGrantedAuthority")
@Table(name = "user_granted_authority")
public final class UserGrantedAuthorityEntity implements UserGrantedAuthority { public final class UserGrantedAuthorityEntity implements UserGrantedAuthority {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false) @Column(nullable = false)
private Long id; private Integer id;
private String authority; private String authority;
@ManyToOne @ManyToOne
@JoinColumn(name = "user_entity_id") @JoinColumn(name = "user_id")
private UserEntity userEntity; private UserEntity userEntity;
@Override @Override

View File

@ -11,14 +11,14 @@ public class UserInfoView {
return userInfoView; return userInfoView;
} }
private long id; private Integer id;
private String username; private String username;
public long getId() { public Integer getId() {
return this.id; return this.id;
} }
public void setId(long id) { public void setId(Integer id) {
this.id = id; this.id = id;
} }

View File

@ -1,5 +1,5 @@
spring.application.name=meals-made-easy-api spring.application.name=meals-made-easy-api
spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.hibernate.ddl-auto=none
spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:meals_made_easy_api} spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:meals_made_easy_api}
spring.datasource.username=${POSTGRES_USER:meals-made-easy-api-user} spring.datasource.username=${POSTGRES_USER:meals-made-easy-api-user}
spring.datasource.password=${POSTGRES_PASSWORD} spring.datasource.password=${POSTGRES_PASSWORD}

View File

@ -0,0 +1,108 @@
CREATE TABLE "user" (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
locked BOOLEAN NOT NULL,
expired BOOLEAN NOT NULL,
enabled BOOLEAN NOT NULL,
credentials_expired BOOLEAN NOT NULL
);
CREATE TABLE user_granted_authority (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id INT NOT NULL,
authority VARCHAR(255),
FOREIGN KEY (user_id) REFERENCES "user"
);
CREATE TABLE refresh_token (
token UUID PRIMARY KEY,
issued TIMESTAMPTZ(6) NOT NULL,
expiration TIMESTAMPTZ(6) NOT NULL,
owner_id INT NOT NULL,
deleted BOOLEAN NOT NULL,
revoked BOOLEAN NOT NULL,
FOREIGN KEY (owner_id) REFERENCES "user"
);
CREATE TABLE image (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created TIMESTAMPTZ(6) NOT NULL,
modified TIMESTAMPTZ(6),
owner_id INT NOT NULL,
is_public BOOLEAN NOT NULL,
object_name VARCHAR(255) NOT NULL,
user_filename VARCHAR(255) NOT NULL,
mime_type VARCHAR(255) NOT NULL,
alt VARCHAR(255),
caption VARCHAR(255),
height INTEGER,
width INTEGER,
FOREIGN KEY (owner_id) REFERENCES "user"
);
CREATE TABLE image_viewer (
image_id INT NOT NULL,
viewer_id INT NOT NULL,
PRIMARY KEY (image_id, viewer_id),
FOREIGN KEY (image_id) REFERENCES image,
FOREIGN KEY (viewer_id) REFERENCES "user"
);
CREATE TABLE recipe (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created TIMESTAMPTZ(6) NOT NULL,
modified TIMESTAMPTZ(6),
owner_id INT NOT NULL,
slug VARCHAR(255) NOT NULL,
is_public BOOLEAN NOT NULL,
title VARCHAR(255) NOT NULL,
raw_text TEXT NOT NULL,
cached_rendered_text TEXT,
main_image_id INT,
preparation_time INT,
cooking_time INT,
total_time INT,
FOREIGN KEY (owner_id) REFERENCES "user",
FOREIGN KEY (main_image_id) REFERENCES image
);
CREATE INDEX recipe_username_slug ON recipe (owner_id, slug);
CREATE TABLE recipe_viewer (
recipe_id INT NOT NULL,
viewer_id INT NOT NULL,
PRIMARY KEY (recipe_id, viewer_id),
FOREIGN KEY (recipe_id) REFERENCES recipe,
FOREIGN KEY (viewer_id) REFERENCES "user"
);
CREATE TABLE recipe_comment (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created TIMESTAMPTZ(6) NOT NULL,
modified TIMESTAMPTZ(6),
owner_id INT NOT NULL,
recipe_id INT NOT NULL,
raw_text TEXT NOT NULL,
cached_rendered_text TEXT,
FOREIGN KEY (owner_id) REFERENCES "user",
FOREIGN KEY (recipe_id) REFERENCES recipe
);
CREATE TABLE recipe_star (
timestamp TIMESTAMPTZ(6) NOT NULL,
recipe_id INT NOT NULL,
owner_id INT NOT NULL,
PRIMARY KEY (recipe_id, owner_id),
FOREIGN KEY (recipe_id) references recipe,
FOREIGN KEY (owner_id) references "user"
);

View File

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

View File

@ -4,7 +4,7 @@ import app.mealsmadeeasy.api.matchers.ContainsItemsMatcher;
import java.util.List; import java.util.List;
public final class ContainsImagesMatcher extends ContainsItemsMatcher<Image, Image, Long> { public final class ContainsImagesMatcher extends ContainsItemsMatcher<Image, Image, Integer> {
public static ContainsImagesMatcher containsImages(Image... expected) { public static ContainsImagesMatcher containsImages(Image... expected) {
return new ContainsImagesMatcher(expected); return new ContainsImagesMatcher(expected);

View File

@ -5,7 +5,7 @@ import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
import java.util.List; import java.util.List;
public class ContainsRecipeInfoViewsForRecipesMatcher extends ContainsItemsMatcher<RecipeInfoView, Recipe, Long> { public class ContainsRecipeInfoViewsForRecipesMatcher extends ContainsItemsMatcher<RecipeInfoView, Recipe, Integer> {
public static ContainsRecipeInfoViewsForRecipesMatcher containsRecipeInfoViewsForRecipes(Recipe... expected) { public static ContainsRecipeInfoViewsForRecipesMatcher containsRecipeInfoViewsForRecipes(Recipe... expected) {
return new ContainsRecipeInfoViewsForRecipesMatcher(List.of(expected)); return new ContainsRecipeInfoViewsForRecipesMatcher(List.of(expected));

View File

@ -21,7 +21,7 @@ public class ContainsRecipeStarsMatcher extends ContainsItemsMatcher<RecipeStar,
recipeStar -> ((RecipeStarEntity) recipeStar).getId(), recipeStar -> ((RecipeStarEntity) recipeStar).getId(),
recipeStar -> ((RecipeStarEntity) recipeStar).getId(), recipeStar -> ((RecipeStarEntity) recipeStar).getId(),
(id0, id1) -> Objects.equals(id0.getRecipeId(), id1.getRecipeId()) (id0, id1) -> Objects.equals(id0.getRecipeId(), id1.getRecipeId())
&& Objects.equals(id0.getOwnerUsername(), id1.getOwnerUsername()) && Objects.equals(id0.getOwnerId(), id1.getOwnerId())
); );
} }

View File

@ -4,7 +4,7 @@ import app.mealsmadeeasy.api.matchers.ContainsItemsMatcher;
import java.util.List; import java.util.List;
public final class ContainsRecipesMatcher extends ContainsItemsMatcher<Recipe, Recipe, Long> { public final class ContainsRecipesMatcher extends ContainsItemsMatcher<Recipe, Recipe, Integer> {
public static ContainsRecipesMatcher containsRecipes(Recipe... expected) { public static ContainsRecipesMatcher containsRecipes(Recipe... expected) {
return new ContainsRecipesMatcher(expected); return new ContainsRecipesMatcher(expected);

View File

@ -4,7 +4,7 @@ import app.mealsmadeeasy.api.matchers.ContainsItemsMatcher;
import java.util.List; import java.util.List;
public class ContainsUsersMatcher extends ContainsItemsMatcher<User, User, Long> { public class ContainsUsersMatcher extends ContainsItemsMatcher<User, User, Integer> {
public static ContainsUsersMatcher containsUsers(User... allExpected) { public static ContainsUsersMatcher containsUsers(User... allExpected) {
return new ContainsUsersMatcher(allExpected); return new ContainsUsersMatcher(allExpected);