From 66afdf631a3fb6761e937ec73fc3e4b63ca7ab19 Mon Sep 17 00:00:00 2001 From: Pipatpong Primna <114746788+Pipatq@users.noreply.github.com> Date: Sat, 22 Nov 2025 14:01:16 +0700 Subject: [PATCH] Add test --- .../UnitControllertest.java | 156 ++++++++++++++++++ frontend/cypress/e2e/maintenance.cy.js | 91 ++++++++++ frontend/cypress/e2e/report.cy.js | 61 +++++++ note | 27 +++ 4 files changed, 335 insertions(+) create mode 100644 backend/src/test/java/apartment/example/backend/controllerIntegration/UnitControllertest.java create mode 100644 frontend/cypress/e2e/maintenance.cy.js create mode 100644 frontend/cypress/e2e/report.cy.js create mode 100644 note diff --git a/backend/src/test/java/apartment/example/backend/controllerIntegration/UnitControllertest.java b/backend/src/test/java/apartment/example/backend/controllerIntegration/UnitControllertest.java new file mode 100644 index 0000000..9842669 --- /dev/null +++ b/backend/src/test/java/apartment/example/backend/controllerIntegration/UnitControllertest.java @@ -0,0 +1,156 @@ +package apartment.example.backend.controller.Integrationtest; + + +import apartment.example.backend.entity.Unit; +import apartment.example.backend.entity.enums.UnitStatus; +import apartment.example.backend.repository.UnitRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +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 java.math.BigDecimal; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@WithMockUser(username = "admin", roles = {"ADMIN"}) +public class UnitControllertest { + + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UnitRepository unitRepository; + + @Autowired + private ObjectMapper objectMapper; + + private Long savedId; + + @DynamicPropertySource + static void jwtProperties(DynamicPropertyRegistry registry) { + // Override JWT properties for testing + registry.add("jwt.secret", () -> "dGVzdHNlY3JldA=="); + registry.add("jwt.expiration", () -> "3600000"); + + // Override CORS properties for testing + registry.add("cors.allowed-origins", () -> "http://localhost:5173"); + + // Override database properties for Testcontainers + registry.add("spring.datasource.url", () -> "jdbc:tc:mysql:8.0:///testdb"); + registry.add("spring.datasource.driver-class-name", () -> "org.testcontainers.jdbc.ContainerDatabaseDriver"); + registry.add("spring.jpa.hibernate.ddl-auto", () -> "update"); + registry.add("spring.sql.init.mode", () -> "never"); + + registry.add("spring.jpa.show-sql", () -> "true"); + } + + @BeforeEach + void setup() { + unitRepository.deleteAll(); + + Unit u = new Unit(); + u.setRoomNumber("A101"); + u.setFloor(1); + u.setStatus(UnitStatus.AVAILABLE); + u.setRentAmount(BigDecimal.valueOf(8000)); + u.setUnitType("Standard"); + savedId = unitRepository.saveAndFlush(u).getId(); + } + + @Test + void testGetAllUnits() throws Exception { + mockMvc.perform(get("/units")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].roomNumber").value("A101")); + } + + @Test + void testGetUnitById() throws Exception { + mockMvc.perform(get("/units/" + savedId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.roomNumber").value("A101")); + } + + @Test + void testCreateUnit() throws Exception { + Unit newUnit = new Unit(); + newUnit.setRoomNumber("102"); + newUnit.setFloor(1); + newUnit.setStatus(UnitStatus.AVAILABLE); + newUnit.setRentAmount(BigDecimal.valueOf(9000)); + + // --- เพิ่มบรรทัดนี้ --- + newUnit.setUnitType("Standard"); + // ------------------- + + mockMvc.perform(post("/units") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newUnit))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.roomNumber").value("102")); + } + + @Test + void testUpdateUnit() throws Exception { + Unit update = new Unit(); + update.setRoomNumber("101-UPDATE"); + update.setFloor(1); + update.setStatus(UnitStatus.MAINTENANCE); + update.setRentAmount(BigDecimal.valueOf(7000)); + + // --- เพิ่มบรรทัดนี้ --- + update.setUnitType("Standard"); + // ------------------- + + mockMvc.perform(put("/units/" + savedId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(update))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.roomNumber").value("101-UPDATE")) + .andExpect(jsonPath("$.status").value("MAINTENANCE")); + } + + @Test + void testPatchStatus() throws Exception { + String json = """ + { "status": "OCCUPIED" } + """; + + mockMvc.perform(patch("/units/" + savedId + "/status") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("OCCUPIED")); + } + + @Test + void testDeleteUnit() throws Exception { + mockMvc.perform(delete("/units/" + savedId)) + .andExpect(status().isNoContent()); + + mockMvc.perform(get("/units/" + savedId)) + .andExpect(status().isNotFound()); + } + + @Test + void testDashboard() throws Exception { + mockMvc.perform(get("/units/dashboard")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalUnits").value(1)) + .andExpect(jsonPath("$.availableUnits").value(1)); + } +} \ No newline at end of file diff --git a/frontend/cypress/e2e/maintenance.cy.js b/frontend/cypress/e2e/maintenance.cy.js new file mode 100644 index 0000000..b458a19 --- /dev/null +++ b/frontend/cypress/e2e/maintenance.cy.js @@ -0,0 +1,91 @@ +describe('Admin Maintenance - Create Schedule Form', () => { + + it('can open Create Schedule modal and fill all fields', () => { + + // ---- LOGIN ---- + cy.visit('/login'); + + cy.get('#username').type('apartment_admin'); + cy.get('#password').type('Admin@2024!Secure'); + + cy.intercept('POST', '**/api/auth/login').as('login'); + cy.intercept('GET', '**/api/auth/me').as('me'); + + cy.get('button[type="submit"]').click(); + + cy.wait('@login'); + cy.wait('@me'); + + // ---- OPEN SIDEBAR ---- + cy.get('button') + .filter((i, el) => el.innerHTML.includes('svg')) + .first() + .click({ force: true }); + + // ---- GO TO MAINTENANCE ---- + cy.contains('Maintenance') + .scrollIntoView() + .click({ force: true }); + + cy.url().should('include', '/maintenance'); + cy.wait(1000); + + // ---- OPEN MODAL ---- + cy.contains('Create Schedule') + .should('be.visible') + .click({ force: true }); + + // ---- VERIFY MODAL ---- + cy.contains('Create Schedule') + .should('be.visible'); + + // ---- BASIC INFORMATION ---- + cy.get('input[placeholder="e.g., Monthly Air Conditioning Cleaning"]') + .type('Air Conditioner Cleaning'); + + cy.get('textarea[placeholder="Describe the maintenance task..."]') + .type('General AC cleaning for all units.'); + + // ---- Category dropdown ---- + cy.contains('label', 'Category') + .next('select') + .select('OTHER'); + + // ---- Priority dropdown ---- + cy.contains('label', 'Priority') + .next('select') + .select('MEDIUM'); + + // ---- RECURRENCE SETTINGS ---- + cy.contains('label', 'Recurrence Type') + .next('select') + .select('ONE_TIME'); + + // ---- TARGET UNITS ---- + cy.contains('label', 'Target Type') + .next('select') + .select('ALL_UNITS'); + + // ---- SCHEDULE DATES ---- + const today = new Date().toISOString().split('T')[0]; + + cy.get('input[type="date"]').eq(0).type(today); // Start Date + cy.get('input[type="date"]').eq(1).type(today); // Next Trigger Date + + // ---- BUTTONS ---- + cy.contains('Cancel').should('exist'); + cy.contains('Create Schedule').should('be.visible'); + + // ---- CLICK CREATE BUTTON ---- + cy.contains('button', 'Create Schedule') + .should('be.enabled') + .click({ force: true }); + + // ---- HANDLE SUCCESS ALERT ---- + cy.on('window:alert', (msg) => { + expect(msg).to.include('Schedule created successfully'); + }); + + }); + +}); \ No newline at end of file diff --git a/frontend/cypress/e2e/report.cy.js b/frontend/cypress/e2e/report.cy.js new file mode 100644 index 0000000..5f98114 --- /dev/null +++ b/frontend/cypress/e2e/report.cy.js @@ -0,0 +1,61 @@ +describe('Admin Report Page', () => { + + it('can open Report page and display correct UI', () => { + + // ---- LOGIN ---- + cy.visit('/login'); + + cy.get('#username').type('apartment_admin'); + cy.get('#password').type('Admin@2024!Secure'); + + cy.intercept('POST', '/api/auth/login').as('login'); + cy.intercept('GET', '/api/auth/me').as('me'); + + cy.get('button[type="submit"]').click(); + + cy.wait('@login'); + cy.wait('@me'); + + + // ---- OPEN SIDEBAR ---- + cy.get('button') + .filter((i, el) => el.innerHTML.includes('svg')) + .first() + .click({ force: true }); + + // ---- CLICK REPORT ---- + cy.contains('Report') + .scrollIntoView() + .click({ force: true }); + + cy.url().should('include', '/report'); + cy.wait(1000); + + + + // ---- SUMMARY CARDS ---- + cy.contains('Total Profit').should('exist'); + cy.contains('Total Revenue').should('exist'); + cy.contains('Total Expense').should('exist'); + + + // ---- FINANCIAL SUMMARY ---- + cy.contains('Financial Summary').should('exist'); + + // chart container (รองรับทุก lib) + cy.get('canvas, svg, .recharts-wrapper, .chart-container') + .should('exist'); + + + // ---- DROPDOWNS ---- + cy.contains('All Revenue').should('exist'); + cy.contains('Monthly').should('exist'); + + + // ---- LOWER SECTIONS ---- + cy.contains('Booking Unit Types').should('exist'); + cy.contains('Booking Summary').should('exist'); + + }); + +}); \ No newline at end of file diff --git a/note b/note new file mode 100644 index 0000000..44ca28e --- /dev/null +++ b/note @@ -0,0 +1,27 @@ +kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.4/deploy/static/provider/cloud/deploy.yaml + + +kubectl get pods -n ingress-nginx +kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=120s + + + +cd k8s +.\deploy.ps1 up +cd ..\monitor +.\deploy-monitoring.ps1 + +kubectl apply -n superproject-ns -f .\backend\servicemonitor.yaml + +Grafana +admin +SuperSecure2024! + + +cd monitor +.\deploy-monitoring.ps1 # ติดตั้ง CRDs ก่อน +cd ..\k8s +.\deploy.ps1 up # จะ apply ServiceMonitor เองอัตโนมัติ + + +kubectl config get-contexts \ No newline at end of file