Compare commits

..

No commits in common. "dce0db03850b4bf3ff2c5c4b731c8296a0173877" and "52136a34bd1b5c385ba4f297fb7dd97c14f965f4" have entirely different histories.

52 changed files with 717 additions and 828 deletions

View File

@ -49,17 +49,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-jackson'
implementation 'org.springframework.boot:spring-boot-starter-flyway'
runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testImplementation 'org.springframework.boot:spring-boot-starter-jackson-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// flyway
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'
// Custom
implementation 'io.jsonwebtoken:jjwt-api:0.13.0'
implementation 'io.jsonwebtoken:jjwt-jackson:0.13.0'
@ -83,10 +78,10 @@ dependencies {
runtimeOnly 'org.apache.xmlgraphics:batik-all:1.19'
// Custom testing
testRuntimeOnly 'com.h2database:h2'
testImplementation 'org.testcontainers:testcontainers:1.21.4'
testImplementation 'org.testcontainers:junit-jupiter:1.21.4'
testImplementation 'org.testcontainers:postgresql:1.21.4'
testImplementation 'org.testcontainers:minio:1.21.4'
testImplementation "org.testcontainers:minio:1.21.4"
testFixturesImplementation 'org.hamcrest:hamcrest:3.0'
}

View File

@ -1,19 +0,0 @@
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,14 +1,12 @@
package app.mealsmadeeasy.api;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@ExtendWith(IntegrationTestsExtension.class)
public class MealsMadeEasyApiApplicationTests {
class MealsMadeEasyApiApplicationTests {
@Test
public void contextLoads() {}
void contextLoads() {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,7 @@
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa
app.mealsmadeeasy.api.baseUrl=http://localhost:8080
app.mealsmadeeasy.api.security.access-token-lifetime=60
app.mealsmadeeasy.api.security.refresh-token-lifetime=120
@ -5,8 +9,3 @@ app.mealsmadeeasy.api.minio.endpoint=http://localhost:9000
app.mealsmadeeasy.api.minio.accessKey=minio-root
app.mealsmadeeasy.api.minio.secretKey=test0123
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,5 +1,6 @@
package app.mealsmadeeasy.api.auth;
import app.mealsmadeeasy.api.security.AuthToken;
import org.jetbrains.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@ -7,8 +8,6 @@ import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@RequestMapping("/auth")
public final class AuthController {
@ -32,9 +31,9 @@ public final class AuthController {
}
private ResponseEntity<LoginView> getLoginViewResponseEntity(LoginDetails loginDetails) {
final RefreshToken refreshToken = loginDetails.getRefreshToken();
final AuthToken refreshToken = loginDetails.getRefreshToken();
final ResponseCookie refreshCookie = getRefreshTokenCookie(
refreshToken.getToken().toString(),
refreshToken.getToken(),
refreshToken.getLifetime()
);
final var loginView = new LoginView(
@ -61,7 +60,7 @@ public final class AuthController {
@PostMapping("/refresh")
public ResponseEntity<LoginView> refresh(
@CookieValue(value = "refresh-token", required = false) @Nullable UUID oldRefreshToken
@CookieValue(value = "refresh-token", required = false) @Nullable String oldRefreshToken
) throws LoginException {
final LoginDetails loginDetails = this.authService.refresh(oldRefreshToken);
return this.getLoginViewResponseEntity(loginDetails);
@ -69,7 +68,7 @@ public final class AuthController {
@PostMapping("/logout")
public ResponseEntity<?> logout(
@CookieValue(value = "refresh-token", required = false) @Nullable UUID refreshToken
@CookieValue(value = "refresh-token", required = false) @Nullable String refreshToken
) {
if (refreshToken != null) {
this.authService.logout(refreshToken);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -225,7 +225,7 @@ public class S3ImageService implements ImageService {
}
}
if (didUpdate) {
entity.setModified(OffsetDateTime.now());
entity.setModified(LocalDateTime.now());
}
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 org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.stream.Collectors;
@ -33,8 +33,8 @@ public class ImageView {
}
private String url;
private OffsetDateTime created;
private @Nullable OffsetDateTime modified;
private LocalDateTime created;
private @Nullable LocalDateTime modified;
private String filename;
private String mimeType;
private @Nullable String alt;
@ -53,19 +53,19 @@ public class ImageView {
this.url = url;
}
public OffsetDateTime getCreated() {
public LocalDateTime getCreated() {
return this.created;
}
public void setCreated(OffsetDateTime created) {
public void setCreated(LocalDateTime created) {
this.created = created;
}
public @Nullable OffsetDateTime getModified() {
public @Nullable LocalDateTime getModified() {
return this.modified;
}
public void setModified(@Nullable OffsetDateTime modified) {
public void setModified(@Nullable LocalDateTime modified) {
this.modified = modified;
}

View File

@ -1,21 +0,0 @@
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 org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
import java.util.Set;
public interface Recipe {
Integer getId();
OffsetDateTime getCreated();
@Nullable OffsetDateTime getModified();
Long getId();
LocalDateTime getCreated();
@Nullable LocalDateTime getModified();
String getSlug();
String getTitle();
@Nullable Integer getPreparationTime();

View File

@ -10,7 +10,7 @@ import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*;
import org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
@ -18,14 +18,14 @@ import java.util.Set;
public final class RecipeEntity implements Recipe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable = false, updatable = false)
private Integer id;
private Long id;
@Column(nullable = false)
private OffsetDateTime created;
private LocalDateTime created;
private OffsetDateTime modified;
private LocalDateTime modified;
@Column(nullable = false, unique = true)
private String slug;
@ -57,7 +57,7 @@ public final class RecipeEntity implements Recipe {
private UserEntity owner;
@OneToMany
@JoinColumn(name = "recipe_id")
@JoinColumn(name = "recipeId")
private Set<RecipeStarEntity> stars = new HashSet<>();
@OneToMany(mappedBy = "recipe")
@ -67,41 +67,35 @@ public final class RecipeEntity implements Recipe {
private Boolean isPublic = false;
@ManyToMany
@JoinTable(
name = "recipe_viewer",
joinColumns = @JoinColumn(name = "recipe_id"),
inverseJoinColumns = @JoinColumn(name = "viewer_id")
)
private Set<UserEntity> viewers = new HashSet<>();
@ManyToOne
@JoinColumn(name = "main_image_id")
private S3ImageEntity mainImage;
@Override
public Integer getId() {
public Long getId() {
return this.id;
}
public void setId(Integer id) {
public void setId(Long id) {
this.id = id;
}
@Override
public OffsetDateTime getCreated() {
public LocalDateTime getCreated() {
return this.created;
}
public void setCreated(OffsetDateTime created) {
public void setCreated(LocalDateTime created) {
this.created = created;
}
@Override
public @Nullable OffsetDateTime getModified() {
public @Nullable LocalDateTime getModified() {
return this.modified;
}
public void setModified(@Nullable OffsetDateTime modified) {
public void setModified(@Nullable LocalDateTime 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.stereotype.Service;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -53,7 +53,7 @@ public class RecipeServiceImpl implements RecipeService {
throw new AccessDeniedException("Must be logged in.");
}
final RecipeEntity draft = new RecipeEntity();
draft.setCreated(OffsetDateTime.now());
draft.setCreated(LocalDateTime.now());
draft.setOwner((UserEntity) owner);
draft.setSlug(spec.getSlug());
draft.setTitle(spec.getTitle());
@ -222,7 +222,7 @@ public class RecipeServiceImpl implements RecipeService {
}
recipe.setMainImage(mainImage);
recipe.setModified(OffsetDateTime.now());
recipe.setModified(LocalDateTime.now());
return this.recipeRepository.save(recipe);
}
@ -279,7 +279,7 @@ public class RecipeServiceImpl implements RecipeService {
if (viewer == null) {
return null;
}
return this.recipeStarRepository.isStarer(username, slug, viewer.getId());
return this.recipeStarRepository.isStarer(username, slug, viewer.getUsername());
}
@Override

View File

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

View File

@ -4,21 +4,20 @@ import app.mealsmadeeasy.api.recipe.RecipeEntity;
import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
@Entity(name = "RecipeComment")
@Table(name = "recipe_comment")
public final class RecipeCommentEntity implements RecipeComment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable = false)
private Integer id;
private Long id;
@Column(nullable = false, updatable = false)
private OffsetDateTime created = OffsetDateTime.now();
private LocalDateTime created = LocalDateTime.now();
private OffsetDateTime modified;
private LocalDateTime modified;
@Lob
@Basic(fetch = FetchType.LAZY)
@ -36,29 +35,29 @@ public final class RecipeCommentEntity implements RecipeComment {
@JoinColumn(name = "recipe_id", nullable = false, updatable = false)
private RecipeEntity recipe;
public Integer getId() {
public Long getId() {
return this.id;
}
public void setId(Integer id) {
public void setId(Long id) {
this.id = id;
}
@Override
public OffsetDateTime getCreated() {
public LocalDateTime getCreated() {
return this.created;
}
public void setCreated(OffsetDateTime created) {
public void setCreated(LocalDateTime created) {
this.created = created;
}
@Override
public OffsetDateTime getModified() {
public LocalDateTime getModified() {
return this.modified;
}
public void setModified(OffsetDateTime modified) {
public void setModified(LocalDateTime 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.stereotype.Service;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
import static java.util.Objects.requireNonNull;
@ -43,7 +43,7 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
) throws RecipeException {
requireNonNull(commenter);
final RecipeCommentEntity draft = new RecipeCommentEntity();
draft.setCreated(OffsetDateTime.now());
draft.setCreated(LocalDateTime.now());
draft.setRawText(body.getText());
draft.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(body.getText()));
draft.setOwner((UserEntity) commenter);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
spring.application.name=meals-made-easy-api
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.ddl-auto=create-drop
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.password=${POSTGRES_PASSWORD}

View File

@ -1,108 +0,0 @@
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

@ -1 +0,0 @@
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;
public final class ContainsImagesMatcher extends ContainsItemsMatcher<Image, Image, Integer> {
public final class ContainsImagesMatcher extends ContainsItemsMatcher<Image, Image, Long> {
public static ContainsImagesMatcher containsImages(Image... expected) {
return new ContainsImagesMatcher(expected);

View File

@ -5,7 +5,7 @@ import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
import java.util.List;
public class ContainsRecipeInfoViewsForRecipesMatcher extends ContainsItemsMatcher<RecipeInfoView, Recipe, Integer> {
public class ContainsRecipeInfoViewsForRecipesMatcher extends ContainsItemsMatcher<RecipeInfoView, Recipe, Long> {
public static ContainsRecipeInfoViewsForRecipesMatcher containsRecipeInfoViewsForRecipes(Recipe... 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(),
(id0, id1) -> Objects.equals(id0.getRecipeId(), id1.getRecipeId())
&& Objects.equals(id0.getOwnerId(), id1.getOwnerId())
&& Objects.equals(id0.getOwnerUsername(), id1.getOwnerUsername())
);
}

View File

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

View File

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