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
@@ -0,0 +1,199 @@
package inu.codin.codin.domain.lecture.controller;

import inu.codin.codin.domain.lecture.dto.MetaMode;
import inu.codin.codin.domain.lecture.service.LectureUploadService;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
* Test stack: JUnit 5 (Jupiter) + Spring Boot Test (@WebMvcTest) + MockMvc + Mockito + spring-security-test.
* Focus: LectureUploadController endpoints and behavior added in the PR.
*/
@WebMvcTest(controllers = LectureUploadController.class)
@Import(LectureUploadControllerTest.MethodSecurityConfig.class)
class LectureUploadControllerTest {

@TestConfiguration
@EnableMethodSecurity(prePostEnabled = true)
static class MethodSecurityConfig {
// Uses default ROLE_ prefix; note controller uses hasAnyRole('ROLE_ADMIN','ROLE_MANAGER'),
// hence tests use roles="ROLE_ADMIN"/"ROLE_MANAGER" to match (Spring adds ROLE_ prefix).
}

@Autowired
private MockMvc mockMvc;

@MockBean
private LectureUploadService lectureUploadService;

@Test
@DisplayName("POST /upload as ROLE_ADMIN: returns 201 and calls service with uploaded file")
@WithMockUser(roles = "ROLE_ADMIN")
void uploadNewSemesterLectures_asAdmin_returns201_andCallsService() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"excelFile",
"24-1.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
new byte[]{1, 2, 3}
);

doNothing().when(lectureUploadService).uploadNewSemesterLectures(any());

mockMvc.perform(multipart("/upload").file(file).contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isCreated())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.code").value(201))
.andExpect(jsonPath("$.message", containsString("24-1.xlsx")))
.andExpect(jsonPath("$.message", containsString("강의 내역 업로드")));

ArgumentCaptor<org.springframework.web.multipart.MultipartFile> captor =
ArgumentCaptor.forClass(org.springframework.web.multipart.MultipartFile.class);
verify(lectureUploadService, times(1)).uploadNewSemesterLectures(captor.capture());
org.springframework.web.multipart.MultipartFile passed = captor.getValue();
// Verify the same filename reaches the service
org.junit.jupiter.api.Assertions.assertEquals("24-1.xlsx", passed.getOriginalFilename());
}

@Test
@DisplayName("POST /upload/rooms as ROLE_MANAGER: returns 201 and calls service with uploaded file")
@WithMockUser(roles = "ROLE_MANAGER")
void uploadNewSemesterRooms_asManager_returns201_andCallsService() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"excelFile",
"24-2.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
new byte[]{9, 8, 7}
);

doNothing().when(lectureUploadService).uploadNewSemesterRooms(any());

mockMvc.perform(multipart("/upload/rooms").file(file).contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isCreated())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.code").value(201))
.andExpect(jsonPath("$.message", containsString("24-2.xlsx")))
.andExpect(jsonPath("$.message", containsString("강의실 현황 업데이트")));

verify(lectureUploadService, times(1)).uploadNewSemesterRooms(any());
}

@Test
@DisplayName("POST /upload/meta default mode: ALL (no mode param) -> 201 and service called with MetaMode.ALL")
@WithMockUser(roles = "ROLE_ADMIN")
void uploadLectureMeta_defaultMode_callsServiceWithALL() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"excelFile",
"info_25_2_meta.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"dummy".getBytes()
);

doNothing().when(lectureUploadService).uploadLectureMeta(any(), any());

mockMvc.perform(multipart("/upload/meta").file(file).contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isCreated())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.code").value(201))
.andExpect(jsonPath("$.message", containsString("info_25_2_meta.xlsx")))
.andExpect(jsonPath("$.message", containsString("메타데이터")));

verify(lectureUploadService, times(1)).uploadLectureMeta(any(), eq(MetaMode.ALL));
}

