init push
This commit is contained in:
615
internal/transport/http/handlers/complaint_handler_test.go
Normal file
615
internal/transport/http/handlers/complaint_handler_test.go
Normal file
@@ -0,0 +1,615 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
actionsignoff "wucher/internal/domain/action_signoff"
|
||||
"wucher/internal/domain/complaint"
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type complaintSignoffMock struct {
|
||||
getByComplaintIDsFn func(context.Context, [][]byte) (map[string]*actionsignoff.ActionSignoff, error)
|
||||
unsignByComplaintFn func(context.Context, []byte) (*actionsignoff.ActionSignoff, error)
|
||||
}
|
||||
|
||||
func (m *complaintSignoffMock) GetByComplaintIDs(ctx context.Context, ids [][]byte) (map[string]*actionsignoff.ActionSignoff, error) {
|
||||
if m.getByComplaintIDsFn != nil {
|
||||
return m.getByComplaintIDsFn(ctx, ids)
|
||||
}
|
||||
return map[string]*actionsignoff.ActionSignoff{}, nil
|
||||
}
|
||||
|
||||
func (m *complaintSignoffMock) UnsignByComplaint(ctx context.Context, complaintID []byte) (*actionsignoff.ActionSignoff, error) {
|
||||
if m.unsignByComplaintFn != nil {
|
||||
return m.unsignByComplaintFn(ctx, complaintID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type complaintServiceMock struct {
|
||||
createFn func(context.Context, *complaint.Complaint) error
|
||||
updateFn func(context.Context, *complaint.Complaint) error
|
||||
getByIDFn func(context.Context, []byte) (*complaint.Complaint, error)
|
||||
hasOpenFn func(context.Context, []byte) (bool, error)
|
||||
listFn func(context.Context, complaint.ListFilter, string, int, int) ([]complaint.Complaint, int64, error)
|
||||
listByHelicopterFn func(context.Context, []byte, int, int) ([]complaint.Complaint, int64, error)
|
||||
resolveNamesFn func(context.Context, []complaint.Complaint) (map[string]string, error)
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) Create(ctx context.Context, row *complaint.Complaint) error {
|
||||
if m.createFn != nil {
|
||||
return m.createFn(ctx, row)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) Update(ctx context.Context, row *complaint.Complaint) error {
|
||||
if m.updateFn != nil {
|
||||
return m.updateFn(ctx, row)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) Delete(context.Context, []byte, []byte) error { return nil }
|
||||
|
||||
func (m *complaintServiceMock) GetByID(ctx context.Context, id []byte) (*complaint.Complaint, error) {
|
||||
if m.getByIDFn != nil {
|
||||
return m.getByIDFn(ctx, id)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) ListByHelicopter(ctx context.Context, helicopterID []byte, limit, offset int) ([]complaint.Complaint, int64, error) {
|
||||
if m.listByHelicopterFn != nil {
|
||||
return m.listByHelicopterFn(ctx, helicopterID, limit, offset)
|
||||
}
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) List(ctx context.Context, filter complaint.ListFilter, sort string, limit, offset int) ([]complaint.Complaint, int64, error) {
|
||||
if m.listFn != nil {
|
||||
return m.listFn(ctx, filter, sort, limit, offset)
|
||||
}
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) HelicopterHasOpenComplaint(ctx context.Context, helicopterID []byte) (bool, error) {
|
||||
if m.hasOpenFn != nil {
|
||||
return m.hasOpenFn(ctx, helicopterID)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) ActiveGroundingByHelicopterIDs(context.Context, [][]byte) ([]complaint.Complaint, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *complaintServiceMock) OpenByHelicopterIDs(context.Context, [][]byte) ([]complaint.Complaint, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) HelicopterHasUnactionedComplaint(context.Context, []byte) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) MarkFixedByHelicopter(context.Context, []byte, []byte, []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) MarkFixedByComplaint(context.Context, []byte, []byte, []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) MarkFixedByFlight(context.Context, []byte, []byte, []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *complaintServiceMock) ResolveUserNames(ctx context.Context, rows []complaint.Complaint) (map[string]string, error) {
|
||||
if m.resolveNamesFn != nil {
|
||||
return m.resolveNamesFn(ctx, rows)
|
||||
}
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
func newComplaintApp(h *ComplaintHandler, userID []byte) *fiber.App {
|
||||
app := fiber.New()
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
c.Locals("user_id", userID)
|
||||
return c.Next()
|
||||
})
|
||||
app.Post("/api/v1/complaints/create", h.Create)
|
||||
app.Get("/api/v1/complaints/get/:id", h.GetByID)
|
||||
app.Patch("/api/v1/complaints/update/:id", h.Update)
|
||||
app.Post("/api/v1/complaints/action-taken/create/:id", h.ActionTaken)
|
||||
app.Patch("/api/v1/complaints/action-taken/update/:id", h.UpdateActionTaken)
|
||||
app.Post("/api/v1/complaints/nsr/sign/:id", h.NSRSign)
|
||||
app.Post("/api/v1/complaints/nsr/unsign/:id", h.NSRUnsign)
|
||||
app.Get("/api/v1/complaints/hold-items/get-all", h.ListHoldItems)
|
||||
app.Get("/api/v1/complaints/hold-items/get-all/dt", h.ListHoldItemsDatatable)
|
||||
return app
|
||||
}
|
||||
|
||||
func TestComplaintHandler_ListHoldItems_FiltersOpenMEL(t *testing.T) {
|
||||
var got complaint.ListFilter
|
||||
svc := &complaintServiceMock{
|
||||
listFn: func(_ context.Context, filter complaint.ListFilter, _ string, _, _ int) ([]complaint.Complaint, int64, error) {
|
||||
got = filter
|
||||
return nil, 0, nil
|
||||
},
|
||||
}
|
||||
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/complaints/hold-items/get-all?date_from=2026-06-01&date_to=2026-06-30&person=Ganahl&search=vibration", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
if !got.HoldItemsOnly {
|
||||
t.Fatalf("expected HoldItemsOnly=true in filter, got %#v", got)
|
||||
}
|
||||
if got.DateFrom != "2026-06-01" || got.DateTo != "2026-06-30" || got.Person != "Ganahl" || got.Search != "vibration" {
|
||||
t.Fatalf("expected Von/bis/person/search filters passed, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Action-taken now only records the corrective action TEXT — signing moved to
|
||||
// POST /action-signoffs/sign (with complaint_id). No sign field, no 422.
|
||||
func TestComplaintHandler_ActionTaken_RecordsTextWithoutSign(t *testing.T) {
|
||||
complaintID := uuidv7.MustBytes()
|
||||
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
|
||||
var recorded *complaint.Complaint
|
||||
svc := &complaintServiceMock{
|
||||
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
|
||||
if recorded != nil {
|
||||
return recorded, nil
|
||||
}
|
||||
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: func() *time.Time { t := time.Now().UTC(); return &t }()}, nil
|
||||
},
|
||||
updateFn: func(_ context.Context, row *complaint.Complaint) error {
|
||||
if row.ActionTaken == nil || *row.ActionTaken != "tightened mount" {
|
||||
t.Fatalf("unexpected action_taken: %#v", row.ActionTaken)
|
||||
}
|
||||
recorded = row
|
||||
return nil
|
||||
},
|
||||
}
|
||||
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
|
||||
|
||||
// No `sign` field anymore.
|
||||
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":"tightened mount"}}}`
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/action-taken/create/"+complaintIDStr, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
if recorded == nil {
|
||||
t.Fatal("expected update to be called recording the action text")
|
||||
}
|
||||
}
|
||||
|
||||
// A pending (unclassified) complaint — mel_classified_at NULL — cannot record action
|
||||
// taken until MEL is classified; the gate returns 422 and never touches the row.
|
||||
func TestComplaintHandler_ActionTaken_BlockedWhenMELPending(t *testing.T) {
|
||||
complaintID := uuidv7.MustBytes()
|
||||
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
|
||||
svc := &complaintServiceMock{
|
||||
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
|
||||
// Pending: no MELClassifiedAt.
|
||||
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration"}, nil
|
||||
},
|
||||
updateFn: func(context.Context, *complaint.Complaint) error {
|
||||
t.Fatal("update must not be called while MEL is pending")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
|
||||
|
||||
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":"tightened mount"}}}`
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/action-taken/create/"+complaintIDStr, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 422 (classify MEL first), got %d body=%s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
}
|
||||
|
||||
// NSR is its own flow: nsr/sign sets the deferral on a classified complaint; a pending
|
||||
// (unclassified) complaint cannot be deferred and returns 422.
|
||||
func TestComplaintHandler_NSRSign_SetsDeferralWhenClassified(t *testing.T) {
|
||||
complaintID := uuidv7.MustBytes()
|
||||
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
classified := time.Now().UTC()
|
||||
|
||||
var saved *complaint.Complaint
|
||||
svc := &complaintServiceMock{
|
||||
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
|
||||
if saved != nil {
|
||||
return saved, nil
|
||||
}
|
||||
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: &classified}, nil
|
||||
},
|
||||
updateFn: func(_ context.Context, row *complaint.Complaint) error {
|
||||
if !row.IsNSR || row.NSRDecidedAt == nil {
|
||||
t.Fatalf("expected NSR set with decided_at, got %#v", row)
|
||||
}
|
||||
saved = row
|
||||
return nil
|
||||
},
|
||||
}
|
||||
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
|
||||
|
||||
body := `{"data":{"type":"complaint_nsr_sign","attributes":{"nsr_reason":"deferred under MEL B"}}}`
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/nsr/sign/"+complaintIDStr, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(b))
|
||||
}
|
||||
if saved == nil || !saved.IsNSR {
|
||||
t.Fatal("expected NSR to be recorded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplaintHandler_NSRSign_BlockedWhenMELPending(t *testing.T) {
|
||||
complaintID := uuidv7.MustBytes()
|
||||
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
|
||||
svc := &complaintServiceMock{
|
||||
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
|
||||
// Pending: no MELClassifiedAt.
|
||||
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration"}, nil
|
||||
},
|
||||
updateFn: func(context.Context, *complaint.Complaint) error {
|
||||
t.Fatal("update must not be called: cannot defer a pending complaint")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
|
||||
|
||||
body := `{"data":{"type":"complaint_nsr_sign","attributes":{}}}`
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/nsr/sign/"+complaintIDStr, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 422 (classify MEL first), got %d body=%s", resp.StatusCode, string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplaintHandler_NSRUnsign_ClearsDeferral(t *testing.T) {
|
||||
complaintID := uuidv7.MustBytes()
|
||||
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
classified := time.Now().UTC()
|
||||
|
||||
var saved *complaint.Complaint
|
||||
svc := &complaintServiceMock{
|
||||
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
|
||||
if saved != nil {
|
||||
return saved, nil
|
||||
}
|
||||
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: &classified, IsNSR: true, NSRDecidedAt: &classified}, nil
|
||||
},
|
||||
updateFn: func(_ context.Context, row *complaint.Complaint) error {
|
||||
if row.IsNSR || row.NSRDecidedAt != nil {
|
||||
t.Fatalf("expected NSR cleared, got %#v", row)
|
||||
}
|
||||
saved = row
|
||||
return nil
|
||||
},
|
||||
}
|
||||
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/nsr/unsign/"+complaintIDStr, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(b))
|
||||
}
|
||||
if saved == nil || saved.IsNSR {
|
||||
t.Fatal("expected NSR to be cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplaintHandler_UpdateActionTaken_RecordsTextWithoutSign(t *testing.T) {
|
||||
complaintID := uuidv7.MustBytes()
|
||||
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
|
||||
var recorded *complaint.Complaint
|
||||
svc := &complaintServiceMock{
|
||||
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
|
||||
if recorded != nil {
|
||||
return recorded, nil
|
||||
}
|
||||
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: func() *time.Time { t := time.Now().UTC(); return &t }()}, nil
|
||||
},
|
||||
updateFn: func(_ context.Context, row *complaint.Complaint) error {
|
||||
if row.ActionTaken == nil || *row.ActionTaken != "re-torqued mount" {
|
||||
t.Fatalf("unexpected action_taken: %#v", row.ActionTaken)
|
||||
}
|
||||
recorded = row
|
||||
return nil
|
||||
},
|
||||
}
|
||||
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
|
||||
|
||||
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":"re-torqued mount"}}}`
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/action-taken/update/"+complaintIDStr, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
if recorded == nil {
|
||||
t.Fatal("expected update to be called recording the action text")
|
||||
}
|
||||
}
|
||||
|
||||
// The complaint response reads its action-sign state from action_signoff (by
|
||||
// complaint_id), not from the (now-unset) complaint.ActionSignedAt/By columns.
|
||||
func TestComplaintHandler_GetByID_ReadsSignFromSignoff(t *testing.T) {
|
||||
complaintID := uuidv7.MustBytes()
|
||||
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
signer := uuidv7.MustBytes()
|
||||
signedAt := time.Now().UTC()
|
||||
|
||||
action := "replaced part"
|
||||
svc := &complaintServiceMock{
|
||||
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
|
||||
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, ActionTaken: &action}, nil
|
||||
},
|
||||
}
|
||||
var lookedUp [][]byte
|
||||
signoffSvc := &complaintSignoffMock{
|
||||
getByComplaintIDsFn: func(_ context.Context, ids [][]byte) (map[string]*actionsignoff.ActionSignoff, error) {
|
||||
lookedUp = ids
|
||||
return map[string]*actionsignoff.ActionSignoff{
|
||||
string(complaintID): {ComplaintID: complaintID, HelicopterID: helicopterID, SignedAt: &signedAt, SignedBy: signer},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
app := newComplaintApp(NewComplaintHandler(svc).WithActionSignoffService(signoffSvc), uuidv7.MustBytes())
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/complaints/get/"+complaintIDStr, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
if len(lookedUp) != 1 || string(lookedUp[0]) != string(complaintID) {
|
||||
t.Fatalf("expected signoff lookup by complaint id, got %#v", lookedUp)
|
||||
}
|
||||
attrs := struct {
|
||||
ActionSigned bool `json:"action_signed"`
|
||||
ActionSignedAt *string `json:"action_signed_at"`
|
||||
}{}
|
||||
var payload struct {
|
||||
Data struct {
|
||||
Data struct {
|
||||
Attributes json.RawMessage `json:"attributes"`
|
||||
} `json:"data"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
|
||||
t.Fatalf("bad json: %v body=%s", err, string(bodyBytes))
|
||||
}
|
||||
if err := json.Unmarshal(payload.Data.Data.Attributes, &attrs); err != nil {
|
||||
t.Fatalf("bad attributes json: %v body=%s", err, string(bodyBytes))
|
||||
}
|
||||
if !attrs.ActionSigned {
|
||||
t.Fatalf("expected action_signed=true from signoff, got body=%s", string(bodyBytes))
|
||||
}
|
||||
if attrs.ActionSignedAt == nil {
|
||||
t.Fatalf("expected action_signed_at from signoff, got body=%s", string(bodyBytes))
|
||||
}
|
||||
}
|
||||
|
||||
type complaintAOGRecalcMock struct {
|
||||
fn func(context.Context, []byte) error
|
||||
}
|
||||
|
||||
func (m *complaintAOGRecalcMock) RecalculateAOGByHelicopter(ctx context.Context, helicopterID []byte) error {
|
||||
if m.fn != nil {
|
||||
return m.fn(ctx, helicopterID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Updating a complaint that was already closed reopens its lifecycle: the complaint is
|
||||
// unfixed, its corrective-action sign-off is unsigned, and AOG is recomputed.
|
||||
func TestComplaintHandler_Update_ReopensAndUnsignsWhenClosed(t *testing.T) {
|
||||
complaintID := uuidv7.MustBytes()
|
||||
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
easaID := uuidv7.MustBytes()
|
||||
fixedAt := time.Now().UTC()
|
||||
|
||||
var updatedRow *complaint.Complaint
|
||||
svc := &complaintServiceMock{
|
||||
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
|
||||
// A fresh CLOSED complaint on each call (fixed by a past EASA release).
|
||||
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, FixedAt: &fixedAt, FixedByEASAID: easaID}, nil
|
||||
},
|
||||
updateFn: func(_ context.Context, row *complaint.Complaint) error {
|
||||
updatedRow = row
|
||||
return nil
|
||||
},
|
||||
}
|
||||
var unsignedComplaint []byte
|
||||
signoffSvc := &complaintSignoffMock{
|
||||
unsignByComplaintFn: func(_ context.Context, cid []byte) (*actionsignoff.ActionSignoff, error) {
|
||||
unsignedComplaint = cid
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
var recalced []byte
|
||||
aog := &complaintAOGRecalcMock{fn: func(_ context.Context, id []byte) error { recalced = id; return nil }}
|
||||
|
||||
h := NewComplaintHandler(svc).WithActionSignoffService(signoffSvc).WithAOGRecalculator(aog)
|
||||
app := newComplaintApp(h, uuidv7.MustBytes())
|
||||
|
||||
body := `{"data":{"type":"complaint_update","id":"` + complaintIDStr + `","attributes":{"description":"vibration + extra work"}}}`
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/update/"+complaintIDStr, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
if updatedRow == nil {
|
||||
t.Fatal("expected update to be called")
|
||||
}
|
||||
if updatedRow.FixedAt != nil || len(updatedRow.FixedByEASAID) != 0 {
|
||||
t.Fatalf("expected complaint reopened (fixed_at/by cleared), got at=%v by=%x", updatedRow.FixedAt, updatedRow.FixedByEASAID)
|
||||
}
|
||||
if string(unsignedComplaint) != string(complaintID) {
|
||||
t.Fatalf("expected sign-off unsigned for complaint, got %x", unsignedComplaint)
|
||||
}
|
||||
if string(recalced) != string(helicopterID) {
|
||||
t.Fatalf("expected AOG recomputed for helicopter, got %x", recalced)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplaintHandler_UpdateActionTaken_ClearWhenNotSigned(t *testing.T) {
|
||||
complaintID := uuidv7.MustBytes()
|
||||
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
|
||||
action := "old action"
|
||||
now := time.Now().UTC()
|
||||
|
||||
var cleared bool
|
||||
svc := &complaintServiceMock{
|
||||
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
|
||||
return &complaint.Complaint{ID: complaintID, HelicopterID: uuidv7.MustBytes(), MELSeverity: 2, MELClassifiedAt: &now, ActionTaken: &action}, nil
|
||||
},
|
||||
updateFn: func(_ context.Context, row *complaint.Complaint) error {
|
||||
if row.ActionTaken != nil {
|
||||
t.Fatalf("expected action_taken cleared to nil, got %v", *row.ActionTaken)
|
||||
}
|
||||
cleared = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
// unsigned sign-off → clearing allowed
|
||||
app := newComplaintApp(NewComplaintHandler(svc).WithActionSignoffService(&complaintSignoffMock{}), uuidv7.MustBytes())
|
||||
|
||||
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":""}}}`
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/action-taken/update/"+complaintIDStr, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
if !cleared {
|
||||
t.Fatal("expected action_taken to be cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplaintHandler_UpdateActionTaken_ClearBlockedWhenSigned(t *testing.T) {
|
||||
complaintID := uuidv7.MustBytes()
|
||||
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
|
||||
action := "old action"
|
||||
now := time.Now().UTC()
|
||||
|
||||
svc := &complaintServiceMock{
|
||||
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
|
||||
return &complaint.Complaint{ID: complaintID, HelicopterID: uuidv7.MustBytes(), MELSeverity: 2, MELClassifiedAt: &now, ActionTaken: &action}, nil
|
||||
},
|
||||
updateFn: func(context.Context, *complaint.Complaint) error {
|
||||
t.Fatal("must not update when clearing a signed action")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
signoffSvc := &complaintSignoffMock{
|
||||
getByComplaintIDsFn: func(_ context.Context, _ [][]byte) (map[string]*actionsignoff.ActionSignoff, error) {
|
||||
return map[string]*actionsignoff.ActionSignoff{
|
||||
string(complaintID): {ComplaintID: complaintID, SignedAt: &now},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
app := newComplaintApp(NewComplaintHandler(svc).WithActionSignoffService(signoffSvc), uuidv7.MustBytes())
|
||||
|
||||
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":""}}}`
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/action-taken/update/"+complaintIDStr, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusConflict {
|
||||
t.Fatalf("expected 409, got %d body=%s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
if !strings.Contains(string(bodyBytes), "COMPLAINT_ACTION_SIGNED_LOCKED") {
|
||||
t.Fatalf("expected COMPLAINT_ACTION_SIGNED_LOCKED, got %s", string(bodyBytes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplaintHandler_ActionTaken_CreateStillRequiresText(t *testing.T) {
|
||||
complaintIDStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
app := newComplaintApp(NewComplaintHandler(&complaintServiceMock{}), uuidv7.MustBytes())
|
||||
|
||||
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":""}}}`
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/action-taken/create/"+complaintIDStr, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("create with empty action_taken must be 422, got %d body=%s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user