package handlers import ( "bytes" "context" "encoding/json" "io" "net/http" "strings" "testing" "time" "github.com/gofiber/fiber/v2" flightdata "wucher/internal/domain/flight_data" "wucher/internal/domain/mission" "wucher/internal/shared/pkg/uuidv7" ) func doReqMission(t *testing.T, app *fiber.App, method, path string, body []byte) (int, string) { t.Helper() var rdr io.Reader if body != nil { rdr = bytes.NewReader(body) } req, err := http.NewRequest(method, path, rdr) if err != nil { t.Fatalf("new req: %v", err) } if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(b) } func TestMissionHandlerDeleteByID(t *testing.T) { missionID := uuidv7.MustBytes() deletedBy := uuidv7.MustBytes() called := false h := NewMissionHandler(&missionServiceMock{ deleteByIDFn: func(_ context.Context, gotMissionID []byte, gotDeletedBy []byte) error { called = true if string(gotMissionID) != string(missionID) { t.Fatalf("unexpected mission id: %x", gotMissionID) } if string(gotDeletedBy) != string(deletedBy) { t.Fatalf("unexpected deleted by: %x", gotDeletedBy) } return nil }, }) app := fiber.New() app.Use(func(c *fiber.Ctx) error { c.Locals("user_id", deletedBy) return c.Next() }) app.Delete("/api/v1/mission/delete-by-id/:mission_id", h.DeleteByID) missionIDStr, err := uuidv7.BytesToString(missionID) if err != nil { t.Fatalf("mission id string: %v", err) } code, body := doReqMission(t, app, http.MethodDelete, "/api/v1/mission/delete-by-id/"+missionIDStr, nil) if code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", code, body) } if !strings.Contains(body, `"type":"mission_delete"`) || !strings.Contains(body, `"deleted":true`) { t.Fatalf("unexpected body: %s", body) } if !called { t.Fatal("expected service DeleteByID to be called") } } func TestMissionHandlerDeleteByIDValidation(t *testing.T) { h := NewMissionHandler(&missionServiceMock{}) app := fiber.New() app.Delete("/api/v1/mission/delete-by-id/:mission_id", h.DeleteByID) code, body := doReqMission(t, app, http.MethodDelete, "/api/v1/mission/delete-by-id/bad", nil) if code != http.StatusUnprocessableEntity { t.Fatalf("unexpected status: %d body=%s", code, body) } if !strings.Contains(body, mission.ErrorMessageInvalidMissionID) { t.Fatalf("unexpected validation body: %s", body) } } func TestMissionHandlerList_ReturnsKeyedFlightDataForms(t *testing.T) { missionID := uuidv7.MustBytes() flightID := uuidv7.MustBytes() flightDataID := uuidv7.MustBytes() h := NewMissionHandler(&missionServiceMock{ listFn: func(context.Context, string, string, []byte, int, int) ([]mission.Mission, int64, error) { return []mission.Mission{{ ID: missionID, FlightID: flightID, Type: "CAT", CreatedAt: time.Date(2026, time.June, 8, 8, 0, 0, 0, time.UTC), UpdatedAt: time.Date(2026, time.June, 8, 8, 30, 0, 0, time.UTC), }}, 1, nil }, }).WithFlightDataService(&flightDataServiceMock{ listFn: func(_ context.Context, _ string, gotMissionID []byte, _ []byte, _ int, _ int) ([]flightdata.FlightData, int64, error) { if string(gotMissionID) != string(missionID) { t.Fatalf("unexpected mission id: %x", gotMissionID) } return []flightdata.FlightData{{ ID: flightDataID, MissionID: gotMissionID, Mission: &mission.Mission{Type: "CAT"}, Status: flightdata.StatusCompleted, CreatedAt: time.Date(2026, time.June, 8, 8, 28, 20, 0, time.UTC), UpdatedAt: time.Date(2026, time.June, 8, 8, 28, 14, 0, time.UTC), }}, 1, nil }, }) app := fiber.New() app.Get("/api/v1/mission/get-all", h.List) flightIDStr, err := uuidv7.BytesToString(flightID) if err != nil { t.Fatalf("flight id string: %v", err) } code, body := doReqMission(t, app, http.MethodGet, "/api/v1/mission/get-all?flight_id="+flightIDStr, nil) if code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", code, body) } var doc map[string]any if err := json.Unmarshal([]byte(body), &doc); err != nil { t.Fatalf("unmarshal response: %v body=%s", err, body) } data, ok := doc["data"].([]any) if !ok || len(data) != 1 { t.Fatalf("unexpected data payload: %#v", doc["data"]) } resource, ok := data[0].(map[string]any) if !ok { t.Fatalf("unexpected resource payload: %#v", data[0]) } attributes, ok := resource["attributes"].(map[string]any) if !ok { t.Fatalf("unexpected attributes payload: %#v", resource["attributes"]) } if _, ok := attributes["flight_data"]; ok { t.Fatalf("expected top-level flight_data to be removed: %s", body) } if got := attributes["status"]; got != "completed" { t.Fatalf("unexpected status: %#v", got) } forms, ok := attributes["forms"].(map[string]any) if !ok { t.Fatalf("unexpected forms payload: %#v", attributes["forms"]) } items, ok := forms["flight_data"].([]any) if !ok { t.Fatalf("unexpected flight_data form payload: %#v", forms["flight_data"]) } if len(items) != 1 { t.Fatalf("expected 1 flight_data item, got %d", len(items)) } formItem, ok := items[0].(map[string]any) if !ok { t.Fatalf("unexpected flight_data item payload: %#v", items[0]) } if got := formItem["type"]; got != "flight_data" { t.Fatalf("unexpected flight_data type: %#v", got) } } func TestMissionHandlerList_StaysInProgressUntilAllFlightDataComplete(t *testing.T) { missionID := uuidv7.MustBytes() flightID := uuidv7.MustBytes() flightDataID := uuidv7.MustBytes() h := NewMissionHandler(&missionServiceMock{ listFn: func(context.Context, string, string, []byte, int, int) ([]mission.Mission, int64, error) { return []mission.Mission{{ ID: missionID, FlightID: flightID, Type: "CAT", CreatedAt: time.Date(2026, time.June, 8, 8, 0, 0, 0, time.UTC), UpdatedAt: time.Date(2026, time.June, 8, 8, 30, 0, 0, time.UTC), }}, 1, nil }, }).WithFlightDataService(&flightDataServiceMock{ listFn: func(_ context.Context, _ string, gotMissionID []byte, _ []byte, _ int, _ int) ([]flightdata.FlightData, int64, error) { if string(gotMissionID) != string(missionID) { t.Fatalf("unexpected mission id: %x", gotMissionID) } return []flightdata.FlightData{{ ID: flightDataID, MissionID: gotMissionID, Mission: &mission.Mission{Type: "CAT"}, Status: flightdata.StatusInProgress, CreatedAt: time.Date(2026, time.June, 8, 8, 28, 20, 0, time.UTC), UpdatedAt: time.Date(2026, time.June, 8, 8, 28, 14, 0, time.UTC), }}, 1, nil }, }) app := fiber.New() app.Get("/api/v1/mission/get-all", h.List) flightIDStr, err := uuidv7.BytesToString(flightID) if err != nil { t.Fatalf("flight id string: %v", err) } code, body := doReqMission(t, app, http.MethodGet, "/api/v1/mission/get-all?flight_id="+flightIDStr, nil) if code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", code, body) } var doc map[string]any if err := json.Unmarshal([]byte(body), &doc); err != nil { t.Fatalf("unmarshal response: %v body=%s", err, body) } data, ok := doc["data"].([]any) if !ok || len(data) != 1 { t.Fatalf("unexpected data payload: %#v", doc["data"]) } resource, ok := data[0].(map[string]any) if !ok { t.Fatalf("unexpected resource payload: %#v", data[0]) } attributes, ok := resource["attributes"].(map[string]any) if !ok { t.Fatalf("unexpected attributes payload: %#v", resource["attributes"]) } if got := attributes["status"]; got != "in_progress" { t.Fatalf("unexpected status: %#v", got) } forms, ok := attributes["forms"].(map[string]any) if !ok { t.Fatalf("unexpected forms payload: %#v", attributes["forms"]) } items, ok := forms["flight_data"].([]any) if !ok || len(items) != 1 { t.Fatalf("unexpected flight_data form payload: %#v", forms["flight_data"]) } } func TestMissionHandlerListDatatable_FiltersAndMeta(t *testing.T) { missionID := uuidv7.MustBytes() flightID := uuidv7.MustBytes() pilotID := uuidv7.MustBytes() helicopterID := uuidv7.MustBytes() h := NewMissionHandler(&missionServiceMock{ listDTFn: func(_ context.Context, got mission.ListFilter, limit, offset int) ([]mission.Mission, int64, int64, error) { if got.Search != "needle" { t.Fatalf("unexpected search: %q", got.Search) } if got.MissionType != "CAT" { t.Fatalf("unexpected mission type: %q", got.MissionType) } if got.StartDate == nil || got.StartDate.Format("2006-01-02") != "2026-06-01" { t.Fatalf("unexpected start date: %#v", got.StartDate) } if got.EndDate == nil || got.EndDate.Format("2006-01-02") != "2026-06-30" { t.Fatalf("unexpected end date: %#v", got.EndDate) } if string(got.PilotID) != string(pilotID) { t.Fatalf("unexpected pilot id: %x", got.PilotID) } if string(got.HelicopterID) != string(helicopterID) { t.Fatalf("unexpected helicopter id: %x", got.HelicopterID) } if limit != 20 || offset != 0 { t.Fatalf("unexpected pagination: limit=%d offset=%d", limit, offset) } return []mission.Mission{{ ID: missionID, FlightID: flightID, Type: "CAT", CreatedAt: time.Date(2026, time.June, 8, 8, 0, 0, 0, time.UTC), UpdatedAt: time.Date(2026, time.June, 8, 8, 30, 0, 0, time.UTC), }}, 10, 2, nil }, }).WithFlightDataService(&flightDataServiceMock{ listFn: func(_ context.Context, _ string, gotMissionID []byte, _ []byte, _ int, _ int) ([]flightdata.FlightData, int64, error) { if string(gotMissionID) != string(missionID) { t.Fatalf("unexpected mission id: %x", gotMissionID) } return nil, 0, nil }, }) app := fiber.New() app.Get("/api/v1/mission/get-all/dt", h.ListDatatable) flightIDStr, err := uuidv7.BytesToString(flightID) if err != nil { t.Fatalf("flight id string: %v", err) } pilotIDStr, err := uuidv7.BytesToString(pilotID) if err != nil { t.Fatalf("pilot id string: %v", err) } helicopterIDStr, err := uuidv7.BytesToString(helicopterID) if err != nil { t.Fatalf("helicopter id string: %v", err) } code, body := doReqMission(t, app, http.MethodGet, "/api/v1/mission/get-all/dt?search=needle&date_from=2026-06-01&date_to=2026-06-30&mission_type=CAT&pilot_id="+pilotIDStr+"&helicopter_id="+helicopterIDStr+"&flight_id="+flightIDStr+"&draw=7&start=0&length=20", nil) if code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", code, body) } var doc map[string]any if err := json.Unmarshal([]byte(body), &doc); err != nil { t.Fatalf("unmarshal response: %v body=%s", err, body) } meta, ok := doc["meta"].(map[string]any) if !ok { t.Fatalf("unexpected meta payload: %#v", doc["meta"]) } if meta["draw"] != float64(7) { t.Fatalf("unexpected draw: %#v", meta["draw"]) } if meta["records_total"] != float64(10) || meta["records_filtered"] != float64(2) { t.Fatalf("unexpected meta counts: %#v", meta) } data, ok := doc["data"].([]any) if !ok || len(data) != 1 { t.Fatalf("unexpected data payload: %#v", doc["data"]) } }