@Test
@DisplayName("POST /upload/meta with mode=TAGS -> 201 and service called with MetaMode.TAGS")
@WithMockUser(roles = "ROLE_ADMIN")
void uploadLectureMeta_withTags_callsServiceWithTAGS() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"excelFile",
"info_25_2_meta.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
new byte[]{0x1}
);

doNothing().when(lectureUploadService).uploadLectureMeta(any(), any());

mockMvc.perform(
multipart("/upload/meta")
.file(file)
.param("mode", "TAGS")
.contentType(MediaType.MULTIPART_FORM_DATA)
)
.andExpect(status().isCreated())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.code").value(201))
.andExpect(jsonPath("$.message", containsString("메타데이터")));

verify(lectureUploadService, times(1)).uploadLectureMeta(any(), eq(MetaMode.TAGS));
}

@Test
@DisplayName("POST /upload without authentication -> 401 Unauthorized")
void uploadNewSemesterLectures_unauthenticated_returns401() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"excelFile", "24-1.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", new byte[]{1}
);

mockMvc.perform(multipart("/upload").file(file).contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isUnauthorized());
}

@Test
@DisplayName("POST /upload as ROLE_USER (insufficient) -> 403 Forbidden")
@WithMockUser(roles = "USER")
void uploadNewSemesterLectures_insufficientRole_returns403() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"excelFile", "24-1.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", new byte[]{1}
);

mockMvc.perform(multipart("/upload").file(file).contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isForbidden());
}

@Test
@DisplayName("POST /upload/rooms missing file parameter -> 400 Bad Request and service not called")
@WithMockUser(roles = "ROLE_ADMIN")
void uploadNewSemesterRooms_missingFile_returns400() throws Exception {
mockMvc.perform(multipart("/upload/rooms").contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isBadRequest());

verify(lectureUploadService, never()).uploadNewSemesterRooms(any());
}

@Test
@DisplayName("POST /upload/meta with invalid mode value -> 400 Bad Request and service not called")
@WithMockUser(roles = "ROLE_ADMIN")
void uploadLectureMeta_invalidMode_returns400() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"excelFile", "info_25_2_meta.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", new byte[]{2,3}
);

mockMvc.perform(
multipart("/upload/meta")
.file(file)
.param("mode", "INVALID")
.contentType(MediaType.MULTIPART_FORM_DATA)
)
.andExpect(status().isBadRequest());

verify(lectureUploadService, never()).uploadLectureMeta(any(), any());
}
}
143 changes: 143 additions & 0 deletions src/test/java/inu/codin/codin/domain/lecture/dto/MetaModeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package inu.codin.codin.domain.lecture.dto;

import static org.junit.jupiter.api.Assertions.*;

import java.util.Arrays;
import java.util.EnumSet;
import java.util.Set;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;

