test(integration): add e2e flows for flutter app and java backend features
This commit is contained in:
+143
@@ -0,0 +1,143 @@
|
||||
package com.walkguide.integration;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.walkguide.dto.request.LoginRequest;
|
||||
import com.walkguide.dto.request.RegisterRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Base class untuk semua Integration Test menggunakan Testcontainers.
|
||||
* Spin up PostgreSQL nyata dalam Docker container, jalankan Flyway migration,
|
||||
* lalu test real HTTP request via MockMvc terhadap Spring context penuh.
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@AutoConfigureMockMvc
|
||||
@Testcontainers
|
||||
@ActiveProfiles("test")
|
||||
public abstract class AbstractIntegrationTest {
|
||||
|
||||
// =========================================================================
|
||||
// SHARED POSTGRESQL TESTCONTAINER — satu instance untuk semua test class
|
||||
// =========================================================================
|
||||
|
||||
@Container
|
||||
static final PostgreSQLContainer<?> postgres =
|
||||
new PostgreSQLContainer<>("postgres:15-alpine")
|
||||
.withDatabaseName("walkguide_test")
|
||||
.withUsername("testuser")
|
||||
.withPassword("testpass");
|
||||
|
||||
/**
|
||||
* Override spring datasource properties menggunakan URL dari Testcontainer.
|
||||
* Menggantikan koneksi ke server kampus saat test berjalan.
|
||||
*/
|
||||
@DynamicPropertySource
|
||||
static void configureProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
||||
registry.add("spring.datasource.username", postgres::getUsername);
|
||||
registry.add("spring.datasource.password", postgres::getPassword);
|
||||
registry.add("spring.flyway.enabled", () -> "true");
|
||||
registry.add("spring.jpa.hibernate.ddl-auto", () -> "validate");
|
||||
registry.add("firebase.enabled", () -> "false");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AUTOWIRED FIELDS
|
||||
// =========================================================================
|
||||
|
||||
@Autowired
|
||||
protected MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
protected ObjectMapper objectMapper;
|
||||
|
||||
// =========================================================================
|
||||
// HELPER METHODS
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Register user baru dan return access token.
|
||||
* DTO RegisterRequest pakai @Data (Lombok) — setter tersedia saat runtime Maven,
|
||||
* meskipun NetBeans IDE menampilkan false-positive error karena Lombok-NetBeans incompatibility.
|
||||
*/
|
||||
protected String registerAndGetToken(String email, String password, String role)
|
||||
throws Exception {
|
||||
RegisterRequest req = new RegisterRequest();
|
||||
req.setEmail(email);
|
||||
req.setPassword(password);
|
||||
req.setDisplayName("Test " + role);
|
||||
req.setRole(role);
|
||||
|
||||
MvcResult result = mockMvc.perform(
|
||||
post("/api/v1/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
JsonNode json = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||
return json.path("data").path("accessToken").asText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Login dengan credentials dan return access token.
|
||||
*/
|
||||
protected String loginAndGetToken(String email, String password) throws Exception {
|
||||
LoginRequest req = new LoginRequest();
|
||||
req.setEmail(email);
|
||||
req.setPassword(password);
|
||||
|
||||
MvcResult result = mockMvc.perform(
|
||||
post("/api/v1/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
JsonNode json = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||
return json.path("data").path("accessToken").asText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register user dan return seluruh AuthDataResponse sebagai JsonNode.
|
||||
* Berguna ketika test butuh uniqueUserId, userId, role, dll.
|
||||
*/
|
||||
protected JsonNode registerAndGetFullResponse(String email, String password, String role)
|
||||
throws Exception {
|
||||
RegisterRequest req = new RegisterRequest();
|
||||
req.setEmail(email);
|
||||
req.setPassword(password);
|
||||
req.setDisplayName("Test " + role);
|
||||
req.setRole(role);
|
||||
|
||||
MvcResult result = mockMvc.perform(
|
||||
post("/api/v1/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
JsonNode json = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||
return json.path("data");
|
||||
}
|
||||
|
||||
/** Buat Authorization header dari token. */
|
||||
protected String bearerToken(String token) {
|
||||
return "Bearer " + token;
|
||||
}
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
package com.walkguide.integration;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.walkguide.dto.request.LoginRequest;
|
||||
import com.walkguide.dto.request.RefreshTokenRequest;
|
||||
import com.walkguide.dto.request.RegisterRequest;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Integration Test — Alur Auth (Register / Login / Refresh / Logout).
|
||||
*
|
||||
* Menggunakan Testcontainers PostgreSQL nyata + @SpringBootTest penuh.
|
||||
* Tidak ada mocking — semua hit database container sungguhan.
|
||||
*
|
||||
* CATATAN IDE: Error "cannot find symbol setEmail/setPassword/dll" di NetBeans
|
||||
* adalah FALSE POSITIVE — Lombok @Data tidak diproses oleh NetBeans LSP.
|
||||
* Test ini berjalan normal dengan: mvn test
|
||||
*/
|
||||
@DisplayName("Integration Test — Auth Flow (Testcontainers)")
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class AuthIntegrationTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final String USER_EMAIL = "integ_user@walkguide.test";
|
||||
private static final String USER_PASS = "password123";
|
||||
private static final String GUARDIAN_EMAIL = "integ_guardian@walkguide.test";
|
||||
private static final String GUARDIAN_PASS = "guardian123";
|
||||
|
||||
// Shared state antar test (ordered)
|
||||
private static String userAccessToken;
|
||||
private static String userRefreshToken;
|
||||
private static String guardianAccessToken;
|
||||
|
||||
// ==========================================================================
|
||||
// 1. Register
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
@DisplayName("POST /register — ROLE_USER sukses dapat access + refresh token")
|
||||
void register_roleUser_success() throws Exception {
|
||||
RegisterRequest req = new RegisterRequest();
|
||||
req.setEmail(USER_EMAIL);
|
||||
req.setPassword(USER_PASS);
|
||||
req.setDisplayName("Integration User");
|
||||
req.setRole("USER");
|
||||
|
||||
String responseBody = mockMvc.perform(post("/api/v1/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Registrasi berhasil"))
|
||||
.andExpect(jsonPath("$.data.accessToken").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.refreshToken").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.role").value("ROLE_USER"))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
|
||||
JsonNode data = objectMapper.readTree(responseBody).path("data");
|
||||
userAccessToken = data.path("accessToken").asText();
|
||||
userRefreshToken = data.path("refreshToken").asText();
|
||||
|
||||
assertThat(userAccessToken).isNotBlank();
|
||||
assertThat(userRefreshToken).isNotBlank();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
@DisplayName("POST /register — ROLE_GUARDIAN sukses")
|
||||
void register_roleGuardian_success() throws Exception {
|
||||
RegisterRequest req = new RegisterRequest();
|
||||
req.setEmail(GUARDIAN_EMAIL);
|
||||
req.setPassword(GUARDIAN_PASS);
|
||||
req.setDisplayName("Integration Guardian");
|
||||
req.setRole("GUARDIAN");
|
||||
|
||||
String responseBody = mockMvc.perform(post("/api/v1/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.role").value("ROLE_GUARDIAN"))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
|
||||
guardianAccessToken = objectMapper.readTree(responseBody)
|
||||
.path("data").path("accessToken").asText();
|
||||
assertThat(guardianAccessToken).isNotBlank();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@DisplayName("POST /register — email duplikat harus return error (bukan 200)")
|
||||
void register_duplicateEmail_returnsError() throws Exception {
|
||||
RegisterRequest req = new RegisterRequest();
|
||||
req.setEmail(USER_EMAIL); // sudah ada dari Order(1)
|
||||
req.setPassword("anotherpass");
|
||||
req.setDisplayName("Duplicate");
|
||||
req.setRole("USER");
|
||||
|
||||
mockMvc.perform(post("/api/v1/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(result ->
|
||||
assertThat(result.getResponse().getStatus())
|
||||
.isGreaterThanOrEqualTo(400));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@DisplayName("POST /register — body kosong harus return 400 validation error")
|
||||
void register_emptyBody_returns400() throws Exception {
|
||||
mockMvc.perform(post("/api/v1/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 2. Login
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@DisplayName("POST /login — credentials valid dapat token baru")
|
||||
void login_validCredentials_returnsNewToken() throws Exception {
|
||||
LoginRequest req = new LoginRequest();
|
||||
req.setEmail(USER_EMAIL);
|
||||
req.setPassword(USER_PASS);
|
||||
|
||||
mockMvc.perform(post("/api/v1/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Login berhasil"))
|
||||
.andExpect(jsonPath("$.data.accessToken").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.role").value("ROLE_USER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
@DisplayName("POST /login — email tidak terdaftar harus return error")
|
||||
void login_unknownEmail_returnsError() throws Exception {
|
||||
LoginRequest req = new LoginRequest();
|
||||
req.setEmail("tidakada@walkguide.test");
|
||||
req.setPassword("irrelevant");
|
||||
|
||||
mockMvc.perform(post("/api/v1/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(result ->
|
||||
assertThat(result.getResponse().getStatus())
|
||||
.isGreaterThanOrEqualTo(400));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
@DisplayName("POST /login — password salah harus return error")
|
||||
void login_wrongPassword_returnsError() throws Exception {
|
||||
LoginRequest req = new LoginRequest();
|
||||
req.setEmail(USER_EMAIL);
|
||||
req.setPassword("wrongpassword");
|
||||
|
||||
mockMvc.perform(post("/api/v1/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(result ->
|
||||
assertThat(result.getResponse().getStatus())
|
||||
.isGreaterThanOrEqualTo(400));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 3. Refresh Token
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(8)
|
||||
@DisplayName("POST /refresh — refresh token valid dapat access token baru")
|
||||
void refresh_validToken_returnsNewAccessToken() throws Exception {
|
||||
assertThat(userRefreshToken).isNotBlank();
|
||||
|
||||
RefreshTokenRequest req = new RefreshTokenRequest();
|
||||
req.setRefreshToken(userRefreshToken);
|
||||
|
||||
mockMvc.perform(post("/api/v1/auth/refresh")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Token diperbarui"))
|
||||
.andExpect(jsonPath("$.data.accessToken").isNotEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(9)
|
||||
@DisplayName("POST /refresh — token palsu harus return error")
|
||||
void refresh_fakeToken_returnsError() throws Exception {
|
||||
RefreshTokenRequest req = new RefreshTokenRequest();
|
||||
req.setRefreshToken("totally-invalid-refresh-token-xyz");
|
||||
|
||||
mockMvc.perform(post("/api/v1/auth/refresh")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(result ->
|
||||
assertThat(result.getResponse().getStatus())
|
||||
.isGreaterThanOrEqualTo(400));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 4. Logout
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(10)
|
||||
@DisplayName("POST /logout — dengan token valid harus return 200 logout berhasil")
|
||||
void logout_withValidToken_returns200() throws Exception {
|
||||
assertThat(userAccessToken).isNotBlank();
|
||||
|
||||
mockMvc.perform(post("/api/v1/auth/logout")
|
||||
.header("Authorization", bearerToken(userAccessToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Logout berhasil"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(11)
|
||||
@DisplayName("POST /logout — tanpa token harus return 403")
|
||||
void logout_withoutToken_returns403() throws Exception {
|
||||
mockMvc.perform(post("/api/v1/auth/logout"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 5. RBAC
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(12)
|
||||
@DisplayName("RBAC — USER token tidak bisa akses /api/v1/guardian/** → 403")
|
||||
void rbac_userCannotAccessGuardianEndpoint() throws Exception {
|
||||
// Login ulang karena token dari Order(1) sudah di-logout di Order(10)
|
||||
String freshUserToken = loginAndGetToken(USER_EMAIL, USER_PASS);
|
||||
|
||||
mockMvc.perform(get("/api/v1/guardian/dashboard")
|
||||
.header("Authorization", bearerToken(freshUserToken)))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(13)
|
||||
@DisplayName("RBAC — GUARDIAN token tidak bisa akses /api/v1/user/** → 403")
|
||||
void rbac_guardianCannotAccessUserEndpoint() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/user/profile")
|
||||
.header("Authorization", bearerToken(guardianAccessToken)))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(14)
|
||||
@DisplayName("RBAC — Request tanpa token ke protected endpoint → 403")
|
||||
void rbac_noToken_protectedEndpoint_returns403() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/user/profile"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 6. Endpoint publik
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(15)
|
||||
@DisplayName("GET /ping — endpoint publik, tidak perlu auth → 200 pong")
|
||||
void ping_publicEndpoint_returns200() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/auth/ping"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data").value("pong"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(16)
|
||||
@DisplayName("GET /swagger-ui.html — endpoint publik → 2xx atau redirect")
|
||||
void swaggerUi_publicEndpoint_accessible() throws Exception {
|
||||
mockMvc.perform(get("/swagger-ui.html"))
|
||||
.andExpect(result ->
|
||||
assertThat(result.getResponse().getStatus())
|
||||
.isBetween(200, 399));
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
package com.walkguide.integration;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.walkguide.dto.request.InviteUserRequest;
|
||||
import com.walkguide.dto.request.PairingResponseRequest;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Integration Test — Alur Pairing Guardian ↔ User.
|
||||
*
|
||||
* Menggunakan Testcontainers PostgreSQL nyata.
|
||||
* State transitions yang di-test:
|
||||
* Guardian invite User → PENDING → User accept → ACTIVE → unpair
|
||||
* Guardian invite User → PENDING → User reject → REJECTED
|
||||
*
|
||||
* CATATAN IDE: Error "setUniqueUserId/setPairingId/setAccept" di NetBeans
|
||||
* adalah FALSE POSITIVE karena Lombok @Data tidak diproses IDE.
|
||||
* Berjalan normal dengan: mvn test
|
||||
*/
|
||||
@DisplayName("Integration Test — Pairing Flow (Testcontainers)")
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class PairingIntegrationTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final String USER_EMAIL = "pairing_user@walkguide.test";
|
||||
private static final String USER_PASS = "userpass123";
|
||||
private static final String GUARDIAN_EMAIL = "pairing_guardian@walkguide.test";
|
||||
private static final String GUARDIAN_PASS = "guardianpass123";
|
||||
|
||||
private static String userToken;
|
||||
private static String guardianToken;
|
||||
private static String uniqueUserId;
|
||||
private static Long pairingId;
|
||||
|
||||
// ==========================================================================
|
||||
// Setup — register kedua user
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
@DisplayName("Setup — Register User dan Guardian, simpan token + uniqueUserId")
|
||||
void setup_registerBothRoles() throws Exception {
|
||||
JsonNode userData = registerAndGetFullResponse(USER_EMAIL, USER_PASS, "USER");
|
||||
userToken = userData.path("accessToken").asText();
|
||||
uniqueUserId = userData.path("uniqueUserId").asText();
|
||||
|
||||
assertThat(userToken).isNotBlank();
|
||||
assertThat(uniqueUserId).hasSize(12);
|
||||
|
||||
JsonNode guardianData = registerAndGetFullResponse(GUARDIAN_EMAIL, GUARDIAN_PASS, "GUARDIAN");
|
||||
guardianToken = guardianData.path("accessToken").asText();
|
||||
|
||||
assertThat(guardianToken).isNotBlank();
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Invite
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
@DisplayName("POST /shared/pairing/invite — Guardian invite User dengan uniqueUserId valid → PENDING")
|
||||
void invite_validUniqueUserId_returnsPending() throws Exception {
|
||||
// InviteUserRequest: field = uniqueUserId (@Data → setter tersedia di runtime)
|
||||
InviteUserRequest req = new InviteUserRequest();
|
||||
req.setUniqueUserId(uniqueUserId);
|
||||
|
||||
String responseBody = mockMvc.perform(post("/api/v1/shared/pairing/invite")
|
||||
.header("Authorization", bearerToken(guardianToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Undangan dikirim ke user"))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
|
||||
JsonNode data = objectMapper.readTree(responseBody).path("data");
|
||||
pairingId = data.path("pairingId").asLong();
|
||||
assertThat(pairingId).isGreaterThan(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@DisplayName("POST /shared/pairing/invite — uniqueUserId tidak ada → error")
|
||||
void invite_unknownUniqueUserId_returnsError() throws Exception {
|
||||
InviteUserRequest req = new InviteUserRequest();
|
||||
req.setUniqueUserId("XXXXXXXXXXXX"); // 12 char tapi tidak ada di DB
|
||||
|
||||
mockMvc.perform(post("/api/v1/shared/pairing/invite")
|
||||
.header("Authorization", bearerToken(guardianToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(result ->
|
||||
assertThat(result.getResponse().getStatus())
|
||||
.isGreaterThanOrEqualTo(400));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@DisplayName("POST /shared/pairing/invite — User (bukan Guardian) coba invite → error")
|
||||
void invite_asUser_returnsError() throws Exception {
|
||||
InviteUserRequest req = new InviteUserRequest();
|
||||
req.setUniqueUserId("SOMEUSRXXXXX");
|
||||
|
||||
mockMvc.perform(post("/api/v1/shared/pairing/invite")
|
||||
.header("Authorization", bearerToken(userToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(result ->
|
||||
assertThat(result.getResponse().getStatus())
|
||||
.isGreaterThanOrEqualTo(400));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Status PENDING
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@DisplayName("GET /shared/pairing/status — User lihat status PENDING")
|
||||
void status_asUser_pending() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/shared/pairing/status")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Status pairing"))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
@DisplayName("GET /shared/pairing/status — Guardian lihat status PENDING")
|
||||
void status_asGuardian_pending() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/shared/pairing/status")
|
||||
.header("Authorization", bearerToken(guardianToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Accept → ACTIVE
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
@DisplayName("POST /shared/pairing/respond — User accept pairing → ACTIVE")
|
||||
void respond_accept_returnsActive() throws Exception {
|
||||
assertThat(pairingId).isGreaterThan(0);
|
||||
|
||||
// PairingResponseRequest: fields = pairingId, accept (@Data → setter tersedia)
|
||||
PairingResponseRequest req = new PairingResponseRequest();
|
||||
req.setPairingId(pairingId);
|
||||
req.setAccept(true);
|
||||
|
||||
mockMvc.perform(post("/api/v1/shared/pairing/respond")
|
||||
.header("Authorization", bearerToken(userToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Pairing diterima"))
|
||||
.andExpect(jsonPath("$.data.status").value("ACTIVE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(8)
|
||||
@DisplayName("GET /shared/pairing/status — setelah accept, status ACTIVE")
|
||||
void status_afterAccept_isActive() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/shared/pairing/status")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("ACTIVE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(9)
|
||||
@DisplayName("GET /guardian/dashboard — setelah paired, Guardian bisa akses dashboard")
|
||||
void guardianDashboard_afterPairing_returns200() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/guardian/dashboard")
|
||||
.header("Authorization", bearerToken(guardianToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Dashboard Guardian"));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Unpair
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(10)
|
||||
@DisplayName("DELETE /shared/pairing/unpair — User unpair → 200 pairing diakhiri")
|
||||
void unpair_asUser_returns200() throws Exception {
|
||||
mockMvc.perform(delete("/api/v1/shared/pairing/unpair")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Pairing diakhiri"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(11)
|
||||
@DisplayName("GET /shared/pairing/status — setelah unpair, tidak ada pairing ACTIVE")
|
||||
void status_afterUnpair_isNotActive() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/shared/pairing/status")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(result -> {
|
||||
String body = result.getResponse().getContentAsString();
|
||||
JsonNode json = objectMapper.readTree(body);
|
||||
String statusVal = json.path("data").path("status").asText("NONE");
|
||||
assertThat(statusVal).isNotEqualTo("ACTIVE");
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Skenario Reject
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(12)
|
||||
@DisplayName("Skenario Reject: Guardian invite baru → User tolak → REJECTED")
|
||||
void scenario_reject_flow() throws Exception {
|
||||
String rejectUserEmail = "reject_user@walkguide.test";
|
||||
String rejectGuardianEmail = "reject_guardian@walkguide.test";
|
||||
|
||||
JsonNode rejectUserData = registerAndGetFullResponse(rejectUserEmail, "pass123", "USER");
|
||||
String rejectUserToken = rejectUserData.path("accessToken").asText();
|
||||
String rejectUniqueId = rejectUserData.path("uniqueUserId").asText();
|
||||
|
||||
JsonNode rejectGuardData = registerAndGetFullResponse(rejectGuardianEmail, "pass123", "GUARDIAN");
|
||||
String rejectGuardToken = rejectGuardData.path("accessToken").asText();
|
||||
|
||||
// Guardian invite
|
||||
InviteUserRequest inviteReq = new InviteUserRequest();
|
||||
inviteReq.setUniqueUserId(rejectUniqueId);
|
||||
|
||||
String inviteResponse = mockMvc.perform(post("/api/v1/shared/pairing/invite")
|
||||
.header("Authorization", bearerToken(rejectGuardToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(inviteReq)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
|
||||
Long rejectPairingId = objectMapper.readTree(inviteResponse)
|
||||
.path("data").path("pairingId").asLong();
|
||||
|
||||
// User tolak
|
||||
PairingResponseRequest rejectReq = new PairingResponseRequest();
|
||||
rejectReq.setPairingId(rejectPairingId);
|
||||
rejectReq.setAccept(false);
|
||||
|
||||
mockMvc.perform(post("/api/v1/shared/pairing/respond")
|
||||
.header("Authorization", bearerToken(rejectUserToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(rejectReq)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.message").value("Pairing ditolak"))
|
||||
.andExpect(jsonPath("$.data.status").value("REJECTED"));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Endpoint tanpa auth
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(13)
|
||||
@DisplayName("DELETE /shared/pairing/unpair — tanpa token → 403")
|
||||
void unpair_withoutToken_returns403() throws Exception {
|
||||
mockMvc.perform(delete("/api/v1/shared/pairing/unpair"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(14)
|
||||
@DisplayName("GET /shared/pairing/status — tanpa token → 403")
|
||||
void status_withoutToken_returns403() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/shared/pairing/status"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
}
|
||||
+488
@@ -0,0 +1,488 @@
|
||||
package com.walkguide.integration;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.walkguide.dto.request.*;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Integration Test — Fitur Inti User: Location, Obstacle, SOS, Notification, WalkGuide.
|
||||
*
|
||||
* Menggunakan Testcontainers PostgreSQL nyata + full Spring context.
|
||||
*
|
||||
* FIELD NAMES SESUAI DTO AKTUAL:
|
||||
* - LocationUpdateRequest : lat, lng (bukan latitude/longitude)
|
||||
* - ObstacleLogRequest : label, confidence (Double), direction, estimatedDist (bukan distance)
|
||||
* - SosRequest : triggerType, lat, lng
|
||||
* - SendNotificationRequest: notifType, content (bukan title/message)
|
||||
* - UserSettingsUpdateRequest: ttsLanguage, ttsSpeed (Double, bukan float)
|
||||
*
|
||||
* CATATAN IDE: Error field/method di NetBeans adalah FALSE POSITIVE (Lombok-NetBeans).
|
||||
* Berjalan normal dengan: mvn test
|
||||
*/
|
||||
@DisplayName("Integration Test — User Core Features (Testcontainers)")
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class UserFeatureIntegrationTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final String USER_EMAIL = "feature_user@walkguide.test";
|
||||
private static final String USER_PASS = "userpass123";
|
||||
private static final String GUARDIAN_EMAIL = "feature_guardian@walkguide.test";
|
||||
private static final String GUARDIAN_PASS = "guardpass123";
|
||||
|
||||
private static String userToken;
|
||||
private static String guardianToken;
|
||||
private static String uniqueUserId;
|
||||
private static Long sosEventId;
|
||||
private static Long notificationId;
|
||||
|
||||
// ==========================================================================
|
||||
// Setup — register & pair
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
@DisplayName("Setup — Register & Pair Guardian ↔ User")
|
||||
void setup_registerAndPair() throws Exception {
|
||||
JsonNode userData = registerAndGetFullResponse(USER_EMAIL, USER_PASS, "USER");
|
||||
userToken = userData.path("accessToken").asText();
|
||||
uniqueUserId = userData.path("uniqueUserId").asText();
|
||||
|
||||
JsonNode guardianData = registerAndGetFullResponse(GUARDIAN_EMAIL, GUARDIAN_PASS, "GUARDIAN");
|
||||
guardianToken = guardianData.path("accessToken").asText();
|
||||
|
||||
// Guardian invite User
|
||||
InviteUserRequest inviteReq = new InviteUserRequest();
|
||||
inviteReq.setUniqueUserId(uniqueUserId);
|
||||
|
||||
String inviteResponse = mockMvc.perform(post("/api/v1/shared/pairing/invite")
|
||||
.header("Authorization", bearerToken(guardianToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(inviteReq)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
|
||||
Long pairingId = objectMapper.readTree(inviteResponse)
|
||||
.path("data").path("pairingId").asLong();
|
||||
|
||||
// User accept
|
||||
// PairingResponseRequest: pairingId, accept
|
||||
PairingResponseRequest acceptReq = new PairingResponseRequest();
|
||||
acceptReq.setPairingId(pairingId);
|
||||
acceptReq.setAccept(true);
|
||||
|
||||
mockMvc.perform(post("/api/v1/shared/pairing/respond")
|
||||
.header("Authorization", bearerToken(userToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(acceptReq)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("ACTIVE"));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Location
|
||||
// PENTING: LocationUpdateRequest field = lat, lng (BUKAN latitude/longitude)
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
@DisplayName("POST /user/location — update lokasi GPS berhasil")
|
||||
void updateLocation_validCoords_returns200() throws Exception {
|
||||
// LocationUpdateRequest fields: lat, lng, accuracy, speed, heading
|
||||
LocationUpdateRequest req = new LocationUpdateRequest();
|
||||
req.setLat(-7.2575);
|
||||
req.setLng(112.7521);
|
||||
|
||||
mockMvc.perform(post("/api/v1/user/location")
|
||||
.header("Authorization", bearerToken(userToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Lokasi diperbarui"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@DisplayName("POST /user/location — body kosong tetap valid (semua field optional)")
|
||||
void updateLocation_emptyBody_returns200OrError() throws Exception {
|
||||
// LocationUpdateRequest tidak ada @NotNull — bisa kirim kosong
|
||||
mockMvc.perform(post("/api/v1/user/location")
|
||||
.header("Authorization", bearerToken(userToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(result ->
|
||||
assertThat(result.getResponse().getStatus())
|
||||
.isBetween(200, 599)); // terima apapun, asal tidak crash 5xx karena NPE
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@DisplayName("GET /guardian/user-location — Guardian dapat melihat lokasi terakhir User")
|
||||
void guardianGetUserLocation_afterUpdate_returnsLocation() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/guardian/user-location")
|
||||
.header("Authorization", bearerToken(guardianToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Lokasi terakhir user"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@DisplayName("GET /guardian/location-history — Guardian dapat riwayat lokasi terpaginasi")
|
||||
void guardianGetLocationHistory_returns200WithPage() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/guardian/location-history")
|
||||
.header("Authorization", bearerToken(guardianToken))
|
||||
.param("page", "0")
|
||||
.param("size", "10"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Riwayat lokasi"));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Obstacle
|
||||
// PENTING: ObstacleLogRequest fields = label, confidence (Double), direction,
|
||||
// estimatedDist (String) — TIDAK ADA field "distance"
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
@DisplayName("POST /user/obstacle — log obstacle berhasil")
|
||||
void logObstacle_validRequest_returns200() throws Exception {
|
||||
// ObstacleLogRequest fields: label, confidence (Double), direction, estimatedDist, lat, lng
|
||||
ObstacleLogRequest req = new ObstacleLogRequest();
|
||||
req.setLabel("person");
|
||||
req.setConfidence(0.87); // Double, bukan float
|
||||
req.setDirection("depan");
|
||||
req.setEstimatedDist("1.5m"); // String, bukan float
|
||||
req.setLat(-7.2575);
|
||||
req.setLng(112.7521);
|
||||
|
||||
mockMvc.perform(post("/api/v1/user/obstacle")
|
||||
.header("Authorization", bearerToken(userToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Obstacle dicatat"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
@DisplayName("GET /guardian/obstacle-logs — Guardian lihat obstacle logs User")
|
||||
void guardianGetObstacleLogs_returns200() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/guardian/obstacle-logs")
|
||||
.header("Authorization", bearerToken(guardianToken))
|
||||
.param("page", "0").param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Log obstacle user"));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// WalkGuide Start / Stop
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(8)
|
||||
@DisplayName("POST /user/walkguide/start — log WALKGUIDE_START berhasil")
|
||||
void walkGuideStart_returns200() throws Exception {
|
||||
mockMvc.perform(post("/api/v1/user/walkguide/start")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("WalkGuide dimulai"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(9)
|
||||
@DisplayName("POST /user/walkguide/stop — log WALKGUIDE_STOP berhasil")
|
||||
void walkGuideStop_returns200() throws Exception {
|
||||
mockMvc.perform(post("/api/v1/user/walkguide/stop")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("WalkGuide dihentikan"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(10)
|
||||
@DisplayName("GET /user/activity-logs — User lihat log aktivitas terpaginasi")
|
||||
void userActivityLogs_returns200WithEntries() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/user/activity-logs")
|
||||
.header("Authorization", bearerToken(userToken))
|
||||
.param("page", "0").param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Log aktivitas"))
|
||||
.andExpect(result -> {
|
||||
String body = result.getResponse().getContentAsString();
|
||||
JsonNode json = objectMapper.readTree(body);
|
||||
int totalElements = json.path("data").path("totalElements").asInt(0);
|
||||
assertThat(totalElements).isGreaterThanOrEqualTo(2); // min: start + stop
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(11)
|
||||
@DisplayName("GET /guardian/activity-logs — Guardian lihat log aktivitas User")
|
||||
void guardianActivityLogs_returns200() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/guardian/activity-logs")
|
||||
.header("Authorization", bearerToken(guardianToken))
|
||||
.param("page", "0").param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Log aktivitas user"));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// SOS
|
||||
// PENTING: SosRequest fields = triggerType, lat, lng
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(12)
|
||||
@DisplayName("POST /user/sos — User kirim SOS → 200, status TRIGGERED")
|
||||
void triggerSos_returns200WithTriggeredStatus() throws Exception {
|
||||
// SosRequest fields: triggerType, lat, lng
|
||||
SosRequest req = new SosRequest();
|
||||
req.setTriggerType("MANUAL");
|
||||
req.setLat(-7.2575);
|
||||
req.setLng(112.7521);
|
||||
|
||||
String responseBody = mockMvc.perform(post("/api/v1/user/sos")
|
||||
.header("Authorization", bearerToken(userToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("SOS dikirim! Guardian sudah diberitahu."))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
|
||||
JsonNode data = objectMapper.readTree(responseBody).path("data");
|
||||
sosEventId = data.path("id").asLong();
|
||||
String sosStatus = data.path("status").asText();
|
||||
|
||||
assertThat(sosEventId).isGreaterThan(0);
|
||||
assertThat(sosStatus).isEqualTo("TRIGGERED");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(13)
|
||||
@DisplayName("GET /guardian/sos-events — Guardian lihat SOS events User")
|
||||
void guardianGetSosEvents_returns200() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/guardian/sos-events")
|
||||
.header("Authorization", bearerToken(guardianToken))
|
||||
.param("page", "0").param("size", "10"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("SOS events"))
|
||||
.andExpect(result -> {
|
||||
String body = result.getResponse().getContentAsString();
|
||||
JsonNode json = objectMapper.readTree(body);
|
||||
int total = json.path("data").path("totalElements").asInt(0);
|
||||
assertThat(total).isGreaterThanOrEqualTo(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(14)
|
||||
@DisplayName("PUT /guardian/sos/{id}/acknowledge — Guardian akui SOS → ACKNOWLEDGED")
|
||||
void guardianAcknowledgeSos_returns200WithAcknowledgedStatus() throws Exception {
|
||||
assertThat(sosEventId).isGreaterThan(0);
|
||||
|
||||
mockMvc.perform(put("/api/v1/guardian/sos/" + sosEventId + "/acknowledge")
|
||||
.header("Authorization", bearerToken(guardianToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("SOS diakui"))
|
||||
.andExpect(jsonPath("$.data.status").value("ACKNOWLEDGED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(15)
|
||||
@DisplayName("POST /user/sos — tanpa token → 403")
|
||||
void triggerSos_withoutToken_returns403() throws Exception {
|
||||
SosRequest req = new SosRequest();
|
||||
req.setTriggerType("MANUAL");
|
||||
|
||||
mockMvc.perform(post("/api/v1/user/sos")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Notifications
|
||||
// PENTING: SendNotificationRequest fields = notifType, content (BUKAN title/message)
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(16)
|
||||
@DisplayName("POST /guardian/notifications/send — Guardian kirim notifikasi ke User")
|
||||
void sendNotification_returns200WithNotifResponse() throws Exception {
|
||||
// SendNotificationRequest fields: notifType, content, voiceNoteUrl, voiceNoteDuration
|
||||
SendNotificationRequest req = new SendNotificationRequest();
|
||||
req.setNotifType("TEXT");
|
||||
req.setContent("Hati-hati! Ada kendaraan di depanmu");
|
||||
|
||||
String responseBody = mockMvc.perform(post("/api/v1/guardian/notifications/send")
|
||||
.header("Authorization", bearerToken(guardianToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Notifikasi terkirim"))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
|
||||
notificationId = objectMapper.readTree(responseBody)
|
||||
.path("data").path("id").asLong();
|
||||
assertThat(notificationId).isGreaterThan(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(17)
|
||||
@DisplayName("GET /user/notifications — User lihat notifikasi yang diterima")
|
||||
void getNotifications_returns200WithItems() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/user/notifications")
|
||||
.header("Authorization", bearerToken(userToken))
|
||||
.param("page", "0").param("size", "10"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Notifikasi"))
|
||||
.andExpect(result -> {
|
||||
String body = result.getResponse().getContentAsString();
|
||||
JsonNode json = objectMapper.readTree(body);
|
||||
int total = json.path("data").path("totalElements").asInt(0);
|
||||
assertThat(total).isGreaterThanOrEqualTo(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(18)
|
||||
@DisplayName("GET /user/notifications/unread-count — count notifikasi belum dibaca ≥ 1")
|
||||
void getUnreadCount_afterSend_isAtLeastOne() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/user/notifications/unread-count")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Jumlah notifikasi belum dibaca"))
|
||||
.andExpect(result -> {
|
||||
String body = result.getResponse().getContentAsString();
|
||||
int count = objectMapper.readTree(body).path("data").asInt(0);
|
||||
assertThat(count).isGreaterThanOrEqualTo(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(19)
|
||||
@DisplayName("PUT /user/notifications/{id}/read — mark satu notifikasi sebagai dibaca")
|
||||
void markOneRead_returns200() throws Exception {
|
||||
assertThat(notificationId).isGreaterThan(0);
|
||||
|
||||
mockMvc.perform(put("/api/v1/user/notifications/" + notificationId + "/read")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Notifikasi ditandai sudah dibaca"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(20)
|
||||
@DisplayName("PUT /user/notifications/mark-all-read — mark semua sebagai dibaca")
|
||||
void markAllRead_returns200() throws Exception {
|
||||
mockMvc.perform(put("/api/v1/user/notifications/mark-all-read")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Semua notifikasi ditandai sudah dibaca"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(21)
|
||||
@DisplayName("GET /user/notifications/unread-count — setelah mark-all, count = 0")
|
||||
void getUnreadCount_afterMarkAll_isZero() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/user/notifications/unread-count")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data").value(0));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// User Profile & Settings
|
||||
// PENTING: UserSettingsUpdateRequest.ttsSpeed = Double (pakai 1.2, bukan 1.2f)
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(22)
|
||||
@DisplayName("GET /user/profile — User lihat profil diri sendiri")
|
||||
void getProfile_returns200WithEmail() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/user/profile")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.email").value(USER_EMAIL))
|
||||
.andExpect(jsonPath("$.data.uniqueUserId").isNotEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(23)
|
||||
@DisplayName("GET /user/settings — User lihat settings dirinya")
|
||||
void getSettings_returns200() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/user/settings")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Settings user"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(24)
|
||||
@DisplayName("PUT /user/settings — User update TTS language dan speed")
|
||||
void updateSettings_returns200WithUpdatedData() throws Exception {
|
||||
// UserSettingsUpdateRequest fields: ttsLanguage, ttsPitch, ttsSpeed (Double), warnNoGuardian, hapticEnabled
|
||||
UserSettingsUpdateRequest req = new UserSettingsUpdateRequest();
|
||||
req.setTtsLanguage("id");
|
||||
req.setTtsSpeed(1.2); // Double, bukan float — tidak pakai suffix 'f'
|
||||
|
||||
mockMvc.perform(put("/api/v1/user/settings")
|
||||
.header("Authorization", bearerToken(userToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("Settings diperbarui"));
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// RBAC guard pada feature endpoints
|
||||
// ==========================================================================
|
||||
|
||||
@Test
|
||||
@Order(25)
|
||||
@DisplayName("POST /user/location — Guardian token coba akses endpoint User → 403")
|
||||
void userEndpoint_withGuardianToken_returns403() throws Exception {
|
||||
LocationUpdateRequest req = new LocationUpdateRequest();
|
||||
req.setLat(-7.2575);
|
||||
req.setLng(112.7521);
|
||||
|
||||
mockMvc.perform(post("/api/v1/user/location")
|
||||
.header("Authorization", bearerToken(guardianToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(26)
|
||||
@DisplayName("GET /guardian/dashboard — User token coba akses endpoint Guardian → 403")
|
||||
void guardianEndpoint_withUserToken_returns403() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/guardian/dashboard")
|
||||
.header("Authorization", bearerToken(userToken)))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user