Some checks failed
Deploy to NAS / deploy (push) Failing after 14s
- Introduce store interfaces (TrackpointStorer, StopStorer, SuggestionStorer) in internal/db/interfaces.go so handlers can be tested without a real DB. - Refactor HandleSingleTrackpoint, HandleBatchTrackpoints, HandleListDays, HandleListTrackpoints, HandleListStops, HandleListSuggestions to accept the new interfaces instead of concrete *db.*Store pointers (no behaviour change; concrete types satisfy the interfaces implicitly). - internal/api/ingest_test.go: 13 handler tests covering happy path, invalid JSON, invalid timestamp, missing event_id/device_id, out-of-range lat/lon, empty/oversized batch, store errors, and idempotency (single + batch). - internal/api/query_test.go: 14 handler tests covering missing query params (400) and empty-result-is-array guarantees for all four query endpoints. - internal/auth/auth_test.go: 5 unit tests for HashPassword / VerifyPassword (correct password, wrong password, empty password, malformed hash, salt uniqueness). - internal/db/trackpoints_test.go: 6 unit tests for the validateTrackpoint helper (happy path, missing fields, coordinate bounds, invalid source). - .gitea/workflows/deploy.yml: add "Test" step (go test ./...) before "Build & Deploy" so a failing test aborts the deployment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
319 lines
8.6 KiB
Go
319 lines
8.6 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jacek/pamietnik/backend/internal/db"
|
|
"github.com/jacek/pamietnik/backend/internal/domain"
|
|
)
|
|
|
|
// fakeQueryTrackpointStore implements db.TrackpointStorer for query handler tests.
|
|
type fakeQueryTrackpointStore struct {
|
|
days []domain.DaySummary
|
|
points []domain.Trackpoint
|
|
err error
|
|
}
|
|
|
|
func (f *fakeQueryTrackpointStore) UpsertBatch(_ context.Context, _ string, _ []domain.Trackpoint) ([]string, []db.RejectedItem, error) {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
func (f *fakeQueryTrackpointStore) ListByDate(_ context.Context, _, _ string) ([]domain.Trackpoint, error) {
|
|
return f.points, f.err
|
|
}
|
|
|
|
func (f *fakeQueryTrackpointStore) ListDays(_ context.Context, _, _, _ string) ([]domain.DaySummary, error) {
|
|
return f.days, f.err
|
|
}
|
|
|
|
// fakeStopStore implements db.StopStorer.
|
|
type fakeStopStore struct {
|
|
stops []domain.Stop
|
|
err error
|
|
}
|
|
|
|
func (f *fakeStopStore) ListByDate(_ context.Context, _, _ string) ([]domain.Stop, error) {
|
|
return f.stops, f.err
|
|
}
|
|
|
|
// fakeSuggestionStore implements db.SuggestionStorer.
|
|
type fakeSuggestionStore struct {
|
|
suggestions []domain.Suggestion
|
|
err error
|
|
}
|
|
|
|
func (f *fakeSuggestionStore) ListByDate(_ context.Context, _, _ string) ([]domain.Suggestion, error) {
|
|
return f.suggestions, f.err
|
|
}
|
|
|
|
// --- HandleListDays ---
|
|
|
|
func TestHandleListDays_MissingFromParam(t *testing.T) {
|
|
handler := HandleListDays(&fakeQueryTrackpointStore{})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/days?to=2024-06-30", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 when 'from' missing, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleListDays_MissingToParam(t *testing.T) {
|
|
handler := HandleListDays(&fakeQueryTrackpointStore{})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/days?from=2024-06-01", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 when 'to' missing, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleListDays_BothParamsMissing(t *testing.T) {
|
|
handler := HandleListDays(&fakeQueryTrackpointStore{})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/days", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 when both params missing, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleListDays_EmptyResultIsArray(t *testing.T) {
|
|
handler := HandleListDays(&fakeQueryTrackpointStore{days: nil})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/days?from=2024-06-01&to=2024-06-30", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
// Must be a JSON array, not null
|
|
body := strings.TrimSpace(rec.Body.String())
|
|
if !strings.HasPrefix(body, "[") {
|
|
t.Errorf("expected JSON array, got: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestHandleListDays_ReturnsDays(t *testing.T) {
|
|
ts := time.Now()
|
|
store := &fakeQueryTrackpointStore{
|
|
days: []domain.DaySummary{
|
|
{Date: "2024-06-01", Count: 42, FirstTS: &ts, LastTS: &ts},
|
|
},
|
|
}
|
|
handler := HandleListDays(store)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/days?from=2024-06-01&to=2024-06-30", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
var days []domain.DaySummary
|
|
json.NewDecoder(rec.Body).Decode(&days)
|
|
if len(days) != 1 || days[0].Date != "2024-06-01" || days[0].Count != 42 {
|
|
t.Errorf("unexpected response: %+v", days)
|
|
}
|
|
}
|
|
|
|
// --- HandleListTrackpoints ---
|
|
|
|
func TestHandleListTrackpoints_MissingDateParam(t *testing.T) {
|
|
handler := HandleListTrackpoints(&fakeQueryTrackpointStore{})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/trackpoints", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 when 'date' missing, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleListTrackpoints_EmptyResultIsArray(t *testing.T) {
|
|
handler := HandleListTrackpoints(&fakeQueryTrackpointStore{points: nil})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/trackpoints?date=2024-06-01", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
body := strings.TrimSpace(rec.Body.String())
|
|
if !strings.HasPrefix(body, "[") {
|
|
t.Errorf("expected JSON array, got: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestHandleListTrackpoints_ReturnsPoints(t *testing.T) {
|
|
store := &fakeQueryTrackpointStore{
|
|
points: []domain.Trackpoint{
|
|
{EventID: "e1", DeviceID: "d1", Lat: 52.5, Lon: 13.4, Source: "gps"},
|
|
{EventID: "e2", DeviceID: "d1", Lat: 52.6, Lon: 13.5, Source: "gps"},
|
|
},
|
|
}
|
|
handler := HandleListTrackpoints(store)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/trackpoints?date=2024-06-01", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
var pts []domain.Trackpoint
|
|
json.NewDecoder(rec.Body).Decode(&pts)
|
|
if len(pts) != 2 {
|
|
t.Errorf("expected 2 trackpoints, got %d", len(pts))
|
|
}
|
|
}
|
|
|
|
// --- HandleListStops ---
|
|
|
|
func TestHandleListStops_MissingDateParam(t *testing.T) {
|
|
handler := HandleListStops(&fakeStopStore{})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/stops", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 when 'date' missing, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleListStops_EmptyResultIsArray(t *testing.T) {
|
|
handler := HandleListStops(&fakeStopStore{stops: nil})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/stops?date=2024-06-01", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
body := strings.TrimSpace(rec.Body.String())
|
|
if !strings.HasPrefix(body, "[") {
|
|
t.Errorf("expected JSON array, got: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestHandleListStops_ReturnsStops(t *testing.T) {
|
|
now := time.Now()
|
|
store := &fakeStopStore{
|
|
stops: []domain.Stop{
|
|
{StopID: "stop-1", DeviceID: "d1", StartTS: now, EndTS: now, CenterLat: 52.5, CenterLon: 13.4, DurationS: 600},
|
|
},
|
|
}
|
|
handler := HandleListStops(store)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/stops?date=2024-06-01", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
var stops []domain.Stop
|
|
json.NewDecoder(rec.Body).Decode(&stops)
|
|
if len(stops) != 1 || stops[0].StopID != "stop-1" {
|
|
t.Errorf("unexpected response: %+v", stops)
|
|
}
|
|
}
|
|
|
|
// --- HandleListSuggestions ---
|
|
|
|
func TestHandleListSuggestions_MissingDateParam(t *testing.T) {
|
|
handler := HandleListSuggestions(&fakeSuggestionStore{})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/suggestions", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 when 'date' missing, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleListSuggestions_EmptyResultIsArray(t *testing.T) {
|
|
handler := HandleListSuggestions(&fakeSuggestionStore{suggestions: nil})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/suggestions?date=2024-06-01", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
body := strings.TrimSpace(rec.Body.String())
|
|
if !strings.HasPrefix(body, "[") {
|
|
t.Errorf("expected JSON array, got: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestHandleListSuggestions_ReturnsSuggestions(t *testing.T) {
|
|
now := time.Now()
|
|
store := &fakeSuggestionStore{
|
|
suggestions: []domain.Suggestion{
|
|
{SuggestionID: "sug-1", StopID: "stop-1", Type: "highlight", Title: "Nice spot", CreatedAt: now},
|
|
},
|
|
}
|
|
handler := HandleListSuggestions(store)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/suggestions?date=2024-06-01", nil)
|
|
req = authContext(req)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
var suggestions []domain.Suggestion
|
|
json.NewDecoder(rec.Body).Decode(&suggestions)
|
|
if len(suggestions) != 1 || suggestions[0].SuggestionID != "sug-1" {
|
|
t.Errorf("unexpected response: %+v", suggestions)
|
|
}
|
|
}
|