package com.vaessl.app.sync; import static com.vaessl.app.shared.Endpoint.HOMEBOX_LOGIN; import static com.vaessl.app.shared.Endpoint.HOMEBOX_QUERY_ALL_ITEMS; import static com.vaessl.app.shared.Endpoint.LOGIN; import static com.vaessl.app.shared.Endpoint.SYNC; import static com.vaessl.app.Mockdata.MOCK_USER; import static com.vaessl.app.Mockdata.MOCK_SERVICE_TYPE; import static com.vaessl.app.Mockdata.MOCK_PASS; import static com.vaessl.app.Mockdata.VALID_HOMEBOX_ALL_ITEMS_QUERY_RESPONSE; import static com.vaessl.app.Mockdata.VALID_HOMEBOX_LOGIN_RESPONSE; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.jupiter.api.Test; 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 com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import jakarta.servlet.http.Cookie; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; /** * Integration tests for {@code POST /api/sync}, verifying the session-gated contract against a * mocked Homebox backend (via WireMock) and a real Spring MVC dispatch chain (via {@link MockMvc}). */ @SpringBootTest @AutoConfigureMockMvc @WireMockTest class SyncControllerTest { @Autowired MockMvc mockMvc; private static final String SYNC_PATH = SYNC.getValue(); /** * Logs in against a stubbed Homebox instance, then performs a sync using the resulting session * cookie. Expects a {@code 204 No Content} response once the mocked item catalog has been paged * through and embedded. * * @param wm WireMock runtime info, injected by {@link WireMockTest}, used to point the client * at the stub server */ @Test void shouldReturn204NoContentOnSuccessfulSync(WireMockRuntimeInfo wm) throws Exception { WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue()) .willReturn(WireMock.okJson(VALID_HOMEBOX_LOGIN_RESPONSE))); WireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo(HOMEBOX_QUERY_ALL_ITEMS.getValue())) .willReturn(WireMock.okJson(VALID_HOMEBOX_ALL_ITEMS_QUERY_RESPONSE))); MvcResult loginResult = mockMvc.perform(post(LOGIN.getValue()).contentType(MediaType.APPLICATION_JSON) .content(connectionRequestBody(wm))).andExpect(status().isOk()).andReturn(); Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION"); mockMvc.perform(post(SYNC_PATH).cookie(sessionCookie) .contentType(MediaType.APPLICATION_JSON).content(syncRequestBody(wm))) .andExpect(status().isNoContent()); } /** * Calls {@code /api/sync} with no session cookie attached and expects {@code 401 Unauthorized}, * confirming the endpoint is session-gated. * * @param wm WireMock runtime info, injected by {@link WireMockTest}, used only to build a * well-formed request body */ @Test void shouldReturn401UnauthorizedWhenSessionIsNull(WireMockRuntimeInfo wm) throws Exception { mockMvc.perform(post(SYNC_PATH).contentType(MediaType.APPLICATION_JSON) .content(syncRequestBody(wm))).andExpect(status().isUnauthorized()); } private String connectionRequestBody(WireMockRuntimeInfo wm) { return """ { "appUrl": "%s", "serviceType": "%s", "username": "%s", "password": "%s" } """.formatted(wm.getHttpBaseUrl(), MOCK_SERVICE_TYPE, MOCK_USER, MOCK_PASS); } private String syncRequestBody(WireMockRuntimeInfo wm) { return """ { "appUrl": "%s", "serviceType": "%s", "username": "%s" } """.formatted(wm.getHttpBaseUrl(), MOCK_SERVICE_TYPE, MOCK_USER); } }