Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions _bruno/Product/Create Product.yml
Original file line number Diff line number Diff line change
@@ -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 |
52 changes: 52 additions & 0 deletions _bruno/Product/Delete Product.yml
Original file line number Diff line number Diff line change
@@ -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 |
7 changes: 7 additions & 0 deletions _bruno/Product/folder.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
info:
name: Product
type: folder
seq: 2

request:
auth: inherit
27 changes: 27 additions & 0 deletions api/product/delete.go
Original file line number Diff line number Diff line change
@@ -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)
}
100 changes: 100 additions & 0 deletions api/product/product_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"net/http/httptest"
"products/internal/db/product"
"testing"
"time"

"github.com/jackc/pgx/v5"
)

type mockProductQuerier struct {
Expand Down Expand Up @@ -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)
}
})
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
6 changes: 5 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ stop:
main:
git switch main
git fetch
git pull
git pull

# Lint code
lint:
golangci-lint run
1 change: 1 addition & 0 deletions router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
Loading