Skip to content
Merged
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,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));
}
}
91 changes: 91 additions & 0 deletions frontend/cypress/e2e/maintenance.cy.js
Original file line number Diff line number Diff line change
@@ -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');
});

});

});
61 changes: 61 additions & 0 deletions frontend/cypress/e2e/report.cy.js
Original file line number Diff line number Diff line change
@@ -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');

});

});
27 changes: 27 additions & 0 deletions note
Original file line number Diff line number Diff line change
@@ -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
Loading