diff --git a/api/product/create.go b/api/product/create.go new file mode 100644 index 0000000..fdd8388 --- /dev/null +++ b/api/product/create.go @@ -0,0 +1,52 @@ +package productHandler + +import ( + "context" + "encoding/json" + "net/http" + "products/internal/db/product" + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +type CreateProductRequest struct { + Name string `json:"name"` + PlatformID int32 `json:"platform_id"` + Description string `json:"description"` +} + +func (h *ProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { + var req CreateProductRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + if req.Name == "" || req.PlatformID == 0 { + http.Error(w, "Name and platform ID are required", http.StatusBadRequest) + return + } + + params := product.CreateProductParams{ + Name: req.Name, + PlatformID: req.PlatformID, + Description: pgtype.Text{ + Valid: req.Description != "", + String: req.Description, + }, + Timestamp: pgtype.Timestamptz{ + Valid: true, + Time: time.Now().UTC(), + }, + } + + contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + if err := h.queries.CreateProduct(contextWithTimeOut, params); err != nil { + http.Error(w, "Failed to create product", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + +} diff --git a/api/product/product.go b/api/product/product.go index e987bb7..42cc8dc 100644 --- a/api/product/product.go +++ b/api/product/product.go @@ -5,3 +5,9 @@ import "products/internal/db/product" type ProductHandler struct { queries product.Querier } + +func NewProductHandler(q product.Querier) *ProductHandler { + return &ProductHandler{ + queries: q, + } +} diff --git a/api/product/product_test.go b/api/product/product_test.go new file mode 100644 index 0000000..c110489 --- /dev/null +++ b/api/product/product_test.go @@ -0,0 +1,132 @@ +package productHandler + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "products/internal/db/product" + "testing" +) + +type mockProductQuerier struct { + createProductFunc func(ctx context.Context, arg product.CreateProductParams) error + deleteProductFunc func(ctx context.Context, id int32) error + getProductByIdFunc func(ctx context.Context, id int32) (product.Product, error) + getProductsByPlatformFunc func(ctx context.Context, platformID int32) ([]product.Product, error) + updateProductFunc func(ctx context.Context, arg product.UpdateProductParams) error +} + +func (m *mockProductQuerier) CreateProduct(ctx context.Context, arg product.CreateProductParams) error { + return m.createProductFunc(ctx, arg) +} + +func (m *mockProductQuerier) DeleteProduct(ctx context.Context, id int32) error { + return m.deleteProductFunc(ctx, id) +} + +func (m *mockProductQuerier) GetProductById(ctx context.Context, id int32) (product.Product, error) { + return m.getProductByIdFunc(ctx, id) +} + +func (m *mockProductQuerier) GetProductsByPlatform(ctx context.Context, platformID int32) ([]product.Product, error) { + return m.getProductsByPlatformFunc(ctx, platformID) +} + +func (m *mockProductQuerier) UpdateProduct(ctx context.Context, arg product.UpdateProductParams) error { + return m.updateProductFunc(ctx, arg) +} + +func TestCreateProduct(t *testing.T) { + tests := []struct { + name string + requestBody any + mockSetup func(m *mockProductQuerier) + expectedStatus int + }{ + { + name: "Success", + requestBody: CreateProductRequest{ + PlatformID: 1, + Name: "Test Product", + }, + mockSetup: func(m *mockProductQuerier) { + m.createProductFunc = func(ctx context.Context, arg product.CreateProductParams) error { + if arg.Name != "Test Product" { + return errors.New("unexpected name") + } + if arg.PlatformID != 1 { + return errors.New("unexpected platform id") + } + return nil + } + }, + expectedStatus: http.StatusCreated, + }, + { + name: "Invalid JSON", + requestBody: "invalid json", + mockSetup: func(m *mockProductQuerier) {}, + expectedStatus: http.StatusBadRequest, + }, + { + name: "DB Failure", + requestBody: CreateProductRequest{ + PlatformID: 1, + Name: "Fail Product", + }, + mockSetup: func(m *mockProductQuerier) { + m.createProductFunc = func(ctx context.Context, arg product.CreateProductParams) error { + return errors.New("db error") + } + }, + expectedStatus: http.StatusInternalServerError, + }, + { + name: "Missing Name", + requestBody: CreateProductRequest{ + PlatformID: 1, + }, + mockSetup: func(m *mockProductQuerier) {}, + expectedStatus: http.StatusBadRequest, + }, + { + name: "Missing PlatformID", + requestBody: CreateProductRequest{ + Name: "Test Product", + }, + mockSetup: func(m *mockProductQuerier) {}, + expectedStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := &mockProductQuerier{} + tt.mockSetup(mock) + h := NewProductHandler(mock) + + var body []byte + if s, ok := tt.requestBody.(string); ok { + body = []byte(s) + } else { + var err error + body, err = json.Marshal(tt.requestBody) + if err != nil { + t.Fatalf("json.Marshal requestBody failed: %v", err) + } + } + + req := httptest.NewRequest(http.MethodPost, "/products", bytes.NewBuffer(body)) + rr := httptest.NewRecorder() + + h.CreateProduct(rr, req) + + if rr.Code != tt.expectedStatus { + t.Errorf("expected status %v, got %v", tt.expectedStatus, rr.Code) + } + }) + } +} diff --git a/internal/db/product/products.sql.go b/internal/db/product/products.sql.go index 6a0dda0..690e92d 100644 --- a/internal/db/product/products.sql.go +++ b/internal/db/product/products.sql.go @@ -12,15 +12,14 @@ import ( ) const createProduct = `-- name: CreateProduct :exec -INSERT INTO products (platform_id, name, description, created_at, updated_at) VALUES ($1, $2, $3, $4, $5) +INSERT INTO products (platform_id, name, description, created_at, updated_at) VALUES ($1, $2, $3, $4, $4) ` type CreateProductParams struct { PlatformID int32 Name string Description pgtype.Text - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + Timestamp pgtype.Timestamptz } func (q *Queries) CreateProduct(ctx context.Context, arg CreateProductParams) error { @@ -28,8 +27,7 @@ func (q *Queries) CreateProduct(ctx context.Context, arg CreateProductParams) er arg.PlatformID, arg.Name, arg.Description, - arg.CreatedAt, - arg.UpdatedAt, + arg.Timestamp, ) return err } diff --git a/internal/structured_logger_test.go b/internal/structured_logger_test.go index cd3c9d3..9ac9ffd 100644 --- a/internal/structured_logger_test.go +++ b/internal/structured_logger_test.go @@ -20,7 +20,7 @@ func TestStructuredLogger(t *testing.T) { // Create a test HTTP handler that returns a specific status code testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) - w.Write([]byte("test response")) + _, _ = w.Write([]byte("test response")) }) // Wrap our test handler with the StructuredLogger middleware @@ -153,7 +153,7 @@ func TestStructuredLoggerWithDefaultStatus(t *testing.T) { // Create a test HTTP handler that doesn't explicitly set a status code testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("default status test")) + _, _ = w.Write([]byte("default status test")) }) // Wrap our test handler with the StructuredLogger middleware diff --git a/queries/products.sql b/queries/products.sql index 0e2f15b..62fb97e 100644 --- a/queries/products.sql +++ b/queries/products.sql @@ -5,7 +5,7 @@ SELECT id, platform_id, name, description, created_at, updated_at FROM products SELECT id, platform_id, name, description, created_at, updated_at FROM products WHERE id = @id; -- name: CreateProduct :exec -INSERT INTO products (platform_id, name, description, created_at, updated_at) VALUES (@platform_id, @name, @description, @created_at, @updated_at); +INSERT INTO products (platform_id, name, description, created_at, updated_at) VALUES (@platform_id, @name, @description, @timestamp, @timestamp); -- name: UpdateProduct :exec UPDATE products SET name = @name, description = @description, updated_at = @updated_at WHERE id = @id RETURNING id; diff --git a/router/router.go b/router/router.go index d1463b7..336c344 100644 --- a/router/router.go +++ b/router/router.go @@ -4,10 +4,12 @@ import ( "log/slog" "net/http" platformHandler "products/api/platform" + productHandler "products/api/product" systemHandler "products/api/system" "products/internal" "products/internal/db" platformDb "products/internal/db/platform" + productDb "products/internal/db/product" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -33,6 +35,7 @@ func SetupRouter(dbConn db.DBTX) http.Handler { registerSystemCallHandler(router) registerPlatformCallHandler(store.Platform, router) + registerProductCallHandler(store.Product, router) slog.Debug("Router setup complete") return router } @@ -52,3 +55,10 @@ func registerPlatformCallHandler(q platformDb.Querier, r *chi.Mux) { u.Put("/{id}", handler.UpdatePlatform) }) } + +func registerProductCallHandler(q productDb.Querier, r *chi.Mux) { + handler := productHandler.NewProductHandler(q) + r.Route("/api/products", func(u chi.Router) { + u.Post("/", handler.CreateProduct) + }) +}