Erweitere VoTiAn NG um Auftrags-, Stamm- und Verwaltungsfunktionen
Ergänzt Auftragserfassung und -bearbeitung, Unternehmerbelege, Stornos, Lagerkorrekturen, Stammdaten, Abrechnung, CRM, Formulare, Berichte und Preisverwaltung auf Basis der aktuellen PHP-Anwendung. Enthält Integrations- und Oberflächentests sowie dokumentierte Abgleiche und Prüfstände.
This commit is contained in:
1 parent
42c87282ee
commit
363bc27c26
190 files changed
+14815
-1857
No files matched your search
@@ -64,6 +64,87 @@ class ApiSecurityTest {
|
||||
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 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 businessWorkflowsRequireSessionCsrfAndDomainRights() throws Exception {
|
||||
for(String path:List.of("/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"))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());
|
||||
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"));
|
||||
@@ -80,6 +161,20 @@ class ApiSecurityTest {
|
||||
.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();
|
||||
@@ -94,7 +189,7 @@ class ApiSecurityTest {
|
||||
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(false));
|
||||
.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());
|
||||
@@ -148,7 +243,7 @@ class ApiSecurityTest {
|
||||
verify(database,never()).queryForList(anyString(),any(),any(),any());
|
||||
}
|
||||
|
||||
@Test void noWriteEndpointCanModifyTheDatabase() throws Exception {
|
||||
@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())
|
||||
@@ -157,6 +252,46 @@ class ApiSecurityTest {
|
||||
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());
|
||||
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/customers").session(session)).andExpect(status().isForbidden());
|
||||
for(String endpoint:List.of("/api/orders","/api/orders/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());
|
||||
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());
|
||||
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"));
|
||||
@@ -238,4 +373,48 @@ class ApiSecurityTest {
|
||||
.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());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user