/**
* Test framework: JUnit Jupiter (JUnit 5) provided by spring-boot-starter-test.
* Scope: Thorough unit tests for MetaMode enum focusing on public behavior:
* - values() ordering and size
* - valueOf(String) happy paths and failure conditions
* - toString(), name(), ordinal stability
* - EnumSet interoperability and membership checks
*/
class MetaModeTest {

@Test
@DisplayName("values() should include all defined constants in declared order")
void values_shouldContainAllConstantsInOrder() {
MetaMode[] values = MetaMode.values();
assertAll(
() -> assertEquals(4, values.length, "Unexpected enum constant count"),
() -> assertEquals(MetaMode.ALL, values[0]),
() -> assertEquals(MetaMode.KEYWORDS, values[1]),
() -> assertEquals(MetaMode.TAGS, values[2]),
() -> assertEquals(MetaMode.PRE_COURSES, values[3])
);
}

@Test
@DisplayName("EnumSet.allOf should return exactly the 4 MetaMode values")
void enumSet_allOf_hasAllFour() {
Set<MetaMode> set = EnumSet.allOf(MetaMode.class);
assertEquals(EnumSet.of(MetaMode.ALL, MetaMode.KEYWORDS, MetaMode.TAGS, MetaMode.PRE_COURSES), set);
}

@ParameterizedTest(name = "valueOf should resolve \"{0}\"")
@EnumSource(MetaMode.class)
void valueOf_shouldResolveEveryConstant(MetaMode mode) {
MetaMode resolved = MetaMode.valueOf(mode.name());
assertSame(mode, resolved);
}

@ParameterizedTest(name = "toString should equal name for {0}")
@EnumSource(MetaMode.class)
void toString_shouldEqualName(MetaMode mode) {
assertEquals(mode.name(), mode.toString());
}

@Nested
@DisplayName("Failure conditions for valueOf(String)")
class ValueOfFailures {

@Test
@DisplayName("Invalid names should throw IllegalArgumentException")
void invalidNames_throwIllegalArgumentException() {
for (String bad : Arrays.asList(
"", " ", "ALL ", " ALL", "all", "All",
"KEYWORD", "TAGS_", "PRE-COURSES", "PRE_COURSE", "COURSES"
)) {
String msg = "Expected IllegalArgumentException for input: '" + bad + "'";
assertThrows(IllegalArgumentException.class, () -> MetaMode.valueOf(bad), msg);
}
}

@ParameterizedTest
@NullSource
@DisplayName("Passing null should throw NullPointerException")
void nullName_throwsNullPointerException(String input) {
assertThrows(NullPointerException.class, () -> MetaMode.valueOf(input));
}
}

@Test
@DisplayName("Ordinals remain stable with the declared order")
void ordinals_shouldMatchDeclaredOrder() {
assertAll(
() -> assertEquals(0, MetaMode.ALL.ordinal()),
() -> assertEquals(1, MetaMode.KEYWORDS.ordinal()),
() -> assertEquals(2, MetaMode.TAGS.ordinal()),
() -> assertEquals(3, MetaMode.PRE_COURSES.ordinal())
);
}

@ParameterizedTest(name = "name() should be uppercase for {0}")
@EnumSource(MetaMode.class)
void name_isUppercase(MetaMode mode) {
assertEquals(mode.name().toUpperCase(), mode.name());
}

@ParameterizedTest(name = "Enum membership should include {0}")
@EnumSource(MetaMode.class)
void membership_containsEachConstant(MetaMode mode) {
assertTrue(EnumSet.allOf(MetaMode.class).contains(mode));
}

@ParameterizedTest(name = "valueOf should be case-sensitive; lower-case of {0} must fail")
@EnumSource(MetaMode.class)
void valueOf_isCaseSensitive(MetaMode mode) {
String lower = mode.name().toLowerCase();
assertThrows(IllegalArgumentException.class, () -> MetaMode.valueOf(lower));
}

@ParameterizedTest(name = "valueOf should not accept extra whitespace around {0}")
@EnumSource(MetaMode.class)
void valueOf_doesNotAllowWhitespace(MetaMode mode) {
for (String s : Arrays.asList(" " + mode.name(), mode.name() + " ", " " + mode.name() + " ")) {
assertThrows(IllegalArgumentException.class, () -> MetaMode.valueOf(s));
}
}

@Test
@DisplayName("Duplicate constants do not exist (EnumSet.copyOf size equals array length)")
void noDuplicates() {
MetaMode[] values = MetaMode.values();
assertEquals(values.length, EnumSet.copyOf(Arrays.asList(values)).size());
}

@ParameterizedTest
@ValueSource(strings = {"ALL", "KEYWORDS", "TAGS", "PRE_COURSES"})
@DisplayName("Basic sanity: valueOf returns a MetaMode for all expected literals")
void sanity_valueOfRecognizesLiterals(String literal) {
assertNotNull(MetaMode.valueOf(literal));
}

@Test
@Tag("regression")
@DisplayName("Regression: contents of values() remain unchanged (snapshot)")
void regression_valuesSnapshot() {
assertEquals("[ALL, KEYWORDS, TAGS, PRE_COURSES]", Arrays.toString(MetaMode.values()));
}
}
Loading