Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private Meme getMemeBy(Long memeId) {

private GeneratedImagesResponse editMemeImgWithUserRequestImg(String prompt, String existingMemeImgUrl, MultipartFile file) {
try {
Base64Image existingImage = convertUrlToBase64Image(existingMemeImgUrl);
Base64Image existingImage = imageGenerator.convertUrlToBase64Image(existingMemeImgUrl);
Base64Image userRequestImg = convertMultipartFileToBase64Image(file);
List<Base64Image> requestImages = List.of(existingImage, userRequestImg);

Expand All @@ -78,18 +78,6 @@ private GeneratedImagesResponse editMemeImgWithUserRequestImg(String prompt, Str
}
}

private Base64Image convertUrlToBase64Image(String imageUrl) {
try {
byte[] imageBytes = ImageUtils.downloadBytes(imageUrl);
String mimeType = ImageUtils.detectMimeType(imageUrl, imageBytes);
String base64Data = Base64.getEncoder().encodeToString(imageBytes);
return new Base64Image(mimeType, base64Data);
} catch (IOException e) {
log.error("Failed to convert URL to Base64 image: {}", imageUrl, e);
throw new MemeWikiApplicationException(ErrorType.DEFAULT_ERROR);
}
}

private Base64Image convertMultipartFileToBase64Image(MultipartFile file) throws IOException {
byte[] bytes = file.getBytes();
String mimeType = file.getContentType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,23 @@ public GenerateContentResponse generateImage(String naturalLanguage) {

public GenerateContentResponse generateImageWithExistingImage(String naturalLanguage, String imageUrl) {
try {
byte[] imageBytes = ImageUtils.downloadBytes(imageUrl);
String mimeType = ImageUtils.detectMimeType(imageUrl, imageBytes);
Base64Image base64Image = new Base64Image(mimeType, getEncoder().encodeToString(imageBytes));

Base64Image base64Image = convertUrlToBase64Image(imageUrl);
return generateImageCombine(naturalLanguage, List.of(base64Image));
} catch (Exception e) {
throw new RuntimeException("Failed to generate image with existing image", e);
}
}

public Base64Image convertUrlToBase64Image(String imageUrl) {
try {
byte[] imageBytes = ImageUtils.downloadBytes(imageUrl);
String mimeType = ImageUtils.detectMimeType(imageUrl, imageBytes);
return new Base64Image(mimeType, getEncoder().encodeToString(imageBytes));
} catch (Exception e) {
throw new RuntimeException("Failed to convert URL to Base64 image: " + imageUrl, e);
}
}

public GenerateContentResponse generateImageCombine(String naturalLanguage, List<Base64Image> images) {
List<GenerateContentRequest.Part> parts = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package spring.memewikibe.api.controller.image;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import spring.memewikibe.api.controller.image.response.Base64Image;
import spring.memewikibe.api.controller.image.response.GeneratedImagesResponse;
import spring.memewikibe.application.ImageEditService;
import spring.memewikibe.support.error.ErrorType;
import spring.memewikibe.support.error.MemeWikiApplicationException;

import java.util.List;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(ImageController.class)
@AutoConfigureMockMvc(addFilters = false)
class ImageControllerTest {

@Autowired
private MockMvc mockMvc;

@MockitoBean
private ImageEditService imageEditService;

@Test
@DisplayName("POST /api/images/edit/meme/{memeId}: 프롬프트만으로 밈 이미지 편집 성공")
void editWithMemeMultipart_withPromptOnly_success() throws Exception {
// given
Long memeId = 1L;
String prompt = "Add funny text to the meme";

GeneratedImagesResponse mockResponse = new GeneratedImagesResponse(
List.of(new Base64Image("image/png", "base64encodeddata")),
List.of("Generated successfully")
);

when(imageEditService.editMemeImg(eq(prompt), eq(memeId), isNull()))
.thenReturn(mockResponse);

MockMultipartFile promptPart = new MockMultipartFile(
"prompt",
"",
MediaType.TEXT_PLAIN_VALUE,
prompt.getBytes()
);

// when & then
mockMvc.perform(multipart("/api/images/edit/meme/{memeId}", memeId)
.file(promptPart))
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultType").value("SUCCESS"))
.andExpect(jsonPath("$.success.images").isArray())
.andExpect(jsonPath("$.success.images[0].mimeType").value("image/png"))
.andExpect(jsonPath("$.success.images[0].data").value("base64encodeddata"))
.andExpect(jsonPath("$.success.text").isArray())
.andExpect(jsonPath("$.success.text[0]").value("Generated successfully"));

verify(imageEditService).editMemeImg(prompt, memeId, null);
}

@Test
@DisplayName("POST /api/images/edit/meme/{memeId}: 프롬프트와 이미지로 밈 이미지 편집 성공")
void editWithMemeMultipart_withPromptAndImage_success() throws Exception {
// given
Long memeId = 1L;
String prompt = "Combine these images";

GeneratedImagesResponse mockResponse = new GeneratedImagesResponse(
List.of(new Base64Image("image/png", "combinedImageData")),
List.of("Images combined")
);

when(imageEditService.editMemeImg(eq(prompt), eq(memeId), any()))
.thenReturn(mockResponse);

MockMultipartFile promptPart = new MockMultipartFile(
"prompt",
"",
MediaType.TEXT_PLAIN_VALUE,
prompt.getBytes()
);

MockMultipartFile imagePart = new MockMultipartFile(
"image",
"test.jpg",
MediaType.IMAGE_JPEG_VALUE,
new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF}
);

// when & then
mockMvc.perform(multipart("/api/images/edit/meme/{memeId}", memeId)
.file(promptPart)
.file(imagePart)
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultType").value("SUCCESS"))
.andExpect(jsonPath("$.success.images").isArray())
.andExpect(jsonPath("$.success.images[0].mimeType").value("image/png"))
.andExpect(jsonPath("$.success.images[0].data").value("combinedImageData"))
.andExpect(jsonPath("$.success.text").isArray())
.andExpect(jsonPath("$.success.text[0]").value("Images combined"));

verify(imageEditService).editMemeImg(eq(prompt), eq(memeId), any());
}

@Test
@DisplayName("POST /api/images/edit/meme/{memeId}: 존재하지 않는 밈 ID로 요청 시 404 에러")
void editWithMemeMultipart_withNonExistentMemeId_returns404() throws Exception {
// given
Long nonExistentMemeId = 99999L;
String prompt = "Edit this meme";

when(imageEditService.editMemeImg(eq(prompt), eq(nonExistentMemeId), isNull()))
.thenThrow(new MemeWikiApplicationException(ErrorType.MEME_NOT_FOUND));

MockMultipartFile promptPart = new MockMultipartFile(
"prompt",
"",
MediaType.TEXT_PLAIN_VALUE,
prompt.getBytes()
);

// when & then
mockMvc.perform(multipart("/api/images/edit/meme/{memeId}", nonExistentMemeId)
.file(promptPart)
)
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.resultType").value("ERROR"))
.andExpect(jsonPath("$.error.code").value("E404"));

verify(imageEditService).editMemeImg(prompt, nonExistentMemeId, null);
}

@Test
@DisplayName("POST /api/images/edit/meme/{memeId}: 빈 응답 처리")
void editWithMemeMultipart_withEmptyResponse_success() throws Exception {
// given
Long memeId = 1L;
String prompt = "Generate image";

GeneratedImagesResponse emptyResponse = new GeneratedImagesResponse(
List.of(),
List.of()
);

when(imageEditService.editMemeImg(eq(prompt), eq(memeId), isNull()))
.thenReturn(emptyResponse);

MockMultipartFile promptPart = new MockMultipartFile(
"prompt",
"",
MediaType.TEXT_PLAIN_VALUE,
prompt.getBytes()
);

// when & then
mockMvc.perform(multipart("/api/images/edit/meme/{memeId}", memeId)
.file(promptPart)
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultType").value("SUCCESS"))
.andExpect(jsonPath("$.success.images").isEmpty())
.andExpect(jsonPath("$.success.text").isEmpty());

verify(imageEditService).editMemeImg(prompt, memeId, null);
}

@Test
@DisplayName("POST /api/images/edit/meme/{memeId}: 여러 이미지와 텍스트 응답 처리")
void editWithMemeMultipart_withMultipleImagesAndTexts_success() throws Exception {
// given
Long memeId = 1L;
String prompt = "Generate multiple variations";

GeneratedImagesResponse multiResponse = new GeneratedImagesResponse(
List.of(
new Base64Image("image/png", "image1data"),
new Base64Image("image/jpeg", "image2data"),
new Base64Image("image/webp", "image3data")
),
List.of("Variation 1", "Variation 2", "Variation 3")
);

when(imageEditService.editMemeImg(eq(prompt), eq(memeId), isNull()))
.thenReturn(multiResponse);

MockMultipartFile promptPart = new MockMultipartFile(
"prompt",
"",
MediaType.TEXT_PLAIN_VALUE,
prompt.getBytes()
);

// when & then
mockMvc.perform(multipart("/api/images/edit/meme/{memeId}", memeId)
.file(promptPart)
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultType").value("SUCCESS"))
.andExpect(jsonPath("$.success.images").isArray())
.andExpect(jsonPath("$.success.images.length()").value(3))
.andExpect(jsonPath("$.success.images[0].mimeType").value("image/png"))
.andExpect(jsonPath("$.success.images[1].mimeType").value("image/jpeg"))
.andExpect(jsonPath("$.success.images[2].mimeType").value("image/webp"))
.andExpect(jsonPath("$.success.text.length()").value(3))
.andExpect(jsonPath("$.success.text[0]").value("Variation 1"))
.andExpect(jsonPath("$.success.text[1]").value("Variation 2"))
.andExpect(jsonPath("$.success.text[2]").value("Variation 3"));

verify(imageEditService).editMemeImg(prompt, memeId, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.web.multipart.MultipartFile;
import spring.memewikibe.annotation.IntegrationTest;
import spring.memewikibe.api.controller.image.response.Base64Image;
import spring.memewikibe.api.controller.image.response.GeneratedImagesResponse;
import spring.memewikibe.common.util.ImageUtils;
import spring.memewikibe.domain.meme.Meme;
import spring.memewikibe.external.google.application.ImageGenerator;
import spring.memewikibe.external.google.client.response.GenerateContentResponse;
Expand Down Expand Up @@ -88,27 +86,26 @@ void editMemeImg_withUserImage_success() {
new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x00, 0x00}
);

Base64Image mockExistingImage = new Base64Image("image/jpeg", "existingImageBase64");
when(imageGenerator.convertUrlToBase64Image(anyString()))
.thenReturn(mockExistingImage);

GenerateContentResponse mockResponse = createMockResponse(
List.of(new Base64Image("image/png", "combinedBase64data")),
List.of("Combined image")
);
when(imageGenerator.generateImageCombine(anyString(), anyList()))
.thenReturn(mockResponse);

// when & then
try (MockedStatic<ImageUtils> mockedImageUtils = mockStatic(ImageUtils.class)) {
mockedImageUtils.when(() -> ImageUtils.downloadBytes(anyString()))
.thenReturn(new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF});
mockedImageUtils.when(() -> ImageUtils.detectMimeType(anyString(), any()))
.thenReturn("image/jpeg");

GeneratedImagesResponse response = imageEditService.editMemeImg("Combine images", meme.getId(), userImage);

assertThat(response).isNotNull();
assertThat(response.images()).hasSize(1);
assertThat(response.text()).hasSize(1);
verify(imageGenerator).generateImageCombine(anyString(), anyList());
}
// when
GeneratedImagesResponse response = imageEditService.editMemeImg("Combine images", meme.getId(), userImage);

// then
assertThat(response).isNotNull();
assertThat(response.images()).hasSize(1);
assertThat(response.text()).hasSize(1);
verify(imageGenerator).convertUrlToBase64Image(meme.getImgUrl());
verify(imageGenerator).generateImageCombine(anyString(), anyList());
}

@Test
Expand Down