diff --git a/go.mod b/go.mod index 7b93f6e..83c77d9 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,16 @@ module products go 1.26 require ( - github.com/go-chi/chi/v5 v5.2.5 + github.com/go-chi/chi/v5 v5.3.0 github.com/go-chi/cors v1.2.2 github.com/google/uuid v1.6.0 - github.com/jackc/pgx/v5 v5.9.2 + github.com/jackc/pgx/v5 v5.10.0 ) require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/text v0.38.0 // indirect ) diff --git a/go.sum b/go.sum index bfe0a9f..d3f9f0c 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= +github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -13,6 +15,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -24,8 +28,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/capability/handler.go b/internal/capability/handler.go index 0b8074a..60cb3b6 100644 --- a/internal/capability/handler.go +++ b/internal/capability/handler.go @@ -7,32 +7,20 @@ import ( "log/slog" "net/http" "products/internal" - "products/internal/capability/db" "time" ) -type Handler interface { - CreateCapability(w http.ResponseWriter, r *http.Request) - GetCapability(w http.ResponseWriter, r *http.Request) - GetCapabilitiesByFlow(w http.ResponseWriter, r *http.Request) - GetCapabilitiesByProduct(w http.ResponseWriter, r *http.Request) -} - -func NewHandler(dbConn db.DBTX) Handler { - queries := db.New(dbConn) - service := &postgresService{ - queries: queries, - } - return &capabilityHandler{ - service: service, +func newHandler(svc capabilityService) *handler { + return &handler{ + service: svc, } } -type capabilityHandler struct { +type handler struct { service capabilityService } -func (h *capabilityHandler) CreateCapability(w http.ResponseWriter, r *http.Request) { +func (h *handler) CreateCapability(w http.ResponseWriter, r *http.Request) { capReq := &createCapabilityRequest{} if err := json.NewDecoder(r.Body).Decode(capReq); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -52,7 +40,7 @@ func (h *capabilityHandler) CreateCapability(w http.ResponseWriter, r *http.Requ internal.WriteJSONResponse(w, r, http.StatusCreated, newCap) } -func (h *capabilityHandler) GetCapability(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetCapability(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid capability ID"}, http.StatusBadRequest) @@ -73,7 +61,7 @@ func (h *capabilityHandler) GetCapability(w http.ResponseWriter, r *http.Request internal.WriteJSONResponse(w, r, http.StatusOK, capability) } -func (h *capabilityHandler) GetCapabilitiesByFlow(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetCapabilitiesByFlow(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid capability ID"}, http.StatusBadRequest) @@ -94,7 +82,7 @@ func (h *capabilityHandler) GetCapabilitiesByFlow(w http.ResponseWriter, r *http internal.WriteJSONResponse(w, r, http.StatusOK, capabilities) } -func (h *capabilityHandler) GetCapabilitiesByProduct(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetCapabilitiesByProduct(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid capability ID"}, http.StatusBadRequest) diff --git a/internal/capability/handler_test.go b/internal/capability/handler_test.go index 592d56c..682b9d1 100644 --- a/internal/capability/handler_test.go +++ b/internal/capability/handler_test.go @@ -104,7 +104,7 @@ func TestHandler_CreateCapability(t *testing.T) { tt.mockSetup(mockSvc) } - h := &capabilityHandler{service: mockSvc} + h := &handler{service: mockSvc} h.CreateCapability(w, req) if w.Code != tt.expectedStatus { @@ -129,8 +129,8 @@ func (m *mockDBTX) QueryRow(context.Context, string, ...interface{}) pgx.Row { } func TestNewHandler(t *testing.T) { - mockDB := &mockDBTX{} - h := NewHandler(mockDB) + mockService := &mockCapabilityService{} + h := newHandler(mockService) if h == nil { t.Error("expected handler to be non-nil") } @@ -241,7 +241,7 @@ func TestHandler_GetCapability(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mockSvc) } - h := &capabilityHandler{service: mockSvc} + h := &handler{service: mockSvc} h.GetCapability(w, req) @@ -360,7 +360,7 @@ func TestHandler_GetCapabilitiesByFlow(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mockSvc) } - h := &capabilityHandler{service: mockSvc} + h := &handler{service: mockSvc} h.GetCapabilitiesByFlow(w, req) @@ -479,7 +479,7 @@ func TestHandler_GetCapabilitiesByProduct(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mockSvc) } - h := &capabilityHandler{service: mockSvc} + h := &handler{service: mockSvc} h.GetCapabilitiesByProduct(w, req) diff --git a/internal/capability/router.go b/internal/capability/router.go new file mode 100644 index 0000000..087d208 --- /dev/null +++ b/internal/capability/router.go @@ -0,0 +1,30 @@ +package capability + +import ( + "context" + "products/internal/capability/db" + + "github.com/go-chi/chi/v5" +) + +type capabilityService interface { + CreateCapability(ctx context.Context, req createCapabilityRequest) (db.Capability, error) + GetCapability(ctx context.Context, id int) (db.Capability, error) + GetCapabilitiesByProduct(ctx context.Context, id int) ([]db.GetCapabilitiesByProductRow, error) + GetCapabilitiesByFlow(ctx context.Context, id int) ([]db.Capability, error) +} + +func RegisterRoutes(r chi.Router, dbConn db.DBTX) { + s := newPostgresService(dbConn) + capHandler := newHandler(s) + registerRoutesWithHandler(r, capHandler) +} + +func registerRoutesWithHandler(r chi.Router, capHandler *handler) { + r.Get("/products/{id}/capabilities", capHandler.GetCapabilitiesByProduct) + r.Get("/flows/{id}/capabilities", capHandler.GetCapabilitiesByFlow) + r.Route("/capabilities", func(u chi.Router) { + u.Post("/", capHandler.CreateCapability) + u.Get("/{id}", capHandler.GetCapability) + }) +} diff --git a/internal/capability/router_test.go b/internal/capability/router_test.go new file mode 100644 index 0000000..15aec13 --- /dev/null +++ b/internal/capability/router_test.go @@ -0,0 +1,65 @@ +package capability + +import ( + "context" + "net/http" + "products/internal/capability/db" + "testing" + + "github.com/go-chi/chi/v5" +) + +type mockService struct { + called map[string]bool +} + +func newMockService() *mockService { + return &mockService{called: make(map[string]bool)} +} + +func (m *mockService) CreateCapability(ctx context.Context, req createCapabilityRequest) (db.Capability, error) { + m.called["CreateCapability"] = true + return db.Capability{}, nil +} +func (m *mockService) GetCapability(ctx context.Context, id int) (db.Capability, error) { + m.called["GetCapability"] = true + return db.Capability{}, nil +} +func (m *mockService) GetCapabilitiesByProduct(ctx context.Context, id int) ([]db.GetCapabilitiesByProductRow, error) { + m.called["GetCapabilitiesByProduct"] = true + return nil, nil +} +func (m *mockService) GetCapabilitiesByFlow(ctx context.Context, id int) ([]db.Capability, error) { + m.called["GetCapabilitiesByFlow"] = true + return nil, nil +} + +func TestRegisterRoutesWithHandler(t *testing.T) { + h := newHandler(newMockService()) + + r := chi.NewRouter() + registerRoutesWithHandler(r, h) + + got := map[string]bool{} + + err := chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + got[method+" "+route] = true + return nil + }) + if err != nil { + t.Fatal(err) + } + + want := []string{ + "GET /products/{id}/capabilities", + "GET /flows/{id}/capabilities", + "POST /capabilities/", + "GET /capabilities/{id}", + } + + for _, route := range want { + if !got[route] { + t.Errorf("expected route %q to be registered", route) + } + } +} diff --git a/internal/capability/service.go b/internal/capability/service.go index be1da69..1829bd3 100644 --- a/internal/capability/service.go +++ b/internal/capability/service.go @@ -1,101 +1,16 @@ package capability import ( - "context" - "errors" - "log/slog" "products/internal/capability/db" - - "github.com/jackc/pgx/v5" ) -type capabilityService interface { - capabilityStepService - CreateCapability(ctx context.Context, req createCapabilityRequest) (db.Capability, error) - GetCapability(ctx context.Context, id int) (db.Capability, error) - GetCapabilitiesByProduct(ctx context.Context, id int) ([]db.GetCapabilitiesByProductRow, error) - GetCapabilitiesByFlow(ctx context.Context, id int) ([]db.Capability, error) -} - -type capabilityStepService interface { +func newPostgresService(dbConn db.DBTX) capabilityService { + queries := db.New(dbConn) + return &postgresService{ + queries: queries, + } } type postgresService struct { queries db.Querier } - -func (s *postgresService) CreateCapability(ctx context.Context, req createCapabilityRequest) (db.Capability, error) { - name, err := s.queries.GetFlow(ctx, req.FlowId) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return db.Capability{}, NotFoundError{ - "Flow not found", - } - } - return db.Capability{}, err - } - if name == "" { - return db.Capability{}, NotFoundError{ - "Flow not found", - } - } - newCap, err := s.queries.CreateCapability(ctx, req.ToParams()) - if err != nil { - slog.Error("error creating capability: ", slog.String("error", err.Error())) - return db.Capability{}, errors.New("error creating capability") - } - return newCap, nil -} - -func (s *postgresService) GetCapability(ctx context.Context, id int) (db.Capability, error) { - capability, err := s.queries.GetCapability(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return db.Capability{}, NotFoundError{ - "Capability not found", - } - } - return db.Capability{}, err - } - return capability, nil -} - -func (s *postgresService) GetCapabilitiesByProduct(ctx context.Context, id int) ([]db.GetCapabilitiesByProductRow, error) { - _, err := s.queries.GetProduct(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, NotFoundError{ - "Product not found", - } - } - return nil, err - } - capabilities, err := s.queries.GetCapabilitiesByProduct(ctx, id) - if err != nil { - return nil, err - } - if capabilities == nil { - capabilities = []db.GetCapabilitiesByProductRow{} - } - return capabilities, nil -} - -func (s *postgresService) GetCapabilitiesByFlow(ctx context.Context, id int) ([]db.Capability, error) { - _, err := s.queries.GetFlow(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, NotFoundError{ - "Flow not found", - } - } - return nil, err - } - capabilities, err := s.queries.GetCapabilitiesByFlow(ctx, id) - if err != nil { - return nil, err - } - if capabilities == nil { - capabilities = []db.Capability{} - } - return capabilities, nil -} diff --git a/internal/capability/service_capability.go b/internal/capability/service_capability.go new file mode 100644 index 0000000..314dee4 --- /dev/null +++ b/internal/capability/service_capability.go @@ -0,0 +1,86 @@ +package capability + +import ( + "context" + "errors" + "log/slog" + "products/internal/capability/db" + + "github.com/jackc/pgx/v5" +) + +func (s *postgresService) CreateCapability(ctx context.Context, req createCapabilityRequest) (db.Capability, error) { + name, err := s.queries.GetFlow(ctx, req.FlowId) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return db.Capability{}, NotFoundError{ + "Flow not found", + } + } + return db.Capability{}, err + } + if name == "" { + return db.Capability{}, NotFoundError{ + "Flow not found", + } + } + newCap, err := s.queries.CreateCapability(ctx, req.ToParams()) + if err != nil { + slog.Error("error creating capability: ", slog.String("error", err.Error())) + return db.Capability{}, errors.New("error creating capability") + } + return newCap, nil +} + +func (s *postgresService) GetCapability(ctx context.Context, id int) (db.Capability, error) { + capability, err := s.queries.GetCapability(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return db.Capability{}, NotFoundError{ + "Capability not found", + } + } + return db.Capability{}, err + } + return capability, nil +} + +func (s *postgresService) GetCapabilitiesByProduct(ctx context.Context, id int) ([]db.GetCapabilitiesByProductRow, error) { + _, err := s.queries.GetProduct(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, NotFoundError{ + "Product not found", + } + } + return nil, err + } + capabilities, err := s.queries.GetCapabilitiesByProduct(ctx, id) + if err != nil { + return nil, err + } + if capabilities == nil { + capabilities = []db.GetCapabilitiesByProductRow{} + } + return capabilities, nil +} + +func (s *postgresService) GetCapabilitiesByFlow(ctx context.Context, id int) ([]db.Capability, error) { + _, err := s.queries.GetFlow(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, NotFoundError{ + "Flow not found", + } + } + return nil, err + } + capabilities, err := s.queries.GetCapabilitiesByFlow(ctx, id) + if err != nil { + return nil, err + } + if capabilities == nil { + capabilities = []db.Capability{} + } + return capabilities, nil +} diff --git a/internal/flow/handler.go b/internal/flow/handler.go index 11ede62..572df79 100644 --- a/internal/flow/handler.go +++ b/internal/flow/handler.go @@ -7,40 +7,21 @@ import ( "log/slog" "net/http" "products/internal" - "products/internal/flow/db" "strings" "time" ) -type Handler interface { - CreateFlow(w http.ResponseWriter, r *http.Request) - GetFlowById(w http.ResponseWriter, r *http.Request) - GetFlowsByProduct(w http.ResponseWriter, r *http.Request) - UpdateFlow(w http.ResponseWriter, r *http.Request) - DeleteFlow(w http.ResponseWriter, r *http.Request) - CreateFlowStep(w http.ResponseWriter, r *http.Request) - DeleteFlowStep(w http.ResponseWriter, r *http.Request) - GetFlowSteps(w http.ResponseWriter, r *http.Request) - GetFlowPath(w http.ResponseWriter, r *http.Request) -} - -func NewHandler(dbConn db.DBTX) Handler { - queries := db.New(dbConn) - client := &http.Client{Timeout: 5 * time.Second} - service := &postgresService{ - queries: queries, - client: client, - } - return &flowHandler{ +func newHandler(service flowService) *handler { + return &handler{ flowService: service, } } -type flowHandler struct { +type handler struct { flowService flowService } -func (h *flowHandler) CreateFlow(w http.ResponseWriter, r *http.Request) { +func (h *handler) CreateFlow(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid product ID"}, http.StatusBadRequest) @@ -70,7 +51,7 @@ func (h *flowHandler) CreateFlow(w http.ResponseWriter, r *http.Request) { internal.WriteJSONResponse(w, r, http.StatusCreated, flow) } -func (h *flowHandler) GetFlowById(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetFlowById(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid flow ID", Instance: r.URL.Path}, http.StatusBadRequest) @@ -90,7 +71,7 @@ func (h *flowHandler) GetFlowById(w http.ResponseWriter, r *http.Request) { internal.WriteJSONResponse(w, r, http.StatusOK, flow) } -func (h *flowHandler) GetFlowsByProduct(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetFlowsByProduct(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid product ID", Instance: r.URL.Path}, http.StatusBadRequest) @@ -110,7 +91,7 @@ func (h *flowHandler) GetFlowsByProduct(w http.ResponseWriter, r *http.Request) internal.WriteJSONResponse(w, r, http.StatusOK, flows) } -func (h *flowHandler) UpdateFlow(w http.ResponseWriter, r *http.Request) { +func (h *handler) UpdateFlow(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid flow ID", Instance: r.URL.Path}, http.StatusBadRequest) @@ -144,7 +125,7 @@ func (h *flowHandler) UpdateFlow(w http.ResponseWriter, r *http.Request) { internal.WriteJSONResponse(w, r, http.StatusOK, flow) } -func (h *flowHandler) DeleteFlow(w http.ResponseWriter, r *http.Request) { +func (h *handler) DeleteFlow(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid flow ID", Instance: r.URL.Path}, http.StatusBadRequest) @@ -167,7 +148,7 @@ func (h *flowHandler) DeleteFlow(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func (h *flowHandler) CreateFlowStep(w http.ResponseWriter, r *http.Request) { +func (h *handler) CreateFlowStep(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid flow ID", Instance: r.URL.Path}, http.StatusBadRequest) @@ -223,7 +204,7 @@ func (h *flowHandler) CreateFlowStep(w http.ResponseWriter, r *http.Request) { internal.WriteJSONResponse(w, r, http.StatusCreated, flowStep) } -func (h *flowHandler) DeleteFlowStep(w http.ResponseWriter, r *http.Request) { +func (h *handler) DeleteFlowStep(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid flow step ID", Instance: r.URL.Path}, http.StatusBadRequest) @@ -247,7 +228,7 @@ func (h *flowHandler) DeleteFlowStep(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func (h *flowHandler) GetFlowSteps(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetFlowSteps(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid flow ID", Instance: r.URL.Path}, http.StatusBadRequest) @@ -267,7 +248,7 @@ func (h *flowHandler) GetFlowSteps(w http.ResponseWriter, r *http.Request) { internal.WriteJSONResponse(w, r, http.StatusOK, flowSteps) } -func (h *flowHandler) GetFlowPath(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetFlowPath(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid flow ID", Instance: r.URL.Path}, http.StatusBadRequest) diff --git a/internal/flow/handler_test.go b/internal/flow/handler_test.go index 42c726e..c100897 100644 --- a/internal/flow/handler_test.go +++ b/internal/flow/handler_test.go @@ -153,7 +153,7 @@ func TestUpdateFlow(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mock) } - h := &flowHandler{ + h := &handler{ flowService: &postgresService{ queries: mock, }, @@ -287,7 +287,7 @@ func TestCreateFlow(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mock) } - h := &flowHandler{flowService: &postgresService{queries: mock}} + h := &handler{flowService: &postgresService{queries: mock}} var body []byte if s, ok := tt.requestBody.(string); ok { @@ -380,7 +380,7 @@ func TestGetFlowById(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mock) } - h := &flowHandler{flowService: &postgresService{queries: mock}} + h := &handler{flowService: &postgresService{queries: mock}} req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/flows/1", nil) req.SetPathValue("id", tt.pathID) @@ -487,7 +487,7 @@ func TestGetFlowsByProduct(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mock) } - h := &flowHandler{flowService: &postgresService{queries: mock}} + h := &handler{flowService: &postgresService{queries: mock}} req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/products/1/flows", nil) req.SetPathValue("id", tt.pathID) @@ -569,7 +569,7 @@ func TestDeleteFlow(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mock) } - h := &flowHandler{flowService: &postgresService{queries: mock}} + h := &handler{flowService: &postgresService{queries: mock}} req := httptest.NewRequestWithContext(t.Context(), http.MethodDelete, "/flows/1", nil) req.SetPathValue("id", tt.pathID) @@ -638,7 +638,7 @@ func TestDeleteFlowStep(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mock) } - h := &flowHandler{flowService: &postgresService{queries: mock}} + h := &handler{flowService: &postgresService{queries: mock}} req := httptest.NewRequestWithContext(t.Context(), http.MethodDelete, "/flows/steps/1", nil) req.SetPathValue("id", tt.pathID) @@ -811,7 +811,7 @@ func TestCreateFlowStep(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mock) } - h := &flowHandler{ + h := &handler{ flowService: &postgresService{ queries: mock, client: client, @@ -923,7 +923,7 @@ func TestGetFlowSteps(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mock) } - h := &flowHandler{ + h := &handler{ flowService: &postgresService{ queries: mock, client: client, @@ -1020,7 +1020,7 @@ func TestGetFlowPath(t *testing.T) { if tt.mockSetup != nil { tt.mockSetup(mock) } - h := &flowHandler{ + h := &handler{ flowService: &postgresService{ queries: mock, client: client, diff --git a/internal/flow/router.go b/internal/flow/router.go new file mode 100644 index 0000000..408cd6a --- /dev/null +++ b/internal/flow/router.go @@ -0,0 +1,42 @@ +package flow + +import ( + "context" + "products/internal/flow/db" + + "github.com/go-chi/chi/v5" +) + +type flowService interface { + CreateFlow(ctx context.Context, req createFlowRequest, id int) (db.Flow, error) + GetFlowById(ctx context.Context, id int) (db.Flow, error) + GetFlowsByProduct(ctx context.Context, id int) ([]db.Flow, error) + UpdateFlow(ctx context.Context, req updateFlowRequest, id int) (db.Flow, error) + DeleteFlow(ctx context.Context, id int) error + + CreateFlowStep(ctx context.Context, req createFlowStepRequest) (db.FlowStep, error) + DeleteFlowStep(ctx context.Context, id int) error + GetFlowSteps(ctx context.Context, id int) ([]db.FlowStep, error) + GetFlowPath(ctx context.Context, id int) (FlowPath, error) +} + +func RegisterRoutes(r chi.Router, dbConn db.DBTX) { + s := newPostgresService(dbConn) + flowHandler := newHandler(s) + registerRoutesWithHandler(r, flowHandler) +} + +func registerRoutesWithHandler(r chi.Router, flowHandler *handler) { + r.Route("/flows/{id}", func(u chi.Router) { + u.Post("/steps", flowHandler.CreateFlowStep) + u.Get("/steps", flowHandler.GetFlowSteps) + u.Get("/path", flowHandler.GetFlowPath) + u.Get("/", flowHandler.GetFlowById) + u.Put("/", flowHandler.UpdateFlow) + u.Delete("/", flowHandler.DeleteFlow) + }) + + r.Delete("/flow-steps/{id}", flowHandler.DeleteFlowStep) + r.Post("/products/{id}/flows", flowHandler.CreateFlow) + r.Get("/products/{id}/flows", flowHandler.GetFlowsByProduct) +} diff --git a/internal/flow/router_test.go b/internal/flow/router_test.go new file mode 100644 index 0000000..6a9c56c --- /dev/null +++ b/internal/flow/router_test.go @@ -0,0 +1,90 @@ +package flow + +import ( + "context" + "net/http" + "products/internal/flow/db" + "testing" + + "github.com/go-chi/chi/v5" +) + +type mockService struct { + called map[string]bool +} + +func newMockService() *mockService { + return &mockService{called: make(map[string]bool)} +} + +func (m *mockService) CreateFlow(ctx context.Context, req createFlowRequest, id int) (db.Flow, error) { + m.called["CreateFlow"] = true + return db.Flow{}, nil +} +func (m *mockService) GetFlowById(ctx context.Context, id int) (db.Flow, error) { + m.called["GetFlowById"] = true + return db.Flow{}, nil +} +func (m *mockService) GetFlowsByProduct(ctx context.Context, id int) ([]db.Flow, error) { + m.called["GetFlowsByProduct"] = true + return nil, nil +} +func (m *mockService) UpdateFlow(ctx context.Context, req updateFlowRequest, id int) (db.Flow, error) { + m.called["UpdateFlow"] = true + return db.Flow{}, nil +} +func (m *mockService) DeleteFlow(ctx context.Context, id int) error { + m.called["DeleteFlow"] = true + return nil +} +func (m *mockService) CreateFlowStep(ctx context.Context, req createFlowStepRequest) (db.FlowStep, error) { + m.called["CreateFlowStep"] = true + return db.FlowStep{}, nil +} +func (m *mockService) DeleteFlowStep(ctx context.Context, id int) error { + m.called["DeleteFlowStep"] = true + return nil +} +func (m *mockService) GetFlowSteps(ctx context.Context, id int) ([]db.FlowStep, error) { + m.called["GetFlowSteps"] = true + return nil, nil +} +func (m *mockService) GetFlowPath(ctx context.Context, id int) (FlowPath, error) { + m.called["GetFlowPath"] = true + return FlowPath{}, nil +} + +func TestRegisterRoutesWithHandler(t *testing.T) { + h := newHandler(newMockService()) + + r := chi.NewRouter() + registerRoutesWithHandler(r, h) + + got := map[string]bool{} + + err := chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + got[method+" "+route] = true + return nil + }) + if err != nil { + t.Fatal(err) + } + + want := []string{ + "POST /flows/{id}/steps", + "GET /flows/{id}/steps", + "GET /flows/{id}/path", + "GET /flows/{id}/", + "PUT /flows/{id}/", + "DELETE /flows/{id}/", + "DELETE /flow-steps/{id}", + "POST /products/{id}/flows", + "GET /products/{id}/flows", + } + + for _, route := range want { + if !got[route] { + t.Errorf("expected route %q to be registered", route) + } + } +} diff --git a/internal/flow/serivice_flow.go b/internal/flow/serivice_flow.go new file mode 100644 index 0000000..6889cd4 --- /dev/null +++ b/internal/flow/serivice_flow.go @@ -0,0 +1,82 @@ +package flow + +import ( + "context" + "errors" + "fmt" + "products/internal" + "products/internal/flow/db" + + "github.com/jackc/pgx/v5" +) + +func (s *postgresService) CreateFlow(ctx context.Context, req createFlowRequest, id int) (db.Flow, error) { + + flow, err := s.queries.CreateFlow(ctx, req.ToParams(id)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return db.Flow{}, internal.NewNotFoundError(id, "Product") + } + return db.Flow{}, fmt.Errorf("failed to create flow: %w", err) + } + return flow, nil +} + +func (s *postgresService) GetFlowById(ctx context.Context, id int) (db.Flow, error) { + flow, err := s.queries.GetFlow(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return db.Flow{}, internal.NewNotFoundError(id, "Flow") + } + return db.Flow{}, fmt.Errorf("failed to fetch flow: %w", err) + } + return flow, nil +} + +func (s *postgresService) GetFlowsByProduct(ctx context.Context, id int) ([]db.Flow, error) { + _, err := s.queries.GetProductById(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, internal.NewNotFoundError(id, "Product") + } + return nil, fmt.Errorf("failed to fetch flows: %w", err) + } + flows, err := s.queries.GetFlowsByProduct(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return []db.Flow{}, nil + } + return nil, fmt.Errorf("failed to fetch flows: %w", err) + } + if flows == nil { + flows = []db.Flow{} + } + return flows, nil +} + +func (s *postgresService) UpdateFlow(ctx context.Context, req updateFlowRequest, id int) (db.Flow, error) { + existing, err := s.GetFlowById(ctx, id) + if err != nil { + return db.Flow{}, err + } + + rowsAffected, err := s.queries.UpdateFlow(ctx, req.ToParams(id, existing)) + if err != nil { + return db.Flow{}, fmt.Errorf("failed to update flow: %w", err) + } + if rowsAffected == 0 { + return db.Flow{}, internal.NewNotFoundError(id, "Flow") + } + + return s.GetFlowById(ctx, id) +} +func (s *postgresService) DeleteFlow(ctx context.Context, id int) error { + rowsAffected, err := s.queries.DeleteFlow(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete flow: %w", err) + } + if rowsAffected == 0 { + return internal.NewNotFoundError(id, "Flow") + } + return nil +} diff --git a/internal/flow/service.go b/internal/flow/service.go index 6aedec3..d596fb4 100644 --- a/internal/flow/service.go +++ b/internal/flow/service.go @@ -3,38 +3,23 @@ package flow import ( "context" "encoding/json" - "errors" "fmt" "io" "log/slog" "net/http" "os" - "products/internal" "products/internal/flow/db" "strings" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" + "time" ) -type flow interface { - CreateFlow(ctx context.Context, req createFlowRequest, id int) (db.Flow, error) - GetFlowById(ctx context.Context, id int) (db.Flow, error) - GetFlowsByProduct(ctx context.Context, id int) ([]db.Flow, error) - UpdateFlow(ctx context.Context, req updateFlowRequest, id int) (db.Flow, error) - DeleteFlow(ctx context.Context, id int) error -} - -type flowStep interface { - CreateFlowStep(ctx context.Context, req createFlowStepRequest) (db.FlowStep, error) - DeleteFlowStep(ctx context.Context, id int) error - GetFlowSteps(ctx context.Context, id int) ([]db.FlowStep, error) - GetFlowPath(ctx context.Context, id int) (FlowPath, error) -} - -type flowService interface { - flow - flowStep +func newPostgresService(dbConn db.DBTX) *postgresService { + queries := db.New(dbConn) + client := &http.Client{Timeout: 5 * time.Second} + return &postgresService{ + queries: queries, + client: client, + } } type postgresService struct { @@ -83,172 +68,3 @@ func (s *postgresService) validateDependency(ctx context.Context, current, next return false, nil } - -func (s *postgresService) CreateFlow(ctx context.Context, req createFlowRequest, id int) (db.Flow, error) { - - flow, err := s.queries.CreateFlow(ctx, req.ToParams(id)) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return db.Flow{}, internal.NewNotFoundError(id, "Product") - } - return db.Flow{}, fmt.Errorf("failed to create flow: %w", err) - } - return flow, nil -} - -func (s *postgresService) GetFlowById(ctx context.Context, id int) (db.Flow, error) { - flow, err := s.queries.GetFlow(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return db.Flow{}, internal.NewNotFoundError(id, "Flow") - } - return db.Flow{}, fmt.Errorf("failed to fetch flow: %w", err) - } - return flow, nil -} - -func (s *postgresService) GetFlowsByProduct(ctx context.Context, id int) ([]db.Flow, error) { - _, err := s.queries.GetProductById(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, internal.NewNotFoundError(id, "Product") - } - return nil, fmt.Errorf("failed to fetch flows: %w", err) - } - flows, err := s.queries.GetFlowsByProduct(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return []db.Flow{}, nil - } - return nil, fmt.Errorf("failed to fetch flows: %w", err) - } - if flows == nil { - flows = []db.Flow{} - } - return flows, nil -} - -func (s *postgresService) UpdateFlow(ctx context.Context, req updateFlowRequest, id int) (db.Flow, error) { - existing, err := s.GetFlowById(ctx, id) - if err != nil { - return db.Flow{}, err - } - - rowsAffected, err := s.queries.UpdateFlow(ctx, req.ToParams(id, existing)) - if err != nil { - return db.Flow{}, fmt.Errorf("failed to update flow: %w", err) - } - if rowsAffected == 0 { - return db.Flow{}, internal.NewNotFoundError(id, "Flow") - } - - return s.GetFlowById(ctx, id) -} -func (s *postgresService) DeleteFlow(ctx context.Context, id int) error { - rowsAffected, err := s.queries.DeleteFlow(ctx, id) - if err != nil { - return fmt.Errorf("failed to delete flow: %w", err) - } - if rowsAffected == 0 { - return internal.NewNotFoundError(id, "Flow") - } - return nil -} - -func (s *postgresService) CreateFlowStep(ctx context.Context, req createFlowStepRequest) (db.FlowStep, error) { - _, err := s.GetFlowById(ctx, req.FlowId) - if err != nil { - return db.FlowStep{}, err - } - params, err := req.ToParams() - if err != nil { - return db.FlowStep{}, err - } - - ok, err := s.validateDependency(ctx, req.Current, req.Next) - if err != nil { - return db.FlowStep{}, err - } - if !ok { - return db.FlowStep{}, DependencyValidationError{} - } - - flowStep, err := s.queries.CreateFlowStep(ctx, params) - if err != nil { - if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok && pgErr.Code == "23505" { - return db.FlowStep{}, ConflictError{Message: "Flow step already exists"} - } - return db.FlowStep{}, fmt.Errorf("failed to create flow step: %w", err) - } - return flowStep, nil -} - -func (s *postgresService) DeleteFlowStep(ctx context.Context, id int) error { - rowsAffected, err := s.queries.DeleteFlowStep(ctx, id) - if err != nil { - return fmt.Errorf("failed to delete flow step: %w", err) - } - if rowsAffected == 0 { - return internal.NewNotFoundError(id, "FlowStep") - } - return nil -} - -func (s *postgresService) GetFlowSteps(ctx context.Context, id int) ([]db.FlowStep, error) { - _, err := s.GetFlowById(ctx, id) - if err != nil { - return nil, err - } - flowSteps, err := s.queries.GetFlowSteps(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return []db.FlowStep{}, nil - } - return nil, err - } - if flowSteps == nil { - flowSteps = []db.FlowStep{} - } - return flowSteps, nil -} - -func (s *postgresService) GetFlowPath(ctx context.Context, id int) (FlowPath, error) { - flowSteps, err := s.GetFlowSteps(ctx, id) - if err != nil { - return FlowPath{}, err - } - pathMap := make(map[string][]string) - nextSet := make(map[string]bool) - for _, step := range flowSteps { - current, next := step.Current.String(), step.Next.String() - if _, ok := pathMap[current]; !ok { - pathMap[current] = []string{} - } - pathMap[current] = append(pathMap[current], next) - nextSet[next] = true - } - var queue []string - for k := range pathMap { - if _, ok := nextSet[k]; !ok { - queue = append(queue, k) - } - } - - if len(queue) == 0 && len(pathMap) > 0 { - return FlowPath{}, fmt.Errorf("no entry point found in flow") - } - if len(queue) > 1 { - return FlowPath{}, fmt.Errorf("multiple entry points found in flow") - } - var path []PathItem - for len(queue) > 0 { - current := queue[0] - queue = queue[1:] - if nexts, ok := pathMap[current]; ok { - queue = append(queue, nexts...) - path = append(path, PathItem{Current: current, Next: nexts}) - } - } - - return FlowPath{FlowID: id, Path: path}, nil -} diff --git a/internal/flow/service_flow_steps.go b/internal/flow/service_flow_steps.go new file mode 100644 index 0000000..f53d232 --- /dev/null +++ b/internal/flow/service_flow_steps.go @@ -0,0 +1,110 @@ +package flow + +import ( + "context" + "errors" + "fmt" + "products/internal" + "products/internal/flow/db" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +func (s *postgresService) CreateFlowStep(ctx context.Context, req createFlowStepRequest) (db.FlowStep, error) { + _, err := s.GetFlowById(ctx, req.FlowId) + if err != nil { + return db.FlowStep{}, err + } + params, err := req.ToParams() + if err != nil { + return db.FlowStep{}, err + } + + ok, err := s.validateDependency(ctx, req.Current, req.Next) + if err != nil { + return db.FlowStep{}, err + } + if !ok { + return db.FlowStep{}, DependencyValidationError{} + } + + flowStep, err := s.queries.CreateFlowStep(ctx, params) + if err != nil { + if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok && pgErr.Code == "23505" { + return db.FlowStep{}, ConflictError{Message: "Flow step already exists"} + } + return db.FlowStep{}, fmt.Errorf("failed to create flow step: %w", err) + } + return flowStep, nil +} + +func (s *postgresService) DeleteFlowStep(ctx context.Context, id int) error { + rowsAffected, err := s.queries.DeleteFlowStep(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete flow step: %w", err) + } + if rowsAffected == 0 { + return internal.NewNotFoundError(id, "FlowStep") + } + return nil +} + +func (s *postgresService) GetFlowSteps(ctx context.Context, id int) ([]db.FlowStep, error) { + _, err := s.GetFlowById(ctx, id) + if err != nil { + return nil, err + } + flowSteps, err := s.queries.GetFlowSteps(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return []db.FlowStep{}, nil + } + return nil, err + } + if flowSteps == nil { + flowSteps = []db.FlowStep{} + } + return flowSteps, nil +} + +func (s *postgresService) GetFlowPath(ctx context.Context, id int) (FlowPath, error) { + flowSteps, err := s.GetFlowSteps(ctx, id) + if err != nil { + return FlowPath{}, err + } + pathMap := make(map[string][]string) + nextSet := make(map[string]bool) + for _, step := range flowSteps { + current, next := step.Current.String(), step.Next.String() + if _, ok := pathMap[current]; !ok { + pathMap[current] = []string{} + } + pathMap[current] = append(pathMap[current], next) + nextSet[next] = true + } + var queue []string + for k := range pathMap { + if _, ok := nextSet[k]; !ok { + queue = append(queue, k) + } + } + + if len(queue) == 0 && len(pathMap) > 0 { + return FlowPath{}, fmt.Errorf("no entry point found in flow") + } + if len(queue) > 1 { + return FlowPath{}, fmt.Errorf("multiple entry points found in flow") + } + var path []PathItem + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + if nexts, ok := pathMap[current]; ok { + queue = append(queue, nexts...) + path = append(path, PathItem{Current: current, Next: nexts}) + } + } + + return FlowPath{FlowID: id, Path: path}, nil +} diff --git a/internal/platform/handler.go b/internal/platform/handler.go index f5d396e..e79ff50 100644 --- a/internal/platform/handler.go +++ b/internal/platform/handler.go @@ -10,26 +10,17 @@ import ( "time" ) -type Handler interface { - CreatePlatform(w http.ResponseWriter, r *http.Request) - GetPlatforms(w http.ResponseWriter, r *http.Request) - GetPlatform(w http.ResponseWriter, r *http.Request) - UpdatePlatform(w http.ResponseWriter, r *http.Request) - DeletePlatform(w http.ResponseWriter, r *http.Request) -} - -func NewPlatformHandler(dbConn db.DBTX) Handler { - queries := db.New(dbConn) - return &platformHandler{ - service: postgresService{db: queries}, +func newHandler(svc platformService) *handler { + return &handler{ + service: svc, } } -type platformHandler struct { +type handler struct { service platformService } -func (h *platformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) { +func (h *handler) CreatePlatform(w http.ResponseWriter, r *http.Request) { var req createPlatformRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid request body"}, http.StatusBadRequest) @@ -51,7 +42,7 @@ func (h *platformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) internal.WriteJSONResponse(w, r, http.StatusCreated, platform) } -func (h *platformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) { +func (h *handler) UpdatePlatform(w http.ResponseWriter, r *http.Request) { var req updatePlatformRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid request body"}, http.StatusBadRequest) @@ -87,7 +78,7 @@ func (h *platformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) w.WriteHeader(http.StatusNoContent) } -func (h *platformHandler) DeletePlatform(w http.ResponseWriter, r *http.Request) { +func (h *handler) DeletePlatform(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid platform ID"}, http.StatusBadRequest) @@ -110,7 +101,7 @@ func (h *platformHandler) DeletePlatform(w http.ResponseWriter, r *http.Request) w.WriteHeader(http.StatusNoContent) } -func (h *platformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetPlatforms(w http.ResponseWriter, r *http.Request) { contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() platforms, err := h.service.GetPlatforms(contextWithTimeOut) @@ -124,7 +115,7 @@ func (h *platformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { internal.WriteJSONResponse(w, r, http.StatusOK, platforms) } -func (h *platformHandler) GetPlatform(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetPlatform(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid platform ID"}, http.StatusBadRequest) diff --git a/internal/platform/handler_test.go b/internal/platform/handler_test.go index 0d9d698..266776f 100644 --- a/internal/platform/handler_test.go +++ b/internal/platform/handler_test.go @@ -106,7 +106,7 @@ func TestCreatePlatform(t *testing.T) { return db.Platform{}, tt.dbErr }, } - h := &platformHandler{service: mSvc} + h := &handler{service: mSvc} var body []byte if s, ok := tt.requestBody.(string); ok { @@ -164,7 +164,7 @@ func TestGetPlatforms(t *testing.T) { return tt.platforms, tt.dbErr }, } - h := &platformHandler{service: mSvc} + h := &handler{service: mSvc} req := httptest.NewRequest(http.MethodGet, "/api/platforms", nil) rr := httptest.NewRecorder() @@ -248,7 +248,7 @@ func TestGetPlatform(t *testing.T) { return tt.dbPlatform, tt.dbErr }, } - h := &platformHandler{service: mSvc} + h := &handler{service: mSvc} req := httptest.NewRequest(http.MethodGet, "/api/platforms/"+tt.id, nil) req.SetPathValue("id", tt.id) @@ -335,7 +335,7 @@ func TestDeletePlatform(t *testing.T) { return id, tt.dbErr }, } - h := &platformHandler{service: mSvc} + h := &handler{service: mSvc} req := httptest.NewRequest(http.MethodDelete, "/api/platforms/"+tt.id, nil) req.SetPathValue("id", tt.id) @@ -491,7 +491,7 @@ func TestUpdatePlatform(t *testing.T) { return id, tt.dbErr }, } - h := &platformHandler{service: mSvc} + h := &handler{service: mSvc} var body []byte if s, ok := tt.requestBody.(string); ok { diff --git a/internal/platform/router.go b/internal/platform/router.go new file mode 100644 index 0000000..d07dd9f --- /dev/null +++ b/internal/platform/router.go @@ -0,0 +1,34 @@ +package platform + +import ( + "context" + "products/internal/platform/db" + + "github.com/go-chi/chi/v5" +) + +type platformService interface { + CreatePlatform(ctx context.Context, req createPlatformRequest) (db.Platform, error) + GetPlatform(ctx context.Context, id int) (db.Platform, error) + GetPlatforms(ctx context.Context) ([]db.Platform, error) + UpdatePlatform(ctx context.Context, req updatePlatformRequest, id int) (int, error) + DeletePlatform(ctx context.Context, id int) (int, error) +} + +func RegisterRoutes(r chi.Router, dbConn db.DBTX) { + s := newPostgresService(dbConn) + h := newHandler(s) + registerRoutesWithHandler(r, h) +} + +func registerRoutesWithHandler(r chi.Router, platformHandler *handler) { + r.Route("/platforms", func(u chi.Router) { + u.Post("/", platformHandler.CreatePlatform) + u.Get("/", platformHandler.GetPlatforms) + u.Route("/{id}", func(u chi.Router) { + u.Get("/", platformHandler.GetPlatform) + u.Put("/", platformHandler.UpdatePlatform) + u.Delete("/", platformHandler.DeletePlatform) + }) + }) +} diff --git a/internal/platform/router_test.go b/internal/platform/router_test.go new file mode 100644 index 0000000..fe58d16 --- /dev/null +++ b/internal/platform/router_test.go @@ -0,0 +1,39 @@ +package platform + +import ( + "net/http" + "testing" + + "github.com/go-chi/chi/v5" +) + +func TestRegisterRoutes(t *testing.T) { + h := newHandler(&mockPlatformService{}) + + r := chi.NewRouter() + registerRoutesWithHandler(r, h) + + got := map[string]bool{} + + err := chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + got[method+" "+route] = true + return nil + }) + if err != nil { + t.Fatal(err) + } + + want := []string{ + "POST /platforms/", + "GET /platforms/", + "GET /platforms/{id}/", + "PUT /platforms/{id}/", + "DELETE /platforms/{id}/", + } + + for _, route := range want { + if !got[route] { + t.Errorf("expected route %q to be registered", route) + } + } +} diff --git a/internal/platform/service.go b/internal/platform/service.go index 32649b1..ca36332 100644 --- a/internal/platform/service.go +++ b/internal/platform/service.go @@ -9,12 +9,9 @@ import ( "github.com/jackc/pgx/v5" ) -type platformService interface { - CreatePlatform(ctx context.Context, req createPlatformRequest) (db.Platform, error) - GetPlatform(ctx context.Context, id int) (db.Platform, error) - GetPlatforms(ctx context.Context) ([]db.Platform, error) - UpdatePlatform(ctx context.Context, req updatePlatformRequest, id int) (int, error) - DeletePlatform(ctx context.Context, id int) (int, error) +func newPostgresService(dbConn db.DBTX) platformService { + queries := db.New(dbConn) + return &postgresService{queries} } type postgresService struct { diff --git a/internal/product/handler.go b/internal/product/handler.go index 1c2799f..b97e9c7 100644 --- a/internal/product/handler.go +++ b/internal/product/handler.go @@ -11,26 +11,18 @@ import ( "time" ) -type Handler interface { - CreateProduct(w http.ResponseWriter, r *http.Request) - GetProductsByPlatform(w http.ResponseWriter, r *http.Request) - GetProductById(w http.ResponseWriter, r *http.Request) - UpdateProduct(w http.ResponseWriter, r *http.Request) - DeleteProduct(w http.ResponseWriter, r *http.Request) -} +func newHandler(svc productService) *handler { -func NewProductHandler(dbConn db.DBTX) Handler { - queries := db.New(dbConn) - return &productHandler{ - service: &postgresService{queries: queries}, + return &handler{ + service: svc, } } -type productHandler struct { +type handler struct { service productService } -func (h *productHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { +func (h *handler) CreateProduct(w http.ResponseWriter, r *http.Request) { var req createProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid request body"}, http.StatusBadRequest) @@ -53,7 +45,7 @@ func (h *productHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { internal.WriteJSONResponse(w, r, http.StatusCreated, newProduct) } -func (h *productHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) { +func (h *handler) DeleteProduct(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid product ID"}, http.StatusBadRequest) @@ -72,7 +64,7 @@ func (h *productHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func (h *productHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { +func (h *handler) UpdateProduct(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid product ID"}, http.StatusBadRequest) @@ -107,7 +99,7 @@ func (h *productHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { } // GetProductsByPlatform fetches products by platform ID. -func (h *productHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetProductsByPlatform(w http.ResponseWriter, r *http.Request) { platformID, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid platform ID"}, http.StatusBadRequest) @@ -129,7 +121,7 @@ func (h *productHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Re internal.WriteJSONResponse(w, r, http.StatusOK, products) } -func (h *productHandler) GetProductById(w http.ResponseWriter, r *http.Request) { +func (h *handler) GetProductById(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid product ID"}, http.StatusBadRequest) diff --git a/internal/product/handler_test.go b/internal/product/handler_test.go index c0756d9..eb79d2b 100644 --- a/internal/product/handler_test.go +++ b/internal/product/handler_test.go @@ -117,7 +117,7 @@ func TestCreateProduct(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockProductService{} tt.mockSetup(mock) - h := &productHandler{service: mock} + h := &handler{service: mock} var body []byte if s, ok := tt.requestBody.(string); ok { @@ -224,7 +224,7 @@ func TestDeleteProduct(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockProductService{} tt.mockSetup(mock) - h := &productHandler{service: mock} + h := &handler{service: mock} req := httptest.NewRequest(http.MethodDelete, "/api/products/"+tt.id, nil) req.SetPathValue("id", tt.id) @@ -297,7 +297,7 @@ func TestGetProductsByPlatform(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockProductService{} tt.mockSetup(mock) - h := &productHandler{service: mock} + h := &handler{service: mock} req := httptest.NewRequest(http.MethodGet, "/api/platforms/"+tt.platformID+"/products", nil) req.SetPathValue("id", tt.platformID) @@ -377,7 +377,7 @@ func TestGetProductById(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockProductService{} tt.mockSetup(mock) - h := &productHandler{service: mock} + h := &handler{service: mock} req := httptest.NewRequest(http.MethodGet, "/api/products/"+tt.id, nil) req.SetPathValue("id", tt.id) @@ -496,7 +496,7 @@ func TestUpdateProduct(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockProductService{} tt.mockSetup(mock) - h := &productHandler{service: mock} + h := &handler{service: mock} var body []byte if s, ok := tt.requestBody.(string); ok { diff --git a/internal/product/router.go b/internal/product/router.go new file mode 100644 index 0000000..b296491 --- /dev/null +++ b/internal/product/router.go @@ -0,0 +1,34 @@ +package product + +import ( + "context" + "products/internal/product/db" + + "github.com/go-chi/chi/v5" +) + +type productService interface { + CreateProduct(ctx context.Context, req createProductRequest) (db.Product, error) + GetProductsByPlatform(ctx context.Context, platformID int) ([]db.Product, error) + GetProductById(ctx context.Context, id int) (db.Product, error) + UpdateProduct(ctx context.Context, req updateProductRequest, id int) (int, error) + DeleteProduct(ctx context.Context, id int) (int, error) +} + +func RegisterRoutes(r chi.Router, dbConn db.DBTX) { + s := newPostgresService(dbConn) + h := newHandler(s) + registerRoutesWithHandler(r, h) +} + +func registerRoutesWithHandler(r chi.Router, prodHandler *handler) { + r.Get("/platforms/{id}/products", prodHandler.GetProductsByPlatform) + r.Route("/products", func(u chi.Router) { + u.Post("/", prodHandler.CreateProduct) + u.Route("/{id}", func(u chi.Router) { + u.Get("/", prodHandler.GetProductById) + u.Put("/", prodHandler.UpdateProduct) + u.Delete("/", prodHandler.DeleteProduct) + }) + }) +} diff --git a/internal/product/router_test.go b/internal/product/router_test.go new file mode 100644 index 0000000..8b54034 --- /dev/null +++ b/internal/product/router_test.go @@ -0,0 +1,39 @@ +package product + +import ( + "net/http" + "testing" + + "github.com/go-chi/chi/v5" +) + +func TestRegisterRoutes(t *testing.T) { + h := newHandler(&mockProductService{}) + + r := chi.NewRouter() + registerRoutesWithHandler(r, h) + + got := map[string]bool{} + + err := chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + got[method+" "+route] = true + return nil + }) + if err != nil { + t.Fatal(err) + } + + want := []string{ + "GET /platforms/{id}/products", + "POST /products/", + "GET /products/{id}/", + "PUT /products/{id}/", + "DELETE /products/{id}/", + } + + for _, route := range want { + if !got[route] { + t.Errorf("expected route %q to be registered", route) + } + } +} diff --git a/internal/product/service.go b/internal/product/service.go index 7f5531e..0d13780 100644 --- a/internal/product/service.go +++ b/internal/product/service.go @@ -9,12 +9,11 @@ import ( "github.com/jackc/pgx/v5" ) -type productService interface { - CreateProduct(ctx context.Context, req createProductRequest) (db.Product, error) - GetProductsByPlatform(ctx context.Context, platformID int) ([]db.Product, error) - GetProductById(ctx context.Context, id int) (db.Product, error) - UpdateProduct(ctx context.Context, req updateProductRequest, id int) (int, error) - DeleteProduct(ctx context.Context, id int) (int, error) +func newPostgresService(dbConn db.DBTX) productService { + queries := db.New(dbConn) + return &postgresService{ + queries: queries, + } } type postgresService struct { diff --git a/main.go b/main.go index ba870e8..a8f34f7 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,7 @@ func main() { } defer dbConn.Close() - r := router.SetupRouter(dbConn) + r := router.InitializeRouter(dbConn) addr := internalConfig.GetConfigValue("ADDRESS") srv := &http.Server{ diff --git a/router/router.go b/router/router.go index 4d030e8..e48c3ef 100644 --- a/router/router.go +++ b/router/router.go @@ -16,60 +16,7 @@ import ( "github.com/go-chi/cors" ) -type productRoutes struct { - productHandler product.Handler - platformHandler platform.Handler - flowHandler flow.Handler - capabilityHandler capability.Handler -} - -func (h *productRoutes) setupRoutes(router *chi.Mux) { - router.Route("/platforms", func(u chi.Router) { - u.Post("/", h.platformHandler.CreatePlatform) - u.Get("/", h.platformHandler.GetPlatforms) - u.Route("/{id}", func(u chi.Router) { - u.Get("/", h.platformHandler.GetPlatform) - u.Delete("/", h.platformHandler.DeletePlatform) - u.Put("/", h.platformHandler.UpdatePlatform) - u.Get("/products", h.productHandler.GetProductsByPlatform) - }) - - }) - router.Route("/products", func(u chi.Router) { - u.Post("/", h.productHandler.CreateProduct) - u.Route("/{id}", func(u chi.Router) { - u.Get("/", h.productHandler.GetProductById) - u.Delete("/", h.productHandler.DeleteProduct) - u.Put("/", h.productHandler.UpdateProduct) - u.Post("/flows", h.flowHandler.CreateFlow) - u.Get("/flows", h.flowHandler.GetFlowsByProduct) - u.Get("/capabilities", h.capabilityHandler.GetCapabilitiesByProduct) - }) - - }) - router.Route("/flows", func(u chi.Router) { - u.Route("/{id}", func(u chi.Router) { - u.Post("/steps", h.flowHandler.CreateFlowStep) - u.Get("/steps", h.flowHandler.GetFlowSteps) - u.Get("/path", h.flowHandler.GetFlowPath) - u.Get("/", h.flowHandler.GetFlowById) - u.Put("/", h.flowHandler.UpdateFlow) - u.Delete("/", h.flowHandler.DeleteFlow) - u.Get("/capabilities", h.capabilityHandler.GetCapabilitiesByFlow) - }) - }) - - router.Route("/flow-steps/{id}", func(u chi.Router) { - u.Delete("/", h.flowHandler.DeleteFlowStep) - }) - - router.Route("/capabilities", func(u chi.Router) { - u.Post("/", h.capabilityHandler.CreateCapability) - u.Get("/{id}", h.capabilityHandler.GetCapability) - }) -} - -func SetupRouter(dbConn db.DBTX) http.Handler { +func InitializeRouter(dbConn db.DBTX) http.Handler { slog.Debug("Setting up router") router := chi.NewRouter() @@ -86,13 +33,10 @@ func SetupRouter(dbConn db.DBTX) http.Handler { })) registerSystemCallHandler(router) - prodRouter := &productRoutes{ - productHandler: product.NewProductHandler(dbConn), - flowHandler: flow.NewHandler(dbConn), - platformHandler: platform.NewPlatformHandler(dbConn), - capabilityHandler: capability.NewHandler(dbConn), - } - prodRouter.setupRoutes(router) + platform.RegisterRoutes(router, dbConn) + product.RegisterRoutes(router, dbConn) + flow.RegisterRoutes(router, dbConn) + capability.RegisterRoutes(router, dbConn) slog.Debug("Router setup complete") return router diff --git a/router/router_test.go b/router/router_test.go index a3c18a3..2d5b9ae 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -1,180 +1,91 @@ package router import ( + "context" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" ) -// Mock Platform Handler -type mockPlatformHandler struct { - called map[string]bool -} - -func newMockPlatformHandler() *mockPlatformHandler { - return &mockPlatformHandler{called: make(map[string]bool)} -} - -func (m *mockPlatformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) { - m.called["CreatePlatform"] = true -} -func (m *mockPlatformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { - m.called["GetPlatforms"] = true -} -func (m *mockPlatformHandler) GetPlatform(w http.ResponseWriter, r *http.Request) { - m.called["GetPlatform"] = true -} -func (m *mockPlatformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) { - m.called["UpdatePlatform"] = true -} -func (m *mockPlatformHandler) DeletePlatform(w http.ResponseWriter, r *http.Request) { - m.called["DeletePlatform"] = true -} - -// Mock Product Handler -type mockProductHandler struct { - called map[string]bool -} - -func newMockProductHandler() *mockProductHandler { - return &mockProductHandler{called: make(map[string]bool)} -} - -func (m *mockProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { - m.called["CreateProduct"] = true -} -func (m *mockProductHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Request) { - m.called["GetProductsByPlatform"] = true -} -func (m *mockProductHandler) GetProductById(w http.ResponseWriter, r *http.Request) { - m.called["GetProductById"] = true -} -func (m *mockProductHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { - m.called["UpdateProduct"] = true -} -func (m *mockProductHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) { - m.called["DeleteProduct"] = true -} - -// Mock Flow Handler -type mockFlowHandler struct { - called map[string]bool -} +type mockDBTX struct{} -func newMockFlowHandler() *mockFlowHandler { - return &mockFlowHandler{called: make(map[string]bool)} +func (m *mockDBTX) Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error) { + return pgconn.CommandTag{}, nil } - -func (m *mockFlowHandler) CreateFlow(w http.ResponseWriter, r *http.Request) { - m.called["CreateFlow"] = true -} -func (m *mockFlowHandler) GetFlowById(w http.ResponseWriter, r *http.Request) { - m.called["GetFlowById"] = true -} -func (m *mockFlowHandler) GetFlowsByProduct(w http.ResponseWriter, r *http.Request) { - m.called["GetFlowsByProduct"] = true -} -func (m *mockFlowHandler) UpdateFlow(w http.ResponseWriter, r *http.Request) { - m.called["UpdateFlow"] = true -} -func (m *mockFlowHandler) DeleteFlow(w http.ResponseWriter, r *http.Request) { - m.called["DeleteFlow"] = true -} -func (m *mockFlowHandler) CreateFlowStep(w http.ResponseWriter, r *http.Request) { - m.called["CreateFlowStep"] = true +func (m *mockDBTX) Query(ctx context.Context, sql string, arguments ...any) (pgx.Rows, error) { + return nil, nil } -func (m *mockFlowHandler) DeleteFlowStep(w http.ResponseWriter, r *http.Request) { - m.called["DeleteFlowStep"] = true -} -func (m *mockFlowHandler) GetFlowSteps(w http.ResponseWriter, r *http.Request) { - m.called["GetFlowSteps"] = true -} -func (m *mockFlowHandler) GetFlowPath(w http.ResponseWriter, r *http.Request) { - m.called["GetFlowPath"] = true +func (m *mockDBTX) QueryRow(ctx context.Context, sql string, arguments ...any) pgx.Row { + return nil } -// Mock Capability Handler -type mockCapabilityHandler struct { - called map[string]bool -} +func TestInitializeRouter(t *testing.T) { + r := InitializeRouter(&mockDBTX{}) -func newMockCapabilityHandler() *mockCapabilityHandler { - return &mockCapabilityHandler{called: make(map[string]bool)} -} + got := map[string]bool{} -func (m *mockCapabilityHandler) CreateCapability(w http.ResponseWriter, r *http.Request) { - m.called["CreateCapability"] = true -} -func (m *mockCapabilityHandler) GetCapability(w http.ResponseWriter, r *http.Request) { - m.called["GetCapability"] = true -} -func (m *mockCapabilityHandler) GetCapabilitiesByFlow(w http.ResponseWriter, r *http.Request) { - m.called["GetCapabilitiesByFlow"] = true -} -func (m *mockCapabilityHandler) GetCapabilitiesByProduct(w http.ResponseWriter, r *http.Request) { - m.called["GetCapabilitiesByProduct"] = true -} - -func TestProductRoutes_SetupRoutes(t *testing.T) { - mockPlatform := newMockPlatformHandler() - mockProduct := newMockProductHandler() - mockFlow := newMockFlowHandler() - mockCapability := newMockCapabilityHandler() - - pr := &productRoutes{ - platformHandler: mockPlatform, - productHandler: mockProduct, - flowHandler: mockFlow, - capabilityHandler: mockCapability, + err := chi.Walk(r.(*chi.Mux), func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + got[method+" "+route] = true + return nil + }) + if err != nil { + t.Fatal(err) } - r := chi.NewRouter() - pr.setupRoutes(r) - - tests := []struct { - method string - path string - handler string - mock map[string]bool - }{ - {"POST", "/platforms/", "CreatePlatform", mockPlatform.called}, - {"GET", "/platforms/", "GetPlatforms", mockPlatform.called}, - {"GET", "/platforms/1", "GetPlatform", mockPlatform.called}, - {"DELETE", "/platforms/1", "DeletePlatform", mockPlatform.called}, - {"PUT", "/platforms/1", "UpdatePlatform", mockPlatform.called}, - {"GET", "/platforms/1/products", "GetProductsByPlatform", mockProduct.called}, - - {"POST", "/products/", "CreateProduct", mockProduct.called}, - {"GET", "/products/1", "GetProductById", mockProduct.called}, - {"DELETE", "/products/1", "DeleteProduct", mockProduct.called}, - {"PUT", "/products/1", "UpdateProduct", mockProduct.called}, - {"POST", "/products/1/flows", "CreateFlow", mockFlow.called}, - {"GET", "/products/1/flows", "GetFlowsByProduct", mockFlow.called}, - {"GET", "/products/1/capabilities", "GetCapabilitiesByProduct", mockCapability.called}, + want := []string{ + "GET /api/time", + "GET /api/version", + "POST /platforms/", + "GET /platforms/", + "GET /platforms/{id}/", + "DELETE /platforms/{id}/", + "PUT /platforms/{id}/", + "POST /products/", + "GET /platforms/{id}/products", + "GET /products/{id}/", + "PUT /products/{id}/", + "DELETE /products/{id}/", + "POST /flows/{id}/steps", + "GET /flows/{id}/steps", + "GET /flows/{id}/path", + "GET /flows/{id}/", + "PUT /flows/{id}/", + "DELETE /flows/{id}/", + "DELETE /flow-steps/{id}", + "POST /products/{id}/flows", + "GET /products/{id}/flows", + "GET /products/{id}/capabilities", + "GET /flows/{id}/capabilities", + "POST /capabilities/", + "GET /capabilities/{id}", + } - {"POST", "/flows/1/steps", "CreateFlowStep", mockFlow.called}, - {"GET", "/flows/1/steps", "GetFlowSteps", mockFlow.called}, - {"GET", "/flows/1/path", "GetFlowPath", mockFlow.called}, - {"GET", "/flows/1", "GetFlowById", mockFlow.called}, - {"PUT", "/flows/1", "UpdateFlow", mockFlow.called}, - {"DELETE", "/flows/1", "DeleteFlow", mockFlow.called}, - {"DELETE", "/flow-steps/1", "DeleteFlowStep", mockFlow.called}, - {"GET", "/flows/1/capabilities", "GetCapabilitiesByFlow", mockCapability.called}, - {"POST", "/capabilities/", "CreateCapability", mockCapability.called}, - {"GET", "/capabilities/1", "GetCapability", mockCapability.called}, + for _, route := range want { + if !got[route] { + t.Errorf("expected route %q to be registered", route) + } } +} - for _, tt := range tests { - t.Run(tt.method+" "+tt.path, func(t *testing.T) { - req := httptest.NewRequest(tt.method, tt.path, nil) - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) +func TestInitializeRouter_NestedRoutesReachHandlers(t *testing.T) { + r := InitializeRouter(&mockDBTX{}) - if !tt.mock[tt.handler] { - t.Errorf("expected %s to be called for %s %s", tt.handler, tt.method, tt.path) - } - }) + cases := []struct{ method, path string }{ + {http.MethodGet, "/platforms/1/products"}, + {http.MethodGet, "/products/1/flows"}, + {http.MethodGet, "/flows/1/capabilities"}, + {http.MethodGet, "/products/1/capabilities"}, + } + for _, c := range cases { + req := httptest.NewRequest(c.method, c.path, nil) + rec := httptest.NewRecorder() + r.ServeHTTP(rec, req) + if rec.Code == http.StatusNotFound { + t.Errorf("%s %s: expected route to be reachable, got 404", c.method, c.path) + } } }