diff --git a/_bruno/Product/Create Product.yml b/_bruno/Product/Create Product.yml new file mode 100644 index 0000000..a1c9569 --- /dev/null +++ b/_bruno/Product/Create Product.yml @@ -0,0 +1,62 @@ +info: + name: Create Product + type: http + seq: 1 + +http: + method: POST + url: "{{baseUrl}}/api/products" + body: + type: json + data: |- + { + "platform_id": 3, + "name": "Product 1", + "description": "Test Product" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 + +examples: + - name: sample + request: + url: "{{baseUrl}}/api/products" + method: POST + body: + type: json + data: |- + { + "platform_id": 3, + "name": "Product 1", + "description": "Test Product" + } + response: + status: 201 + statusText: Created + headers: + - name: vary + value: Origin + - name: date + value: Mon, 04 May 2026 01:20:16 GMT + - name: content-length + value: "0" + body: + type: text + data: "" + +docs: |- + # Create Product + + Creates a new product + + ## Possible Responses + | Status Code | Notes | + | -- | --| + | 201 | Successfully Created | + | 400 | Name Missing or platform missing | + | 500 | Error | diff --git a/_bruno/Product/Delete Product.yml b/_bruno/Product/Delete Product.yml new file mode 100644 index 0000000..e809367 --- /dev/null +++ b/_bruno/Product/Delete Product.yml @@ -0,0 +1,52 @@ +info: + name: Delete Product + type: http + seq: 2 + +http: + method: DELETE + url: "{{baseUrl}}/api/products/:id" + params: + - name: id + value: "1" + type: path + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 + +examples: + - name: sample + request: + url: "{{baseUrl}}/api/products/:id" + method: DELETE + params: + - name: id + value: "1" + type: path + response: + status: 204 + statusText: No Content + headers: + - name: vary + value: Origin + - name: date + value: Mon, 04 May 2026 01:23:27 GMT + body: + type: text + data: "" + +docs: |- + # Delete Product + + Deletes a product. See sample response + + ## Possible Responses + | Status Code | Notes | + | -- | --| + | 204 | Success | + | 400 | Invalid Id | + | 500 | Error | diff --git a/_bruno/Product/folder.yml b/_bruno/Product/folder.yml new file mode 100644 index 0000000..434006f --- /dev/null +++ b/_bruno/Product/folder.yml @@ -0,0 +1,7 @@ +info: + name: Product + type: folder + seq: 2 + +request: + auth: inherit diff --git a/api/product/delete.go b/api/product/delete.go new file mode 100644 index 0000000..2dc5d49 --- /dev/null +++ b/api/product/delete.go @@ -0,0 +1,27 @@ +package productHandler + +import ( + "context" + "errors" + "net/http" + "products/internal" + "time" + + "github.com/jackc/pgx/v5" +) + +func (h *ProductHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) { + id, ok := internal.GetIntFromRequestPath("id", r) + if !ok { + http.Error(w, "Invalid product ID", http.StatusBadRequest) + return + } + contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + err := h.queries.DeleteProduct(contextWithTimeOut, id) + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + http.Error(w, "Failed to delete product", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/api/product/product_test.go b/api/product/product_test.go index c110489..a9896b1 100644 --- a/api/product/product_test.go +++ b/api/product/product_test.go @@ -9,6 +9,9 @@ import ( "net/http/httptest" "products/internal/db/product" "testing" + "time" + + "github.com/jackc/pgx/v5" ) type mockProductQuerier struct { @@ -130,3 +133,100 @@ func TestCreateProduct(t *testing.T) { }) } } + +func TestDeleteProduct(t *testing.T) { + tests := []struct { + name string + id string + mockSetup func(m *mockProductQuerier) + expectedStatus int + }{ + { + name: "Success", + id: "1", + mockSetup: func(m *mockProductQuerier) { + m.deleteProductFunc = func(ctx context.Context, id int32) error { + if id != 1 { + return errors.New("unexpected id") + } + return nil + } + }, + expectedStatus: http.StatusNoContent, + }, + { + name: "Invalid ID (Not numeric)", + id: "abc", + mockSetup: func(m *mockProductQuerier) {}, + expectedStatus: http.StatusBadRequest, + }, + { + name: "Invalid ID (Zero)", + id: "0", + mockSetup: func(m *mockProductQuerier) {}, + expectedStatus: http.StatusBadRequest, + }, + { + name: "Invalid ID (Negative)", + id: "-1", + mockSetup: func(m *mockProductQuerier) {}, + expectedStatus: http.StatusBadRequest, + }, + { + name: "DB Failure", + id: "1", + mockSetup: func(m *mockProductQuerier) { + m.deleteProductFunc = func(ctx context.Context, id int32) error { + return errors.New("db error") + } + }, + expectedStatus: http.StatusInternalServerError, + }, + { + name: "Timeout context set to ~5s", + id: "1", + mockSetup: func(m *mockProductQuerier) { + m.deleteProductFunc = func(ctx context.Context, id int32) error { + deadline, ok := ctx.Deadline() + if !ok { + return errors.New("deadline not set") + } + diff := time.Until(deadline) + if diff < 4900*time.Millisecond || diff > 5100*time.Millisecond { + return errors.New("deadline not approximately 5s") + } + return nil + } + }, + expectedStatus: http.StatusNoContent, + }, + { + name: "Idempotent non-existent delete", + id: "999", + mockSetup: func(m *mockProductQuerier) { + m.deleteProductFunc = func(ctx context.Context, id int32) error { + return pgx.ErrNoRows + } + }, + expectedStatus: http.StatusNoContent, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := &mockProductQuerier{} + tt.mockSetup(mock) + h := NewProductHandler(mock) + + req := httptest.NewRequest(http.MethodDelete, "/api/products/"+tt.id, nil) + req.SetPathValue("id", tt.id) + rr := httptest.NewRecorder() + + h.DeleteProduct(rr, req) + + if rr.Code != tt.expectedStatus { + t.Errorf("expected status %v, got %v", tt.expectedStatus, rr.Code) + } + }) + } +} diff --git a/justfile b/justfile index f7fa469..6e50ff6 100644 --- a/justfile +++ b/justfile @@ -57,4 +57,8 @@ stop: main: git switch main git fetch - git pull \ No newline at end of file + git pull + +# Lint code +lint: + golangci-lint run \ No newline at end of file diff --git a/router/router.go b/router/router.go index 336c344..a86d381 100644 --- a/router/router.go +++ b/router/router.go @@ -60,5 +60,6 @@ func registerProductCallHandler(q productDb.Querier, r *chi.Mux) { handler := productHandler.NewProductHandler(q) r.Route("/api/products", func(u chi.Router) { u.Post("/", handler.CreateProduct) + u.Delete("/{id}", handler.DeleteProduct) }) }