package de.stadtbote.votian; import com.fasterxml.jackson.databind.ObjectMapper; import de.stadtbote.votian.database.DatabaseService; 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.dao.DataAccessResourceFailureException; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import java.util.List; import java.util.Map; import java.util.function.Supplier; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest(properties={"spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration"}) @AutoConfigureMockMvc class ApiSecurityTest { @Autowired MockMvc mvc; @Autowired ObjectMapper json; @MockitoBean DatabaseService database; @MockitoBean LoginActivity loginActivity; @Test void cronReviewAndStatusRequireSessionRightsAndCsrf() throws Exception { mvc.perform(get("/api/administration/cron/database-status?month=2026-10")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); mvc.perform(post("/api/administration/cron/mysql-status/review").session(session).contentType("application/json").content("{\"reviewed\":true}")).andExpect(status().isForbidden()); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("no-cron-rights"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","no-cron-rights","emp_rights","00000000"))); var denied=authenticated("no-cron-rights","pw");var token=csrf(denied); mvc.perform(get("/api/administration/cron/database-status?month=2026-10").session(denied)).andExpect(status().isForbidden()); mvc.perform(post("/api/administration/cron/mysql-status/review").session(denied).header(token.header(),token.token()).contentType("application/json").content("{\"reviewed\":true}")).andExpect(status().isForbidden()); verify(database,never()).cronOperation(anyString(),any()); } @Test void customerAddressPropagationRequiresSessionRightsAndCsrf() throws Exception { String base="/api/master/customers/1/address-propagation"; var anon=csrf(null);var session=authenticated("existing-user","existing-password"); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("no-propagation"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","no-propagation","emp_rights","00000000"))); var denied=authenticated("no-propagation","pw");var token=csrf(denied); mvc.perform(get(base)).andExpect(status().isUnauthorized());mvc.perform(get(base).session(denied)).andExpect(status().isForbidden()); for(String path:List.of(base,base+"/preview")){ mvc.perform(post(path).session(anon.session()).header(anon.header(),anon.token()).contentType("application/json").content("{}")).andExpect(status().isUnauthorized()); mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(denied).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).writeTransaction(any()); } @Test void informationRequiresSessionAndNoticesRequireCsrf() throws Exception { for(String path:List.of("/api/account/info","/api/account/info/search?q=abc","/api/customer-portal/info"))mvc.perform(get(path)).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); mvc.perform(put("/api/account/info/notices").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).employeeActivityWriteTransaction(any()); } @Test void employeeStatusAndInboxRequireSessionAndCsrf() throws Exception { var session=authenticated("existing-user","existing-password"); for(String path:List.of("/api/account/activity","/api/account/inbox"))mvc.perform(get(path)).andExpect(status().isUnauthorized()); mvc.perform(put("/api/account/activity").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post("/api/account/inbox/read").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).employeeActivityWriteTransaction(any());verify(database,never()).employeeInboxWriteTransaction(anyString(),any()); } @Test void loginActivityRunsOnlyAfterSuccessfulAuthentication() throws Exception { login(csrf(null),"existing-user","wrong-password",401); verifyNoInteractions(loginActivity); authenticated("existing-user","existing-password"); verify(loginActivity).record(any(LegacyUser.class)); } @Test void failedLoginRecordingInvalidatesAuthenticatedSession() throws Exception { doThrow(new org.springframework.dao.DataAccessResourceFailureException("PRIVATE_DATABASE_DETAIL")).when(loginActivity).record(any(LegacyUser.class)); var result=login(csrf(null),"existing-user","existing-password",503); assertThat(result.getRequest().getSession(false)).isNull(); assertThat(result.getResponse().getContentAsString()).contains("nicht vollständig gespeichert").doesNotContain("PRIVATE_DATABASE_DETAIL"); mvc.perform(get("/api/me")).andExpect(status().isUnauthorized()); } @Test void masterExportTransferRequiresSessionCsrfAndExportPermission() throws Exception { String base="/api/data-transfer/master-export-files/transfer"; var anon=csrf(null);var session=authenticated("existing-user","existing-password"); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("no-export-transfer"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","no-export-transfer","emp_rights","00000000"))); var denied=authenticated("no-export-transfer","pw");var token=csrf(denied); for(String path:List.of(base,base+"/remote",base+"/1")){ mvc.perform(get(path)).andExpect(status().isUnauthorized()); mvc.perform(get(path).session(denied)).andExpect(status().isForbidden()); } for(String path:List.of(base,base+"/00000000-0000-0000-0000-000000000001/resume",base+"/00000000-0000-0000-0000-000000000001/cancel",base+"/00000000-0000-0000-0000-000000000001/received")){ mvc.perform(post(path).session(anon.session()).header(anon.header(),anon.token()).contentType("application/json").content("{}")).andExpect(status().isUnauthorized()); mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(denied).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).documentOperation(any()); verify(database,never()).customerExportFileWriteTransaction(any()); } @Test void deliveryEvidenceRequiresCurrentOrderReadPermission() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("no-evidence"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","no-evidence","emp_rights","00000000"))); var denied=authenticated("no-evidence","pw"); for(String path:List.of("/api/orders/101/delivery-evidence","/api/orders/101/delivery-evidence/201/images/signature","/api/orders/101/delivery-evidence/201/images/pic-1")){ mvc.perform(get(path)).andExpect(status().isUnauthorized());mvc.perform(get(path).session(denied)).andExpect(status().isForbidden()); }verify(database,never()).writeTransaction(any()); } @Test void confirmedOrderMailRequiresEntrySessionAndCsrf() throws Exception { var anon=csrf(null);var session=authenticated("existing-user","existing-password"); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("no-request-mail"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","no-request-mail","emp_rights","00000000"))); var denied=authenticated("no-request-mail","pw");var token=csrf(denied);String base="/api/orders/entry/request-mail/00000000-0000-0000-0000-000000000001"; for(String path:List.of(base,"/api/orders/entry/request-mail/recent",base+"/1/status","/api/orders/1/mail","/api/orders/1/mail/status","/api/orders/1/mail/preview/00000000-0000-0000-0000-000000000001/pdf/0")){mvc.perform(get(path)).andExpect(status().isUnauthorized());mvc.perform(get(path).session(denied)).andExpect(status().isForbidden());} for(String path:List.of(base+"/1/preview",base+"/1/send","/api/orders/1/mail/preview","/api/orders/1/mail/send")){ mvc.perform(post(path).session(anon.session()).header(anon.header(),anon.token()).contentType("application/json").content("{}")).andExpect(status().isUnauthorized()); mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(denied).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); }verify(database,never()).writeTransaction(any()); } @Test void statusMailLookupRequiresEntryPermission() throws Exception { mvc.perform(get("/api/orders/entry/status-mail?payerId=41&payment=INVOICE&vehicleTypeId=2")).andExpect(status().isUnauthorized()); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("no-entry-mail"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","no-entry-mail","emp_rights","00000000"))); var denied=authenticated("no-entry-mail","pw"); mvc.perform(get("/api/orders/entry/status-mail?payerId=41&payment=INVOICE&vehicleTypeId=2").session(denied)).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void longhaulReturnStatusRequiresSessionAndOrderReadRight() throws Exception { mvc.perform(get("/api/orders/1/bwv-return")).andExpect(status().isUnauthorized()); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("no-orders-return"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","no-orders-return","emp_rights","00000000"))); var denied=authenticated("no-orders-return","pw"); mvc.perform(get("/api/orders/1/bwv-return").session(denied)).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void courierAssignmentRequiresSessionCsrfAndBothOrderRights() throws Exception { var anon=csrf(null);var session=authenticated("existing-user","existing-password"); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("list-only-courier"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","list-only-courier","emp_rights","00001000"))); var denied=authenticated("list-only-courier","pw");var token=csrf(denied); for(String path:List.of("/api/orders/1/courier-assignment","/api/orders/1/courier-assignment/preview","/api/orders/1/courier-assignment/candidates")){ mvc.perform(post(path).session(anon.session()).header(anon.header(),anon.token()).contentType("application/json").content("{}")).andExpect(status().isUnauthorized()); mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(denied).header(token.header(),token.token()).contentType("application/json").content(path.endsWith("/candidates")?"{\"mode\":\"CURRENT\",\"radius\":0}":"{\"vehicleId\":1}")).andExpect(status().isForbidden()); } mvc.perform(get("/api/orders/1/courier-assignment/vehicles/1/orders?courierId=1")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/orders/1/courier-assignment/vehicles/1/orders?courierId=1").session(denied)).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/1/courier-assignment/classic-candidates")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/orders/1/courier-assignment/classic-candidates").session(denied)).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/1/courier-assignment/candidates/options")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/orders/1/courier-assignment/candidates/options").session(denied)).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/1/courier-assignment/vehicles")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/orders/1/courier-assignment/vehicles").session(denied)).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void mediationRequiresSessionCsrfAndBothOrderRights() throws Exception { var anonymous=csrf(null);var session=authenticated("existing-user","existing-password"); mvc.perform(get("/api/orders/1/mediation")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/orders/1/mediation/timeout")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/orders/1/mediation/automatic")).andExpect(status().isUnauthorized()); for(String rights:List.of("00001000","00000001")){ String name="mediation-"+rights; when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq(name),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account",name,"emp_rights",rights))); var denied=authenticated(name,"pw");var token=csrf(denied); mvc.perform(get("/api/orders/1/mediation").session(denied)).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/1/mediation/timeout").session(denied)).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/1/mediation/automatic").session(denied)).andExpect(status().isForbidden()); for(String path:List.of("/api/orders/1/mediation","/api/orders/1/mediation/preview")){ mvc.perform(post(path).session(anonymous.session()).header(anonymous.header(),anonymous.token()).contentType("application/json").content("{}")).andExpect(status().isUnauthorized()); mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(denied).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } } verify(database,never()).mediationWriteTransaction(any()); } @Test void rankingBatchRequiresSessionCsrfAndBothOrderRights() throws Exception { var anon=csrf(null);var session=authenticated("existing-user","existing-password"); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("list-only-ranking"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","list-only-ranking","emp_rights","00001000"))); var denied=authenticated("list-only-ranking","pw");var csrf=csrf(denied); for(String path:List.of("/api/orders/ranking-batch","/api/orders/ranking-batch/preview")){ mvc.perform(post(path).session(anon.session()).header(anon.header(),anon.token()).contentType("application/json").content("{}")).andExpect(status().isUnauthorized()); mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(denied).header(csrf.header(),csrf.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).writeTransaction(any()); } @Test void stationSortingRequiresSessionCsrfAndRights() throws Exception { var session=authenticated("existing-user","existing-password"); mvc.perform(get("/api/orders/1/station-order")).andExpect(status().isUnauthorized()); mvc.perform(put("/api/orders/1/station-order").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post("/api/orders/1/station-order/preview").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/1/station-order").session(session)).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void masterDocumentsRequireSessionAndCsrf() throws Exception { var session=authenticated("existing-user","existing-password"); mvc.perform(get("/api/postal-neighbour-import")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/zone-job-import/1")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/allergo-import/1")).andExpect(status().isUnauthorized()); for(String path:List.of("/api/allergo-import/1","/api/allergo-import/1/preview"))mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(get("/api/tour-csv-import/1")).andExpect(status().isUnauthorized()); for(String path:List.of("/api/tour-csv-import/1","/api/tour-csv-import/1/preview","/api/tour-csv-import/1?format=DHL","/api/tour-csv-import/1/preview?format=DHL","/api/tour-csv-import/1?format=TIMS","/api/tour-csv-import/1/preview?format=TIMS"))mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String path:List.of("/api/zone-job-import/1","/api/zone-job-import/1/preview","/api/zone-job-import/1?format=BMW","/api/zone-job-import/1/preview?format=BMW"))mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(get("/api/postal-neighbour-import/source?kind=general&object=1&name=a&revision=b")).andExpect(status().isUnauthorized()); for(String path:List.of("/api/postal-neighbour-import","/api/postal-neighbour-import/preview"))mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(get("/api/data-transfer")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/data-transfer/objects?kind=orders")).andExpect(status().isUnauthorized()); for(String kind:List.of("customers","couriers","vehicles","orders","employees","general")){ String remote="/api/remote-transfers/"+kind+"/1"; mvc.perform(get(remote)).andExpect(status().isUnauthorized()); mvc.perform(get(remote+"/files?alias=TEST&protocol=SFTP")).andExpect(status().isUnauthorized()); mvc.perform(post(remote).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(remote+"/11111111-1111-4111-8111-111111111111/resume").session(session)).andExpect(status().isForbidden()); String path="/api/master-documents/"+kind+"/1"; mvc.perform(get(path)).andExpect(status().isUnauthorized()); mvc.perform(get(path+"/download?name=test.txt")).andExpect(status().isUnauthorized()); mvc.perform(get(path+"/import-source?format=ZONE&name=test.csv&revision=none")).andExpect(status().isUnauthorized()); mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path+"/operations/11111111-1111-4111-8111-111111111111/resume").session(session)).andExpect(status().isForbidden()); } verify(database,never()).documentOperation(any()); verify(database,never()).documentReceiptTransaction(any()); } @Test void masterDirectoryExportsRequireSessionCsrfAndSeparateExportRights() throws Exception { var anon=csrf(null); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("list-only"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","list-only","emp_rights","11"))); var session=authenticated("list-only","pw");var token=csrf(session); for(String kind:List.of("customers","couriers")){ String path="/api/master-lists/"+kind+"/csv"; mvc.perform(post(path).session(anon.session()).header(anon.header(),anon.token()).contentType("application/json").content("{}")).andExpect(status().isUnauthorized()); mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).writeTransaction(any()); } @Test void headquartersSettingsRequireSessionCsrfAndBothEditingPermissions() throws Exception { String base="/api/administration/headquarters-settings"; mvc.perform(get(base)).andExpect(status().isUnauthorized()); var anon=csrf(null);var session=authenticated("existing-user","existing-password"); for(int missing:List.of(47,45)){ String bits="1".repeat(70);bits=bits.substring(0,missing)+"0"+bits.substring(missing+1);String name="hq-settings-"+missing; when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq(name),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account",name,"emp_rights",bits))); var denied=authenticated(name,"pw");var token=csrf(denied); for(String suffix:List.of("/HOTLINE/0/preview","/HOTLINE/0","/GLN/8/preview","/DUNS/8")){ mvc.perform(post(base+suffix).session(anon.session()).header(anon.header(),anon.token()).contentType("application/json").content("{}")).andExpect(status().isUnauthorized()); mvc.perform(post(base+suffix).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(base+suffix).session(denied).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } } verify(database,never()).writeTransaction(any()); } @Test void workspaceScopeReachesControllersWithoutChangingTheSharedSession() throws Exception { mvc.perform(get("/api/account/headquarters")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/account/headquarters/8")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); when(database.queryForList(eq(WorkspaceHeadquarters.ACTOR_SQL),eq(11L),eq(12L),eq(7L))).thenReturn(List.of(Map.of("emp_rights","1".repeat(70),"usr_account","existing-user","hq_admin",12,"hq_mnemonic","B"))); when(database.queryForList(eq("SELECT md_id FROM mandatorheadquarters WHERE hq_id=?"),eq(7L))).thenReturn(List.of(Map.of("md_id",1))); when(database.queryForList(eq(WorkspaceHeadquarters.BRANCH_SQL),eq(1L))).thenReturn(List.of(Map.of("id",7,"label","B"),Map.of("id",8,"label","HH"))); String parameter="SELECT par_id,par_value FROM parameter WHERE emp_id=0 AND hq_id=0 AND par_key=? ORDER BY par_id"; when(database.queryForList(eq(parameter),eq(CallCenterAccess.EMPLOYEES))).thenReturn(List.of(Map.of("par_id",1,"par_value","|12|"))); mvc.perform(get("/api/me").session(session).header(WorkspaceHeadquartersFilter.HEADER,"8")).andExpect(status().isOk()).andExpect(jsonPath("$.headquartersId").value(8)).andExpect(jsonPath("$.originalHeadquartersId").value(7)); mvc.perform(get("/api/customers").session(session).header(WorkspaceHeadquartersFilter.HEADER,"8")).andExpect(status().isOk()); verify(database).queryForList(contains("FROM customer cs"),eq(8L)); var saved=(SecurityContext)session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); assertThat(((LegacyUser)saved.getAuthentication().getPrincipal()).headquartersId()).isEqualTo(7); mvc.perform(get("/api/me").session(session)).andExpect(status().isOk()).andExpect(jsonPath("$.headquartersId").value(7)); mvc.perform(get("/api/account/headquarters/7").session(session).header(WorkspaceHeadquartersFilter.HEADER,"999")).andExpect(status().isOk()).andExpect(jsonPath("$.headquartersId").value(7)); mvc.perform(post("/api/orders").session(session).header(WorkspaceHeadquartersFilter.HEADER,"8").contentType("application/json").content("{}")).andExpect(status().isForbidden()); when(database.queryForList(eq(parameter),eq(CallCenterAccess.EMPLOYEES))).thenReturn(List.of()); // The HQ administrator still has the ordinary multi-HQ grant after CC24 is removed. mvc.perform(get("/api/me").session(session).header(WorkspaceHeadquartersFilter.HEADER,"8")).andExpect(status().isOk()).andExpect(jsonPath("$.headquartersId").value(8)); when(database.queryForList(eq(WorkspaceHeadquarters.ACTOR_SQL),eq(11L),eq(12L),eq(7L))).thenReturn(List.of(Map.of("emp_rights","1".repeat(10)+"0"+"1".repeat(59),"usr_account","existing-user","hq_admin",12,"hq_mnemonic","B"))); mvc.perform(get("/api/me").session(session).header(WorkspaceHeadquartersFilter.HEADER,"8")).andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("WORKSPACE_DENIED")); var token=csrf(session); mvc.perform(post("/api/orders").session(session).header(token.header(),token.token()).header(WorkspaceHeadquartersFilter.HEADER,"8").contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(get("/api/account/headquarters/7").session(session).header(WorkspaceHeadquartersFilter.HEADER,"8")).andExpect(status().isOk()); verify(database,never()).writeTransaction(any()); } @Test void callCenterRequiresBothEmployeeAndCc24AdministrationRights() throws Exception { for(int missing:List.of(3,34)){ String bits="1".repeat(70);bits=bits.substring(0,missing)+"0"+bits.substring(missing+1); String name="cc24-denied-"+missing; when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq(name),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account",name,"emp_rights",bits))); var session=authenticated(name,"pw");var csrf=csrf(session); mvc.perform(get("/api/administration/employees/20/call-center").session(session)).andExpect(status().isForbidden()); mvc.perform(put("/api/administration/employees/20/call-center").session(session).header(csrf.header(),csrf.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).callCenterWriteTransaction(any()); } @Test void personalListPreferencesRequireSessionAndCsrf() throws Exception { for(String path:List.of("/api/administration/employees/20/call-center","/api/administration/employees/20/stock-rights","/api/account/order-columns","/api/administration/employees/20/order-columns","/api/account/master-list-columns/customers","/api/administration/employees/20/master-list-columns/couriers","/api/account/list-preferences","/api/administration/employees/20/list-preferences","/api/account/parallel-lists","/api/administration/employees/20/parallel-lists","/api/account/cost-center-columns","/api/administration/employees/20/cost-center-columns")){ mvc.perform(get(path)).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); mvc.perform(put(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("no-employees"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","no-employees","emp_rights","1"))); var denied=authenticated("no-employees","pw");mvc.perform(get("/api/administration/employees/20/list-preferences").session(denied)).andExpect(status().isForbidden()); mvc.perform(get("/api/administration/employees/20/stock-rights").session(denied)).andExpect(status().isForbidden()); mvc.perform(get("/api/administration/employees/20/parallel-lists").session(denied)).andExpect(status().isForbidden()); mvc.perform(get("/api/orders?personal=true&listStatus=99").session(authenticated("existing-user","existing-password"))).andExpect(status().isBadRequest()); mvc.perform(get("/api/orders?listStatus=8").session(authenticated("existing-user","existing-password"))).andExpect(status().isBadRequest()); mvc.perform(get("/api/orders/list")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/orders/list").session(denied)).andExpect(status().isForbidden()); var docAnonymous=csrf(null);var docDenied=csrf(denied); mvc.perform(post("/api/orders/list/document-presence").session(docAnonymous.session()).header(docAnonymous.header(),docAnonymous.token()).contentType("application/json").content("{\"customers\":[1]}")).andExpect(status().isUnauthorized()); mvc.perform(post("/api/orders/list/document-presence").session(denied).header(docDenied.header(),docDenied.token()).contentType("application/json").content("{\"customers\":[1]}")).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/list/options")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/orders/list/options").session(denied)).andExpect(status().isForbidden()); var listSession=authenticated("existing-user","existing-password"); mvc.perform(get("/api/orders/list").param("filters","{\"unknown\":\"1\"}").session(listSession)).andExpect(status().isBadRequest()); mvc.perform(get("/api/orders").param("filters","{\"future\":\"1\"}").session(listSession)).andExpect(status().isBadRequest()); mvc.perform(get("/api/orders/export.csv").param("filters","{\"future\":\"1\"}").session(listSession)).andExpect(status().isBadRequest()); mvc.perform(get("/api/administration/employees/20/order-columns").session(denied)).andExpect(status().isForbidden()); verify(database,never()).employeePreferencesWriteTransaction(any()); } @Test void batchInvoiceRefreshRequiresSessionCsrfAndBothOrderRights() throws Exception { String path="/api/orders/1/invoice-items"; var anonymous=csrf(null);mvc.perform(post(path).session(anonymous.session()).header(anonymous.header(),anonymous.token()).contentType("application/json").content("{}")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(put(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("no-refresh"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","no-refresh","emp_rights","1"))); var denied=authenticated("no-refresh","pw");var token=csrf(denied); mvc.perform(post(path).session(denied).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(put(path).session(denied).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void batchEntryRequiresSessionCsrfAndEntryRight() throws Exception { mvc.perform(get("/api/order-batches?costCenterId=1")).andExpect(status().isUnauthorized()); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("no-entry"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","no-entry","emp_rights","1"))); var denied=authenticated("no-entry","pw");var token=csrf(denied); mvc.perform(get("/api/order-batches?costCenterId=1").session(denied)).andExpect(status().isForbidden()); for(String path:List.of("/api/order-batches","/api/order-batches/preview","/api/orders/entry/cost-centers/search")) { mvc.perform(post(path).session(denied).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(denied).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).orderBatchWriteTransaction(anyString(),any()); } @Test void fleetMapNeedsItsOwnRightAndOrderReadsNeedBothRights() throws Exception { mvc.perform(get("/api/administration/fleet-map")).andExpect(status().isUnauthorized()); var session=login(csrf(null),"existing-user","existing-password",200).getRequest().getSession(false); for(String path:List.of("","/places","/orders/1","/stocks/1/position","/orders/1/stations/1/position","/orders/1/assignment/1","/couriers/1/location-check")) mvc.perform(get("/api/administration/fleet-map"+path).session((MockHttpSession)session)).andExpect(status().isForbidden()); } @Test void mapOnlyAccountGetsConfiguredTilesWithoutOrderData() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("map-user"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","map-user","emp_rights","0".repeat(19)+"1"))); when(database.queryForList(eq("SELECT NOW() AS at"))).thenReturn(List.of(Map.of("at","2026-10-15 12:00:00"))); when(database.queryForList(eq("SELECT hq_name,hq_gps_lat,hq_gps_long FROM headquarters WHERE hq_id=?"),eq(7L))).thenReturn(List.of(Map.of("hq_name","Test"))); var session=authenticated("map-user","pw"); mvc.perform(get("/api/administration/fleet-map").session(session)).andExpect(status().isOk()).andExpect(jsonPath("$.tiles").value("https://tile.openstreetmap.org/{z}/{x}/{y}.png")).andExpect(jsonPath("$.orders").value(false)).andExpect(jsonPath("$.items").isEmpty()); mvc.perform(get("/api/administration").session(session)).andExpect(jsonPath("$.fleetMap").value(true)); mvc.perform(get("/api/administration/fleet-map/orders/1").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/administration/fleet-map/orders/1/stations/1/position").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/administration/fleet-map/orders/1/assignment/1").session(session)).andExpect(status().isForbidden()); var csrf=csrf(session); mvc.perform(get("/api/administration/fleet-map/couriers/1/location-check").session(session)).andExpect(status().isForbidden()); mvc.perform(post("/api/administration/fleet-map/couriers/1/location-check").session(session).header(csrf.header(),csrf.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post("/api/administration/fleet-map/orders/1/assignment/1").session(session).header(csrf.header(),csrf.token()).contentType("application/json").content("{}" )).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } record Csrf(MockHttpSession session, String token, String header) {} @Test void employeeChangesRequireSessionCsrfAndCurrentEmployeeRight() throws Exception { mvc.perform(get("/api/administration/employees")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); for(String part:List.of("profile","rights"))mvc.perform(put("/api/administration/employees/20/"+part).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); var token=csrf(session); when(database.queryForList(eq(LegacySessionRightsFilter.SQL),any(),any(),any())).thenReturn(List.of(Map.of("usr_account","existing-user","emp_rights","1"))); mvc.perform(get("/api/administration/employees").session(session)).andExpect(status().isForbidden()); for(String part:List.of("profile","rights"))mvc.perform(put("/api/administration/employees/20/"+part).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).employeeWriteTransaction(any()); } @Test void sessionsLoseRevokedRightsAndDeletedAccountsCannotContinue() throws Exception { var session=authenticated("existing-user","existing-password"); when(database.queryForList(eq(LegacySessionRightsFilter.SQL),any(),any(),any())).thenReturn(List.of(Map.of("usr_account","existing-user","emp_rights","0".repeat(70)))); mvc.perform(get("/api/orders").session(session)).andExpect(status().isForbidden()); // The shared session principal itself remains untouched by a request in another tab. var context=(SecurityContext)session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); assertThat(((LegacyUser)context.getAuthentication().getPrincipal()).hasRight(4)).isTrue(); when(database.queryForList(eq(LegacySessionRightsFilter.SQL),any(),any(),any())).thenReturn(List.of()); mvc.perform(get("/api/orders").session(session)).andExpect(status().isUnauthorized()); assertThat(session.isInvalid()).isTrue(); } @Test void unavailableRightsCheckFailsClosedAndNewGrantsRequireLogin() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("limited"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","limited","emp_rights","1"))); var session=authenticated("limited","pw"); when(database.queryForList(eq(LegacySessionRightsFilter.SQL),any(),any(),any())).thenReturn(List.of(Map.of("usr_account","limited","emp_rights","1".repeat(70)))); mvc.perform(get("/api/administration/employees").session(session)).andExpect(status().isForbidden()); when(database.queryForList(eq(LegacySessionRightsFilter.SQL),any(),any(),any())).thenThrow(new DataAccessResourceFailureException("connection failed")); mvc.perform(get("/api/customers").session(session)).andExpect(status().isServiceUnavailable()); } @Test void changedCredentialsInvalidateAnExistingSession() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("existing-user"),eq("existing-password"),eq("existing-password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","existing-user","emp_rights","111111111111","credential_stamp","original"))); var session=authenticated("existing-user","existing-password"); when(database.queryForList(eq(LegacySessionRightsFilter.SQL),any(),any(),any())).thenReturn(List.of(Map.of("usr_account","existing-user","emp_rights","111111111111","credential_stamp","changed"))); mvc.perform(get("/api/me").session(session)).andExpect(status().isUnauthorized());assertThat(session.isInvalid()).isTrue(); } @Test void warehouseAccountsRequireSessionCsrfAndEmployeeRight() throws Exception { mvc.perform(get("/api/administration/warehouse-users")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password");var token=csrf(session); for(String path:List.of("/save","/108/delete"))mvc.perform(post("/api/administration/warehouse-users"+path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String path:List.of("/108/password","/108/stock-rights"))mvc.perform(put("/api/administration/warehouse-users"+path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); when(database.queryForList(eq(LegacySessionRightsFilter.SQL),any(),any(),any())).thenReturn(List.of(Map.of("usr_account","existing-user","emp_rights","1"))); mvc.perform(get("/api/administration/warehouse-users").session(session)).andExpect(status().isForbidden()); for(String path:List.of("/save","/108/delete"))mvc.perform(post("/api/administration/warehouse-users"+path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String path:List.of("/108/password","/108/stock-rights"))mvc.perform(put("/api/administration/warehouse-users"+path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).warehouseUserWriteTransaction(any()); } @Test void accountMutationsRequireSessionCsrfAndEmployeeRight() throws Exception { var session=authenticated("existing-user","existing-password");var token=csrf(session); mvc.perform(post("/api/administration/employees").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post("/api/administration/employees/102/delete").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(put("/api/administration/employees/102/password").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); when(database.queryForList(eq(LegacySessionRightsFilter.SQL),any(),any(),any())).thenReturn(List.of(Map.of("usr_account","existing-user","emp_rights","1"))); mvc.perform(post("/api/administration/employees").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post("/api/administration/employees/102/delete").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(put("/api/administration/employees/102/password").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).employeeWriteTransaction(any()); } @Test void anActivatedSecondFactorCannotBeSkippedOrSuppliedThroughAHeader() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("existing-user"),eq("existing-password"),eq("existing-password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","existing-user","emp_rights","111111111111","usr_totp_activated",1,"usr_totp_secret",LegacyTotpTest.KEY))); var token=csrf(null);login(token,"existing-user","existing-password",401); mvc.perform(get("/api/me").session(token.session())).andExpect(status().isUnauthorized()); mvc.perform(post("/api/login").session(token.session()).header(token.header(),token.token()).header("otp",LegacyTotp.code(LegacyTotpTest.KEY,java.time.Instant.now().getEpochSecond())).contentType("application/x-www-form-urlencoded").param("username","existing-user").param("password","existing-password")).andExpect(status().isUnauthorized()); verify(database,never()).authenticationWriteTransaction(any()); } @Test void ownAccountIsAvailableWithoutEmployeeRightsButCannotSelectAnotherPerson() throws Exception { var session=authenticated("existing-user","existing-password"); when(database.queryForList(eq(LegacySessionRightsFilter.SQL),any(),any(),any())).thenReturn(List.of(Map.of("usr_account","existing-user","emp_rights","0".repeat(70)))); when(database.queryForList(eq(EmployeeAdministration.PERSON_SQL+" AND employee.emp_id=? AND user.usr_id=? AND user.hq_id=?"),eq(12L),eq(11L),eq(7L))).thenReturn(List.of(Map.of("emp_id",12,"usr_id",11,"hq_id",7,"emp_rights","0".repeat(70),"emp_headquarters","7","usr_name","Test","usr_firstname","Mara","usr_account","existing-user"))); when(database.queryForList(eq("SELECT md_id FROM mandatorheadquarters WHERE hq_id=? AND md_id>0"),eq(7L))).thenReturn(List.of(Map.of("md_id",1))); when(database.queryForList(startsWith("SELECT headquarters.hq_id AS id,headquarters.hq_mnemonic AS label"),eq(1L))).thenReturn(List.of(Map.of("id",7,"label","B","administrator",99))); when(database.queryForList(eq("SELECT md_id FROM mandatorheadquarters WHERE hq_id=?"),eq(7L))).thenReturn(List.of(Map.of("md_id",1))); when(database.queryForList(eq("SELECT "+LegacyCredentialState.EXPRESSION+" AS credential_stamp,usr_password_modify,usr_totp_activated FROM user WHERE usr_id=?"),eq(11L))).thenReturn(List.of(Map.of("credential_stamp","server-only","usr_totp_activated",0))); mvc.perform(get("/api/account?employee=999").session(session)).andExpect(status().isOk()).andExpect(jsonPath("$.profile.id").value(12)).andExpect(jsonPath("$.canPassword").value(true)).andExpect(jsonPath("$.canDelete").value(false)).andExpect(jsonPath("$.credential_stamp").doesNotExist()); mvc.perform(get("/api/account/two-factor").session(session)).andExpect(status().isOk()).andExpect(header().string("Cache-Control",org.hamcrest.Matchers.containsString("no-store"))).andExpect(jsonPath("$.canSetup").value(false)).andExpect(jsonPath("$.secret").doesNotExist()); mvc.perform(get("/api/administration/employees/12/access").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/administration/employees/12/history").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/administration/employees/12/two-factor").session(session)).andExpect(status().isForbidden()); var token=csrf(session); mvc.perform(post("/api/administration/employees/12/two-factor/reset").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).employeeWriteTransaction(any()); } @Test void selfServiceAndTwoFactorMutationsRequireSessionAndCsrf() throws Exception { mvc.perform(get("/api/administration/employees/12/history")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/account")).andExpect(status().isUnauthorized());mvc.perform(get("/api/account/two-factor")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); for(String part:List.of("profile","password"))mvc.perform(put("/api/account/"+part).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String part:List.of("setup","activate","cancel","reset"))mvc.perform(post("/api/account/two-factor/"+part).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post("/api/administration/employees/12/two-factor/reset").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).employeeWriteTransaction(any()); } @Test void customerLoginIsConfinedToCustomerPathsAndRevokedOnChange() throws Exception { mvc.perform(get("/api/customer-portal")).andExpect(status().isUnauthorized()); when(database.queryForList(eq(LegacyAuthenticationProvider.CUSTOMER_CANDIDATE_SQL),eq("customer-test"),eq("isolated-password"),eq("isolated-password"))).thenReturn(List.of(Map.of("usr_id",31,"emp_id",32,"hq_id",7))); var row=Map.of("usr_id",31,"emp_id",32,"hq_id",7,"usr_account","customer-test","emp_rights","1".repeat(70),"credential_stamp","customer-stamp"); when(database.queryForList(eq(CustomerPortalScope.ACCOUNT_SQL),any(),any(),any())).thenReturn(List.of(row)); var session=authenticated("customer-test","isolated-password");var token=csrf(session); mvc.perform(get("/api/me").session(session)).andExpect(status().isOk()).andExpect(jsonPath("$.accountType").value(2)).andExpect(jsonPath("$.roles[0]").value("CUSTOMER")).andExpect(jsonPath("$.capabilities.readOrders").value(false)).andExpect(jsonPath("$.capabilities.writeCustomers").value(false)).andExpect(jsonPath("$.credential_stamp").doesNotExist()); for(String path:List.of("/api/account","/api/administration","/api/orders","/api/customers","/api/invoices","/api/warehouse-portal","/api/courier-portal","/api/account/headquarters"))mvc.perform(get(path).session(session)).andExpect(status().isForbidden()); for(String path:List.of("/api/orders/entry","/api/administration/warehouse-users/save","/api/administration/warehouse-movements/save"))mvc.perform(post(path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String path:List.of("/api/customer-portal/entry","/api/customer-portal/entry/quote","/api/customer-portal/query","/api/customer-portal/detail","/api/customer-portal/centers/preview","/api/customer-portal/centers/save","/api/customer-portal/employees/query","/api/customer-portal/employees/save","/api/customer-portal/two-factor/setup","/api/customer-portal/two-factor/activate","/api/customer-portal/two-factor/reset","/api/customer-portal/two-factor/cancel","/api/customer-portal/statement","/api/customer-portal/statement/detail","/api/customer-portal/orders/100/cancellation","/api/customer-portal/orders/100/texts","/api/customer-portal/orders/100/texts/preview","/api/customer-portal/orders/100/edit","/api/customer-portal/orders/100/edit/preview","/api/customer-portal/statistics/query","/api/customer-portal/statistics/download","/api/customer-portal/exports/preview","/api/customer-portal/exports/download","/api/customer-portal/exports/save","/api/customer-portal/export-storage","/api/customer-portal/export-storage/00000000-0000-0000-0000-000000000001/resume","/api/customer-portal/export-storage/00000000-0000-0000-0000-000000000001/cancel","/api/customer-portal/export-removal","/api/customer-portal/export-removal/00000000-0000-0000-0000-000000000001/resume","/api/customer-portal/export-removal/00000000-0000-0000-0000-000000000001/restore")){ mvc.perform(post(path).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } for(String method:List.of("PUT","DELETE","PATCH"))mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request(org.springframework.http.HttpMethod.valueOf(method),"/api/customer-portal/detail").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String header:List.of("X-Workspace-Headquarters","X-Order-Headquarters"))mvc.perform(get("/api/me").session(session).header(header,"7")).andExpect(status().isForbidden()); when(database.queryForList(eq(CustomerPortalScope.ACCOUNT_SQL),any(),any(),any())).thenReturn(List.of(Map.of("credential_stamp","changed"))); mvc.perform(get("/api/me").session(session)).andExpect(status().isUnauthorized()); } @Test void courierLoginCannotReachOtherWorkspacesAndIsRevokedOnChange() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.COURIER_CANDIDATE_SQL),eq("courier-test"),eq("isolated-password"),eq("isolated-password"))).thenReturn(List.of(Map.of("usr_id",21,"hq_id",7))); var row=Map.of("usr_id",21,"hq_id",7,"cr_id",700,"usr_account","courier-test","emp_rights","1".repeat(70),"credential_stamp","courier-stamp"); when(database.queryForList(eq(CourierPortalScope.ACCOUNT_SQL),any(),any(),any())).thenReturn(List.of(row)); var session=authenticated("courier-test","isolated-password");var token=csrf(session); mvc.perform(get("/api/me").session(session)).andExpect(status().isOk()).andExpect(jsonPath("$.accountType").value(3)).andExpect(jsonPath("$.roles[0]").value("COURIER")).andExpect(jsonPath("$.capabilities.readOrders").value(false)).andExpect(jsonPath("$.capabilities.writeCustomers").value(false)); for(String path:List.of("/api/account","/api/administration","/api/orders","/api/customers","/api/invoices","/api/warehouse-portal","/api/account/headquarters","/api/administration/warehouse-users"))mvc.perform(get(path).session(session)).andExpect(status().isForbidden()); for(String path:List.of("/api/orders/entry","/api/administration/warehouse-users/save","/api/administration/warehouse-movements/save"))mvc.perform(post(path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String path:List.of("/api/courier-portal/query","/api/courier-portal/remark","/api/courier-portal/password","/api/courier-portal/history","/api/courier-portal/bourse/query","/api/courier-portal/bourse/detail","/api/courier-portal/disposition/job/remark"))mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(get("/api/courier-portal/disposition/job?vehicle=101&day=2026-10-19&job=20")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/courier-portal/disposition/job?vehicle=101&day=2026-10-19&job=20").session(session)).andExpect(status().isForbidden()); mvc.perform(post("/api/courier-portal/disposition/job/remark").session(session).header(token.header(),token.token()).contentType("application/json").content("{\"requestId\":\"00000000-0000-0000-0000-000000000001\",\"value\":\"test\"}")).andExpect(status().isForbidden()); for(String path:List.of("/api/courier-portal/disposition/rollcard","/api/courier-portal/disposition/rollcard/pdf")){ mvc.perform(get(path+"?vehicle=101&day=2026-10-19&revision=fixture")).andExpect(status().isUnauthorized()); mvc.perform(get(path+"?vehicle=101&day=2026-10-19&revision=fixture").session(session)).andExpect(status().isForbidden()); mvc.perform(post(path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } String courierMail="/api/courier-portal/disposition/101/rollcard/mail",courierMailToken="00000000-0000-0000-0000-000000000001"; for(String suffix:List.of("","/"+courierMailToken,"/"+courierMailToken+"/page")){ mvc.perform(get(courierMail+suffix+"?day=2026-10-19&page=0")).andExpect(status().isUnauthorized()); mvc.perform(get(courierMail+suffix+"?day=2026-10-19&page=0").session(session)).andExpect(status().isForbidden()); } for(String suffix:List.of("/preview","/"+courierMailToken)){ mvc.perform(post(courierMail+suffix).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(courierMail+suffix).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).courierRollcardMailWriteTransaction(any()); mvc.perform(get("/api/courier-portal/disposition/planning?vehicle=101&day=2026-10-19")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/courier-portal/disposition/planning?vehicle=101&day=2026-10-19").session(session)).andExpect(status().isForbidden()); for(String path:List.of("/api/courier-portal/disposition/planning","/api/courier-portal/disposition/planning/preview")){ mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).courierPlanningWriteTransaction(any()); mvc.perform(get("/api/courier-portal/disposition/handover?vehicle=101&day=2026-10-19&job=20")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/courier-portal/disposition/handover?vehicle=101&day=2026-10-19&job=20").session(session)).andExpect(status().isForbidden()); for(String path:List.of("/api/courier-portal/disposition/handover","/api/courier-portal/disposition/handover/preview")){ mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } mvc.perform(get("/api/courier-portal/disposition/replanning?vehicle=101&day=2026-10-19&job=10")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/courier-portal/disposition/replanning?vehicle=101&day=2026-10-19&job=10").session(session)).andExpect(status().isForbidden()); for(String path:List.of("/api/courier-portal/disposition/replanning","/api/courier-portal/disposition/replanning/preview")){ mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).courierReplanningWriteTransaction(any()); mvc.perform(get("/api/courier-portal/disposition/cards?vehicle=101&day=2026-10-19")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/courier-portal/disposition/cards?vehicle=101&day=2026-10-19").session(session)).andExpect(status().isForbidden()); mvc.perform(post("/api/courier-portal/disposition/cards").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(get("/api/administration/vehicle-disposition/cards?vehicles=101&day=2026-10-19").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/administration/vehicle-disposition/unplanned-cards?day=2026-10-19").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/courier-portal/disposition/job-search?q=20")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/courier-portal/disposition/job-search?q=20").session(session)).andExpect(status().isForbidden()); mvc.perform(post("/api/courier-portal/disposition/job-search").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(get("/api/courier-portal/disposition?vehicle=101").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/courier-portal/disposition")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/courier-portal/disposition").session(session)).andExpect(status().isForbidden()); mvc.perform(post("/api/courier-portal/disposition").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String header:List.of("X-Workspace-Headquarters","X-Order-Headquarters"))mvc.perform(get("/api/me").session(session).header(header,"7")).andExpect(status().isForbidden()); when(database.queryForList(eq(CourierPortalScope.ACCOUNT_SQL),any(),any(),any())).thenReturn(List.of(Map.of("credential_stamp","changed")));mvc.perform(get("/api/me").session(session)).andExpect(status().isUnauthorized()); verify(database,never()).courierPortalWriteTransaction(any());verify(database,never()).courierDispositionWriteTransaction(any()); } @Test void warehouseLoginIsConfinedToItsPortalAndRevokedWithTheAccount() throws Exception { var row=Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","warehouse-test","emp_rights","1".repeat(70),"credential_stamp","portal-stamp"); when(database.queryForList(eq(LegacyAuthenticationProvider.PORTAL_AUTH_SQL),eq("warehouse-test"),eq("isolated-password"),eq("isolated-password"))).thenReturn(List.of(row)); when(database.queryForList(eq(WarehousePortalScope.ACCOUNT_SQL),any(),any(),any())).thenReturn(List.of(row)); var session=authenticated("warehouse-test","isolated-password");var token=csrf(session); mvc.perform(get("/api/me").session(session)).andExpect(status().isOk()).andExpect(jsonPath("$.accountType").value(4)).andExpect(jsonPath("$.roles[0]").value("WAREHOUSE")).andExpect(jsonPath("$.capabilities.readOrders").value(false)).andExpect(jsonPath("$.capabilities.writeCustomers").value(false)).andExpect(jsonPath("$.credential_stamp").doesNotExist()); for(String path:List.of("/api/account","/api/administration","/api/orders","/api/customers","/api/invoices","/api/administration/warehouse-details/10","/api/account/headquarters","/api/administration/articles/options","/api/administration/article-groups","/api/account/article-columns","/api/administration/warehouse-users"))mvc.perform(get(path).session(session)).andExpect(status().isForbidden()); for(String path:List.of("/api/administration/warehouses/save","/api/administration/warehouses/preview","/api/administration/warehouse-details/save","/api/administration/warehouse-journal/10/columns","/api/administration/warehouse-journal/csv","/api/administration/warehouse-inventory/csv","/api/orders/entry","/api/administration/warehouse-users/save"))mvc.perform(post(path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String method:List.of("PUT","DELETE","PATCH"))mvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request(org.springframework.http.HttpMethod.valueOf(method),"/api/administration/warehouse-movements/save").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String headerName:List.of("X-Workspace-Headquarters","X-Order-Headquarters"))mvc.perform(get("/api/me").session(session).header(headerName,"7")).andExpect(status().isForbidden()); mvc.perform(post("/api/administration/warehouse-movements/save").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).warehouseMovementTransaction(anyBoolean(),any());verify(database,never()).warehouseWriteTransaction(any()); when(database.queryForList(eq(WarehousePortalScope.ACCOUNT_SQL),any(),any(),any())).thenReturn(List.of(Map.of("credential_stamp","changed"))); mvc.perform(get("/api/me").session(session)).andExpect(status().isUnauthorized()); when(database.queryForList(eq(WarehousePortalScope.ACCOUNT_SQL),any(),any(),any())).thenReturn(List.of(row)); var second=authenticated("warehouse-test","isolated-password");when(database.queryForList(eq(WarehousePortalScope.ACCOUNT_SQL),any(),any(),any())).thenReturn(List.of());mvc.perform(get("/api/me").session(second)).andExpect(status().isUnauthorized()); } @Test void transferLoginIsConfinedToFixedFilesAndRevokedOnMappingChange() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.TRANSFER_CANDIDATE_SQL),eq("transfer-test"),eq("isolated-password"),eq("isolated-password"))).thenReturn(List.of(Map.of("usr_id",31,"hq_id",7))); var row=Map.of("usr_id",31,"emp_id",0,"hq_id",7,"usr_type",101,"usr_account","transfer-test","emp_rights","","credential_stamp","transfer-stamp"); when(database.queryForList(eq(TransferPortalScope.ACCOUNT_SQL),any(),any(),any())).thenReturn(List.of(row)); String config="SELECT par_value FROM parameter WHERE emp_id=0 AND hq_id=0 AND par_key='DATATRANSFER_USERTYPE_SPECIAL'"; when(database.queryForList(config)).thenReturn(List.of(Map.of("par_value","101,7,TEST,CS,1,fixed/B"))); var session=authenticated("transfer-test","isolated-password");var token=csrf(session); mvc.perform(get("/api/me").session(session)).andExpect(status().isOk()).andExpect(jsonPath("$.accountType").value(101)).andExpect(jsonPath("$.roles[0]").value("TRANSFER")).andExpect(jsonPath("$.capabilities.readOrders").value(false)).andExpect(jsonPath("$.capabilities.readTransfers").value(false)).andExpect(jsonPath("$.credential_stamp").doesNotExist()); for(String path:List.of("/api/account","/api/administration","/api/orders","/api/customers","/api/invoices","/api/warehouse-portal","/api/customer-portal","/api/data-transfer","/api/master-documents/customers/1","/api/master-documents/transfer/31/import-source","/api/remote-transfers/customers/1"))mvc.perform(get(path).session(session)).andExpect(status().isForbidden()); for(String path:List.of("/api/orders/entry","/api/administration/warehouses/save","/api/master-documents/customers/1"))mvc.perform(post(path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String path:List.of("/api/master-documents/transfer/31","/api/master-documents/transfer/31/operations/00000000-0000-0000-0000-000000000001/resume","/api/remote-transfers/transfer/31","/api/remote-transfers/transfer/31/00000000-0000-0000-0000-000000000001/resume"))mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String header:List.of("X-Workspace-Headquarters","X-Order-Headquarters"))mvc.perform(get("/api/me").session(session).header(header,"7")).andExpect(status().isForbidden()); when(database.queryForList(config)).thenReturn(List.of(Map.of("par_value","101,7,TEST,CS,2,fixed/B")));mvc.perform(get("/api/me").session(session)).andExpect(status().isForbidden()); when(database.queryForList(config)).thenReturn(List.of(Map.of("par_value","101,7,TEST,CS,1,fixed/B"))); when(database.queryForList(eq(TransferPortalScope.ACCOUNT_SQL),any(),any(),any())).thenReturn(List.of(Map.of("credential_stamp","changed")));mvc.perform(get("/api/me").session(session)).andExpect(status().isUnauthorized()); verify(database,never()).documentOperation(any()); } @BeforeEach void configure() { when(database.queryForList(eq(LegacySessionRightsFilter.SQL),any(),any(),any())).thenAnswer(call -> { var auth=org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication(); var u=(LegacyUser)auth.getPrincipal(); return List.of(Map.of("usr_account",u.username(),"emp_rights",u.rights())); }); when(database.readTransaction(any())).thenAnswer(call -> ((Supplier)call.getArgument(0)).get()); when(database.checkConnection()).thenReturn(new DatabaseService.ConnectionCheck(true,"CONNECTED","Verbunden")); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("existing-user"),eq("existing-password"),eq("existing-password"))) .thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","existing-user","emp_rights","111111111111"))); } Csrf csrf(MockHttpSession session) throws Exception { var request = get("/api/csrf"); if (session != null) request.session(session); MvcResult response = mvc.perform(request).andExpect(status().isOk()) .andExpect(header().string("Cache-Control", org.hamcrest.Matchers.containsString("no-store"))) .andReturn(); var body = json.readTree(response.getResponse().getContentAsString()); return new Csrf((MockHttpSession) response.getRequest().getSession(false), body.get("token").asText(), body.get("headerName").asText()); } MvcResult login(Csrf csrf, String username, String password, int expectedStatus) throws Exception { // Represents upstream htaccess authentication: VoTiAn must ignore this header. return mvc.perform(post("/api/login").session(csrf.session()).header(csrf.header(),csrf.token()) .with(httpBasic("proxy-fixture","proxy-password")) .contentType("application/x-www-form-urlencoded").param("username",username).param("password",password)) .andExpect(status().is(expectedStatus)).andExpect(header().doesNotExist("WWW-Authenticate")).andReturn(); } MockHttpSession authenticated(String username, String password) throws Exception { return (MockHttpSession) login(csrf(null),username,password,200).getRequest().getSession(false); } @Test void crmAndMailMutationsRequireSessionAndCsrf() throws Exception { mvc.perform(get("/api/crm/options")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); for(String path:List.of("/api/crm/contacts","/api/crm/targets","/api/appointments/44/notifications/preview","/api/appointments/notifications/test/send")) mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any());verify(database,never()).contactWriteTransaction(any()); } @Test void messagesAndFormMutationsRequireSessionAndCsrf() throws Exception { for(String path:List.of("/api/messages/options","/api/forms","/api/newsletters/options","/api/newsletters/mail/options","/api/evaluations/options")) mvc.perform(get(path)).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); for(String path:List.of("/api/messages/bulletins","/api/messages/preview","/api/messages/send/token","/api/messages/groups","/api/forms/categories","/api/forms/fields","/api/forms/templates","/api/newsletters","/api/newsletters/images","/api/newsletters/12/mail/preview","/api/newsletters/mail/token/send","/api/evaluations","/api/evaluations/12/mail/preview","/api/evaluations/mail/token/send")) mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(put("/api/forms/categories/10/fields").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); var token=csrf(session); mvc.perform(get("/api/forms").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/newsletters/options").session(session)).andExpect(status().isForbidden()); mvc.perform(post("/api/messages/bulletins").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void extrasRequireSessionCsrfAndDedicatedRight() throws Exception { mvc.perform(get("/api/administration/extras/options")).andExpect(status().isUnauthorized()); mvc.perform(post("/api/administration/extras/query").contentType("application/json").content("{}")).andExpect(status().isForbidden()); var session=authenticated("existing-user","existing-password");var token=csrf(session); for(String path:List.of("options","customers","pzm-comparison/11111111-1111-4111-8111-111111111111","customer-settings/PHOTO/1","customer-settings/SERVICE_PRICE/1","portal-settings/MASK/1","portal-settings/LIST/101","lead-time","routing-settings/PZM","routing-settings/BWV","pzm-zones","recurring/1?customer=1&asOf=2026-10-14"))mvc.perform(get("/api/administration/extras/"+path).session(session)).andExpect(status().isForbidden()); for(String path:List.of("query","csv","pzm-comparison","pzm-comparison/11111111-1111-4111-8111-111111111111/next","pzm-comparison/11111111-1111-4111-8111-111111111111/csv","templates/preview","templates","customer-settings/preview","customer-settings","portal-settings/preview","portal-settings","lead-time/preview","lead-time","routing-settings/preview","routing-settings","pzm-zones/preview","pzm-zones","pzm-zones/operations/11111111-1111-4111-8111-111111111111")){ mvc.perform(post("/api/administration/extras/"+path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post("/api/administration/extras/"+path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).writeTransaction(any()); } @Test void savedToursRequireEditRightsInAdditionToExtras() throws Exception { for(String rights:List.of("0".repeat(46)+"1","1".repeat(70))){ when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("existing-user"),eq("existing-password"),eq("existing-password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","existing-user","emp_rights",rights))); var session=authenticated("existing-user","existing-password");var token=csrf(session); for(String path:List.of("templates/preview","templates","customer-settings/preview","customer-settings","portal-settings/preview","portal-settings","lead-time/preview","lead-time","routing-settings/preview","routing-settings","pzm-zones/preview","pzm-zones")){ mvc.perform(post("/api/administration/extras/"+path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post("/api/administration/extras/"+path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().is(rights.startsWith("0")?403:400)); } } verify(database,never()).writeTransaction(any()); } @Test void zoneTariffsAndManagementRequireSessionCsrfAndAdministrationRight() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("existing-user"),eq("existing-password"),eq("existing-password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","existing-user","emp_rights","1"))); mvc.perform(get("/api/administration/zone-prices")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); for(String path:List.of("/api/administration/zone-prices","/api/administration/zone-prices/management")) mvc.perform(put(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); var token=csrf(session); mvc.perform(put("/api/administration/zone-prices/management").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void tariffMutationsRequireSessionCsrfAndGeneralPriceRight() throws Exception { mvc.perform(get("/api/administration/service-prices")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); mvc.perform(put("/api/administration/service-prices").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(put("/api/administration/service-prices/definitions").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); var token=csrf(session); mvc.perform(post("/api/administration/service-prices/query").session(session).header(token.header(),token.token()).contentType("application/json").content("{\"mode\":0,\"customer\":0,\"date\":\"2026-10-13\"}")).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void filterAdministrationRequiresSessionCsrfAndManageRight() throws Exception { mvc.perform(get("/api/administration/filters")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); mvc.perform(get("/api/administration/filters").session(session)).andExpect(status().isOk()).andExpect(jsonPath("$.manage").value(false)); mvc.perform(post("/api/administration/filters").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); var token=csrf(session); mvc.perform(post("/api/administration/filters").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(delete("/api/administration/filters/1").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post("/api/administration/filters/operations/00000000-0000-0000-0000-000000000000/resume").session(session).header(token.header(),token.token())).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void addressAdministrationRequiresSessionCsrfAndHeadquartersRight() throws Exception { mvc.perform(get("/api/administration/addresses/POSTAL")).andExpect(status().isUnauthorized()); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("existing-user"),eq("existing-password"),eq("existing-password"))) .thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","existing-user","emp_rights","1"))); var session=authenticated("existing-user","existing-password");var token=csrf(session); mvc.perform(get("/api/administration/addresses/POSTAL").session(session)).andExpect(status().isForbidden()); for(String path:List.of("/api/administration/addresses/POSTAL","/api/administration/addresses/AREA/remove","/api/administration/addresses/AREA/query","/api/administration/addresses/AREA/operations/00000000-0000-0000-0000-000000000000/resume")) { mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); mvc.perform(post(path).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).writeTransaction(any()); } @Test void customerDocumentsRequireSessionCsrfAndRights() throws Exception { String base="/api/master/customers/9/service-documents"; for(String path:List.of(base,base+"/protocol/0",base+"/reasons/0",base+"/prices/20",base+"/prices/20/history"))mvc.perform(get(path)).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); for(String path:List.of(base+"/protocol/0",base+"/reasons/0",base+"/prices/20"))mvc.perform(put(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("none"),eq("password"),eq("password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","none","emp_rights","0000000000"))); var restricted=authenticated("none","password");var token=csrf(restricted); for(String path:List.of(base+"/protocol/0",base+"/reasons/0",base+"/prices/20"))mvc.perform(put(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).customerDocumentWriteTransaction(any()); } @Test void customerServiceImportsRequireSessionCsrfAndRights() throws Exception { String base="/api/master/customers/9/service-imports"; mvc.perform(get(base)).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); for(String path:List.of(base,base+"/preview"))mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("none"),eq("password"),eq("password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","none","emp_rights","0000000000"))); var restricted=authenticated("none","password");var token=csrf(restricted); mvc.perform(get(base).session(restricted)).andExpect(status().isForbidden()); mvc.perform(post(base+"/preview").session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void reportCustomerFinanceRequiresSessionAndBothReportAndCustomerRights() throws Exception { String path="/api/administration/reports/CUSTOMER/customers/1/finance"; mvc.perform(get(path)).andExpect(status().isUnauthorized()); for(int only:List.of(0,16,5)){ char[] rights="0".repeat(70).toCharArray();rights[only]='1';String account="finance-"+only; when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq(account),eq("password"),eq("password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account",account,"emp_rights",new String(rights)))); mvc.perform(get(path).session(authenticated(account,"password"))).andExpect(status().isForbidden()); } verify(database,never()).queryForList(startsWith("SELECT COALESCE(SUM(CASE"),any(Object[].class)); } @Test void masterFormsRequireSessionCsrfAndObjectRights() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("none"),eq("password"),eq("password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","none","emp_rights","0000000000"))); var restricted=authenticated("none","password");var token=csrf(restricted); for(String kind:List.of("customers","couriers","vehicles")){ String base="/api/master/"+kind+"/1/forms"; for(String path:List.of(base,base+"/3",base+"/3/history")){mvc.perform(get(path)).andExpect(status().isUnauthorized());mvc.perform(get(path).session(restricted)).andExpect(status().isForbidden());} mvc.perform(put(base+"/3").session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); mvc.perform(put(base+"/3").session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).masterFormWriteTransaction(any()); } @Test void customerCourierRelationsRequireSessionCsrfAndCourierRight() throws Exception { String base="/api/master/relations"; for(String path:List.of(base+"/customers/1",base+"/couriers/1",base+"/search?kind=customers",base+"/history?customer=1&courier=1"))mvc.perform(get(path)).andExpect(status().isUnauthorized()); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("none"),eq("password"),eq("password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","none","emp_rights","0000000000"))); var restricted=authenticated("none","password");var token=csrf(restricted); for(String path:List.of(base+"/customers/1",base+"/couriers/1",base+"/search?kind=customers",base+"/history?customer=1&courier=1"))mvc.perform(get(path).session(restricted)).andExpect(status().isForbidden()); for(String path:List.of(base,base+"/preview",base+"/operations/00000000-0000-0000-0000-000000000000")){ mvc.perform(post(path).session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); mvc.perform(post(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).customerRelationWriteTransaction(any()); } @Test void warehouseAdministrationRequiresItsOwnRightAndCsrf() throws Exception { var restricted=authenticated("existing-user","existing-password");var token=csrf(restricted); mvc.perform(get("/api/administration/warehouses")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/administration/warehouses").session(restricted)).andExpect(status().isForbidden()); for(String path:List.of("/api/administration/warehouses/preview","/api/administration/warehouses/save","/api/administration/warehouse-inventory/query","/api/administration/warehouse-inventory/csv")){ mvc.perform(post(path).session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } mvc.perform(get("/api/administration/warehouse-journal/options?stock=10")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/administration/warehouse-journal/options?stock=10").session(restricted)).andExpect(status().isForbidden()); for(String path:List.of("/api/administration/warehouse-journal/query","/api/administration/warehouse-journal/csv")){ mvc.perform(post(path).session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } mvc.perform(put("/api/administration/warehouse-journal/10/columns").session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(put("/api/administration/warehouse-journal/10/columns").session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String path:List.of("/api/administration/warehouse-movements/options?stock=10","/api/administration/warehouse-movements/articles?stock=10","/api/administration/warehouse-movements/serial?stock=10&article=1&serial=A","/api/administration/warehouse-movements/bundle?stock=10&article=1&quantity=1&to=10","/api/administration/warehouse-movements/journal/1?stock=10&mode=RETURN")){ mvc.perform(get(path)).andExpect(status().isUnauthorized());mvc.perform(get(path).session(restricted)).andExpect(status().isForbidden()); } for(String path:List.of("/api/administration/warehouse-movements/preview","/api/administration/warehouse-movements/save")){ mvc.perform(post(path).session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).warehouseMovementTransaction(any()); for(String path:List.of("/api/administration/warehouse-details/11","/api/administration/warehouse-details/11/cost-centers?headquarters=7","/api/administration/warehouse-details/11/address-suggestions?q=Test&country=DE&sessionToken=x","/api/administration/warehouse-details/11/address-details?placeId=x&sessionToken=x")){ mvc.perform(get(path)).andExpect(status().isUnauthorized());mvc.perform(get(path).session(restricted)).andExpect(status().isForbidden()); } for(String path:List.of("/api/administration/warehouse-details/preview","/api/administration/warehouse-details/save")){ mvc.perform(post(path).session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden());mvc.perform(post(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } for(String path:List.of("/api/administration/warehouse-barcodes?stock=10","/api/administration/warehouse-barcodes?stock=10&kind=COMMANDS")){ mvc.perform(get(path)).andExpect(status().isUnauthorized());mvc.perform(get(path).session(restricted)).andExpect(status().isForbidden()); } for(String path:List.of("/api/administration/warehouse-scanner/resolve?stock=10&mode=OUT&code=ABC","/api/administration/warehouse-scanner/jobs?stock=10")){ mvc.perform(get(path)).andExpect(status().isUnauthorized());mvc.perform(get(path).session(restricted)).andExpect(status().isForbidden()); } verify(database,never()).warehouseDetailsTransaction(any()); verify(database,never()).warehouseWriteTransaction(any()); } @Test void articleAdministrationRequiresArticleRightAndCsrf() throws Exception { var restricted=authenticated("existing-user","existing-password");var token=csrf(restricted); for(String path:List.of("/api/administration/articles","/api/administration/articles/options","/api/administration/articles/1","/api/administration/articles/1/history","/api/administration/article-groups")){ mvc.perform(get(path)).andExpect(status().isUnauthorized()); mvc.perform(get(path).session(restricted)).andExpect(status().isForbidden()); } for(String path:List.of("/api/administration/articles/preview","/api/administration/articles/save","/api/administration/article-groups")){ mvc.perform(post(path).session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } for(String path:List.of("/api/account/article-columns","/api/administration/employees/1/article-columns")){ mvc.perform(get(path)).andExpect(status().isUnauthorized()); mvc.perform(put(path).session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).articleWriteTransaction(any()); } @Test void vehicleDispositionRequiresItsOwnRightAndCsrf() throws Exception { String base="/api/administration/vehicle-disposition"; mvc.perform(get(base)).andExpect(status().isUnauthorized()); mvc.perform(get(base+"/timelines?vehicles=101&day=2026-10-15")).andExpect(status().isUnauthorized()); mvc.perform(get(base+"/planning-data?job=10&day=2026-10-18")).andExpect(status().isUnauthorized()); mvc.perform(get(base+"/order-actions?vehicle=101&job=10&day=2026-10-18")).andExpect(status().isUnauthorized()); mvc.perform(get(base+"/single-handover?vehicle=101&job=10&day=2026-10-18")).andExpect(status().isUnauthorized()); mvc.perform(get(base+"/reservation-release?vehicle=101&job=10&day=2026-10-18")).andExpect(status().isUnauthorized()); mvc.perform(get(base+"/job-search?q=10")).andExpect(status().isUnauthorized()); mvc.perform(get(base+"/multiday/unreserved?day=2026-10-18")).andExpect(status().isUnauthorized()); var restricted=authenticated("existing-user","existing-password");var token=csrf(restricted); mvc.perform(get(base).session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"/1?day=2026-10-15").session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"/timelines?vehicles=101&day=2026-10-15").session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"/planning-data?job=10&day=2026-10-18").session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"/order-actions?vehicle=101&job=10&day=2026-10-18").session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"/single-handover?vehicle=101&job=10&day=2026-10-18").session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"/reservation-release?vehicle=101&job=10&day=2026-10-18").session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"/job-search?q=10").session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"/multiday/unreserved?day=2026-10-18").session(restricted)).andExpect(status().isForbidden()); for(String path:List.of(base,base+"/preview",base+"/customer-day",base+"/customer-day/preview",base+"/preferences",base+"/planning",base+"/planning/preview",base+"/multiday",base+"/multiday/preview",base+"/planning-data",base+"/planning-data/preview",base+"/order-actions",base+"/order-actions/preview",base+"/handover",base+"/handover/preview",base+"/single-handover",base+"/single-handover/preview",base+"/reservation-release",base+"/reservation-release/preview")){ mvc.perform(post(path).session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); mvc.perform(post(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } mvc.perform(get(base+"/customer-day?customer=1&day=2026-10-15").session(restricted)).andExpect(status().isForbidden()); verify(database,never()).vehicleCustomerDayWriteTransaction(anyBoolean(),any()); mvc.perform(get(base+"/planning?vehicle=101&day=2026-10-15").session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"/multiday?vehicle=101&day=2026-10-15&job=10").session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"/handover?vehicle=101&day=2026-10-15").session(restricted)).andExpect(status().isForbidden()); verify(database,never()).vehiclePlanningWriteTransaction(anyBoolean(),any()); verify(database,never()).vehicleOrderWriteTransaction(anyBoolean(),any()); mvc.perform(get(base+"/headquarters")).andExpect(status().isUnauthorized()); mvc.perform(get(base+"/headquarters").session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"?headquarters=8").session(restricted)).andExpect(status().isForbidden()); mvc.perform(get(base+"/preferences")).andExpect(status().isUnauthorized()); mvc.perform(get(base+"/preferences").session(restricted)).andExpect(status().isForbidden()); verify(database,never()).vehiclePreferencesWriteTransaction(anyBoolean(),any()); verify(database,never()).vehicleDispositionWriteTransaction(anyBoolean(),any()); } @Test void rollcardsRequireSessionOrderReadAndDispositionScope() throws Exception { String base="/api/administration/vehicle-disposition/101/rollcard?day=2026-10-15"; mvc.perform(get(base)).andExpect(status().isUnauthorized()); var restricted=authenticated("existing-user","existing-password"); mvc.perform(get(base).session(restricted)).andExpect(status().isForbidden()); var rights=AppointmentServiceTest.user(27).rights(); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("rollcard-user"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","rollcard-user","emp_rights",rights))); var session=authenticated("rollcard-user","pw"); mvc.perform(get(base).session(session)).andExpect(status().isForbidden()); mvc.perform(get(base+"&headquarters=8").session(session)).andExpect(status().isForbidden()); } @Test void customerEntrySettingsRequireBothRightsAndCsrf() throws Exception { String base="/api/master/customers/1/entry-settings"; mvc.perform(get(base)).andExpect(status().isUnauthorized()); for(int omitted:List.of(0,2)){ String account="entry-settings-no-"+omitted; var rights=new StringBuilder("1".repeat(70));rights.setCharAt(omitted,'0'); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq(account),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account",account,"emp_rights",rights.toString()))); var session=authenticated(account,"pw");var token=csrf(session); mvc.perform(get(base).session(session)).andExpect(status().isForbidden()); for(String endpoint:List.of(base,base+"/preview")){ mvc.perform(post(endpoint).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(endpoint).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(endpoint).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } } verify(database,never()).customerMailSettingsWriteTransaction(any()); } @Test void customerReferenceLookupsRequireCustomerRight() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("reference-denied"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","reference-denied","emp_rights","0".repeat(70)))); var session=authenticated("reference-denied","pw"); for(String path:List.of("/api/master-data/customer-references/payer","/api/master-data/customer-references/parent/1")){ mvc.perform(get(path)).andExpect(status().isUnauthorized());mvc.perform(get(path).session(session)).andExpect(status().isForbidden()); } verify(database,never()).writeTransaction(any()); } @Test void customerPreviousNumberRequiresCustomerRightAndCsrf() throws Exception { String base="/api/master/customers/1/previous-number"; mvc.perform(get(base)).andExpect(status().isUnauthorized()); for(int omitted:List.of(0)){ String account="previous-number-no-"+omitted; var rights=new StringBuilder("1".repeat(70));rights.setCharAt(omitted,'0'); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq(account),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account",account,"emp_rights",rights.toString()))); var session=authenticated(account,"pw");var token=csrf(session); mvc.perform(get(base).session(session)).andExpect(status().isForbidden()); for(String endpoint:List.of(base,base+"/preview")){ mvc.perform(post(endpoint).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(endpoint).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(endpoint).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } } verify(database,never()).writeTransaction(any()); } @Test void customerMailSettingsRequireBothRightsAndCsrf() throws Exception { String base="/api/master/customers/1/mail-settings"; mvc.perform(get(base)).andExpect(status().isUnauthorized()); for(int omitted:List.of(0,2)){ String account="mail-settings-no-"+omitted; var rights=new StringBuilder("1".repeat(70));rights.setCharAt(omitted,'0'); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq(account),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account",account,"emp_rights",rights.toString()))); var session=authenticated(account,"pw");var token=csrf(session); mvc.perform(get(base).session(session)).andExpect(status().isForbidden()); for(String endpoint:List.of(base,base+"/preview")){ mvc.perform(post(endpoint).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(endpoint).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(endpoint).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } } verify(database,never()).customerMailSettingsWriteTransaction(any()); } @Test void customerRollcardMailSettingsRequireBothRightsAndCsrf() throws Exception { String base="/api/master/customers/1/rollcard-mail-settings"; mvc.perform(get(base)).andExpect(status().isUnauthorized()); for(int omitted:List.of(0,2)){ String account="mail-settings-no-"+omitted; var rights=new StringBuilder("1".repeat(70));rights.setCharAt(omitted,'0'); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq(account),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account",account,"emp_rights",rights.toString()))); var session=authenticated(account,"pw");var token=csrf(session); mvc.perform(get(base).session(session)).andExpect(status().isForbidden()); for(String endpoint:List.of(base,base+"/preview")){ mvc.perform(post(endpoint).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(endpoint).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(endpoint).session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } } verify(database,never()).customerParameterWriteTransaction(any()); } @Test void rollcardMailRequiresSessionRightsAndCsrf() throws Exception { String base="/api/administration/vehicle-disposition/101/rollcard"; var restricted=authenticated("existing-user","existing-password");var token=csrf(restricted); for(String endpoint:List.of(base+"/pdf?day=2026-10-15&revision=test",base+"/mail?day=2026-10-15",base+"/mail/00000000-0000-0000-0000-000000000001",base+"/mail/00000000-0000-0000-0000-000000000001/page?page=0")){ mvc.perform(get(endpoint)).andExpect(status().isUnauthorized());mvc.perform(get(endpoint).session(restricted)).andExpect(status().isForbidden()); } for(String endpoint:List.of(base+"/mail/preview",base+"/mail/00000000-0000-0000-0000-000000000001")){ mvc.perform(post(endpoint).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(endpoint).session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(endpoint).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).vehicleRollcardMailWriteTransaction(anyBoolean(),any()); } @Test void dispositionScopeCannotBeSelectedWithoutMasterRightOrAlterSession() throws Exception { var rights=AppointmentServiceTest.user(27).rights(); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("fds-user"),any(),any())).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","fds-user","emp_rights",rights))); var session=authenticated("fds-user","pw");var token=csrf(session);String base="/api/administration/vehicle-disposition"; for(String path:List.of(base+"?headquarters=8",base+"/103?day=2026-10-16&headquarters=8",base+"/customer-day?customer=3&day=2026-10-16&headquarters=8"))mvc.perform(get(path).session(session)).andExpect(status().isForbidden()); mvc.perform(post(base+"/preview?headquarters=8").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(base+"/customer-day/preview?headquarters=8").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(get(base+"?headquarters=wrong").session(session)).andExpect(status().isBadRequest()); var context=(SecurityContext)session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); assertThat(((LegacyUser)context.getAuthentication().getPrincipal()).headquartersId()).isEqualTo(7); verify(database,never()).vehicleDispositionWriteTransaction(anyBoolean(),any());verify(database,never()).vehicleCustomerDayWriteTransaction(anyBoolean(),any()); } @Test void vehicleRelationsRequireSessionCsrfAndCourierRight() throws Exception { String base="/api/master/vehicle-relations"; for(String path:List.of(base+"/1",base+"/1/customers/2"))mvc.perform(get(path)).andExpect(status().isUnauthorized()); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("none"),eq("password"),eq("password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","none","emp_rights","0000000000"))); var restricted=authenticated("none","password");var token=csrf(restricted); for(String path:List.of(base+"/1",base+"/1/customers/2"))mvc.perform(get(path).session(restricted)).andExpect(status().isForbidden()); for(String path:List.of(base,base+"/preview")){ mvc.perform(post(path).session(restricted).contentType("application/json").content("{}")).andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); mvc.perform(post(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).vehicleRelationWriteTransaction(any()); } @Test void customerServiceSettingsRequireSessionAndCsrf() throws Exception { String base="/api/master/customers/9/service-settings"; mvc.perform(get(base)).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); for(String path:List.of(base,base+"/preview")) mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void portalEmployeesRequireSessionCsrfAndCustomerPlusEmployeeRights() throws Exception { String base="/api/master-data/customers/1/portal-employees";var session=authenticated("existing-user","existing-password"); for(String path:List.of(base,base+"/2"))mvc.perform(get(path)).andExpect(status().isUnauthorized()); for(String path:List.of(base,base+"/2/delete",base+"/2/two-factor/reset"))mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); for(String path:List.of(base+"/2",base+"/2/password"))mvc.perform(put(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String bits:List.of("1000","0001")){ when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("portal-partial"),eq("password"),eq("password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","portal-partial","emp_rights",bits))); var restricted=authenticated("portal-partial","password");var token=csrf(restricted); for(String path:List.of(base,base+"/2"))mvc.perform(get(path).session(restricted)).andExpect(status().isForbidden()); for(String path:List.of(base,base+"/2/delete",base+"/2/two-factor/reset"))mvc.perform(post(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String path:List.of(base+"/2",base+"/2/password"))mvc.perform(put(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).portalEmployeeWriteTransaction(anyBoolean(),any()); } @Test void portalAccountsRequireSessionCsrfAndDomainRights() throws Exception { var session=authenticated("existing-user","existing-password"); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("no-portal"),eq("password"),eq("password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","no-portal","emp_rights","0".repeat(70)))); var restricted=authenticated("no-portal","password");var token=csrf(restricted); for(String kind:List.of("customers","couriers")){ String path="/api/master-data/"+kind+"/1/access"; mvc.perform(get(path)).andExpect(status().isUnauthorized()); mvc.perform(put(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); mvc.perform(post(path+"/two-factor/reset").session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(get(path).session(restricted)).andExpect(status().isForbidden()); mvc.perform(put(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post(path+"/two-factor/reset").session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).portalAccountsWriteTransaction(any()); } @Test void businessWorkflowsRequireSessionCsrfAndDomainRights() throws Exception { for(String path:List.of("/api/master-data/customers/similar?phrase=Test","/api/master-data/customers/identifier","/api/master-data/couriers/identifier","/api/master-data/customers/1","/api/orders/1/workbench","/api/orders/1/cancellation","/api/orders/1/articles","/api/orders/1/calculation","/api/billing/options"))mvc.perform(get(path)).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); for(String path:List.of("/api/orders/1/workbench","/api/orders/1/price","/api/master-data/customers/1"))mvc.perform(put(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); for(String path:List.of("/api/orders/1/actions","/api/orders/1/cancellation","/api/orders/1/cancellation/preview","/api/orders/1/articles/movement","/api/orders/1/articles/movement/preview","/api/billing/assign","/api/billing/operations/test/resume","/api/billing/invoice-documents/preview","/api/billing/invoice-documents/document.pdf","/api/billing/customer-csv/preview","/api/billing/customer-csv/download","/api/billing/customer-csv/store"))mvc.perform(post(path).session(session).contentType("application/json").content("{}")).andExpect(status().isForbidden()); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("none"),eq("password"),eq("password"))).thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","none","emp_rights","0000000000"))); var restricted=authenticated("none","password");var token=csrf(restricted); mvc.perform(put("/api/master-data/customers/1").session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post("/api/orders/1/actions").session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); mvc.perform(post("/api/billing/assign").session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); for(String path:List.of("/api/billing/invoice-documents/preview","/api/billing/invoice-documents/document.pdf","/api/billing/customer-csv/preview","/api/billing/customer-csv/download","/api/billing/customer-csv/store")){ mvc.perform(get(path)).andExpect(status().isUnauthorized()); mvc.perform(post(path).session(restricted).header(token.header(),token.token()).contentType("application/json").content("{}")).andExpect(status().isForbidden()); } verify(database,never()).writeTransaction(any()); } @Test void healthAndSystemArePublicButDataRequiresSession() throws Exception { mvc.perform(get("/api/health").with(httpBasic("proxy-fixture","proxy-password"))) .andExpect(status().isOk()).andExpect(jsonPath("$.status").value("UP")); mvc.perform(get("/api/system")).andExpect(status().isOk()).andExpect(jsonPath("$.readOnly").value(true)) .andExpect(jsonPath("$.schemaChanges").value(false)).andExpect(jsonPath("$.capabilities.writeOrders").value(false)); MvcResult unauthorized = mvc.perform(get("/api/orders")).andExpect(status().isUnauthorized()) .andExpect(header().doesNotExist("WWW-Authenticate")).andReturn(); assertThat(unauthorized.getRequest().getSession(false)).isNull(); mvc.perform(get("/api/orders/42/history")).andExpect(status().isUnauthorized()) .andExpect(header().doesNotExist("WWW-Authenticate")); Csrf csrf = csrf(null); mvc.perform(post("/api/orders").session(csrf.session()).header(csrf.header(),csrf.token()) .contentType("application/json").content("{}")) .andExpect(status().isUnauthorized()); } @Test void reportsRequireSessionCsrfAndTheirSeparateLegacyRight() throws Exception { mvc.perform(get("/api/administration/reports/CUSTOMER/options")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); mvc.perform(get("/api/administration").session(session)).andExpect(status().isOk()) .andExpect(jsonPath("$.customerReports").value(false)).andExpect(jsonPath("$.courierReports").value(false)); mvc.perform(get("/api/administration/reports/CUSTOMER/options").session(session)).andExpect(status().isForbidden()); mvc.perform(post("/api/administration/reports/CUSTOMER").session(session).contentType("application/json").content("{}")) .andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); var token=csrf(session); mvc.perform(post("/api/administration/reports/CUSTOMER").session(session).header(token.header(),token.token()).contentType("application/json").content("{}")) .andExpect(status().isForbidden()); verify(database,never()).insert(anyString(),anyMap()); } @Test void existingLoginPersistsSessionRotatesIdAndStoresNoPassword() throws Exception { Csrf anonymous = csrf(null); String previousId = anonymous.session().getId(); MvcResult login = login(anonymous,"existing-user","existing-password",200); var body = json.readTree(login.getResponse().getContentAsString()); assertThat(body.get("username").asText()).isEqualTo("existing-user"); assertThat(body.has("password")).isFalse(); MockHttpSession session = (MockHttpSession) login.getRequest().getSession(false); assertThat(session.getId()).isNotEqualTo(previousId); SecurityContext context = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); assertThat(context.getAuthentication().getCredentials()).isNull(); mvc.perform(get("/api/me").session(session)).andExpect(status().isOk()) .andExpect(jsonPath("$.headquartersId").value(7)).andExpect(jsonPath("$.roles[0]").value("VIEWER")) .andExpect(jsonPath("$.password").doesNotExist()).andExpect(jsonPath("$.capabilities.exportOrders").value(true)) .andExpect(jsonPath("$.capabilities.writeOrders").value(true)); mvc.perform(get("/api/me").session(session).with(httpBasic("proxy-fixture","proxy-password"))) .andExpect(status().isOk()).andExpect(jsonPath("$.username").value("existing-user")); verify(database,times(1)).queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),any(),any(),any()); } @Test void basicCredentialsAloneCannotAuthenticateTheApplication() throws Exception { mvc.perform(get("/api/me").with(httpBasic("existing-user","existing-password"))) .andExpect(status().isUnauthorized()).andExpect(header().doesNotExist("WWW-Authenticate")); verify(database,never()).queryForList(anyString(),any(),any(),any()); } @Test void loginRequiresCsrfAndDoesNotAcceptCredentialsInGetQuery() throws Exception { mvc.perform(post("/api/login").param("username","existing-user").param("password","existing-password")) .andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); mvc.perform(get("/api/login").param("username","existing-user").param("password","existing-password")) .andExpect(status().isUnauthorized()); verify(database,never()).queryForList(anyString(),any(),any(),any()); } @Test void loginRotatesCsrfAndLogoutInvalidatesSession() throws Exception { Csrf anonymous = csrf(null); MockHttpSession session = (MockHttpSession) login(anonymous,"existing-user","existing-password",200).getRequest().getSession(false); mvc.perform(post("/api/logout").session(session).header(anonymous.header(),anonymous.token())) .andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); mvc.perform(get("/api/me").session(session)).andExpect(status().isOk()); Csrf authenticated = csrf(session); mvc.perform(post("/api/logout").session(session).header(authenticated.header(),authenticated.token())) .andExpect(status().isNoContent()).andExpect(cookie().maxAge("VOTIANSESSION",0)); assertThat(session.isInvalid()).isTrue(); mvc.perform(get("/api/me")).andExpect(status().isUnauthorized()); Csrf afterLogout = csrf(null); mvc.perform(post("/api/login").session(afterLogout.session()).header(authenticated.header(),authenticated.token()) .param("username","existing-user").param("password","existing-password")) .andExpect(status().isForbidden()); login(afterLogout,"existing-user","existing-password",200); } @Test void logoutRequiresCsrfAndGetCannotLogOut() throws Exception { MockHttpSession session = authenticated("existing-user","existing-password"); mvc.perform(post("/api/logout").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/logout").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/me").session(session)).andExpect(status().isOk()); } @Test void csrfCannotBeReusedAcrossSessions() throws Exception { Csrf first = csrf(null); Csrf second = csrf(null); mvc.perform(post("/api/login").session(second.session()).header(first.header(),first.token()) .param("username","existing-user").param("password","existing-password")) .andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); verify(database,never()).queryForList(anyString(),any(),any(),any()); } @Test void unsupportedMutationsRemainDisabled() throws Exception { MockHttpSession session = authenticated("existing-user","existing-password"); Csrf csrf = csrf(session); mvc.perform(post("/api/orders/12/cancel").session(session).header(csrf.header(),csrf.token()) .contentType("application/json").content("{\"version\":1,\"cancelMode\":\"FREE\",\"reason\":\"test\"}")) .andExpect(status().isNotImplemented()); verify(database,never()).update(anyString(),any(Object[].class)); } @Test void orderEntryHasItsOwnRightAndStillRequiresCsrf() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("entry"),eq("password"),eq("password"))) .thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","entry","emp_rights","0000000100"))); var session=authenticated("entry","password"); mvc.perform(get("/api/me").session(session)).andExpect(status().isOk()) .andExpect(jsonPath("$.capabilities.createOrders").value(true)).andExpect(jsonPath("$.capabilities.readCustomers").value(false)) .andExpect(jsonPath("$.capabilities.writeOrders").value(false)); mvc.perform(get("/api/orders/entry/options").session(session)).andExpect(status().isOk()) .andExpect(jsonPath("$.entryModes.copyAccounting").value(false)).andExpect(jsonPath("$.entryModes.laterAccounting").value(false)); mvc.perform(get("/api/orders/entry/cost-centers").session(session)).andExpect(status().isOk()); mvc.perform(get("/api/orders/entry/vehicles").session(session)).andExpect(status().isOk()); mvc.perform(get("/api/orders/entry/courier-parents?vehicleType=2").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/customers").session(session)).andExpect(status().isForbidden()); for(String endpoint:List.of("/api/orders","/api/orders/quote","/api/orders/entry/optimize","/api/orders/entry/postal-quote")) mvc.perform(post(endpoint).session(session).contentType("application/json").content(json.writeValueAsString(OrderEntryFixtures.request()))) .andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); verify(database,never()).writeTransaction(any()); } @Test void orderEntryRejectsUnknownServerOwnedFieldsAndInvalidCommands() throws Exception { var csrf=csrf(authenticated("existing-user","existing-password")); var input=json.valueToTree(OrderEntryFixtures.request()); ((com.fasterxml.jackson.databind.node.ObjectNode)input).put("hq_id",999); mvc.perform(post("/api/orders").session(csrf.session()).header(csrf.header(),csrf.token()) .contentType("application/json").content(json.writeValueAsString(input))).andExpect(status().isBadRequest()); mvc.perform(post("/api/orders").session(csrf.session()).header(csrf.header(),csrf.token()) .contentType("application/json").content("{}")).andExpect(status().isBadRequest()); verify(database,never()).writeTransaction(any()); } @Test void orderEntryIsForbiddenWithoutRightSeven() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("viewer"),eq("password"),eq("password"))) .thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","viewer","emp_rights","1111111011"))); var csrf=csrf(authenticated("viewer","password")); mvc.perform(get("/api/orders/entry/options").session(csrf.session())).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/entry/courier-parents?vehicleType=2").session(csrf.session())).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/entry/saved-orders/88?dispatch=true").session(csrf.session())).andExpect(status().isForbidden()); for(String endpoint:List.of("/api/orders","/api/orders/quote")) mvc.perform(post(endpoint).session(csrf.session()).header(csrf.header(),csrf.token()) .contentType("application/json").content(json.writeValueAsString(OrderEntryFixtures.request()))) .andExpect(status().isForbidden()); String token=java.util.UUID.randomUUID().toString(); mvc.perform(get("/api/orders/entry/address-suggestions").param("q","Teststraße").param("sessionToken",token).session(csrf.session())).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/entry/address-details").param("placeId","test-id").param("sessionToken",token).session(csrf.session())).andExpect(status().isForbidden()); mvc.perform(post("/api/orders/entry/postal-quote").session(csrf.session()).header(csrf.header(),csrf.token()).contentType("application/json").content("{\"stops\":[{\"postalCode\":\"20095\"},{\"postalCode\":\"20097\"}],\"vehicleTypeId\":2}")).andExpect(status().isForbidden()); var stops=new java.util.ArrayList<>(OrderEntryFixtures.request().stops());stops.addAll(OrderEntryFixtures.request().stops()); mvc.perform(post("/api/orders/entry/optimize").session(csrf.session()).header(csrf.header(),csrf.token()).contentType("application/json").content(json.writeValueAsString(new OrderEntryModels.OptimizeInput(stops,true)))).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } @Test void backendDatabaseFailureIs503NotFakeLoginOrEmptyDataset() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("offline"),eq("password"),eq("password"))) .thenThrow(new DataAccessResourceFailureException("server secret must not be exposed")); var result = login(csrf(null),"offline","password",503); assertThat(json.readTree(result.getResponse().getContentAsString()).get("message").asText()).isEqualTo(ApiErrors.DATABASE_UNAVAILABLE); assertThat(result.getRequest().getSession(false).getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)).isNull(); when(database.checkConnection()).thenReturn(new DatabaseService.ConnectionCheck(false,"UNAVAILABLE",ApiErrors.DATABASE_UNAVAILABLE)); mvc.perform(get("/api/system")).andExpect(status().isOk()).andExpect(jsonPath("$.connected").value(false)); } @Test void wrongOrUnscopedAccountDoesNotAuthenticate() throws Exception { Csrf wrong = csrf(null); login(wrong,"unknown","incorrect",401); mvc.perform(get("/api/me").session(wrong.session())).andExpect(status().isUnauthorized()); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("invalid"),eq("password"),eq("password"))) .thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",0,"usr_account","invalid","emp_rights","111111"))); login(csrf(null),"invalid","password",401); } @Test void rightsAreEnforcedBeyondAuthentication() throws Exception { when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("restricted"),eq("password"),eq("password"))) .thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","restricted","emp_rights","0000000000"))); MockHttpSession session = authenticated("restricted","password"); mvc.perform(get("/api/me").session(session)).andExpect(status().isOk()).andExpect(jsonPath("$.capabilities.exportOrders").value(false)); mvc.perform(get("/api/orders").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/customers").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/export.csv").session(session)).andExpect(status().isForbidden()); mvc.perform(get("/api/orders/42/history").session(session)).andExpect(status().isForbidden()); verify(database,never()).queryForList(contains("phoenix_log"),any(Object[].class)); } @Test void historyOfMissingOrForeignOrderReturns404WithoutReadingLog() throws Exception { MockHttpSession session = authenticated("existing-user","existing-password"); mvc.perform(get("/api/orders/42/history").session(session)).andExpect(status().isNotFound()); verify(database).queryForList("SELECT jb.jb_id FROM job jb WHERE jb.jb_id=? AND jb.hq_id=?",42L,7L); verify(database,never()).queryForList(contains("phoenix_log"),any(Object[].class)); } @Test void accessibleOrderWithNoLogReturnsEmptyHistory() throws Exception { MockHttpSession session = authenticated("existing-user","existing-password"); when(database.queryForList("SELECT jb.jb_id FROM job jb WHERE jb.jb_id=? AND jb.hq_id=?",42L,7L)) .thenReturn(List.of(Map.of("jb_id",42L))); mvc.perform(get("/api/orders/42/history").session(session)) .andExpect(status().isOk()).andExpect(content().json("[]")); verify(database).queryForList(contains("FROM `phoenix_log`.log l"),eq(42L),eq(7L)); } @Test void historyReturnsExistingEvents() throws Exception { MockHttpSession session = authenticated("existing-user","existing-password"); when(database.queryForList("SELECT jb.jb_id FROM job jb WHERE jb.jb_id=? AND jb.hq_id=?",42L,7L)) .thenReturn(List.of(Map.of("jb_id",42L))); when(database.queryForList(contains("FROM `phoenix_log`.log l"),eq(42L),eq(7L))) .thenReturn(List.of(Map.of("log_id",81L,"logo_id",1,"log_createtime","2026-10-07 08:15:00", "logo_op_name","Auftrag angelegt","logo_description","Testhistorie","usr_account","disponent"))); mvc.perform(get("/api/orders/42/history").session(session)).andExpect(status().isOk()) .andExpect(jsonPath("$.length()").value(1)).andExpect(jsonPath("$[0].id").value(81)) .andExpect(jsonPath("$[0].at").value("2026-10-07T08:15:00")) .andExpect(jsonPath("$[0].action").isString()).andExpect(jsonPath("$[0].actor").isString()); } @Test void logFailureReturns503AndDoesNotDiscloseDatabaseDetails() throws Exception { MockHttpSession session = authenticated("existing-user","existing-password"); when(database.queryForList("SELECT jb.jb_id FROM job jb WHERE jb.jb_id=? AND jb.hq_id=?",42L,7L)) .thenReturn(List.of(Map.of("jb_id",42L))); when(database.queryForList(contains("FROM `phoenix_log`.log l"),eq(42L),eq(7L))) .thenThrow(new DataAccessResourceFailureException("private database credentials")); var response=mvc.perform(get("/api/orders/42/history").session(session)) .andExpect(status().isServiceUnavailable()).andReturn(); String message=json.readTree(response.getResponse().getContentAsString()).get("message").asText(); assertThat(message).contains("Auftragshistorie").doesNotContain("private database credentials"); } @Test void databaseAccountDenialIsDistinguishedFromNetworkFailure() throws Exception { when(database.checkConnection()).thenReturn(new DatabaseService.ConnectionCheck(false,"AUTHENTICATION_FAILED","Bitte Hostfreigabe prüfen.")); mvc.perform(get("/api/system")).andExpect(status().isOk()).andExpect(jsonPath("$.connected").value(false)) .andExpect(jsonPath("$.reason").value("AUTHENTICATION_FAILED")) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("Hostfreigabe"))); } @Test void inventoryAndMapEndpointsRequireEntryRightsAndStockRights() throws Exception { mvc.perform(get("/api/orders/entry/articles")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/orders/entry/features")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); mvc.perform(get("/api/orders/entry/features").session(session)).andExpect(status().isOk()).andExpect(jsonPath("$.bookStock").value(false)); mvc.perform(get("/api/orders/entry/stocks").session(session).param("payerId","41").param("articleId","3")).andExpect(status().isForbidden()); mvc.perform(post("/api/orders/entry/map/address").session(session).contentType("application/json").content("{\"latitude\":53,\"longitude\":9}")).andExpect(status().isForbidden()); var csrf=csrf(session); mvc.perform(post("/api/orders/entry/map/address").session(session).header(csrf.header(),csrf.token()).contentType("application/json").content("{\"latitude\":91,\"longitude\":9}")).andExpect(status().isBadRequest()); } @Test void administrationAndStatisticsRequireSessionRightsAndCsrf() throws Exception { mvc.perform(get("/api/administration")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/administration/holidays/2026")).andExpect(status().isUnauthorized()); mvc.perform(get("/api/statistics/options")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); mvc.perform(get("/api/administration").session(session)).andExpect(status().isOk()) .andExpect(jsonPath("$.holidays").value(true)).andExpect(jsonPath("$.groups").value(false)); mvc.perform(put("/api/administration/holidays/2026").session(session) .contentType("application/json").content("{}")) .andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); var token=csrf(session); mvc.perform(post("/api/administration/groups").session(session).header(token.header(),token.token()) .contentType("application/json").content("{\"name\":\"x\",\"shared\":false}")) .andExpect(status().isForbidden()); mvc.perform(get("/api/administration/travel-times").session(session).param("mode","unknown").param("from","1").param("to","2")) .andExpect(status().isBadRequest()); verify(database,never()).update(anyString(),any(Object[].class)); } @Test void appointmentsRequireSessionSalesRightsAndCsrf() throws Exception { mvc.perform(get("/api/appointments/options")).andExpect(status().isUnauthorized()); var session=authenticated("existing-user","existing-password"); mvc.perform(get("/api/me").session(session)).andExpect(status().isOk()).andExpect(jsonPath("$.capabilities.readAppointments").value(true)); for(String endpoint:List.of("/api/appointments","/api/appointments/query","/api/appointments/44/confirm","/api/appointments/44/finish","/api/appointments/44/resubmit")) mvc.perform(post(endpoint).session(session).contentType("application/json").content("{}")) .andExpect(status().isForbidden()).andExpect(jsonPath("$.code").value("CSRF_INVALID")); when(database.queryForList(eq(LegacyAuthenticationProvider.AUTH_SQL),eq("sales-disabled"),eq("password"),eq("password"))) .thenReturn(List.of(Map.of("usr_id",11,"emp_id",12,"hq_id",7,"usr_account","sales-disabled","emp_rights","100000000000"))); var restricted=authenticated("sales-disabled","password"); mvc.perform(get("/api/appointments/options").session(restricted)).andExpect(status().isForbidden()); verify(database,never()).writeTransaction(any()); } }