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 hqs,List comparisons,List 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); } }