init push
This commit is contained in:
36
internal/shared/pkg/apperrorsx/apperror.go
Normal file
36
internal/shared/pkg/apperrorsx/apperror.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package apperrorsx
|
||||
|
||||
import "wucher/internal/transport/http/jsonapi"
|
||||
|
||||
// AppError is the canonical backend error contract used by translators and writers.
|
||||
type AppError struct {
|
||||
Status int
|
||||
Code string
|
||||
ErrorCode string
|
||||
Title string
|
||||
Message string
|
||||
Detail any
|
||||
Source *jsonapi.ErrorSource
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.Message != "" {
|
||||
return e.Message
|
||||
}
|
||||
if e.Cause != nil {
|
||||
return e.Cause.Error()
|
||||
}
|
||||
return e.Code
|
||||
}
|
||||
|
||||
func (e *AppError) WithCause(err error) *AppError {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
e.Cause = err
|
||||
return e
|
||||
}
|
||||
183
internal/shared/pkg/apperrorsx/codes.go
Normal file
183
internal/shared/pkg/apperrorsx/codes.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package apperrorsx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleGeneral = "00"
|
||||
ModuleAuth = "01"
|
||||
ModulePIN = "02"
|
||||
ModuleHelicopter = "03"
|
||||
ModuleHelicopterUsage = "35"
|
||||
ModuleUser = "04"
|
||||
ModuleMasterData = "05"
|
||||
ModuleDutyRoster = "06"
|
||||
ModuleFlight = "07"
|
||||
ModuleContact = "08"
|
||||
ModuleMissionOps = "09"
|
||||
ModuleFacility = "10"
|
||||
ModuleHospital = "11"
|
||||
ModuleFederalState = "12"
|
||||
ModuleNationality = "13"
|
||||
ModuleICAO = "14"
|
||||
ModuleLand = "15"
|
||||
ModuleBase = "16"
|
||||
ModuleHealthInsuranceCompanies = "17"
|
||||
ModuleVocation = "18"
|
||||
ModuleMedicine = "19"
|
||||
ModuleMasterSettings = "20"
|
||||
ModuleFileManager = "21"
|
||||
ModuleOPCData = "22"
|
||||
ModulePatientData = "23"
|
||||
ModuleInsurancePatientData = "24"
|
||||
ModuleRole = "25"
|
||||
ModuleFlightData = "26"
|
||||
ModuleAirRescuerChecklist = "27"
|
||||
ModuleBranding = "28"
|
||||
ModuleDUL = "29"
|
||||
ModuleTakeover = "30"
|
||||
ModuleFleetStatus = "31"
|
||||
ModuleComplaint = "32"
|
||||
ModuleEASARelease = "33"
|
||||
ModuleMCF = "34"
|
||||
ModuleHelicopterUsageSync = "35"
|
||||
ModuleFMReport = "36"
|
||||
ModuleAfterFlightInspection = "37"
|
||||
ModuleActionSignoff = "38"
|
||||
)
|
||||
|
||||
// ContextKeyHTTPErrorDetail stores the original, non-sanitized error detail for
|
||||
// request logging. HTTP response writers should sanitize the public payload but
|
||||
// keep this local available for access logs.
|
||||
const ContextKeyHTTPErrorDetail = "http_error_detail"
|
||||
|
||||
// ContextKeyHTTPErrorInternalDetail stores an internal-only, verbose error
|
||||
// summary for request logging. Unlike ContextKeyHTTPErrorDetail, this is not
|
||||
// meant for public response sanitizers.
|
||||
const ContextKeyHTTPErrorInternalDetail = "http_error_internal_detail"
|
||||
|
||||
type Def struct {
|
||||
Status int
|
||||
Code string
|
||||
ErrorCode string
|
||||
Title string
|
||||
Message string
|
||||
}
|
||||
|
||||
func Build(status int, moduleCode, caseCode, code, message string) Def {
|
||||
return BuildWithTitle(
|
||||
status,
|
||||
moduleCode,
|
||||
caseCode,
|
||||
code,
|
||||
titleForStatus(status),
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
func BuildWithTitle(status int, moduleCode, caseCode, code, title, message string) Def {
|
||||
return Def{
|
||||
Status: status,
|
||||
Code: code,
|
||||
ErrorCode: fmt.Sprintf("%03d%s%s", status, moduleCode, caseCode),
|
||||
Title: title,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func titleForStatus(status int) string {
|
||||
switch status {
|
||||
case http.StatusAccepted:
|
||||
return "Accepted"
|
||||
case http.StatusBadRequest:
|
||||
return "Bad Request"
|
||||
case http.StatusUnauthorized:
|
||||
return "Unauthorized"
|
||||
case http.StatusLocked:
|
||||
return "Locked"
|
||||
case http.StatusForbidden:
|
||||
return "Forbidden"
|
||||
case http.StatusNotFound:
|
||||
return "Not found"
|
||||
case http.StatusConflict:
|
||||
return "Conflict"
|
||||
case http.StatusUnprocessableEntity:
|
||||
return "Validation error"
|
||||
case http.StatusTooManyRequests:
|
||||
return "Too many requests"
|
||||
default:
|
||||
return "Internal server error"
|
||||
}
|
||||
}
|
||||
|
||||
func New(def Def) *AppError {
|
||||
return &AppError{
|
||||
Status: def.Status,
|
||||
Code: def.Code,
|
||||
ErrorCode: def.ErrorCode,
|
||||
Title: def.Title,
|
||||
Message: def.Message,
|
||||
}
|
||||
}
|
||||
|
||||
func DefGeneral(status int, caseCode, code, message string) Def {
|
||||
return Build(status, ModuleGeneral, caseCode, code, message)
|
||||
}
|
||||
|
||||
func DefGeneralWithTitle(status int, caseCode, code, title, message string) Def {
|
||||
return BuildWithTitle(status, ModuleGeneral, caseCode, code, title, message)
|
||||
}
|
||||
|
||||
func DefAuth(status int, caseCode, code, message string) Def {
|
||||
return Build(status, ModuleAuth, caseCode, code, message)
|
||||
}
|
||||
|
||||
func DefPIN(status int, caseCode, code, message string) Def {
|
||||
return Build(status, ModulePIN, caseCode, code, message)
|
||||
}
|
||||
|
||||
func DefPINWithTitle(status int, caseCode, code, title, message string) Def {
|
||||
return BuildWithTitle(status, ModulePIN, caseCode, code, title, message)
|
||||
}
|
||||
|
||||
func DefHelicopter(status int, caseCode, code, message string) Def {
|
||||
return Build(status, ModuleHelicopter, caseCode, code, message)
|
||||
}
|
||||
|
||||
func DefHelicopterWithTitle(status int, caseCode, code, title, message string) Def {
|
||||
return BuildWithTitle(status, ModuleHelicopter, caseCode, code, title, message)
|
||||
}
|
||||
|
||||
func DefHelicopterUsage(status int, caseCode, code, message string) Def {
|
||||
return Build(status, ModuleHelicopterUsage, caseCode, code, message)
|
||||
}
|
||||
|
||||
func DefHelicopterUsageWithTitle(status int, caseCode, code, title, message string) Def {
|
||||
return BuildWithTitle(status, ModuleHelicopterUsage, caseCode, code, title, message)
|
||||
}
|
||||
|
||||
func DefHelicopterUsageSync(status int, caseCode, code, message string) Def {
|
||||
return Build(status, ModuleHelicopterUsageSync, caseCode, code, message)
|
||||
}
|
||||
|
||||
func DefHelicopterUsageSyncWithTitle(status int, caseCode, code, title, message string) Def {
|
||||
return BuildWithTitle(status, ModuleHelicopterUsageSync, caseCode, code, title, message)
|
||||
}
|
||||
|
||||
func DefFMReport(status int, caseCode, code, message string) Def {
|
||||
return Build(status, ModuleFMReport, caseCode, code, message)
|
||||
}
|
||||
|
||||
func DefFMReportWithTitle(status int, caseCode, code, title, message string) Def {
|
||||
return BuildWithTitle(status, ModuleFMReport, caseCode, code, title, message)
|
||||
}
|
||||
|
||||
func DefByModule(moduleCode string, status int, caseCode, code, message string) Def {
|
||||
return Build(status, moduleCode, caseCode, code, message)
|
||||
}
|
||||
|
||||
func DefByModuleWithTitle(moduleCode string, status int, caseCode, code, title, message string) Def {
|
||||
return BuildWithTitle(status, moduleCode, caseCode, code, title, message)
|
||||
}
|
||||
2601
internal/shared/pkg/apperrorsx/codes_defs.go
Normal file
2601
internal/shared/pkg/apperrorsx/codes_defs.go
Normal file
File diff suppressed because it is too large
Load Diff
106
internal/shared/pkg/apperrorsx/enrich.go
Normal file
106
internal/shared/pkg/apperrorsx/enrich.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package apperrorsx
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/transport/http/jsonapi"
|
||||
)
|
||||
|
||||
var (
|
||||
registryByCode map[string]Def
|
||||
registryByErrorCode map[string]Def
|
||||
)
|
||||
|
||||
func init() {
|
||||
registryByCode = make(map[string]Def, len(Registry))
|
||||
registryByErrorCode = make(map[string]Def, len(Registry))
|
||||
for _, def := range Registry {
|
||||
registryByCode[def.Code] = def
|
||||
registryByErrorCode[def.ErrorCode] = def
|
||||
}
|
||||
}
|
||||
|
||||
func EnrichErrorObjects(status int, errs []jsonapi.ErrorObject) []jsonapi.ErrorObject {
|
||||
out := make([]jsonapi.ErrorObject, 0, len(errs))
|
||||
for i := range errs {
|
||||
obj := errs[i]
|
||||
obj.Status = strconv.Itoa(status)
|
||||
|
||||
def := inferDef(status, obj)
|
||||
if obj.Code == "" && def.Code != "" {
|
||||
obj.Code = def.Code
|
||||
}
|
||||
if obj.ErrorCode == "" && def.ErrorCode != "" {
|
||||
obj.ErrorCode = def.ErrorCode
|
||||
}
|
||||
if obj.Title == "" && def.Title != "" {
|
||||
obj.Title = def.Title
|
||||
}
|
||||
out = append(out, obj)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func inferDef(status int, obj jsonapi.ErrorObject) Def {
|
||||
if obj.Code != "" {
|
||||
if def, ok := registryByCode[obj.Code]; ok {
|
||||
def.Status = status
|
||||
def.ErrorCode = Build(status, moduleFromErrorCode(def.ErrorCode), caseFromErrorCode(def.ErrorCode), def.Code, def.Message).ErrorCode
|
||||
return def
|
||||
}
|
||||
}
|
||||
if obj.ErrorCode != "" {
|
||||
if def, ok := registryByErrorCode[obj.ErrorCode]; ok {
|
||||
return def
|
||||
}
|
||||
}
|
||||
|
||||
switch status {
|
||||
case fiber.StatusBadRequest:
|
||||
return ErrInvalidRequest
|
||||
case fiber.StatusUnauthorized:
|
||||
return ErrUserUnauthorized
|
||||
case fiber.StatusForbidden:
|
||||
return ErrForbiddenAccess
|
||||
case fiber.StatusNotFound:
|
||||
return ErrDataNotFound
|
||||
case fiber.StatusConflict:
|
||||
return ErrBusinessRuleFailed
|
||||
case fiber.StatusUnprocessableEntity:
|
||||
return ErrValidation
|
||||
case fiber.StatusTooManyRequests:
|
||||
return ErrPINAttemptLimitReached
|
||||
default:
|
||||
msg := strings.ToLower(strings.TrimSpace(obj.Detail))
|
||||
if strings.Contains(msg, "not found") {
|
||||
return ErrDataNotFound
|
||||
}
|
||||
if strings.Contains(msg, "unauthorized") {
|
||||
return ErrUserUnauthorized
|
||||
}
|
||||
if strings.Contains(msg, "forbidden") {
|
||||
return ErrForbiddenAccess
|
||||
}
|
||||
if strings.Contains(msg, "validation") || strings.Contains(msg, "invalid") {
|
||||
return ErrValidation
|
||||
}
|
||||
return ErrInternal
|
||||
}
|
||||
}
|
||||
|
||||
func moduleFromErrorCode(errorCode string) string {
|
||||
if len(errorCode) >= 5 {
|
||||
return errorCode[3:5]
|
||||
}
|
||||
return ModuleGeneral
|
||||
}
|
||||
|
||||
func caseFromErrorCode(errorCode string) string {
|
||||
if len(errorCode) >= 7 {
|
||||
return errorCode[5:7]
|
||||
}
|
||||
return CaseGeneralInternalError
|
||||
}
|
||||
141
internal/shared/pkg/apperrorsx/mysql_map.go
Normal file
141
internal/shared/pkg/apperrorsx/mysql_map.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package apperrorsx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
mysqldriver "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
mysqlErrAccessDenied uint16 = 1045
|
||||
mysqlErrDuplicateEntry uint16 = 1062
|
||||
mysqlErrCommandDenied uint16 = 1142
|
||||
mysqlErrLockWaitTimeout uint16 = 1205
|
||||
mysqlErrDeadlockFound uint16 = 1213
|
||||
mysqlErrSpecificAccessDenied uint16 = 1227
|
||||
mysqlErrColumnCannotBeNull uint16 = 1048
|
||||
mysqlErrTruncatedWrongValue uint16 = 1292
|
||||
mysqlErrIncorrectValue uint16 = 1366
|
||||
mysqlErrServerGone uint16 = 2006
|
||||
mysqlErrFkParentStillReferenced uint16 = 1451
|
||||
mysqlErrFkChildNotFound uint16 = 1452
|
||||
mysqlErrCheckConstraintViolated uint16 = 3819
|
||||
mysqlErrConstraintFailed uint16 = 4025
|
||||
)
|
||||
|
||||
func mapMySQLError(module string, err error) *AppError {
|
||||
var mysqlErr *mysqldriver.MySQLError
|
||||
if !errors.As(err, &mysqlErr) {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch module {
|
||||
case ModuleHelicopter:
|
||||
return mapMySQLErrorHelicopter(mysqlErr, err)
|
||||
case ModuleHospital:
|
||||
return mapMySQLErrorHospital(mysqlErr, err)
|
||||
case ModuleFleetStatus:
|
||||
return mapMySQLErrorFleetStatus(mysqlErr, err)
|
||||
case ModuleComplaint:
|
||||
return mapMySQLErrorComplaint(mysqlErr, err)
|
||||
default:
|
||||
return mapMySQLErrorGeneral(mysqlErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func mapMySQLErrorFleetStatus(mysqlErr *mysqldriver.MySQLError, cause error) *AppError {
|
||||
switch mysqlErr.Number {
|
||||
case mysqlErrDuplicateEntry:
|
||||
return New(ErrFleetStatusDuplicate).WithCause(cause)
|
||||
case mysqlErrFkParentStillReferenced:
|
||||
return New(ErrFleetStatusRelationDelete).WithCause(cause)
|
||||
case mysqlErrFkChildNotFound:
|
||||
// fleet_status.helicopter_id references a non-existent helicopter.
|
||||
return New(ErrFleetStatusHelicopterInvalid).WithCause(cause)
|
||||
case mysqlErrColumnCannotBeNull,
|
||||
mysqlErrTruncatedWrongValue,
|
||||
mysqlErrIncorrectValue,
|
||||
mysqlErrCheckConstraintViolated,
|
||||
mysqlErrConstraintFailed:
|
||||
return New(ErrFleetStatusInvalidPayload).WithCause(cause)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func mapMySQLErrorGeneral(mysqlErr *mysqldriver.MySQLError, cause error) *AppError {
|
||||
switch mysqlErr.Number {
|
||||
case mysqlErrAccessDenied:
|
||||
return New(ErrUserUnauthorized).WithCause(cause)
|
||||
case mysqlErrCommandDenied, mysqlErrSpecificAccessDenied:
|
||||
return New(ErrForbiddenAccess).WithCause(cause)
|
||||
case mysqlErrLockWaitTimeout, mysqlErrDeadlockFound:
|
||||
return New(ErrBusinessRuleFailed).WithCause(cause)
|
||||
case mysqlErrServerGone:
|
||||
return New(ErrInternal).WithCause(cause)
|
||||
case mysqlErrDuplicateEntry, mysqlErrFkParentStillReferenced:
|
||||
return New(ErrBusinessRuleFailed).WithCause(cause)
|
||||
case mysqlErrColumnCannotBeNull,
|
||||
mysqlErrTruncatedWrongValue,
|
||||
mysqlErrIncorrectValue,
|
||||
mysqlErrFkChildNotFound,
|
||||
mysqlErrCheckConstraintViolated,
|
||||
mysqlErrConstraintFailed:
|
||||
return New(ErrValidation).WithCause(cause)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func mapMySQLErrorHelicopter(mysqlErr *mysqldriver.MySQLError, cause error) *AppError {
|
||||
switch mysqlErr.Number {
|
||||
case mysqlErrDuplicateEntry:
|
||||
return New(ErrHelicopterDuplicate).WithCause(cause)
|
||||
case mysqlErrFkParentStillReferenced:
|
||||
return New(ErrHelicopterRelationDelete).WithCause(cause)
|
||||
case mysqlErrColumnCannotBeNull,
|
||||
mysqlErrTruncatedWrongValue,
|
||||
mysqlErrIncorrectValue,
|
||||
mysqlErrFkChildNotFound,
|
||||
mysqlErrCheckConstraintViolated,
|
||||
mysqlErrConstraintFailed:
|
||||
return New(ErrHelicopterInvalidPayload).WithCause(cause)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func mapMySQLErrorHospital(mysqlErr *mysqldriver.MySQLError, cause error) *AppError {
|
||||
switch mysqlErr.Number {
|
||||
case mysqlErrFkParentStillReferenced:
|
||||
return New(ErrHospitalRelationDelete).WithCause(cause)
|
||||
case mysqlErrColumnCannotBeNull,
|
||||
mysqlErrTruncatedWrongValue,
|
||||
mysqlErrIncorrectValue,
|
||||
mysqlErrFkChildNotFound,
|
||||
mysqlErrCheckConstraintViolated,
|
||||
mysqlErrConstraintFailed:
|
||||
return New(ErrHospitalInvalidPayload).WithCause(cause)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func mapMySQLErrorComplaint(mysqlErr *mysqldriver.MySQLError, cause error) *AppError {
|
||||
switch mysqlErr.Number {
|
||||
case mysqlErrDuplicateEntry:
|
||||
return New(ErrComplaintInvalidPayload).WithCause(cause)
|
||||
case mysqlErrFkParentStillReferenced:
|
||||
return New(ErrComplaintRelationDelete).WithCause(cause)
|
||||
case mysqlErrFkChildNotFound:
|
||||
return New(ErrComplaintInvalidHelicopterID).WithCause(cause)
|
||||
case mysqlErrColumnCannotBeNull,
|
||||
mysqlErrTruncatedWrongValue,
|
||||
mysqlErrIncorrectValue,
|
||||
mysqlErrCheckConstraintViolated,
|
||||
mysqlErrConstraintFailed:
|
||||
return New(ErrComplaintInvalidPayload).WithCause(cause)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
33
internal/shared/pkg/apperrorsx/registry_test.go
Normal file
33
internal/shared/pkg/apperrorsx/registry_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package apperrorsx
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRegistry_UniqueCodeAndErrorCode(t *testing.T) {
|
||||
seenCode := map[string]struct{}{}
|
||||
seenErrorCode := map[string]struct{}{}
|
||||
seenModuleCase := map[string]struct{}{}
|
||||
for _, d := range Registry {
|
||||
if d.Code == "" {
|
||||
t.Fatalf("empty code in registry: %#v", d)
|
||||
}
|
||||
if d.ErrorCode == "" {
|
||||
t.Fatalf("empty error_code in registry: %#v", d)
|
||||
}
|
||||
if _, ok := seenCode[d.Code]; ok {
|
||||
t.Fatalf("duplicate code: %s", d.Code)
|
||||
}
|
||||
if _, ok := seenErrorCode[d.ErrorCode]; ok {
|
||||
t.Fatalf("duplicate error_code: %s", d.ErrorCode)
|
||||
}
|
||||
if len(d.ErrorCode) != 7 {
|
||||
t.Fatalf("invalid error_code length for %s: %s", d.Code, d.ErrorCode)
|
||||
}
|
||||
moduleCase := d.ErrorCode[3:]
|
||||
if _, ok := seenModuleCase[moduleCase]; ok {
|
||||
t.Fatalf("duplicate module+case detected: %s (error_code=%s)", moduleCase, d.ErrorCode)
|
||||
}
|
||||
seenCode[d.Code] = struct{}{}
|
||||
seenErrorCode[d.ErrorCode] = struct{}{}
|
||||
seenModuleCase[moduleCase] = struct{}{}
|
||||
}
|
||||
}
|
||||
68
internal/shared/pkg/apperrorsx/response.go
Normal file
68
internal/shared/pkg/apperrorsx/response.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package apperrorsx
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"wucher/internal/transport/http/jsonapi"
|
||||
)
|
||||
|
||||
func Write(c *fiber.Ctx, e *AppError) error {
|
||||
if e == nil {
|
||||
e = New(ErrInternal)
|
||||
}
|
||||
if e.Status == 0 {
|
||||
e.Status = fiber.StatusInternalServerError
|
||||
}
|
||||
if e.Title == "" {
|
||||
e.Title = defaultTitle(e.Status)
|
||||
}
|
||||
if detail := publicLogDetail(e.Message, e.Cause, e.Title); detail != "" {
|
||||
c.Locals(ContextKeyHTTPErrorDetail, detail)
|
||||
}
|
||||
|
||||
obj := jsonapi.ErrorObject{
|
||||
Status: strconv.Itoa(e.Status),
|
||||
Code: e.Code,
|
||||
ErrorCode: e.ErrorCode,
|
||||
Title: e.Title,
|
||||
Detail: PublicDetail(e.Status),
|
||||
Source: e.Source,
|
||||
}
|
||||
return jsonapi.WriteErrors(c, e.Status, []jsonapi.ErrorObject{obj})
|
||||
}
|
||||
|
||||
func publicLogDetail(message string, cause error, fallback string) string {
|
||||
if cause != nil {
|
||||
if trimmed := strings.TrimSpace(cause.Error()); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
if trimmed := strings.TrimSpace(message); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
return strings.TrimSpace(fallback)
|
||||
}
|
||||
|
||||
func defaultTitle(status int) string {
|
||||
switch status {
|
||||
case fiber.StatusBadRequest:
|
||||
return "Bad Request"
|
||||
case fiber.StatusUnauthorized:
|
||||
return "Unauthorized"
|
||||
case fiber.StatusForbidden:
|
||||
return "Forbidden"
|
||||
case fiber.StatusNotFound:
|
||||
return "Not found"
|
||||
case fiber.StatusConflict:
|
||||
return "Conflict"
|
||||
case fiber.StatusUnprocessableEntity:
|
||||
return "Validation error"
|
||||
case fiber.StatusTooManyRequests:
|
||||
return "Too many requests"
|
||||
default:
|
||||
return "Internal server error"
|
||||
}
|
||||
}
|
||||
54
internal/shared/pkg/apperrorsx/response_test.go
Normal file
54
internal/shared/pkg/apperrorsx/response_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package apperrorsx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TestPublicDetail(t *testing.T) {
|
||||
tests := []struct {
|
||||
status int
|
||||
want string
|
||||
}{
|
||||
{http.StatusBadRequest, "invalid request"},
|
||||
{http.StatusUnauthorized, "unauthorized"},
|
||||
{http.StatusForbidden, "forbidden"},
|
||||
{http.StatusNotFound, "not found"},
|
||||
{http.StatusConflict, "conflict"},
|
||||
{http.StatusUnprocessableEntity, "validation failed"},
|
||||
{http.StatusTooManyRequests, "too many requests"},
|
||||
{http.StatusInternalServerError, "an internal error occurred"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
if got := PublicDetail(tc.status); got != tc.want {
|
||||
t.Fatalf("status %d: expected %q, got %q", tc.status, tc.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSanitizesDetail(t *testing.T) {
|
||||
app := fiber.New()
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
err := New(ErrInternal)
|
||||
err.Message = "raw database message"
|
||||
return Write(c, err)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if bytes.Contains(body, []byte("raw database message")) {
|
||||
t.Fatalf("expected sanitized detail, got %s", string(body))
|
||||
}
|
||||
if !bytes.Contains(body, []byte(`"detail":"an internal error occurred"`)) {
|
||||
t.Fatalf("expected generic detail, got %s", string(body))
|
||||
}
|
||||
}
|
||||
221
internal/shared/pkg/apperrorsx/translator.go
Normal file
221
internal/shared/pkg/apperrorsx/translator.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package apperrorsx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
afterflightinspection "wucher/internal/domain/after_flight_inspection"
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/shared/pkg/apperrors"
|
||||
)
|
||||
|
||||
// Translate is the single entrypoint for mapping domain/service errors to AppError.
|
||||
func Translate(module string, err error) *AppError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch module {
|
||||
case ModuleAuth, ModulePIN:
|
||||
if mapped := mapAuthPIN(err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
case ModuleHelicopter:
|
||||
if mapped := mapHelicopter(err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
case ModuleHospital:
|
||||
if mapped := mapHospital(err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
case ModuleTakeover:
|
||||
if mapped := mapTakeover(err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
case ModuleFleetStatus:
|
||||
if mapped := mapFleetStatus(err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
case ModuleComplaint:
|
||||
if mapped := mapComplaint(err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
case ModuleAfterFlightInspection:
|
||||
if mapped := mapAfterFlightInspection(err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
}
|
||||
|
||||
return mapGeneral(err)
|
||||
}
|
||||
|
||||
func mapAfterFlightInspection(err error) *AppError {
|
||||
switch {
|
||||
case errors.Is(err, afterflightinspection.ErrInvalidFlightInspectionID):
|
||||
return New(ErrAfterFlightInvalidID).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrFlightInspectionNotFound):
|
||||
return New(ErrAfterFlightFlightInspectionNotFound).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrCapabilitiesUnavailable):
|
||||
return New(ErrAfterFlightCapabilitiesUnavailable).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrNR1Required):
|
||||
return New(ErrAfterFlightNR1Required).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrNR1NotAvailable):
|
||||
return New(ErrAfterFlightNR1NotAvailable).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrNR2Required):
|
||||
return New(ErrAfterFlightNR2Required).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrNR2NotAvailable):
|
||||
return New(ErrAfterFlightNR2NotAvailable).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrLHRequired):
|
||||
return New(ErrAfterFlightLHRequired).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrLHNotAvailable):
|
||||
return New(ErrAfterFlightLHNotAvailable).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrRHRequired):
|
||||
return New(ErrAfterFlightRHRequired).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrRHNotAvailable):
|
||||
return New(ErrAfterFlightRHNotAvailable).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrMGBRequired):
|
||||
return New(ErrAfterFlightMGBRequired).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrMGBNotAvailable):
|
||||
return New(ErrAfterFlightMGBNotAvailable).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrIGBRequired):
|
||||
return New(ErrAfterFlightIGBRequired).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrIGBNotAvailable):
|
||||
return New(ErrAfterFlightIGBNotAvailable).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrTGBRequired):
|
||||
return New(ErrAfterFlightTGBRequired).WithCause(err)
|
||||
case errors.Is(err, afterflightinspection.ErrTGBNotAvailable):
|
||||
return New(ErrAfterFlightTGBNotAvailable).WithCause(err)
|
||||
}
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return New(ErrAfterFlightInspectionNotFound).WithCause(err)
|
||||
}
|
||||
|
||||
if mapped := mapMySQLError(ModuleAfterFlightInspection, err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapFleetStatus(err error) *AppError {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return New(ErrFleetStatusNotFound).WithCause(err)
|
||||
}
|
||||
|
||||
if _, ok := apperrors.AsDeleteConflict(err); ok {
|
||||
return New(ErrFleetStatusRelationDelete).WithCause(err)
|
||||
}
|
||||
|
||||
if mapped := mapMySQLError(ModuleFleetStatus, err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapComplaint(err error) *AppError {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return New(ErrComplaintNotFound).WithCause(err)
|
||||
}
|
||||
|
||||
if mapped := mapMySQLError(ModuleComplaint, err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapGeneral(err error) *AppError {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
return New(ErrDataNotFound).WithCause(err)
|
||||
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
|
||||
return New(ErrInvalidRequest).WithCause(err)
|
||||
}
|
||||
|
||||
if mapped := mapMySQLError(ModuleGeneral, err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
|
||||
switch {
|
||||
case errors.Is(err, service.ErrInvalidReauthMethod):
|
||||
return New(ErrValidation).WithCause(err)
|
||||
default:
|
||||
return New(ErrInternal).WithCause(err)
|
||||
}
|
||||
}
|
||||
|
||||
func mapAuthPIN(err error) *AppError {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrInvalidToken):
|
||||
return New(ErrTokenInvalid).WithCause(err)
|
||||
case errors.Is(err, service.ErrInvalidCredentials):
|
||||
return New(ErrUserUnauthorized).WithCause(err)
|
||||
case errors.Is(err, service.ErrSecurityPINNotSet):
|
||||
return New(ErrPINNotSet).WithCause(err)
|
||||
case errors.Is(err, service.ErrSecurityPINLocked), errors.Is(err, service.ErrSecurityPINResetRequired):
|
||||
return New(ErrPINBlocked).WithCause(err)
|
||||
case errors.Is(err, service.ErrSecurityPINAttemptLimit):
|
||||
return New(ErrPINBlocked).WithCause(err)
|
||||
case errors.Is(err, service.ErrInvalidSecurityPIN):
|
||||
return New(ErrPINIncorrect).WithCause(err)
|
||||
case errors.Is(err, service.ErrInvalidPINAction):
|
||||
return New(ErrPINVerificationRequired202).WithCause(err)
|
||||
case errors.Is(err, service.ErrInvalidReauthMethod):
|
||||
return New(ErrValidation).WithCause(err)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func mapHelicopter(err error) *AppError {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return New(ErrHelicopterNotFound).WithCause(err)
|
||||
}
|
||||
|
||||
if _, ok := apperrors.AsDeleteConflict(err); ok {
|
||||
return New(ErrHelicopterRelationDelete).WithCause(err)
|
||||
}
|
||||
|
||||
if mapped := mapMySQLError(ModuleHelicopter, err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
|
||||
switch {
|
||||
case errors.Is(err, service.ErrInvalidReauthMethod):
|
||||
return New(ErrHelicopterInvalidPayload).WithCause(err)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func mapHospital(err error) *AppError {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return New(ErrHospitalNotFound).WithCause(err)
|
||||
}
|
||||
|
||||
if _, ok := apperrors.AsDeleteConflict(err); ok {
|
||||
return New(ErrHospitalRelationDelete).WithCause(err)
|
||||
}
|
||||
|
||||
if mapped := mapMySQLError(ModuleHospital, err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapTakeover(err error) *AppError {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return New(ErrTakeoverNotFound).WithCause(err)
|
||||
}
|
||||
|
||||
if mapped := mapMySQLError(ModuleTakeover, err); mapped != nil {
|
||||
return mapped
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
40
internal/shared/pkg/apperrorsx/translator_test.go
Normal file
40
internal/shared/pkg/apperrorsx/translator_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package apperrorsx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
mysqldriver "github.com/go-sql-driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/service"
|
||||
)
|
||||
|
||||
func TestTranslate_AuthPIN(t *testing.T) {
|
||||
got := Translate(ModulePIN, service.ErrInvalidSecurityPIN)
|
||||
if got == nil || got.Code != CodePINIncorrect {
|
||||
t.Fatalf("expected %s, got %#v", CodePINIncorrect, got)
|
||||
}
|
||||
|
||||
got = Translate(ModuleAuth, service.ErrInvalidToken)
|
||||
if got == nil || got.Code != CodeTokenInvalid {
|
||||
t.Fatalf("expected %s, got %#v", CodeTokenInvalid, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslate_HelicopterAndFallback(t *testing.T) {
|
||||
got := Translate(ModuleHelicopter, gorm.ErrRecordNotFound)
|
||||
if got == nil || got.Code != CodeHelicopterNotFound {
|
||||
t.Fatalf("expected %s, got %#v", CodeHelicopterNotFound, got)
|
||||
}
|
||||
|
||||
got = Translate(ModuleHelicopter, &mysqldriver.MySQLError{Number: 1062, Message: "duplicate"})
|
||||
if got == nil || got.Code != CodeHelicopterDuplicate {
|
||||
t.Fatalf("expected %s, got %#v", CodeHelicopterDuplicate, got)
|
||||
}
|
||||
|
||||
got = Translate("99", errors.New("unknown issue"))
|
||||
if got == nil || got.Code != CodeInternalServerError {
|
||||
t.Fatalf("expected %s, got %#v", CodeInternalServerError, got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user