Files
bot-pontos/backend/internal/rules/rules_test.go
2026-10-23 22:31:02 -03:00

230 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rules
import (
"testing"
"time"
"botpontos/internal/domain"
)
var saoPaulo = domain.LoadLocation("America/Sao_Paulo")
const testDay = "2026-10-14" // quarta-feira
func at(clock string) time.Time {
return domain.MustParseClock(clock).On(testDay, saoPaulo)
}
func workday(clocks ...string) domain.Workday {
points := make([]domain.Point, 0, len(clocks))
for _, c := range clocks {
points = append(points, domain.Point{OccurredAt: at(c)})
}
return CurrentWorkday(points, testDay, saoPaulo)
}
func TestCurrentWorkdayFiltersAndSorts(t *testing.T) {
yesterday := domain.MustParseClock("17:00").On("2026-10-13", saoPaulo)
points := []domain.Point{{OccurredAt: at("12:00")}, {OccurredAt: yesterday}, {OccurredAt: at("08:00")}}
w := CurrentWorkday(points, testDay, saoPaulo)
if w.Count() != 2 || !w.Entry1().Equal(at("08:00")) || !w.Exit1().Equal(at("12:00")) {
t.Fatalf("pontos inesperados: %+v", w.Points)
}
if w.Invalid || w.Overtime {
t.Fatalf("dia deveria ser válido: %+v", w)
}
}
func TestCurrentWorkdayInvalidAndOvertime(t *testing.T) {
if w := workday("08:00", "08:00"); !w.Invalid {
t.Error("pontos repetidos deveriam invalidar o dia")
}
if w := workday("08:00", "12:00", "13:00", "17:00", "18:00"); !w.Overtime {
t.Error("mais de quatro pontos deveria ser hora extra")
}
}
func TestEntrada2AndSaida2(t *testing.T) {
w := workday("08:00", "12:00", "13:00")
entrada2, err := Entrada2Time(w, 60)
if err != nil || !entrada2.Equal(at("13:00")) {
t.Fatalf("entrada2 = %v, %v", entrada2, err)
}
saida2, err := Saida2Time(w, 480)
if err != nil || !saida2.Equal(at("17:00")) {
t.Fatalf("saida2 = %v, %v", saida2, err)
}
if reminder := ReminderAt(saida2, 5); !reminder.Equal(at("16:55")) {
t.Fatalf("lembrete = %v", reminder)
}
if _, err := Saida2Time(workday("08:00", "17:00", "18:00"), 480); err != ErrNoRemainingWork {
t.Fatalf("esperado ErrNoRemainingWork, veio %v", err)
}
if _, err := Entrada2Time(workday("08:00"), 60); err != ErrNotEnoughPoints {
t.Fatalf("esperado ErrNotEnoughPoints, veio %v", err)
}
}
func TestFormat(t *testing.T) {
if got := FormatMinutes(480); got != "8h00" {
t.Errorf("FormatMinutes(480) = %s", got)
}
if got := FormatMinutes(390); got != "6h30" {
t.Errorf("FormatMinutes(390) = %s", got)
}
if got := FormatMinutesShort(15); got != "15 min" {
t.Errorf("FormatMinutesShort(15) = %s", got)
}
}
func TestValidateSchedule(t *testing.T) {
global := domain.DefaultQueryWindow()
ok := domain.DefaultSchedule(1)
if errs := ValidateSchedule(ok, global, 15*time.Minute); len(errs) != 0 {
t.Fatalf("jornada padrão deveria ser válida: %v", errs)
}
short := ok
short.LunchMinutes = 15 // 15 − 5 = 10 < 15
if errs := ValidateSchedule(short, global, 15*time.Minute); len(errs) == 0 {
t.Fatal("almoço de 15 min com antecedência de 5 deveria ser recusado")
}
twenty := ok
twenty.LunchMinutes = 20 // 20 − 5 = 15
if errs := ValidateSchedule(twenty, global, 15*time.Minute); len(errs) != 0 {
t.Fatalf("almoço de 20 min deveria passar: %v", errs)
}
}
func TestValidateWindow(t *testing.T) {
if errs := ValidateWindow(domain.DefaultQueryWindow(), 15*time.Minute, ""); len(errs) != 0 {
t.Fatalf("janela padrão deveria ser válida: %v", errs)
}
misaligned := domain.DefaultQueryWindow()
misaligned.Start = domain.MustParseClock("11:20")
if errs := ValidateWindow(misaligned, 15*time.Minute, ""); len(errs) == 0 {
t.Error("horário fora do alinhamento deveria ser recusado")
}
outOfOrder := domain.DefaultQueryWindow()
outOfOrder.DayCheckDeadline = domain.MustParseClock("16:00")
if errs := ValidateWindow(outOfOrder, 15*time.Minute, ""); len(errs) == 0 {
t.Error("confirmação depois do fim das reverificações deveria ser recusada")
}
tooShort := domain.QueryWindow{
Start: domain.MustParseClock("11:30"),
DayCheckDeadline: domain.MustParseClock("11:45"),
NoPointsRecheckUntil: domain.MustParseClock("13:00"),
End: domain.MustParseClock("15:00"),
}
if errs := ValidateWindow(tooShort, 15*time.Minute, ""); len(errs) == 0 {
t.Error("janela com menos de 4 horas deveria ser recusada")
}
}
func plan(tick string, w domain.Workday, saida2At *time.Time) Plan {
return PlanNextCheck(PlanInput{
Tick: at(tick),
Day: testDay,
Location: saoPaulo,
Window: domain.DefaultQueryWindow(),
Workday: w,
Saida2At: saida2At,
Reconciliation: 15 * time.Minute,
Slow: 60 * time.Minute,
ExitConfirmFrom: 20 * time.Minute,
ExitConfirmTo: 5 * time.Minute,
})
}
func assertPlan(t *testing.T, got Plan, status domain.DayStatus, next string) {
t.Helper()
if got.Status != status {
t.Errorf("status = %s, esperado %s", got.Status, status)
}
switch {
case next == "" && got.NextCheckAt != nil:
t.Errorf("próxima consulta = %s, esperado nenhuma", got.NextCheckAt.In(saoPaulo).Format("15:04"))
case next != "" && got.NextCheckAt == nil:
t.Errorf("próxima consulta = nenhuma, esperado %s", next)
case next != "" && !got.NextCheckAt.Equal(at(next)):
t.Errorf("próxima consulta = %s, esperado %s", got.NextCheckAt.In(saoPaulo).Format("15:04"), next)
}
}
func TestPlanNoPoints(t *testing.T) {
empty := workday()
assertPlan(t, plan("11:30", empty, nil), domain.DayAwaiting, "11:45")
assertPlan(t, plan("11:45", empty, nil), domain.DayNoPoints, "12:45")
assertPlan(t, plan("13:45", empty, nil), domain.DayNoPoints, "14:45")
assertPlan(t, plan("14:45", empty, nil), domain.DayNotWorking, "")
}
func TestPlanResumedWithoutPoints(t *testing.T) {
in := PlanInput{
Tick: at("12:30"), Day: testDay, Location: saoPaulo, Window: domain.DefaultQueryWindow(),
Workday: workday(), Resumed: true, Reconciliation: 15 * time.Minute, Slow: time.Hour,
ExitConfirmFrom: 20 * time.Minute, ExitConfirmTo: 5 * time.Minute,
}
assertPlan(t, PlanNextCheck(in), domain.DayWorking, "12:45")
}
func TestPlanBeforeAndDuringLunch(t *testing.T) {
assertPlan(t, plan("11:30", workday("08:00"), nil), domain.DayWorking, "11:45")
assertPlan(t, plan("12:15", workday("08:00", "12:00"), nil), domain.DayWorking, "12:30")
}
func TestPlanAfternoonWithConfirmation(t *testing.T) {
afternoon := workday("08:00", "12:00", "13:00")
reminder := at("16:55")
assertPlan(t, plan("13:00", afternoon, &reminder), domain.DayWorking, "14:00")
assertPlan(t, plan("16:00", afternoon, &reminder), domain.DayWorking, "16:45")
assertPlan(t, plan("16:45", afternoon, &reminder), domain.DayWorking, "")
}
func TestPlanFinishedAndWindowEnd(t *testing.T) {
assertPlan(t, plan("17:00", workday("08:00", "12:00", "13:00", "17:00"), nil), domain.DayFinished, "")
// 3 pontos às 18:30 sem lembrete agendado: a próxima consulta lenta (19:30) passa do fim da janela.
assertPlan(t, plan("18:30", workday("10:00", "14:00", "15:00"), nil), domain.DayWorking, "")
}
func TestConfirmTick(t *testing.T) {
cases := map[string]string{"16:55": "16:45", "17:00": "16:45", "16:50": "16:30"}
for reminder, expected := range cases {
got := ConfirmTick(at(reminder), 15*time.Minute, 5*time.Minute)
if !got.Equal(at(expected)) {
t.Errorf("ConfirmTick(%s) = %s, esperado %s", reminder, got.In(saoPaulo).Format("15:04"), expected)
}
}
}
func TestResumeNextCheck(t *testing.T) {
w := domain.DefaultQueryWindow()
if next := ResumeNextCheck(at("09:10"), w, testDay, saoPaulo, 15*time.Minute); next == nil || !next.Equal(at("11:30")) {
t.Errorf("antes da janela deveria começar às 11:30, veio %v", next)
}
if next := ResumeNextCheck(at("12:31"), w, testDay, saoPaulo, 15*time.Minute); next == nil || !next.Equal(at("12:45")) {
t.Errorf("dentro da janela deveria ser o próximo tick, veio %v", next)
}
if next := ResumeNextCheck(at("19:10"), w, testDay, saoPaulo, 15*time.Minute); next != nil {
t.Errorf("depois da janela não deveria haver consulta, veio %v", next)
}
}
func TestParseClock(t *testing.T) {
for _, invalid := range []string{"7:30", "24:00", "12:60", "ab:cd", "12-30", ""} {
if _, err := domain.ParseClock(invalid); err == nil {
t.Errorf("ParseClock(%q) deveria falhar", invalid)
}
}
if c, err := domain.ParseClock("11:45"); err != nil || c.String() != "11:45" {
t.Errorf("ParseClock(11:45) = %v, %v", c, err)
}
}