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
52 changes: 52 additions & 0 deletions api/product/create.go
Original file line number Diff line number Diff line change
@@ -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
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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)

}
6 changes: 6 additions & 0 deletions api/product/product.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
132 changes: 132 additions & 0 deletions api/product/product_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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)
}
})
}
}
8 changes: 3 additions & 5 deletions internal/db/product/products.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions internal/structured_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion queries/products.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
Expand All @@ -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)
})
}
Loading