213 lines
5.9 KiB
Go
213 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// create an in-memory DB with schema/index identical to main
|
|
func newTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite", "file:testdb?mode=memory&cache=shared")
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
schema := `CREATE TABLE IF NOT EXISTS drone_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
drone_id TEXT NOT NULL,
|
|
latitude REAL NOT NULL,
|
|
longitude REAL NOT NULL,
|
|
timestamp TEXT NOT NULL
|
|
);`
|
|
if _, err := db.Exec(schema); err != nil {
|
|
t.Fatalf("schema: %v", err)
|
|
}
|
|
if _, err := db.Exec(`CREATE INDEX IF NOT EXISTS idx_drone_timestamp ON drone_events(drone_id, timestamp);`); err != nil {
|
|
t.Fatalf("index: %v", err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func TestIngestAndSnapshot(t *testing.T) {
|
|
db := newTestDB(t)
|
|
srv := &Server{db: db}
|
|
|
|
// ingest two drones with multiple timestamps
|
|
events := []DroneEvent{
|
|
{"d1", 1.0, 2.0, "2025-12-06T00:00:00Z"},
|
|
{"d1", 1.1, 2.1, "2025-12-06T00:00:10Z"},
|
|
{"d2", 3.0, 4.0, "2025-12-06T00:00:05Z"},
|
|
}
|
|
for _, ev := range events {
|
|
body, _ := json.Marshal(ev)
|
|
req := httptest.NewRequest(http.MethodPost, "/ingest", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
srv.ingestHandler(rec, req)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("ingest %v code=%d body=%s", ev, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
// snapshot at 00:00:07 should pick d1@00:00:00? no 00:00:10 too new -> d1@0, d2@5
|
|
q := url.Values{}
|
|
q.Set("time", "2025-12-06T00:00:07Z")
|
|
req := httptest.NewRequest(http.MethodGet, "/snapshot?"+q.Encode(), nil)
|
|
rec := httptest.NewRecorder()
|
|
srv.snapshotHandler(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("snapshot code=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp []DroneEvent
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode snapshot: %v", err)
|
|
}
|
|
if len(resp) != 2 {
|
|
t.Fatalf("expected 2 events, got %d", len(resp))
|
|
}
|
|
// map by drone
|
|
got := map[string]DroneEvent{}
|
|
for _, e := range resp {
|
|
got[e.DroneID] = e
|
|
}
|
|
if got["d1"].Timestamp != "2025-12-06T00:00:00Z" {
|
|
t.Fatalf("d1 timestamp mismatch: %v", got["d1"])
|
|
}
|
|
if got["d2"].Timestamp != "2025-12-06T00:00:05Z" {
|
|
t.Fatalf("d2 timestamp mismatch: %v", got["d2"])
|
|
}
|
|
}
|
|
|
|
func TestSnapshotMissingTime(t *testing.T) {
|
|
db := newTestDB(t)
|
|
srv := &Server{db: db}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/snapshot", nil)
|
|
rec := httptest.NewRecorder()
|
|
srv.snapshotHandler(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHealth(t *testing.T) {
|
|
db := newTestDB(t)
|
|
srv := &Server{db: db}
|
|
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
|
rec := httptest.NewRecorder()
|
|
srv.healthHandler(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("health code=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestIngestBadJSON(t *testing.T) {
|
|
db := newTestDB(t)
|
|
srv := &Server{db: db}
|
|
req := httptest.NewRequest(http.MethodPost, "/ingest", strings.NewReader("{bad"))
|
|
rec := httptest.NewRecorder()
|
|
srv.ingestHandler(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// Ensure graceful shutdown path does not hang: start a server and shut it down quickly.
|
|
func TestGracefulShutdown(t *testing.T) {
|
|
db := newTestDB(t)
|
|
defer db.Close()
|
|
srv := &Server{db: db}
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /health", srv.healthHandler)
|
|
testSrv := &http.Server{Addr: "127.0.0.1:0", Handler: mux}
|
|
|
|
go func() { _ = testSrv.ListenAndServe() }()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
if err := testSrv.Shutdown(ctx); err != nil {
|
|
t.Fatalf("shutdown: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCORSAllowedOriginAndPreflight(t *testing.T) {
|
|
handled := false
|
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
handled = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
handler := corsMiddleware(next)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
|
req.Header.Set("Origin", "http://localhost:5173")
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "http://localhost:5173" {
|
|
t.Fatalf("expected allow-origin header, got %q", got)
|
|
}
|
|
if !handled {
|
|
t.Fatalf("expected handler to be called")
|
|
}
|
|
|
|
preflight := httptest.NewRequest(http.MethodOptions, "/health", nil)
|
|
preflight.Header.Set("Origin", "http://localhost:5173")
|
|
preflightRec := httptest.NewRecorder()
|
|
handler.ServeHTTP(preflightRec, preflight)
|
|
if preflightRec.Code != http.StatusNoContent {
|
|
t.Fatalf("preflight expected 204, got %d", preflightRec.Code)
|
|
}
|
|
}
|
|
|
|
func TestIngestDBError(t *testing.T) {
|
|
db := newTestDB(t)
|
|
db.Close()
|
|
srv := &Server{db: db}
|
|
|
|
ev := DroneEvent{DroneID: "d1", Latitude: 1.0, Longitude: 2.0, Timestamp: "2025-12-06T00:00:00Z"}
|
|
body, _ := json.Marshal(ev)
|
|
req := httptest.NewRequest(http.MethodPost, "/ingest", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
srv.ingestHandler(rec, req)
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected 500, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestSnapshotQueryError(t *testing.T) {
|
|
db := newTestDB(t)
|
|
db.Close()
|
|
srv := &Server{db: db}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/snapshot?time=2025-12-06T00:00:00Z", nil)
|
|
rec := httptest.NewRecorder()
|
|
srv.snapshotHandler(rec, req)
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected 500, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHealthFailure(t *testing.T) {
|
|
db := newTestDB(t)
|
|
db.Close()
|
|
srv := &Server{db: db}
|
|
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
|
rec := httptest.NewRecorder()
|
|
srv.healthHandler(rec, req)
|
|
if rec.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("expected 503, got %d", rec.Code)
|
|
}
|
|
}
|