1556 lines
54 KiB
Go
1556 lines
54 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
mysqldriver "github.com/go-sql-driver/mysql"
|
|
"github.com/gofiber/fiber/v2"
|
|
"gorm.io/gorm"
|
|
|
|
complaint "wucher/internal/domain/complaint"
|
|
filemanager "wucher/internal/domain/file_manager"
|
|
"wucher/internal/domain/helicopter"
|
|
"wucher/internal/service"
|
|
sharedconst "wucher/internal/shared/const"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
responsedto "wucher/internal/transport/http/dto/response"
|
|
)
|
|
|
|
func TestApplyHelicopterFleetStatus_GroundedSetsInactive(t *testing.T) {
|
|
res := responsedto.HelicopterResource{}
|
|
res.Attributes.IsActive = true
|
|
row := &helicopter.Helicopter{IsActive: true, AirOnGround: false}
|
|
|
|
// A non-empty groundReason simulates an open-complaint grounding (not persisted
|
|
// in air_on_ground), and is reflected verbatim as the AOG reason.
|
|
reason := "Grounded due to an open complaint — MEL A grace period expired"
|
|
applyHelicopterFleetStatus(&res, row, false, reason, false, nil)
|
|
|
|
if res.Attributes.Status != helicopter.StatusAOG {
|
|
t.Fatalf("expected status AOG, got %q", res.Attributes.Status)
|
|
}
|
|
if !res.Attributes.AOG {
|
|
t.Fatal("expected aog=true")
|
|
}
|
|
if res.Attributes.IsActive {
|
|
t.Fatal("expected is_active=false for a grounded (AOG) helicopter")
|
|
}
|
|
if res.Attributes.AOGReason != reason {
|
|
t.Fatalf("expected enriched AOG reason %q, got %q", reason, res.Attributes.AOGReason)
|
|
}
|
|
}
|
|
|
|
func TestApplyHelicopterFleetStatus_AvailableStaysActive(t *testing.T) {
|
|
res := responsedto.HelicopterResource{}
|
|
res.Attributes.IsActive = true
|
|
row := &helicopter.Helicopter{IsActive: true, AirOnGround: false}
|
|
|
|
applyHelicopterFleetStatus(&res, row, false, "", false, nil)
|
|
|
|
if res.Attributes.Status != helicopter.StatusAvailable {
|
|
t.Fatalf("expected status available, got %q", res.Attributes.Status)
|
|
}
|
|
if !res.Attributes.IsActive {
|
|
t.Fatal("expected is_active=true for an available helicopter")
|
|
}
|
|
}
|
|
|
|
func TestComplaintGroundingReason_MELvsNonMEL(t *testing.T) {
|
|
mel := complaint.Complaint{MELSeverity: complaint.MELSeverityA}
|
|
if got := complaintGroundingReason(mel); !strings.Contains(got, "MEL A") || !strings.Contains(got, "grace period expired") {
|
|
t.Fatalf("unexpected MEL reason: %q", got)
|
|
}
|
|
nonMEL := complaint.Complaint{MELSeverity: complaint.MELSeverityNonMEL}
|
|
if got := complaintGroundingReason(nonMEL); !strings.Contains(got, "non-MEL") {
|
|
t.Fatalf("unexpected non-MEL reason: %q", got)
|
|
}
|
|
}
|
|
|
|
type memHelicopterRepo struct {
|
|
rows map[string]*helicopter.Helicopter
|
|
|
|
createErr error
|
|
updateErr error
|
|
deleteErr error
|
|
getErr error
|
|
listErr error
|
|
nextErr error
|
|
existsErr error
|
|
|
|
listRows []helicopter.Helicopter
|
|
listTotal int64
|
|
}
|
|
|
|
type helicopterPresignStorageMock struct {
|
|
url string
|
|
err error
|
|
}
|
|
|
|
type helicopterAttachmentRepoMock struct {
|
|
getByIDFn func(id []byte) (*filemanager.Attachment, error)
|
|
}
|
|
|
|
type helicopterPINVerifierMock struct {
|
|
consumeFn func(ctx context.Context, userID []byte, action, token string) error
|
|
}
|
|
|
|
type helicopterPINSessionVerifierMock struct {
|
|
helicopterPINVerifierMock
|
|
accessCookieName string
|
|
parseSessionFn func(ctx context.Context, accessToken string) (string, error)
|
|
consumeSessionFn func(ctx context.Context, userID []byte, action, token, sessionID string) error
|
|
}
|
|
|
|
func intPtr(v int) *int {
|
|
return &v
|
|
}
|
|
|
|
func (m *helicopterAttachmentRepoMock) Create(context.Context, *filemanager.Attachment) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *helicopterAttachmentRepoMock) Delete(context.Context, []byte) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *helicopterAttachmentRepoMock) GetByID(_ context.Context, id []byte) (*filemanager.Attachment, error) {
|
|
if m.getByIDFn != nil {
|
|
return m.getByIDFn(id)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *helicopterAttachmentRepoMock) GetByRefAndFile(context.Context, string, string, []byte) (*filemanager.Attachment, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *helicopterAttachmentRepoMock) ListByReference(context.Context, filemanager.AttachmentListFilter) ([]filemanager.Attachment, int64, error) {
|
|
return nil, 0, nil
|
|
}
|
|
|
|
func (m *helicopterPINVerifierMock) ConsumeSecurityPINActionToken(ctx context.Context, userID []byte, action, token string) error {
|
|
if m.consumeFn != nil {
|
|
return m.consumeFn(ctx, userID, action, token)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *helicopterPINSessionVerifierMock) AccessCookieName() string {
|
|
if strings.TrimSpace(m.accessCookieName) == "" {
|
|
return "access_token"
|
|
}
|
|
return m.accessCookieName
|
|
}
|
|
|
|
func (m *helicopterPINSessionVerifierMock) ParseAccessTokenSessionID(ctx context.Context, accessToken string) (string, error) {
|
|
if m.parseSessionFn != nil {
|
|
return m.parseSessionFn(ctx, accessToken)
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func (m *helicopterPINSessionVerifierMock) ConsumeSecurityPINActionTokenInSession(ctx context.Context, userID []byte, action, token, sessionID string) error {
|
|
if m.consumeSessionFn != nil {
|
|
return m.consumeSessionFn(ctx, userID, action, token, sessionID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *helicopterPresignStorageMock) PresignGetObject(_ context.Context, _ string, _ time.Duration) (string, error) {
|
|
if m.err != nil {
|
|
return "", m.err
|
|
}
|
|
return m.url, nil
|
|
}
|
|
|
|
func newMemHelicopterRepo() *memHelicopterRepo {
|
|
return &memHelicopterRepo{rows: map[string]*helicopter.Helicopter{}}
|
|
}
|
|
|
|
func (r *memHelicopterRepo) Create(_ context.Context, h *helicopter.Helicopter) error {
|
|
if r.createErr != nil {
|
|
return r.createErr
|
|
}
|
|
if len(h.ID) == 0 {
|
|
h.ID = uuidv7.MustBytes()
|
|
}
|
|
now := time.Now().UTC()
|
|
if h.CreatedAt.IsZero() {
|
|
h.CreatedAt = now
|
|
}
|
|
h.UpdatedAt = now
|
|
clone := *h
|
|
r.rows[string(clone.ID)] = &clone
|
|
return nil
|
|
}
|
|
|
|
func (r *memHelicopterRepo) Update(_ context.Context, h *helicopter.Helicopter) error {
|
|
if r.updateErr != nil {
|
|
return r.updateErr
|
|
}
|
|
if existing, ok := r.rows[string(h.ID)]; ok {
|
|
_ = existing
|
|
}
|
|
h.UpdatedAt = time.Now().UTC()
|
|
clone := *h
|
|
r.rows[string(clone.ID)] = &clone
|
|
return nil
|
|
}
|
|
|
|
func (r *memHelicopterRepo) Delete(_ context.Context, id []byte) error {
|
|
if r.deleteErr != nil {
|
|
return r.deleteErr
|
|
}
|
|
delete(r.rows, string(id))
|
|
return nil
|
|
}
|
|
|
|
func (r *memHelicopterRepo) GetByID(_ context.Context, id []byte) (*helicopter.Helicopter, error) {
|
|
if r.getErr != nil {
|
|
return nil, r.getErr
|
|
}
|
|
h := r.rows[string(id)]
|
|
if h == nil {
|
|
return nil, nil
|
|
}
|
|
clone := *h
|
|
return &clone, nil
|
|
}
|
|
|
|
func (r *memHelicopterRepo) List(_ context.Context, _ string, _ []string, _ string, _ int, _ int, _ [][]byte) ([]helicopter.Helicopter, int64, error) {
|
|
if r.listErr != nil {
|
|
return nil, 0, r.listErr
|
|
}
|
|
if r.listRows != nil {
|
|
return r.listRows, r.listTotal, nil
|
|
}
|
|
out := make([]helicopter.Helicopter, 0, len(r.rows))
|
|
for _, row := range r.rows {
|
|
out = append(out, *row)
|
|
}
|
|
return out, int64(len(out)), nil
|
|
}
|
|
|
|
func (r *memHelicopterRepo) NextReportNumber(_ context.Context, id []byte) (string, int, error) {
|
|
if r.nextErr != nil {
|
|
return "", 0, r.nextErr
|
|
}
|
|
h := r.rows[string(id)]
|
|
if h == nil {
|
|
return "", 0, gorm.ErrRecordNotFound
|
|
}
|
|
h.ReportSequence++
|
|
return fmt.Sprintf("%s-%04d", h.Identifier, h.ReportSequence), h.ReportSequence, nil
|
|
}
|
|
|
|
func (r *memHelicopterRepo) ExistsByIdentifier(_ context.Context, identifier string, excludeID []byte) (bool, error) {
|
|
if r.existsErr != nil {
|
|
return false, r.existsErr
|
|
}
|
|
target := strings.ToUpper(strings.TrimSpace(identifier))
|
|
for _, row := range r.rows {
|
|
if len(excludeID) > 0 && string(row.ID) == string(excludeID) {
|
|
continue
|
|
}
|
|
if strings.ToUpper(strings.TrimSpace(row.Identifier)) == target {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (r *memHelicopterRepo) IsInUse(_ context.Context, id []byte) (bool, error) {
|
|
_ = id
|
|
return false, nil
|
|
}
|
|
|
|
func (r *memHelicopterRepo) InUseByAircraftIDs(_ context.Context, ids [][]byte) (map[string]bool, error) {
|
|
_ = ids
|
|
return map[string]bool{}, nil
|
|
}
|
|
func (r *memHelicopterRepo) ActiveFlightIDByAircraftIDs(_ context.Context, _ [][]byte) (map[string][]byte, error) {
|
|
return map[string][]byte{}, nil
|
|
}
|
|
|
|
func TestNewHelicopterHandler(t *testing.T) {
|
|
h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo()))
|
|
if h == nil || h.validate == nil {
|
|
t.Fatalf("expected handler and validator initialized")
|
|
}
|
|
}
|
|
|
|
func TestHelicopterHandler_Create(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
actor := uuidv7.MustBytes()
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_id", actor)
|
|
return c.Next()
|
|
})
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":" OE-XHZ ","identifier":"1234","type":" EC-13 ","sortkey":3,"visible":true}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
|
}
|
|
|
|
if len(repo.rows) != 1 {
|
|
t.Fatalf("expected 1 created row, got %d", len(repo.rows))
|
|
}
|
|
for _, row := range repo.rows {
|
|
if row.Designation != "OE-XHZ" {
|
|
t.Fatalf("expected designation trimmed, got %q", row.Designation)
|
|
}
|
|
if row.Identifier != "1234" {
|
|
t.Fatalf("expected identifier unchanged, got %q", row.Identifier)
|
|
}
|
|
if row.ReportSequence != 0 {
|
|
t.Fatalf("expected report_sequence initialized to 0, got %d", row.ReportSequence)
|
|
}
|
|
if row.SortKey == nil || *row.SortKey != 3 {
|
|
t.Fatalf("expected sortkey set, got %#v", row.SortKey)
|
|
}
|
|
if string(row.CreatedBy) != string(actor) {
|
|
t.Fatalf("expected created_by from actor")
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("invalid json", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader([]byte(`{"data":`)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("validation error", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader([]byte(`{"data":{"type":"","attributes":{}}}`)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate identifier (case-insensitive) is denied", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
repo.rows[string(uuidv7.MustBytes())] = &helicopter.Helicopter{
|
|
ID: uuidv7.MustBytes(),
|
|
Designation: "OLD",
|
|
Identifier: "SN-0001",
|
|
ReportSequence: 1,
|
|
Type: "EC-13",
|
|
IsActive: true,
|
|
}
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":" OE-NEW ","identifier":"sn-0001","type":" EC-13 "}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusConflict {
|
|
t.Fatalf("expected 409, got %d", resp.StatusCode)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if !strings.Contains(string(body), "Serial number already exists") {
|
|
t.Fatalf("expected duplicate serial number message, got %s", string(body))
|
|
}
|
|
if !strings.Contains(string(body), "/data/attributes/identifier") {
|
|
t.Fatalf("expected identifier pointer in error source, got %s", string(body))
|
|
}
|
|
if len(repo.rows) != 1 {
|
|
t.Fatalf("expected no additional row on duplicate identifier")
|
|
}
|
|
})
|
|
|
|
t.Run("missing required attributes", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"","identifier":"","type":""}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("create error", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
repo.createErr = errors.New("db error")
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"1234","type":"EC-13"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusInternalServerError {
|
|
t.Fatalf("expected 500, got %d", resp.StatusCode)
|
|
}
|
|
if strings.Contains(string(body), "db error") {
|
|
t.Fatalf("expected raw error to be hidden, got %s", string(body))
|
|
}
|
|
if !strings.Contains(string(body), "an internal error occurred") {
|
|
t.Fatalf("expected generic internal message, got %s", string(body))
|
|
}
|
|
})
|
|
|
|
t.Run("aog true requires reason", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"1234","type":"EC-13","aog":true}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHelicopterHandler_Update(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &helicopter.Helicopter{
|
|
ID: id,
|
|
Designation: "OE-XHZ",
|
|
Identifier: "1234",
|
|
ReportSequence: 1,
|
|
Type: "EC-13",
|
|
IsActive: true,
|
|
}
|
|
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Patch("/api/v1/helicopters/update/:id", h.Update)
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-NEW","identifier":"9876","sortkey":7}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
updated := repo.rows[string(id)]
|
|
if updated.Designation != "OE-NEW" {
|
|
t.Fatalf("expected designation updated, got %q", updated.Designation)
|
|
}
|
|
if updated.Identifier != "9876" {
|
|
t.Fatalf("expected identifier unchanged, got %q", updated.Identifier)
|
|
}
|
|
if updated.ReportSequence != 1 {
|
|
t.Fatalf("expected report_sequence unchanged, got %d", updated.ReportSequence)
|
|
}
|
|
if updated.SortKey == nil || *updated.SortKey != 7 {
|
|
t.Fatalf("expected sortkey updated, got %#v", updated.SortKey)
|
|
}
|
|
})
|
|
|
|
t.Run("empty file_uuid clears existing attachment", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
fotoID := uuidv7.MustBytes()
|
|
repo.rows[string(id)].FotoAttachmentID = append([]byte(nil), fotoID...)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-KEEP","file_uuid":""}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.rows[string(id)].FotoAttachmentID != nil {
|
|
t.Fatalf("expected foto attachment to be cleared")
|
|
}
|
|
})
|
|
|
|
t.Run("existing attachment uuid is accepted on update", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
attachmentID := uuidv7.MustBytes()
|
|
attachmentIDStr, _ := uuidv7.BytesToString(attachmentID)
|
|
fileSvc := service.NewFileManagerService(nil, nil, &helicopterAttachmentRepoMock{
|
|
getByIDFn: func(gotID []byte) (*filemanager.Attachment, error) {
|
|
if string(gotID) != string(attachmentID) {
|
|
return nil, nil
|
|
}
|
|
return &filemanager.Attachment{ID: append([]byte(nil), attachmentID...)}, nil
|
|
},
|
|
}, service.FileManagerServiceDependencies{})
|
|
hWithFileManager := NewHelicopterHandler(service.NewHelicopterService(repo)).WithFileManagerService(fileSvc)
|
|
appWithFileManager := fiber.New()
|
|
appWithFileManager.Patch("/api/v1/helicopters/update/:id", hWithFileManager.Update)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-ATTACH","file_uuid":"` + attachmentIDStr + `"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := appWithFileManager.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if string(repo.rows[string(id)].FotoAttachmentID) != string(attachmentID) {
|
|
t.Fatalf("expected existing attachment uuid to be accepted")
|
|
}
|
|
})
|
|
|
|
t.Run("invalid path uuid", func(t *testing.T) {
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/bad-uuid", bytes.NewReader([]byte(`{}`)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid json", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader([]byte(`{"data":`)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("id required", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"","attributes":{"designation":"X"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("id mismatch", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
other, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + other + `","attributes":{"designation":"X"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("no attrs", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
missing := uuidv7.MustBytes()
|
|
missingStr, _ := uuidv7.BytesToString(missing)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + missingStr + `","attributes":{"designation":"X"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+missingStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid designation", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":" "}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid identifier", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"identifier":" "}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid type", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"type":" "}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("success update all fields", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-ALL","identifier":"4321","type":"EC-145","engine_1":"TM1","engine_2":"TM2","consumption_lt_min":8.8,"nr1":true,"nr2":false,"lh":true,"rh":false,"mgb":true,"igb":false,"tgb":true,"utc_offset":2,"dry":true,"note":"all fields updated","is_active":false}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
updated := repo.rows[string(id)]
|
|
if updated.Type != "EC-145" || updated.Engine1 != "TM1" || updated.Engine2 != "TM2" {
|
|
t.Fatalf("expected scalar fields updated, got %+v", *updated)
|
|
}
|
|
if !updated.Dry || updated.IsActive {
|
|
t.Fatalf("expected boolean fields updated, got %+v", *updated)
|
|
}
|
|
if updated.Note != "all fields updated" || updated.Identifier != "4321" {
|
|
t.Fatalf("expected note/identifier updated, got %+v", *updated)
|
|
}
|
|
})
|
|
|
|
t.Run("update error", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
repo.updateErr = errors.New("update failed")
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-ZZZ"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
repo.updateErr = nil
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("aog true requires reason", func(t *testing.T) {
|
|
idNoReason := uuidv7.MustBytes()
|
|
repo.rows[string(idNoReason)] = &helicopter.Helicopter{
|
|
ID: idNoReason,
|
|
Designation: "OE-NOREASON",
|
|
Identifier: "2000-0001",
|
|
Type: "EC-13",
|
|
AirOnGround: false,
|
|
AOGReason: "",
|
|
IsActive: true,
|
|
}
|
|
idStr, _ := uuidv7.BytesToString(idNoReason)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"aog":true}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHelicopterHandler_AOGPINVerification(t *testing.T) {
|
|
t.Run("create aog false does not require pin", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
actor := uuidv7.MustBytes()
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(&helicopterPINVerifierMock{
|
|
consumeFn: func(_ context.Context, _ []byte, _ string, _ string) error {
|
|
t.Fatalf("expected pin verifier not to be called when aog is false")
|
|
return nil
|
|
},
|
|
})
|
|
app := fiber.New()
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_id", actor)
|
|
return c.Next()
|
|
})
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-NO-PIN","identifier":"7778","type":"EC-145","aog":false}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
|
}
|
|
if len(repo.rows) != 1 {
|
|
t.Fatalf("expected helicopter created without pin when aog is false")
|
|
}
|
|
})
|
|
|
|
t.Run("create aog true requires pin", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
actor := uuidv7.MustBytes()
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(&helicopterPINVerifierMock{
|
|
consumeFn: func(_ context.Context, userID []byte, action, token string) error {
|
|
if string(userID) != string(actor) {
|
|
t.Fatalf("unexpected actor passed to pin verifier")
|
|
}
|
|
if action != sharedconst.PinActionHelicopterAOGChange {
|
|
t.Fatalf("unexpected pin action %q", action)
|
|
}
|
|
if token == "" {
|
|
return errors.New("missing token")
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
app := fiber.New()
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_id", actor)
|
|
return c.Next()
|
|
})
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-AOG","identifier":"7777","type":"EC-145","aog":true,"aog_reason":"grounded"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
t.Fatalf("expected 202, got %d", resp.StatusCode)
|
|
}
|
|
if len(repo.rows) != 0 {
|
|
t.Fatalf("expected no helicopter created without pin verification")
|
|
}
|
|
})
|
|
|
|
t.Run("update aog change requires pin", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
actor := uuidv7.MustBytes()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &helicopter.Helicopter{
|
|
ID: id,
|
|
Designation: "OE-XHZ",
|
|
Identifier: "1234-0001",
|
|
Type: "EC-13",
|
|
AirOnGround: false,
|
|
IsActive: true,
|
|
}
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(&helicopterPINVerifierMock{
|
|
consumeFn: func(_ context.Context, userID []byte, action, token string) error {
|
|
if string(userID) != string(actor) {
|
|
t.Fatalf("unexpected actor passed to pin verifier")
|
|
}
|
|
if action != sharedconst.PinActionHelicopterAOGChange {
|
|
t.Fatalf("unexpected pin action %q", action)
|
|
}
|
|
if token == "" {
|
|
return errors.New("missing token")
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
app := fiber.New()
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_id", actor)
|
|
return c.Next()
|
|
})
|
|
app.Patch("/api/v1/helicopters/update/:id", h.Update)
|
|
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"aog":true,"aog_reason":"grounded"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
t.Fatalf("expected 202, got %d", resp.StatusCode)
|
|
}
|
|
if repo.rows[string(id)].AirOnGround {
|
|
t.Fatalf("expected aog state not to change without pin verification")
|
|
}
|
|
})
|
|
|
|
t.Run("update without touching aog does not require pin", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &helicopter.Helicopter{
|
|
ID: id,
|
|
Designation: "OE-XHZ",
|
|
Identifier: "1234-0001",
|
|
Type: "EC-13",
|
|
AirOnGround: false,
|
|
IsActive: true,
|
|
}
|
|
pinVerifierCalled := false
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(&helicopterPINVerifierMock{
|
|
consumeFn: func(_ context.Context, _ []byte, _ string, _ string) error {
|
|
pinVerifierCalled = true
|
|
return nil
|
|
},
|
|
})
|
|
app := fiber.New()
|
|
app.Patch("/api/v1/helicopters/update/:id", h.Update)
|
|
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-UPDATED"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if pinVerifierCalled {
|
|
t.Fatalf("expected pin verifier not to be called when aog is untouched")
|
|
}
|
|
})
|
|
|
|
t.Run("update aog uses session-bound token when verifier supports it", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
actor := uuidv7.MustBytes()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &helicopter.Helicopter{
|
|
ID: id,
|
|
Designation: "OE-XHZ",
|
|
Identifier: "1234-0001",
|
|
Type: "EC-13",
|
|
AirOnGround: false,
|
|
IsActive: true,
|
|
}
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(&helicopterPINSessionVerifierMock{
|
|
accessCookieName: "access_token",
|
|
parseSessionFn: func(_ context.Context, accessToken string) (string, error) {
|
|
if accessToken != "access.jwt.token" {
|
|
t.Fatalf("unexpected access token %q", accessToken)
|
|
}
|
|
return "sid-123", nil
|
|
},
|
|
consumeSessionFn: func(_ context.Context, userID []byte, action, token, sessionID string) error {
|
|
if string(userID) != string(actor) {
|
|
t.Fatalf("unexpected actor passed to pin verifier")
|
|
}
|
|
if action != sharedconst.PinActionHelicopterAOGChange {
|
|
t.Fatalf("unexpected pin action %q", action)
|
|
}
|
|
if token != "pin-token" {
|
|
t.Fatalf("unexpected pin token %q", token)
|
|
}
|
|
if sessionID != "sid-123" {
|
|
t.Fatalf("unexpected session id %q", sessionID)
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
app := fiber.New()
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_id", actor)
|
|
return c.Next()
|
|
})
|
|
app.Patch("/api/v1/helicopters/update/:id", h.Update)
|
|
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"aog":true,"aog_reason":"grounded"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set(pinVerificationHeader, "pin-token")
|
|
req.AddCookie(&http.Cookie{Name: "access_token", Value: "access.jwt.token"})
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if !repo.rows[string(id)].AirOnGround {
|
|
t.Fatalf("expected aog state to change after valid session-bound pin verification")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHelicopterHandler_GetWithFoto(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
id := uuidv7.MustBytes()
|
|
fotoID := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &helicopter.Helicopter{
|
|
ID: id,
|
|
Designation: "OE-XHZ",
|
|
Identifier: "1234-0001",
|
|
Type: "EC-13",
|
|
FotoAttachmentID: fotoID,
|
|
FotoAttachment: &filemanager.Attachment{
|
|
ID: fotoID,
|
|
FileID: uuidv7.MustBytes(),
|
|
File: &filemanager.File{ID: uuidv7.MustBytes(), ObjectKey: "helicopter/oe-xhz.jpg"},
|
|
},
|
|
CreatedAt: time.Now().UTC(),
|
|
UpdatedAt: time.Now().UTC(),
|
|
}
|
|
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithFileStorage(&helicopterPresignStorageMock{
|
|
url: "https://signed.example/helicopter/oe-xhz.jpg?sig=abc",
|
|
})
|
|
app := fiber.New()
|
|
app.Get("/api/v1/helicopters/get/:id", h.Get)
|
|
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get/"+idStr, nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
fotoIDStr, _ := uuidv7.BytesToString(fotoID)
|
|
bodyStr := string(body)
|
|
if !strings.Contains(bodyStr, `"uuid":"`+fotoIDStr+`"`) {
|
|
t.Fatalf("expected foto uuid in response, body=%s", bodyStr)
|
|
}
|
|
if !strings.Contains(bodyStr, `"download_url":"https://signed.example/helicopter/oe-xhz.jpg?sig=abc"`) {
|
|
t.Fatalf("expected foto download_url in response, body=%s", bodyStr)
|
|
}
|
|
if !strings.Contains(bodyStr, `"original_size_bytes":0`) {
|
|
t.Fatalf("expected foto original_size_bytes in response, body=%s", bodyStr)
|
|
}
|
|
}
|
|
|
|
func TestHelicopterHandler_Delete(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &helicopter.Helicopter{ID: id, Identifier: "1234"}
|
|
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Delete("/api/v1/helicopters/delete/:id", h.Delete)
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/helicopters/delete/"+idStr, nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid uuid", func(t *testing.T) {
|
|
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/helicopters/delete/bad", nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Delete("/api/v1/helicopters/delete/:id", h.Delete)
|
|
|
|
id := uuidv7.MustBytes()
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/helicopters/delete/"+idStr, nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("delete error", func(t *testing.T) {
|
|
repo.deleteErr = errors.New("delete failed")
|
|
id2 := uuidv7.MustBytes()
|
|
repo.rows[string(id2)] = &helicopter.Helicopter{ID: id2, Identifier: "ERR-DEL"}
|
|
id2Str, _ := uuidv7.BytesToString(id2)
|
|
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/helicopters/delete/"+id2Str, nil)
|
|
resp, _ := app.Test(req)
|
|
repo.deleteErr = nil
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("delete relation conflict", func(t *testing.T) {
|
|
repo.deleteErr = &mysqldriver.MySQLError{
|
|
Number: 1451,
|
|
Message: "Cannot delete or update a parent row: a foreign key constraint fails",
|
|
}
|
|
id2 := uuidv7.MustBytes()
|
|
repo.rows[string(id2)] = &helicopter.Helicopter{ID: id2, Identifier: "REL-DEL"}
|
|
id2Str, _ := uuidv7.BytesToString(id2)
|
|
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/helicopters/delete/"+id2Str, nil)
|
|
resp, _ := app.Test(req)
|
|
repo.deleteErr = nil
|
|
if resp.StatusCode != http.StatusConflict {
|
|
t.Fatalf("expected 409, got %d", resp.StatusCode)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
bodyStr := string(body)
|
|
if !strings.Contains(bodyStr, `"code":"HELICOPTER_RELATION_DELETE_ERROR"`) {
|
|
t.Fatalf("expected HELICOPTER_RELATION_DELETE_ERROR, got %s", bodyStr)
|
|
}
|
|
if !strings.Contains(bodyStr, `"error_code":"4090303"`) {
|
|
t.Fatalf("expected 4090303, got %s", bodyStr)
|
|
}
|
|
})
|
|
|
|
t.Run("identifier lookup error", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
repo.existsErr = errors.New("lookup failed")
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-ERR","identifier":"SN-ERR","type":"EC-13"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("aog requires reason", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"SN-1","type":"EC-13","aog":true,"aog_reason":""}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("aog requires pin token", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
actor := uuidv7.MustBytes()
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_id", actor)
|
|
return c.Next()
|
|
})
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"SN-2","type":"EC-13","aog":true,"aog_reason":"maintenance"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("aog pin verifier rejects token", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
pin := &helicopterPINVerifierMock{
|
|
consumeFn: func(_ context.Context, _ []byte, _ string, _ string) error {
|
|
return errors.New("pin required")
|
|
},
|
|
}
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(pin)
|
|
app := fiber.New()
|
|
actor := uuidv7.MustBytes()
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_id", actor)
|
|
return c.Next()
|
|
})
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"SN-3","type":"EC-13","aog":true,"aog_reason":"maintenance"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
t.Fatalf("expected 202, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("aog pin verifier accepted continues create", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
pin := &helicopterPINVerifierMock{
|
|
consumeFn: func(_ context.Context, _ []byte, _ string, _ string) error {
|
|
return nil
|
|
},
|
|
}
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(pin)
|
|
app := fiber.New()
|
|
actor := uuidv7.MustBytes()
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_id", actor)
|
|
return c.Next()
|
|
})
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"SN-4","type":"EC-13","aog":true,"aog_reason":"maintenance"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("file_uuid invalid returns validation error", func(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Post("/api/v1/helicopters/create", h.Create)
|
|
|
|
payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"SN-5","type":"EC-13","file_uuid":"bad-uuid"}}}`)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 400/422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHelicopterHandler_NextReportNumber(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &helicopter.Helicopter{ID: id, Identifier: "1234"}
|
|
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Post("/api/v1/helicopters/:id/reports/next-number", h.NextReportNumber)
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/"+idStr+"/reports/next-number", nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if !strings.Contains(string(body), "1234-0001") {
|
|
t.Fatalf("expected report number in response body, got %s", string(body))
|
|
}
|
|
})
|
|
|
|
t.Run("invalid uuid", func(t *testing.T) {
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/bad/reports/next-number", nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("not found from generator", func(t *testing.T) {
|
|
missing := uuidv7.MustBytes()
|
|
missingStr, _ := uuidv7.BytesToString(missing)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/"+missingStr+"/reports/next-number", nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("generate failed", func(t *testing.T) {
|
|
repo.nextErr = errors.New("boom")
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/"+idStr+"/reports/next-number", nil)
|
|
resp, _ := app.Test(req)
|
|
repo.nextErr = nil
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("not found on post-check", func(t *testing.T) {
|
|
repo.nextErr = nil
|
|
repo.getErr = errors.New("gone")
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/"+idStr+"/reports/next-number", nil)
|
|
resp, _ := app.Test(req)
|
|
repo.getErr = nil
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHelicopterHandler_Get(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &helicopter.Helicopter{ID: id, Designation: "OE-XHZ", Identifier: "1234", Type: "EC-13"}
|
|
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Get("/api/v1/helicopters/get/:id", h.Get)
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get/"+idStr, nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid uuid", func(t *testing.T) {
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get/bad", nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
missing := uuidv7.MustBytes()
|
|
missingStr, _ := uuidv7.BytesToString(missing)
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get/"+missingStr, nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHelicopterHandler_List(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
repo.listRows = []helicopter.Helicopter{{ID: uuidv7.MustBytes(), Designation: "OE-XHZ", Identifier: "1234", Type: "EC-13", CreatedAt: time.Now(), UpdatedAt: time.Now()}}
|
|
repo.listTotal = 1
|
|
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Get("/api/v1/helicopters/get-all", h.List)
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get-all?filter[search]=OE&page[number]=1&page[size]=20", nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("list failed", func(t *testing.T) {
|
|
repo.listErr = errors.New("list failed")
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get-all", nil)
|
|
resp, _ := app.Test(req)
|
|
repo.listErr = nil
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHelicopterHandler_ListDatatable(t *testing.T) {
|
|
repo := newMemHelicopterRepo()
|
|
repo.listRows = []helicopter.Helicopter{{ID: uuidv7.MustBytes(), Designation: "OE-XHZ", Identifier: "1234", Type: "EC-13", CreatedAt: time.Now(), UpdatedAt: time.Now()}}
|
|
repo.listTotal = 1
|
|
|
|
h := NewHelicopterHandler(service.NewHelicopterService(repo))
|
|
app := fiber.New()
|
|
app.Get("/api/v1/helicopters/get-all/dt", h.ListDatatable)
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get-all/dt?page=1&limit=10&draw=3&search=OE", nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("list failed", func(t *testing.T) {
|
|
repo.listErr = errors.New("list failed")
|
|
req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get-all/dt", nil)
|
|
resp, _ := app.Test(req)
|
|
repo.listErr = nil
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHelicopterResource(t *testing.T) {
|
|
id := uuidv7.MustBytes()
|
|
row := &helicopter.Helicopter{
|
|
ID: id,
|
|
Designation: "OE-XHZ",
|
|
Identifier: "1234",
|
|
Type: "EC-13",
|
|
SortKey: intPtr(10),
|
|
Engine1: "TM",
|
|
Engine2: "TM",
|
|
Consumption: 4.1,
|
|
NR1: true,
|
|
NR2: true,
|
|
LH: true,
|
|
RH: true,
|
|
MGB: true,
|
|
IGB: false,
|
|
TGB: true,
|
|
UTCOffset: -1,
|
|
Dry: false,
|
|
Note: "primary",
|
|
IsActive: true,
|
|
CreatedBy: uuidv7.MustBytes(),
|
|
CreatedAt: time.Now().UTC(),
|
|
UpdatedAt: time.Now().UTC(),
|
|
}
|
|
|
|
res := helicopterResource(row)
|
|
if res.Type != "helicopter" {
|
|
t.Fatalf("expected resource type helicopter, got %s", res.Type)
|
|
}
|
|
if res.Attributes.Identifier != "1234" {
|
|
t.Fatalf("expected identifier mapped, got %q", res.Attributes.Identifier)
|
|
}
|
|
if res.Attributes.SortKey == nil || *res.Attributes.SortKey != 10 {
|
|
t.Fatalf("expected sortkey mapped, got %#v", res.Attributes.SortKey)
|
|
}
|
|
if res.Attributes.IsInUse {
|
|
t.Fatalf("expected is_in_use default false")
|
|
}
|
|
if res.Attributes.CreatedBy == nil || res.Attributes.CreatedBy.ID == "" {
|
|
t.Fatalf("expected created_by mapped")
|
|
}
|
|
if res.Attributes.CreatedAt == "" || res.Attributes.UpdatedAt == "" {
|
|
t.Fatalf("expected created_at and updated_at mapped")
|
|
}
|
|
}
|
|
|
|
func TestHelicopterHandler_DefaultHelpers(t *testing.T) {
|
|
if boolOrDefault(nil, true) != true {
|
|
t.Fatalf("expected bool default")
|
|
}
|
|
vBool := false
|
|
if boolOrDefault(&vBool, true) != false {
|
|
t.Fatalf("expected bool pointer value")
|
|
}
|
|
|
|
if intOrDefault(nil, 7) != 7 {
|
|
t.Fatalf("expected int default")
|
|
}
|
|
vInt := 12
|
|
if intOrDefault(&vInt, 7) != 12 {
|
|
t.Fatalf("expected int pointer value")
|
|
}
|
|
|
|
if floatOrDefault(nil, 1.5) != 1.5 {
|
|
t.Fatalf("expected float default")
|
|
}
|
|
vFloat := 9.9
|
|
if floatOrDefault(&vFloat, 1.5) != 9.9 {
|
|
t.Fatalf("expected float pointer value")
|
|
}
|
|
|
|
if stringOrDefault(nil, "a") != "a" {
|
|
t.Fatalf("expected string default")
|
|
}
|
|
vStr := "b"
|
|
if stringOrDefault(&vStr, "a") != "b" {
|
|
t.Fatalf("expected string pointer value")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeHelicopterIdentifier(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw string
|
|
want string
|
|
}{
|
|
{name: "empty", raw: "", want: ""},
|
|
{name: "plain", raw: "1234", want: "1234"},
|
|
{name: "trim spaces", raw: " ABC ", want: "ABC"},
|
|
{name: "keep free text", raw: "s/n 1234", want: "S/N 1234"},
|
|
{name: "keep free text upper", raw: "S/N 1234", want: "S/N 1234"},
|
|
{name: "spaces only", raw: " ", want: ""},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := normalizeHelicopterIdentifier(tc.raw)
|
|
if got != tc.want {
|
|
t.Fatalf("normalizeHelicopterIdentifier(%q) = %q, want %q", tc.raw, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHelicopterHandler_HelperBranches(t *testing.T) {
|
|
t.Run("with setters return same handler", func(t *testing.T) {
|
|
h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo()))
|
|
if h.WithFileManagerService(nil) != h {
|
|
t.Fatalf("expected WithFileManagerService to return same handler")
|
|
}
|
|
if h.WithFileStorage(nil) != h {
|
|
t.Fatalf("expected WithFileStorage to return same handler")
|
|
}
|
|
})
|
|
|
|
t.Run("resolve foto attachment via file uuid without file manager writes validation error", func(t *testing.T) {
|
|
h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo()))
|
|
app := fiber.New()
|
|
app.Get("/", func(c *fiber.Ctx) error {
|
|
raw := "bad-uuid"
|
|
_, handled := h.resolveFotoAttachmentID(c, uuidv7.MustBytes(), &raw, "Create failed")
|
|
if !handled {
|
|
t.Fatalf("expected invalid uuid to be handled")
|
|
}
|
|
return nil
|
|
})
|
|
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("resolve existing foto attachment returns direct attachment id", func(t *testing.T) {
|
|
attachmentID := uuidv7.MustBytes()
|
|
fileManagerSvc := service.NewFileManagerService(nil, nil, &helicopterAttachmentRepoMock{
|
|
getByIDFn: func(id []byte) (*filemanager.Attachment, error) {
|
|
return &filemanager.Attachment{ID: id}, nil
|
|
},
|
|
}, service.FileManagerServiceDependencies{})
|
|
h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo())).WithFileManagerService(fileManagerSvc)
|
|
app := fiber.New()
|
|
app.Get("/", func(c *fiber.Ctx) error {
|
|
raw := mustUUIDStringHelicopter(attachmentID)
|
|
got, handled := h.resolveExistingFotoAttachmentOrFileID(c, uuidv7.MustBytes(), &raw, "Update failed")
|
|
if handled {
|
|
t.Fatalf("expected existing attachment path to succeed")
|
|
}
|
|
if string(got) != string(attachmentID) {
|
|
t.Fatalf("expected direct attachment id returned")
|
|
}
|
|
return c.SendStatus(fiber.StatusOK)
|
|
})
|
|
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
if _, err := app.Test(req); err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("resolve existing foto attachment surfaces repository error", func(t *testing.T) {
|
|
fileManagerSvc := service.NewFileManagerService(nil, nil, &helicopterAttachmentRepoMock{
|
|
getByIDFn: func([]byte) (*filemanager.Attachment, error) {
|
|
return nil, errors.New("attachment lookup failed")
|
|
},
|
|
}, service.FileManagerServiceDependencies{})
|
|
h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo())).WithFileManagerService(fileManagerSvc)
|
|
app := fiber.New()
|
|
app.Get("/", func(c *fiber.Ctx) error {
|
|
raw := mustUUIDStringHelicopter(uuidv7.MustBytes())
|
|
_, handled := h.resolveExistingFotoAttachmentOrFileID(c, uuidv7.MustBytes(), &raw, "Update failed")
|
|
if !handled {
|
|
t.Fatalf("expected repository error to be handled")
|
|
}
|
|
return nil
|
|
})
|
|
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("require AOG pin verification branches", func(t *testing.T) {
|
|
h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo()))
|
|
app := fiber.New()
|
|
app.Get("/missing-config", func(c *fiber.Ctx) error {
|
|
if !h.requireAOGPINVerification(c) {
|
|
t.Fatalf("expected missing pin verifier to block request")
|
|
}
|
|
return nil
|
|
})
|
|
req, _ := http.NewRequest(http.MethodGet, "/missing-config", nil)
|
|
resp, _ := app.Test(req)
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", resp.StatusCode)
|
|
}
|
|
|
|
h.pinVerifier = &helicopterPINVerifierMock{}
|
|
app.Get("/missing-user", func(c *fiber.Ctx) error {
|
|
if !h.requireAOGPINVerification(c) {
|
|
t.Fatalf("expected missing actor to block request")
|
|
}
|
|
return nil
|
|
})
|
|
req, _ = http.NewRequest(http.MethodGet, "/missing-user", nil)
|
|
resp, _ = app.Test(req)
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", resp.StatusCode)
|
|
}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_id", uuidv7.MustBytes())
|
|
return c.Next()
|
|
})
|
|
h.pinVerifier = &helicopterPINVerifierMock{
|
|
consumeFn: func(context.Context, []byte, string, string) error { return errors.New("missing pin") },
|
|
}
|
|
app.Get("/pin-required", func(c *fiber.Ctx) error {
|
|
if !h.requireAOGPINVerification(c) {
|
|
t.Fatalf("expected invalid token to require pin")
|
|
}
|
|
return nil
|
|
})
|
|
req, _ = http.NewRequest(http.MethodGet, "/pin-required", nil)
|
|
resp, _ = app.Test(req)
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
t.Fatalf("expected 202, got %d", resp.StatusCode)
|
|
}
|
|
|
|
h.pinVerifier = &helicopterPINVerifierMock{
|
|
consumeFn: func(context.Context, []byte, string, string) error { return nil },
|
|
}
|
|
app.Get("/pin-ok", func(c *fiber.Ctx) error {
|
|
if h.requireAOGPINVerification(c) {
|
|
t.Fatalf("expected valid token to pass")
|
|
}
|
|
return c.SendStatus(fiber.StatusOK)
|
|
})
|
|
req, _ = http.NewRequest(http.MethodGet, "/pin-ok", nil)
|
|
req.Header.Set(pinVerificationHeader, "token")
|
|
resp, _ = app.Test(req)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func mustUUIDStringHelicopter(id []byte) string {
|
|
v, err := uuidv7.BytesToString(id)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return v
|
|
}
|