From 1dd92734c214b3977e9d10651a60da3b12cf0e12 Mon Sep 17 00:00:00 2001 From: Maikel Veen Date: Sun, 8 Sep 2024 12:17:27 +0200 Subject: [PATCH] Implement get item endpoint (#25) * chore: npm audit fix * feat: Implement Get item end to end * refactor: Rename MediaType middleware to ContentType * feat: Add Accept middleware for checking the Accept header * feat: Add default error in ErrorHandler * test: Add test for ItemsGet * chore: Fix linting issues --- api/v1/item.go | 16 ++++ go.mod | 2 +- item/item.go | 18 ++-- item/repository.go | 6 +- item/service.go | 13 ++- lib/middleware/accept.go | 35 +++++++ lib/middleware/accept_test.go | 73 +++++++++++++++ .../{mediatype.go => content_type.go} | 4 +- ...mediatype_test.go => content_type_test.go} | 4 +- lib/resource/resource.go | 20 +++- lib/test/database.go | 4 +- server/handler/json.go | 18 ---- server/handler/serialize.go | 32 +++++++ server/handler/v1/error_handler.go | 13 ++- server/handler/v1/error_handler_test.go | 93 +++++++++++++++++++ server/handler/v1/errors.go | 17 ++++ server/handler/v1/errors_test.go | 48 ++++++++++ server/handler/v1/handler_v1.go | 5 + server/handler/v1/item.go | 20 +++- server/handler/v1/item_test.go | 74 +++++++++++---- server/server.go | 4 +- typespec/package-lock.json | 6 +- 22 files changed, 458 insertions(+), 67 deletions(-) create mode 100644 lib/middleware/accept.go create mode 100644 lib/middleware/accept_test.go rename lib/middleware/{mediatype.go => content_type.go} (82%) rename lib/middleware/{mediatype_test.go => content_type_test.go} (94%) delete mode 100644 server/handler/json.go create mode 100644 server/handler/serialize.go diff --git a/api/v1/item.go b/api/v1/item.go index b2f3517..b59f5c5 100644 --- a/api/v1/item.go +++ b/api/v1/item.go @@ -22,3 +22,19 @@ func (i *ItemCreate) ToItem() *item.Item { Metadata: i.Metadata, } } + +func FromItem(item *item.Item) *Item { + if item == nil { + return nil + } + + return &Item{ + Name: item.Name, + DisplayName: item.DisplayName, + Content: item.Content, + CreateTime: item.CreateTime, + UpdateTime: item.UpdateTime, + Properties: item.Properties, + Metadata: item.Metadata, + } +} diff --git a/go.mod b/go.mod index cf4a0b0..87ecd7e 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/matryer/moq v0.5.0 github.com/mattn/go-sqlite3 v1.14.22 github.com/oapi-codegen/oapi-codegen/v2 v2.3.0 + github.com/oapi-codegen/runtime v1.1.1 github.com/pressly/goose/v3 v3.21.1 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 @@ -39,7 +40,6 @@ require ( github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/runtime v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/item/item.go b/item/item.go index c98fefe..2dc111a 100644 --- a/item/item.go +++ b/item/item.go @@ -7,13 +7,13 @@ const ItemResource = "item" // Item is the core data structure for the content management system. // An item represent a single piece of content. It is the structured version of a markdown file. type Item struct { - Name string `mapstructure:"name"` - DisplayName string `mapstructure:"display_name"` - Content string `mapstructure:"content"` - Hash string `mapstructure:"hash"` - CreateTime time.Time `mapstructure:"create_time"` - UpdateTime time.Time `mapstructure:"update_time"` - DeleteTime *time.Time `mapstructure:"delete_time"` - Properties map[string]any `mapstructure:"properties"` - Metadata map[string]any `mapstructure:"metadata"` + Name string + DisplayName string + Content string + Hash string + CreateTime time.Time + UpdateTime time.Time + DeleteTime *time.Time + Properties map[string]any + Metadata map[string]any } diff --git a/item/repository.go b/item/repository.go index c2e01a4..7cc8b1a 100644 --- a/item/repository.go +++ b/item/repository.go @@ -15,7 +15,7 @@ type Repository interface { // TODO: Add transaction to all methods. // TODO: Add method to get a transaction. - GetItem(ctx context.Context, uid string) (*Item, error) + GetItem(ctx context.Context, name string) (*Item, error) UpdateItem(ctx context.Context, item *Item) error DeleteItem(ctx context.Context, uid string) error } @@ -98,14 +98,12 @@ func (r *repository) CreateItem(ctx context.Context, tx *sql.Tx, item *Item) err return nil } -// TODO: Where delete time is null. - // GetItem retrieves an item from the database by its resource name. func (r *repository) GetItem(ctx context.Context, name string) (*Item, error) { query := ` SELECT name, display_name, create_time, update_time, delete_time, hash, content, properties, metadata FROM items - WHERE name = $1 + WHERE name = $1 AND delete_time IS NULL ` var item Item var propertiesJSON []byte diff --git a/item/service.go b/item/service.go index f3d92a2..74f97bd 100644 --- a/item/service.go +++ b/item/service.go @@ -8,17 +8,18 @@ import ( "github.com/glass-cms/glasscms/lib/resource" ) +// Service is a service for managing items. type Service struct { repo Repository } -// NewService returns a new instance of Service. func NewService(repo Repository) *Service { return &Service{ repo: repo, } } +// CreateItem creates a new item. func (s *Service) CreateItem(ctx context.Context, item *Item) error { err := s.repo.CreateItem(ctx, nil, item) if errors.Is(err, database.ErrDuplicatePrimaryKey) { @@ -27,3 +28,13 @@ func (s *Service) CreateItem(ctx context.Context, item *Item) error { return err } + +// GetItem retrieves an item by name. +func (s *Service) GetItem(ctx context.Context, name string) (*Item, error) { + item, err := s.repo.GetItem(ctx, name) + if errors.Is(err, database.ErrNotFound) { + return nil, resource.NewNotFoundError(name, ItemResource, err) + } + + return item, err +} diff --git a/lib/middleware/accept.go b/lib/middleware/accept.go new file mode 100644 index 0000000..43c2b8a --- /dev/null +++ b/lib/middleware/accept.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "net/http" + "slices" + + "github.com/glass-cms/glasscms/lib/mediatype" +) + +// Accept generates a handler that writes a 415 Unsupported Media Type header +// if the request's Accept header does not match the provided media type. +func Accept(accepted ...string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Accept") == "" { + // Set default media type of Accept header to application/json + r.Header.Set("Accept", "application/json") + } + + header := r.Header.Get("Accept") + mdt, err := mediatype.Parse(header) + if err != nil { + http.Error(w, "Invalid media type for accept header", http.StatusBadRequest) + return + } + + if !slices.Contains(accepted, mdt.MediaType) { + http.Error(w, "Unsupported Accept Media Type", http.StatusNotAcceptable) + return + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/lib/middleware/accept_test.go b/lib/middleware/accept_test.go new file mode 100644 index 0000000..49799f8 --- /dev/null +++ b/lib/middleware/accept_test.go @@ -0,0 +1,73 @@ +package middleware_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/glass-cms/glasscms/lib/middleware" +) + +func Test_Accept(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + acceptHeader string + accepted []string + expected int + }{ + "application/json": { + acceptHeader: "application/json", + accepted: []string{"application/json"}, + expected: http.StatusOK, + }, + "application/json with charset": { + acceptHeader: "application/json; charset=utf-8", + accepted: []string{"application/json"}, + expected: http.StatusOK, + }, + "application/xml": { + acceptHeader: "application/xml", + accepted: []string{"application/xml"}, + expected: http.StatusOK, + }, + "unsupported": { + acceptHeader: "text/plain", + accepted: []string{"application/json"}, + expected: http.StatusNotAcceptable, + }, + "invalid": { + acceptHeader: "text/", + accepted: []string{"application/json"}, + expected: http.StatusBadRequest, + }, + "empty": { + acceptHeader: "", + accepted: []string{"application/json"}, + expected: http.StatusOK, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + req, err := http.NewRequest(http.MethodGet, "/test", nil) + if err != nil { + t.Fatal(err) + } + + req.Header.Set("Accept", test.acceptHeader) + + rr := httptest.NewRecorder() + + middleware.Accept(test.accepted...)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })).ServeHTTP(rr, req) + + if rr.Code != test.expected { + t.Errorf("expected %d; got %d", test.expected, rr.Code) + } + }) + } +} diff --git a/lib/middleware/mediatype.go b/lib/middleware/content_type.go similarity index 82% rename from lib/middleware/mediatype.go rename to lib/middleware/content_type.go index 19ff0e2..327a394 100644 --- a/lib/middleware/mediatype.go +++ b/lib/middleware/content_type.go @@ -7,9 +7,9 @@ import ( "github.com/glass-cms/glasscms/lib/mediatype" ) -// MediaType generates a handler that writes a 415 Unsupported Media Type header +// ContentType generates a handler that writes a 415 Unsupported Media Type header // if the request's Content-Type header does not match the provided media type. -func MediaType(accepted ...string) func(next http.Handler) http.Handler { +func ContentType(accepted ...string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { header := r.Header.Get("Content-Type") diff --git a/lib/middleware/mediatype_test.go b/lib/middleware/content_type_test.go similarity index 94% rename from lib/middleware/mediatype_test.go rename to lib/middleware/content_type_test.go index 4c62eed..20c60d1 100644 --- a/lib/middleware/mediatype_test.go +++ b/lib/middleware/content_type_test.go @@ -9,7 +9,7 @@ import ( "github.com/glass-cms/glasscms/lib/middleware" ) -func Test_MediaType(t *testing.T) { +func Test_ContentType(t *testing.T) { t.Parallel() tests := map[string]struct { @@ -65,7 +65,7 @@ func Test_MediaType(t *testing.T) { }) w := httptest.NewRecorder() - middleware.MediaType(test.accepted...)(handler).ServeHTTP(w, req) + middleware.ContentType(test.accepted...)(handler).ServeHTTP(w, req) if w.Code != test.expected { t.Errorf("expected %d, got %d", test.expected, w.Code) diff --git a/lib/resource/resource.go b/lib/resource/resource.go index f3cfa82..ac9553c 100644 --- a/lib/resource/resource.go +++ b/lib/resource/resource.go @@ -11,7 +11,6 @@ func (e *AlreadyExistsError) Error() string { return e.err.Error() } -// NewAlreadyExistsError creates a new ResourceAlreadyExistsError. func NewAlreadyExistsError(name, resource string, err error) *AlreadyExistsError { return &AlreadyExistsError{ Name: name, @@ -19,3 +18,22 @@ func NewAlreadyExistsError(name, resource string, err error) *AlreadyExistsError err: err, } } + +// NotFoundError represents an error when a resource is not found. +type NotFoundError struct { + Name string + Resource string + err error +} + +func (e *NotFoundError) Error() string { + return e.err.Error() +} + +func NewNotFoundError(name, resource string, err error) *NotFoundError { + return &NotFoundError{ + Name: name, + Resource: resource, + err: err, + } +} diff --git a/lib/test/database.go b/lib/test/database.go index 67786b6..d485601 100644 --- a/lib/test/database.go +++ b/lib/test/database.go @@ -2,8 +2,10 @@ package test import ( "database/sql" + "fmt" "github.com/glass-cms/glasscms/database" + "github.com/google/uuid" ) // NewDB creates a new in-memory SQLite database with the necessary schema @@ -11,7 +13,7 @@ import ( func NewDB() (*sql.DB, error) { config := database.Config{ Driver: database.DriverName[int32(database.DriverSqlite)], - DSN: "file::memory:?cache=shared", + DSN: fmt.Sprintf("file:%s?mode=memory&cache=shared", uuid.New().String()), } db, err := database.NewConnection(config) diff --git a/server/handler/json.go b/server/handler/json.go deleted file mode 100644 index 909d846..0000000 --- a/server/handler/json.go +++ /dev/null @@ -1,18 +0,0 @@ -package handler - -import ( - "encoding/json" - "net/http" - - "github.com/glass-cms/glasscms/lib/mediatype" -) - -// RespondWithJSON writes a JSON response to the response. -func RespondWithJSON[T any](w http.ResponseWriter, statusCode int, data T) { - w.Header().Set("Content-Type", mediatype.ApplicationJSON) - w.WriteHeader(statusCode) - - if err := json.NewEncoder(w).Encode(data); err != nil { - http.Error(w, "Failed to encode JSON", http.StatusInternalServerError) - } -} diff --git a/server/handler/serialize.go b/server/handler/serialize.go new file mode 100644 index 0000000..5f0023d --- /dev/null +++ b/server/handler/serialize.go @@ -0,0 +1,32 @@ +package handler + +import ( + "encoding/json" + "encoding/xml" + "net/http" + + "github.com/glass-cms/glasscms/lib/mediatype" +) + +// SerializeResponse writes the provided data to the response writer in the +// appropriate media type based on the request's Accept header. +func SerializeResponse[T any](w http.ResponseWriter, r *http.Request, statusCode int, data T) { + w.WriteHeader(statusCode) + + switch r.Header.Get("Accept") { + case mediatype.ApplicationJSON: + w.Header().Set("Content-Type", mediatype.ApplicationJSON) + + if err := json.NewEncoder(w).Encode(data); err != nil { + http.Error(w, "Failed to encode JSON", http.StatusInternalServerError) + } + case mediatype.ApplicationXML: + w.Header().Set("Content-Type", mediatype.ApplicationXML) + + if err := xml.NewEncoder(w).Encode(data); err != nil { + http.Error(w, "Failed to encode XML", http.StatusInternalServerError) + } + default: + http.Error(w, "Unsupported media type", http.StatusNotAcceptable) + } +} diff --git a/server/handler/v1/error_handler.go b/server/handler/v1/error_handler.go index 7f7b7bd..d71b604 100644 --- a/server/handler/v1/error_handler.go +++ b/server/handler/v1/error_handler.go @@ -28,7 +28,7 @@ func (h *ErrorHandler) RegisterErrorMapper(errType reflect.Type, mapper ErrorMap } // HandleError handles an error by writing an appropriate response to the client. -func (h *ErrorHandler) HandleError(w http.ResponseWriter, _ *http.Request, err error) { +func (h *ErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, err error) { if err == nil { return } @@ -42,8 +42,15 @@ func (h *ErrorHandler) HandleError(w http.ResponseWriter, _ *http.Request, err e statusCode = http.StatusInternalServerError } - handler.RespondWithJSON(w, statusCode, errResp) + handler.SerializeResponse(w, r, statusCode, errResp) + return } - // TODO: Default error handling. + // Fallback on generic error response if we don't have a specific error mapper. + errResp := &v1.Error{ + Code: v1.ProcessingError, + Message: "An error occurred while processing the request.", + Type: v1.ApiError, + } + handler.SerializeResponse(w, r, http.StatusInternalServerError, errResp) } diff --git a/server/handler/v1/error_handler_test.go b/server/handler/v1/error_handler_test.go index ba42722..4226d8f 100644 --- a/server/handler/v1/error_handler_test.go +++ b/server/handler/v1/error_handler_test.go @@ -1 +1,94 @@ package v1_test + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + api "github.com/glass-cms/glasscms/api/v1" + "github.com/glass-cms/glasscms/lib/resource" + v1 "github.com/glass-cms/glasscms/server/handler/v1" +) + +func TestErrorHandler_HandleError(t *testing.T) { + t.Parallel() + + errorHandler := v1.NewErrorHandler() + errorHandler.RegisterErrorMapper(reflect.TypeOf(&resource.AlreadyExistsError{}), func(_ error) *api.Error { + return &api.Error{ + Code: api.ResourceAlreadyExists, + Message: "ResourceType already exists", + Type: api.ApiError, + } + }) + + tests := []struct { + name string + err error + expectedStatus int + expectedCode string + expectedMsg string + }{ + { + name: "No Error", + err: nil, + expectedStatus: http.StatusOK, + expectedCode: "", + expectedMsg: "", + }, + { + name: "Mapped Error", + err: resource.NewAlreadyExistsError("ResourceType", "ResourceID", errors.New("test error")), + expectedStatus: http.StatusConflict, + expectedCode: string(api.ResourceAlreadyExists), + expectedMsg: "ResourceType already exists", + }, + { + name: "Unmapped Error", + err: errors.New("test error"), + expectedStatus: http.StatusInternalServerError, + expectedCode: string(api.ProcessingError), + expectedMsg: "An error occurred while processing the request.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Accept", "application/json") + rr := httptest.NewRecorder() + + // Handle the error + errorHandler.HandleError(rr, req, tt.err) + + // Assert the response status code + if rr.Code != tt.expectedStatus { + t.Errorf("expected status code %d, got %d", tt.expectedStatus, rr.Code) + } + + // If no error is expected, skip further checks + if tt.err == nil { + return + } + + // Assert the response body + var errResp api.Error + if err := json.NewDecoder(rr.Body).Decode(&errResp); err != nil { + t.Fatal(err) + } + + if string(errResp.Code) != tt.expectedCode { + t.Errorf("expected code %s, got %s", tt.expectedCode, errResp.Code) + } + + if errResp.Message != tt.expectedMsg { + t.Errorf("expected message %q, got %q", tt.expectedMsg, errResp.Message) + } + }) + } +} diff --git a/server/handler/v1/errors.go b/server/handler/v1/errors.go index bf201a3..b323ebc 100644 --- a/server/handler/v1/errors.go +++ b/server/handler/v1/errors.go @@ -25,3 +25,20 @@ func ErrorMapperAlreadyExistsError(err error) *v1.Error { }, } } + +func ErrorMapperNotFoundError(err error) *v1.Error { + var notFoundErr *resource.NotFoundError + if !errors.As(err, ¬FoundErr) { + panic("error is not a resource.NotFoundError") + } + + return &v1.Error{ + Code: v1.ResourceMissing, + Message: fmt.Sprintf("The %s with the name was not found", notFoundErr.Resource), + Type: v1.ApiError, + Details: map[string]interface{}{ + "resource": notFoundErr.Resource, + "name": notFoundErr.Name, + }, + } +} diff --git a/server/handler/v1/errors_test.go b/server/handler/v1/errors_test.go index 04c4e0d..6de00b0 100644 --- a/server/handler/v1/errors_test.go +++ b/server/handler/v1/errors_test.go @@ -57,3 +57,51 @@ func TestErrorMapperAlreadyExistsError(t *testing.T) { }) } } + +func TestErrorMapperNotFoundError(t *testing.T) { + t.Parallel() + + type args struct { + err error + } + tests := map[string]struct { + args args + want *v1.Error + expectPanics bool + }{ + "maps resource.NotFoundError to an API error response": { + args: args{ + err: resource.NewNotFoundError("item1", "item", errors.New("underlying error")), + }, + want: &v1.Error{ + Code: v1.ResourceMissing, + Message: "The item with the name was not found", + Type: v1.ApiError, + Details: map[string]interface{}{ + "resource": "item", + "name": "item1", + }, + }, + }, + "panics if error is not a resource.NotFoundError": { + args: args{ + err: errors.New("some error"), + }, + expectPanics: true, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if tt.expectPanics { + require.Panics(t, func() { + v1_handler.ErrorMapperNotFoundError(tt.args.err) + }) + return + } + + require.Equal(t, tt.want, v1_handler.ErrorMapperNotFoundError(tt.args.err)) + }) + } +} diff --git a/server/handler/v1/handler_v1.go b/server/handler/v1/handler_v1.go index 89c9583..fb715b8 100644 --- a/server/handler/v1/handler_v1.go +++ b/server/handler/v1/handler_v1.go @@ -56,6 +56,11 @@ func (s *APIHandler) registerErrorMappers() { reflect.TypeOf(&resource.AlreadyExistsError{}), ErrorMapperAlreadyExistsError, ) + + s.errorHandler.RegisterErrorMapper( + reflect.TypeOf(&resource.NotFoundError{}), + ErrorMapperNotFoundError, + ) } var _ v1.ServerInterface = (*APIHandler)(nil) diff --git a/server/handler/v1/item.go b/server/handler/v1/item.go index b661d74..ea04dd3 100644 --- a/server/handler/v1/item.go +++ b/server/handler/v1/item.go @@ -7,6 +7,7 @@ import ( "net/http" v1 "github.com/glass-cms/glasscms/api/v1" + "github.com/glass-cms/glasscms/server/handler" ) func (s *APIHandler) ItemsCreate(w http.ResponseWriter, r *http.Request) { @@ -15,14 +16,14 @@ func (s *APIHandler) ItemsCreate(w http.ResponseWriter, r *http.Request) { reqBody, err := io.ReadAll(r.Body) if err != nil { s.logger.ErrorContext(ctx, fmt.Errorf("failed to read request body: %w", err).Error()) - w.WriteHeader(http.StatusInternalServerError) + s.errorHandler.HandleError(w, r, err) return } var request *v1.ItemsCreateJSONRequestBody if err = json.Unmarshal(reqBody, &request); err != nil { s.logger.ErrorContext(ctx, fmt.Errorf("failed to unmarshal request body: %w", err).Error()) - w.WriteHeader(http.StatusBadRequest) + s.errorHandler.HandleError(w, r, err) return } @@ -33,9 +34,20 @@ func (s *APIHandler) ItemsCreate(w http.ResponseWriter, r *http.Request) { return } + // TODO: Write response. + w.WriteHeader(http.StatusCreated) } -func (s *APIHandler) ItemsGet(w http.ResponseWriter, _ *http.Request, _ v1.ItemKey) { - w.WriteHeader(http.StatusTeapot) +func (s *APIHandler) ItemsGet(w http.ResponseWriter, r *http.Request, name v1.ItemKey) { + ctx := r.Context() + + item, err := s.itemService.GetItem(ctx, name) + if err != nil { + s.logger.ErrorContext(ctx, fmt.Errorf("failed to get item: %w", err).Error()) + s.errorHandler.HandleError(w, r, err) + return + } + + handler.SerializeResponse(w, r, http.StatusOK, item) } diff --git a/server/handler/v1/item_test.go b/server/handler/v1/item_test.go index 6c1eb60..fd9ef35 100644 --- a/server/handler/v1/item_test.go +++ b/server/handler/v1/item_test.go @@ -19,30 +19,15 @@ import ( func TestAPIHandler_ItemsCreate(t *testing.T) { t.Parallel() - testdb, err := test.NewDB() - if err != nil { - t.Fatal(err) - } - - repo := item.NewRepository(testdb, &database.SqliteErrorHandler{}) - tests := map[string]struct { req func() *http.Request expected int }{ - "returns a 500 status code when the request body cannot be read": { - req: func() *http.Request { - return &http.Request{ - Body: &test.ErrorReadCloser{}, - } - }, - expected: http.StatusInternalServerError, - }, - "returns a 400 status code when the buffer cannot be unmarshalled": { + "returns a 500 status code when the buffer cannot be unmarshalled": { req: func() *http.Request { return httptest.NewRequest(http.MethodPost, "/v1/items", nil) }, - expected: http.StatusBadRequest, + expected: http.StatusInternalServerError, }, "returns a 201 status code when the item is created successfully": { req: func() *http.Request { @@ -61,6 +46,14 @@ func TestAPIHandler_ItemsCreate(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + testdb, err := test.NewDB() + if err != nil { + t.Fatal(err) + } + defer testdb.Close() + + repo := item.NewRepository(testdb, &database.SqliteErrorHandler{}) + handler := v1.NewAPIHandler( log.NoopLogger(), item.NewService(repo), @@ -68,6 +61,7 @@ func TestAPIHandler_ItemsCreate(t *testing.T) { rr := httptest.NewRecorder() request := tt.req() + request.Header.Set("Accept", "application/json") // Act handler.ItemsCreate(rr, request) @@ -77,3 +71,49 @@ func TestAPIHandler_ItemsCreate(t *testing.T) { }) } } + +func TestAPIHandler_ItemsGet(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + req func() *http.Request + expected int + }{ + "returns a 404 status code when the item cannot be found": { + req: func() *http.Request { + return httptest.NewRequest(http.MethodGet, "/v1/items/missing", nil) + }, + expected: http.StatusNotFound, + }, + // TODO: Add more tests. + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + testdb, err := test.NewDB() + if err != nil { + t.Fatal(err) + } + defer testdb.Close() + + repo := item.NewRepository(testdb, &database.SqliteErrorHandler{}) + + handler := v1.NewAPIHandler( + log.NoopLogger(), + item.NewService(repo), + ).Handler(http.NewServeMux(), []func(http.Handler) http.Handler{}) + + rr := httptest.NewRecorder() + request := tt.req() + request.Header.Set("Accept", "application/json") + + // Make the request + handler.ServeHTTP(rr, request) + + // Assert + assert.Equal(t, tt.expected, rr.Code) + }) + } +} diff --git a/server/server.go b/server/server.go index c6163f1..4d00a49 100644 --- a/server/server.go +++ b/server/server.go @@ -7,6 +7,7 @@ import ( "net/http" "time" + "github.com/glass-cms/glasscms/lib/mediatype" "github.com/glass-cms/glasscms/lib/middleware" "github.com/glass-cms/glasscms/server/handler" ) @@ -36,7 +37,8 @@ func New( for _, h := range handlers { _ = h.Handler(serveMux, []func(http.Handler) http.Handler{ - middleware.MediaType("application/json"), + middleware.ContentType(mediatype.ApplicationJSON), + middleware.Accept(mediatype.ApplicationJSON), }) } diff --git a/typespec/package-lock.json b/typespec/package-lock.json index 63dd5ed..8b5c961 100644 --- a/typespec/package-lock.json +++ b/typespec/package-lock.json @@ -448,9 +448,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1"