package pontomais import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "botpontos/internal/domain" ) type plainKeys struct{} func (plainKeys) Decrypt(data []byte, _ int16) (string, error) { return string(data), nil } var saoPaulo = domain.LoadLocation("America/Sao_Paulo") func newServer(t *testing.T, historyStatus func(call int32) int) (*httptest.Server, *int32, *int32) { t.Helper() var logins, histories int32 mux := http.NewServeMux() mux.HandleFunc("POST /api/auth/sign_in", func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&logins, 1) var body authRequest _ = json.NewDecoder(r.Body).Decode(&body) if body.Password != "certa" { w.WriteHeader(http.StatusUnauthorized) return } _ = json.NewEncoder(w).Encode(map[string]any{"token": "tok", "client_id": "cli", "data": map[string]string{"login": body.Login}}) }) mux.HandleFunc("GET /api/time_cards/current/last_cached", func(w http.ResponseWriter, r *http.Request) { call := atomic.AddInt32(&histories, 1) if status := historyStatus(call); status != http.StatusOK { w.WriteHeader(status) return } if r.Header.Get("access-token") != "tok" { w.WriteHeader(http.StatusUnauthorized) return } _ = json.NewEncoder(w).Encode(map[string]any{"time_cards": []map[string]any{ {"date": "2026-10-14", "time": "08:00:00"}, {"date": "2026-10-14", "time": "12:00:00"}, {"date": "2026-10-14", "time": "12:30:00", "disabled": true}, }}) }) server := httptest.NewServer(mux) t.Cleanup(server.Close) return server, &logins, &histories } func newClient(server *httptest.Server) *Client { c := NewClient(server.URL+"/api/auth/sign_in", server.URL+"/api/time_cards/current/last_cached", 2*time.Second, plainKeys{}) c.retryDelay = time.Millisecond return c } func cred(password string) domain.Credential { return domain.Credential{UserID: 1, APIEmail: "a@b.c", EncryptedPassword: []byte(password), UpdatedAt: time.Unix(100, 0)} } func TestHistoryParsesAndCachesSession(t *testing.T) { server, logins, _ := newServer(t, func(int32) int { return http.StatusOK }) c := newClient(server) points, err := c.History(context.Background(), cred("certa"), saoPaulo) if err != nil { t.Fatal(err) } if len(points) != 2 { t.Fatalf("esperados 2 pontos (o desabilitado é ignorado), vieram %d", len(points)) } if got := points[0].OccurredAt.In(saoPaulo).Format("15:04"); got != "08:00" { t.Errorf("primeiro ponto = %s", got) } if _, err := c.History(context.Background(), cred("certa"), saoPaulo); err != nil { t.Fatal(err) } if *logins != 1 { t.Errorf("a sessão deveria ser reaproveitada; logins = %d", *logins) } } func TestHistoryRetriesTransientError(t *testing.T) { server, _, histories := newServer(t, func(call int32) int { if call == 1 { return http.StatusBadGateway } return http.StatusOK }) if _, err := newClient(server).History(context.Background(), cred("certa"), saoPaulo); err != nil { t.Fatalf("a segunda tentativa deveria funcionar: %v", err) } if *histories != 2 { t.Errorf("esperadas 2 chamadas ao histórico, vieram %d", *histories) } } func TestHistoryRejectedCredential(t *testing.T) { server, _, _ := newServer(t, func(int32) int { return http.StatusOK }) _, err := newClient(server).History(context.Background(), cred("errada"), saoPaulo) if !errors.Is(err, ErrAuthRejected) { t.Fatalf("esperado ErrAuthRejected, veio %v", err) } } func TestHistoryRelogsAfterUnauthorized(t *testing.T) { server, logins, _ := newServer(t, func(call int32) int { if call == 2 { return http.StatusUnauthorized } return http.StatusOK }) c := newClient(server) if _, err := c.History(context.Background(), cred("certa"), saoPaulo); err != nil { t.Fatal(err) } if _, err := c.History(context.Background(), cred("certa"), saoPaulo); err != nil { t.Fatalf("deveria refazer o login após 401: %v", err) } if *logins != 2 { t.Errorf("esperados 2 logins, vieram %d", *logins) } } func TestValidate(t *testing.T) { server, _, _ := newServer(t, func(int32) int { return http.StatusOK }) c := newClient(server) if err := c.Validate(context.Background(), "a@b.c", "certa", saoPaulo); err != nil { t.Fatalf("credencial certa: %v", err) } if err := c.Validate(context.Background(), "a@b.c", "errada", saoPaulo); !errors.Is(err, ErrAuthRejected) { t.Fatalf("credencial errada: %v", err) } }