Files
votianng/backend/src/test/java/de/stadtbote/votian/StatisticsServiceTest.java
T
Sven 363bc27c26 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.
2026-10-13 20:31:31 +02:00

70 lines
5.2 KiB
Java

package de.stadtbote.votian;
import de.stadtbote.votian.database.DatabaseService;
import org.junit.jupiter.api.*;
import org.springframework.security.access.AccessDeniedException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.function.Supplier;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
import static de.stadtbote.votian.StatisticsService.*;
import static de.stadtbote.votian.OrderEntryService.fields;
class StatisticsServiceTest {
final DatabaseService db=mock(DatabaseService.class);
final StatisticsService service=new StatisticsService(db,new OrderEntryCatalog(db),"phoenix_log");
final LegacyUser user=new LegacyUser(11,12,7,"test","1".repeat(70));
static Query query(List<Long> hqs,List<Interval> comparisons,List<Split> splits,String number,LocalTime timeFrom,LocalTime timeTo){return new Query(new Interval(LocalDate.of(2026,1,1),LocalDate.of(2026,12,31)),comparisons,hqs,DateBasis.FINISHED,State.FINISHED,Amount.ALL,Payment.ALL,Period.MONTH,splits,number,"",null,null,true,null,null,timeFrom,timeTo,true,false);}
@BeforeEach void setup(){
when(db.readTransaction(any())).thenAnswer(c->((Supplier<?>)c.getArgument(0)).get());
when(db.queryForList(anyString(),any(Object[].class))).thenAnswer(c->{String sql=c.getArgument(0);if(sql.contains("target.hq_id"))return List.of(fields("hq_id",7),fields("hq_id",8));return List.of(fields("orders",1234L,"revenue",12000));});
}
@Test void fullPeriodIsAggregatedWithoutTheOrderListLimit(){
var q=query(List.of(7L),List.of(),List.of(Split.CUSTOMER,Split.HEADQUARTERS),"",null,null);
var sql=StatisticsService.sql(q,q.interval(),List.of(7L),false,"`phoenix_log`.log");
assertThat(sql.text()).contains("COUNT(*) AS orders","SUM(j.jb_totalprice)","GROUP BY period,split0,split1","LIMIT 5001").doesNotContain("LIMIT 500 ");
assertThat(sql.values()).containsExactly(7L,LocalDateTime.of(2026,1,1,0,0),LocalDateTime.of(2027,1,1,0,0));
var result=service.calculate(user,q);assertThat(result.get("completePeriod")).isEqualTo(true);
assertThat(result.toString()).contains("1234");verify(db,never()).writeTransaction(any());
}
@Test void midnightTimeWindowAndTextAreBoundParameters(){
String input="x' OR 1=1";var q=query(List.of(7L),List.of(),List.of(),input,LocalTime.of(22,0),LocalTime.of(5,0));
var sql=StatisticsService.sql(q,q.interval(),List.of(7L),false,"`phoenix_log`.log");
assertThat(sql.text()).contains("TIME(j.jb_finishtime)>=? OR TIME(j.jb_finishtime)<=?","cs.cs_eid=?").doesNotContain(input);
assertThat(sql.values()).contains(input,LocalTime.of(22,0),LocalTime.of(5,0,59));
}
@Test void retrospectiveUsesLegacyHistoryAndAuxiliaryRowsDoNotMultiplyOrders(){
var q=query(List.of(7L),List.of(),List.of(Split.START_POSTAL),"",null,null);
var sql=StatisticsService.sql(q,q.interval(),List.of(7L),false,"`history`.log");
assertThat(sql.text()).contains("EXISTS (SELECT 1 FROM `history`.log", "logo_id IN (1,25)","Nacherfassung%", "SUM(p.jbprc_price)").doesNotContain("JOIN jobprice", "JOIN tour ");
assertThat(sql.text()).contains("COUNT(*)=1 THEN MAX(v.crvh_prov) ELSE NULL", "missingCourierValues");
}
@Test void foreignMandatorAndMissingMasterRightAreRejectedBeforeAggregation(){
var q=query(List.of(9L),List.of(),List.of(),"",null,null);
assertThatThrownBy(()->service.calculate(user,q)).isInstanceOf(AccessDeniedException.class);
String rights="1".repeat(10)+"0"+"1".repeat(59);
assertThatThrownBy(()->service.calculate(new LegacyUser(11,12,7,"test",rights),query(List.of(8L),List.of(),List.of(),"",null,null))).isInstanceOf(AccessDeniedException.class);
verify(db,never()).queryForList(startsWith("SELECT 'Gesamt'"),any(Object[].class));
}
@Test void statisticsRightIsIndependentOfOrderReadRight(){
assertThatThrownBy(()->service.calculate(new LegacyUser(11,12,7,"test","000010000000"),query(List.of(),List.of(),List.of(),"",null,null))).isInstanceOf(AccessDeniedException.class);
verifyNoInteractions(db);
var onlyStatistics=new LegacyUser(11,12,7,"stats","000000001000");
assertThat(onlyStatistics.capabilities().readOrders()).isFalse();
assertThat(onlyStatistics.capabilities().readStatistics()).isTrue();
assertThat(service.calculate(onlyStatistics,query(List.of(7L),List.of(),List.of(),"",null,null))).containsEntry("completePeriod",true);
}
@Test void comparisonPeriodsAreIndependentlyBoundAndAllGetTotals(){
var prior=new Interval(LocalDate.of(2025,1,1),LocalDate.of(2025,12,31));
var result=service.calculate(user,query(List.of(7L),List.of(prior),List.of(),"",null,null));
assertThat((List<?>)result.get("periods")).hasSize(2);
verify(db,times(4)).queryForList(startsWith("SELECT "),any(Object[].class));
}
@Test void invalidRangeAndOversizedComparisonsAreRejected(){
var q=query(List.of(7L),Collections.nCopies(6,new Interval(LocalDate.now(),LocalDate.now())),List.of(),"",null,null);
assertThatThrownBy(()->service.calculate(user,q)).hasMessageContaining("fünf");verifyNoInteractions(db);
}
}