Skip to content

Commit 0690b52

Browse files
committedNov 27, 2024·
httpserver: don't cancel requests for write operations externally;
1 parent 6dd761f commit 0690b52

File tree

20 files changed

+615
-401
lines changed

20 files changed

+615
-401
lines changed
 

Diff for: ‎docs/docs/how-to/logging/src/log-context/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func main() {
2929
}
3030

3131
// Add CRUD handlers to your definitions. This is a convenience method for adding handlers for Create, Read, Update, Delete, and List.
32-
crud.AddCrudHandlers(logger, def, 0, "todo", handler)
32+
crud.AddCrudHandlers(config, logger, def, 0, "todo", handler)
3333

3434
return def, nil
3535
}

Diff for: ‎docs/docs/quickstart/http-server/src/write-crud-sql-app/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func main() {
2929
}
3030

3131
// Add CRUD handlers to your definitions. This is a convenience method for adding handlers for Create, Read, Update, Delete, and List.
32-
crud.AddCrudHandlers(logger, def, 0, "todo", handler)
32+
crud.AddCrudHandlers(config, logger, def, 0, "todo", handler)
3333

3434
return def, nil
3535
}

Diff for: ‎examples/apiserver/simple-handlers/main.go renamed to ‎examples/httpserver/simple-handlers/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func apiDefiner(ctx context.Context, config cfg.Config, logger log.Logger) (*htt
3737

3838
group.GET("/authenticated", httpserver.CreateHandler(&AdminAuthenticatedHandler{}))
3939

40-
crud.AddCrudHandlers(logger, definitions, 0, "/myEntity", &MyEntityHandler{
40+
crud.AddCrudHandlers(config, logger, definitions, 0, "/myEntity", &MyEntityHandler{
4141
repo: &MyEntityRepository{},
4242
})
4343

Diff for: ‎pkg/httpserver/crud/create.go

+19-20
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,65 @@ package crud
22

33
import (
44
"context"
5-
"errors"
6-
"net/http"
75

86
"github.com/gin-gonic/gin"
9-
"github.com/justtrackio/gosoline/pkg/db"
7+
"github.com/justtrackio/gosoline/pkg/cfg"
8+
"github.com/justtrackio/gosoline/pkg/exec"
109
"github.com/justtrackio/gosoline/pkg/httpserver"
1110
"github.com/justtrackio/gosoline/pkg/log"
12-
"github.com/justtrackio/gosoline/pkg/validation"
1311
)
1412

1513
type createHandler struct {
1614
logger log.Logger
1715
transformer CreateHandler
16+
settings Settings
1817
}
1918

20-
func NewCreateHandler(logger log.Logger, transformer CreateHandler) gin.HandlerFunc {
19+
func NewCreateHandler(config cfg.Config, logger log.Logger, transformer CreateHandler) gin.HandlerFunc {
20+
settings := Settings{}
21+
config.UnmarshalKey(SettingsConfigKey, &settings)
22+
2123
ch := createHandler{
2224
transformer: transformer,
2325
logger: logger,
26+
settings: settings,
2427
}
2528

2629
return httpserver.CreateJsonHandler(ch)
2730
}
2831

29-
func (ch createHandler) GetInput() interface{} {
32+
func (ch createHandler) GetInput() any {
3033
return ch.transformer.GetCreateInput()
3134
}
3235

33-
func (ch createHandler) Handle(ctx context.Context, request *httpserver.Request) (*httpserver.Response, error) {
36+
func (ch createHandler) Handle(reqCtx context.Context, request *httpserver.Request) (*httpserver.Response, error) {
37+
// replace context with a new one to prevent cancellations from client side
38+
// include a new timeout to ensure that requests will be cancelled
39+
ctx, cancel := exec.WithDelayedCancelContext(reqCtx, ch.settings.WriteTimeout)
40+
defer cancel()
41+
3442
model := ch.transformer.GetModel()
3543
err := ch.transformer.TransformCreate(ctx, request.Body, model)
3644
if err != nil {
37-
return nil, err
45+
return handleErrorOnWrite(ctx, ch.logger, err)
3846
}
3947

4048
repo := ch.transformer.GetRepository()
4149
err = repo.Create(ctx, model)
42-
43-
if db.IsDuplicateEntryError(err) {
44-
return httpserver.NewStatusResponse(http.StatusConflict), nil
45-
}
46-
47-
if errors.Is(err, &validation.Error{}) {
48-
return httpserver.GetErrorHandler()(http.StatusBadRequest, err), nil
49-
}
50-
5150
if err != nil {
52-
return nil, err
51+
return handleErrorOnWrite(ctx, ch.logger, err)
5352
}
5453

5554
reload := ch.transformer.GetModel()
5655
err = repo.Read(ctx, model.GetId(), reload)
5756
if err != nil {
58-
return nil, err
57+
return handleErrorOnWrite(ctx, ch.logger, err)
5958
}
6059

6160
apiView := GetApiViewFromHeader(request.Header)
6261
out, err := ch.transformer.TransformOutput(ctx, reload, apiView)
6362
if err != nil {
64-
return nil, err
63+
return handleErrorOnWrite(ctx, ch.logger, err)
6564
}
6665

6766
return httpserver.NewJsonResponse(out), nil

Diff for: ‎pkg/httpserver/crud/create_test.go

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package crud_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"testing"
8+
"time"
9+
10+
"github.com/gin-gonic/gin"
11+
"github.com/justtrackio/gosoline/pkg/cfg"
12+
configMocks "github.com/justtrackio/gosoline/pkg/cfg/mocks"
13+
"github.com/justtrackio/gosoline/pkg/db-repo"
14+
"github.com/justtrackio/gosoline/pkg/httpserver"
15+
"github.com/justtrackio/gosoline/pkg/httpserver/crud"
16+
logMocks "github.com/justtrackio/gosoline/pkg/log/mocks"
17+
"github.com/justtrackio/gosoline/pkg/mdl"
18+
"github.com/justtrackio/gosoline/pkg/validation"
19+
"github.com/stretchr/testify/mock"
20+
"github.com/stretchr/testify/suite"
21+
)
22+
23+
type createTestSuite struct {
24+
suite.Suite
25+
26+
handler handler
27+
createHandler gin.HandlerFunc
28+
}
29+
30+
func Test_RunCreateTestSuite(t *testing.T) {
31+
suite.Run(t, new(createTestSuite))
32+
}
33+
34+
func (s *createTestSuite) SetupTest() {
35+
config := configMocks.NewConfig(s.T())
36+
config.EXPECT().UnmarshalKey("crud", mock.AnythingOfType("*crud.Settings")).Run(func(key string, val any, additionalDefaults ...cfg.UnmarshalDefaults) {
37+
settings := val.(*crud.Settings)
38+
settings.WriteTimeout = time.Minute
39+
})
40+
41+
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(s.T()))
42+
43+
s.handler = newHandler(s.T())
44+
s.createHandler = crud.NewCreateHandler(config, logger, s.handler)
45+
}
46+
47+
func (s *createTestSuite) TestCreate() {
48+
model := &Model{
49+
Name: mdl.Box("foobar"),
50+
}
51+
52+
s.handler.Repo.EXPECT().Create(mock.AnythingOfType("*exec.stoppableContext"), model).Run(func(_ context.Context, value db_repo.ModelBased) {
53+
model := value.(*Model)
54+
model.Id = mdl.Box(uint(1))
55+
}).Return(nil)
56+
s.handler.Repo.EXPECT().Read(mock.AnythingOfType("*exec.stoppableContext"), mdl.Box(uint(1)), &Model{}).Run(func(_ context.Context, id *uint, out db_repo.ModelBased) {
57+
model := out.(*Model)
58+
model.Id = mdl.Box(uint(1))
59+
model.Name = mdl.Box("foobar")
60+
model.UpdatedAt = &time.Time{}
61+
model.CreatedAt = &time.Time{}
62+
}).Return(nil)
63+
64+
body := `{"name": "foobar"}`
65+
response := httpserver.HttpTest("POST", "/create", "/create", body, s.createHandler)
66+
67+
s.Equal(http.StatusOK, response.Code)
68+
s.JSONEq(`{"id":1,"updatedAt":"0001-01-01T00:00:00Z","createdAt":"0001-01-01T00:00:00Z","name":"foobar"}`, response.Body.String())
69+
}
70+
71+
func (s *createTestSuite) TestCreate_ValidationError() {
72+
model := &Model{
73+
Name: mdl.Box("foobar"),
74+
}
75+
76+
s.handler.Repo.EXPECT().Create(mock.AnythingOfType("*exec.stoppableContext"), model).Return(&validation.Error{
77+
Errors: []error{fmt.Errorf("invalid foobar")},
78+
})
79+
80+
body := `{"name": "foobar"}`
81+
response := httpserver.HttpTest("POST", "/create", "/create", body, s.createHandler)
82+
83+
s.Equal(http.StatusBadRequest, response.Code)
84+
s.JSONEq(`{"err":"validation: invalid foobar"}`, response.Body.String())
85+
}

Diff for: ‎pkg/httpserver/crud/crud_test.go

+12-273
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,13 @@ package crud_test
22

33
import (
44
"context"
5-
"fmt"
6-
"net/http"
75
"testing"
86
"time"
97

108
"github.com/justtrackio/gosoline/pkg/db-repo"
11-
"github.com/justtrackio/gosoline/pkg/httpserver"
129
"github.com/justtrackio/gosoline/pkg/httpserver/crud"
1310
"github.com/justtrackio/gosoline/pkg/httpserver/crud/mocks"
14-
logMocks "github.com/justtrackio/gosoline/pkg/log/mocks"
1511
"github.com/justtrackio/gosoline/pkg/mdl"
16-
"github.com/justtrackio/gosoline/pkg/validation"
17-
"github.com/stretchr/testify/assert"
18-
"github.com/stretchr/testify/mock"
1912
)
2013

2114
type Model struct {
@@ -38,27 +31,27 @@ type UpdateInput struct {
3831
Name *string `json:"name" binding:"required"`
3932
}
4033

41-
type Handler struct {
34+
type handler struct {
4235
Repo *mocks.Repository
4336
}
4437

45-
func (h Handler) GetRepository() crud.Repository {
38+
func (h handler) GetRepository() crud.Repository {
4639
return h.Repo
4740
}
4841

49-
func (h Handler) GetModel() db_repo.ModelBased {
42+
func (h handler) GetModel() db_repo.ModelBased {
5043
return &Model{}
5144
}
5245

53-
func (h Handler) GetCreateInput() interface{} {
46+
func (h handler) GetCreateInput() any {
5447
return &CreateInput{}
5548
}
5649

57-
func (h Handler) GetUpdateInput() interface{} {
50+
func (h handler) GetUpdateInput() any {
5851
return &UpdateInput{}
5952
}
6053

61-
func (h Handler) TransformCreate(_ctx context.Context, inp interface{}, model db_repo.ModelBased) (err error) {
54+
func (h handler) TransformCreate(_ context.Context, inp any, model db_repo.ModelBased) (err error) {
6255
input := inp.(*CreateInput)
6356
m := model.(*Model)
6457

@@ -67,7 +60,7 @@ func (h Handler) TransformCreate(_ctx context.Context, inp interface{}, model db
6760
return nil
6861
}
6962

70-
func (h Handler) TransformUpdate(_ context.Context, inp interface{}, model db_repo.ModelBased) (err error) {
63+
func (h handler) TransformUpdate(_ context.Context, inp any, model db_repo.ModelBased) (err error) {
7164
input := inp.(*UpdateInput)
7265
m := model.(*Model)
7366

@@ -76,7 +69,7 @@ func (h Handler) TransformUpdate(_ context.Context, inp interface{}, model db_re
7669
return nil
7770
}
7871

79-
func (h Handler) TransformOutput(ctx context.Context, model db_repo.ModelBased, _ string) (interface{}, error) {
72+
func (h handler) TransformOutput(_ context.Context, model db_repo.ModelBased, _ string) (any, error) {
8073
m := model.(*Model)
8174

8275
out := &Output{
@@ -89,7 +82,7 @@ func (h Handler) TransformOutput(ctx context.Context, model db_repo.ModelBased,
8982
return out, nil
9083
}
9184

92-
func (h Handler) List(_ context.Context, _ *db_repo.QueryBuilder, _ string) (interface{}, error) {
85+
func (h handler) List(_ context.Context, _ *db_repo.QueryBuilder, _ string) (any, error) {
9386
date, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
9487
if err != nil {
9588
panic(err)
@@ -109,264 +102,10 @@ func (h Handler) List(_ context.Context, _ *db_repo.QueryBuilder, _ string) (int
109102
}, nil
110103
}
111104

112-
func NewTransformer() Handler {
113-
repo := new(mocks.Repository)
105+
func newHandler(t *testing.T) handler {
106+
repo := mocks.NewRepository(t)
114107

115-
return Handler{
108+
return handler{
116109
Repo: repo,
117110
}
118111
}
119-
120-
var id1 = mdl.Box(uint(1))
121-
122-
func TestCreateHandler_Handle(t *testing.T) {
123-
model := &Model{
124-
Name: mdl.Box("foobar"),
125-
}
126-
127-
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(t))
128-
transformer := NewTransformer()
129-
130-
transformer.Repo.On("Create", mock.AnythingOfType("context.backgroundCtx"), model).Run(func(args mock.Arguments) {
131-
model := args.Get(1).(*Model)
132-
model.Id = mdl.Box(uint(1))
133-
}).Return(nil)
134-
transformer.Repo.On("Read", mock.AnythingOfType("context.backgroundCtx"), mdl.Box(uint(1)), &Model{}).Run(func(args mock.Arguments) {
135-
model := args.Get(2).(*Model)
136-
model.Id = mdl.Box(uint(1))
137-
model.Name = mdl.Box("foobar")
138-
model.UpdatedAt = &time.Time{}
139-
model.CreatedAt = &time.Time{}
140-
}).Return(nil)
141-
142-
handler := crud.NewCreateHandler(logger, transformer)
143-
144-
body := `{"name": "foobar"}`
145-
response := httpserver.HttpTest("POST", "/create", "/create", body, handler)
146-
147-
assert.Equal(t, http.StatusOK, response.Code)
148-
assert.JSONEq(t, `{"id":1,"updatedAt":"0001-01-01T00:00:00Z","createdAt":"0001-01-01T00:00:00Z","name":"foobar"}`, response.Body.String())
149-
150-
transformer.Repo.AssertExpectations(t)
151-
}
152-
153-
func TestCreateHandler_Handle_ValidationError(t *testing.T) {
154-
model := &Model{
155-
Name: mdl.Box("foobar"),
156-
}
157-
158-
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(t))
159-
transformer := NewTransformer()
160-
161-
transformer.Repo.On("Create", mock.AnythingOfType("context.backgroundCtx"), model).Return(&validation.Error{
162-
Errors: []error{fmt.Errorf("invalid foobar")},
163-
})
164-
165-
handler := crud.NewCreateHandler(logger, transformer)
166-
167-
body := `{"name": "foobar"}`
168-
response := httpserver.HttpTest("POST", "/create", "/create", body, handler)
169-
170-
assert.Equal(t, http.StatusBadRequest, response.Code)
171-
assert.JSONEq(t, `{"err":"validation: invalid foobar"}`, response.Body.String())
172-
173-
transformer.Repo.AssertExpectations(t)
174-
}
175-
176-
func TestReadHandler_Handle(t *testing.T) {
177-
model := &Model{}
178-
179-
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(t))
180-
transformer := NewTransformer()
181-
transformer.Repo.On("Read", mock.AnythingOfType("context.backgroundCtx"), mdl.Box(uint(1)), model).Run(func(args mock.Arguments) {
182-
model := args.Get(2).(*Model)
183-
model.Id = mdl.Box(uint(1))
184-
model.Name = mdl.Box("foobar")
185-
model.UpdatedAt = &time.Time{}
186-
model.CreatedAt = &time.Time{}
187-
}).Return(nil)
188-
189-
handler := crud.NewReadHandler(logger, transformer)
190-
191-
response := httpserver.HttpTest("GET", "/:id", "/1", "", handler)
192-
193-
assert.Equal(t, http.StatusOK, response.Code)
194-
assert.JSONEq(t, `{"id":1,"updatedAt":"0001-01-01T00:00:00Z","createdAt":"0001-01-01T00:00:00Z","name":"foobar"}`, response.Body.String())
195-
196-
transformer.Repo.AssertExpectations(t)
197-
}
198-
199-
func TestUpdateHandler_Handle(t *testing.T) {
200-
readModel := &Model{}
201-
updateModel := &Model{
202-
Model: db_repo.Model{
203-
Id: mdl.Box(uint(1)),
204-
Timestamps: db_repo.Timestamps{
205-
UpdatedAt: &time.Time{},
206-
CreatedAt: &time.Time{},
207-
},
208-
},
209-
Name: mdl.Box("updated"),
210-
}
211-
212-
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(t))
213-
transformer := NewTransformer()
214-
215-
transformer.Repo.On("Update", mock.AnythingOfType("context.backgroundCtx"), updateModel).Return(nil)
216-
transformer.Repo.On("Read", mock.AnythingOfType("context.backgroundCtx"), mdl.Box(uint(1)), readModel).Run(func(args mock.Arguments) {
217-
model := args.Get(2).(*Model)
218-
model.Id = mdl.Box(uint(1))
219-
model.Name = mdl.Box("updated")
220-
model.UpdatedAt = &time.Time{}
221-
model.CreatedAt = &time.Time{}
222-
}).Return(nil)
223-
224-
handler := crud.NewUpdateHandler(logger, transformer)
225-
226-
body := `{"name": "updated"}`
227-
response := httpserver.HttpTest("PUT", "/:id", "/1", body, handler)
228-
229-
assert.Equal(t, http.StatusOK, response.Code)
230-
assert.JSONEq(t, `{"id":1,"updatedAt":"0001-01-01T00:00:00Z","createdAt":"0001-01-01T00:00:00Z","name":"updated"}`, response.Body.String())
231-
232-
transformer.Repo.AssertExpectations(t)
233-
}
234-
235-
func TestUpdateHandler_Handle_ValidationError(t *testing.T) {
236-
readModel := &Model{}
237-
updateModel := &Model{
238-
Model: db_repo.Model{
239-
Id: mdl.Box(uint(1)),
240-
Timestamps: db_repo.Timestamps{
241-
UpdatedAt: &time.Time{},
242-
CreatedAt: &time.Time{},
243-
},
244-
},
245-
Name: mdl.Box("updated"),
246-
}
247-
248-
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(t))
249-
transformer := NewTransformer()
250-
251-
transformer.Repo.On("Update", mock.AnythingOfType("context.backgroundCtx"), updateModel).Return(&validation.Error{
252-
Errors: []error{fmt.Errorf("invalid foobar")},
253-
})
254-
transformer.Repo.On("Read", mock.AnythingOfType("context.backgroundCtx"), mdl.Box(uint(1)), readModel).Run(func(args mock.Arguments) {
255-
model := args.Get(2).(*Model)
256-
model.Id = mdl.Box(uint(1))
257-
model.Name = mdl.Box("updated")
258-
model.UpdatedAt = &time.Time{}
259-
model.CreatedAt = &time.Time{}
260-
}).Return(nil)
261-
262-
handler := crud.NewUpdateHandler(logger, transformer)
263-
264-
body := `{"name": "updated"}`
265-
response := httpserver.HttpTest("PUT", "/:id", "/1", body, handler)
266-
267-
assert.Equal(t, http.StatusBadRequest, response.Code)
268-
assert.JSONEq(t, `{"err":"validation: invalid foobar"}`, response.Body.String())
269-
270-
transformer.Repo.AssertExpectations(t)
271-
}
272-
273-
func TestDeleteHandler_Handle(t *testing.T) {
274-
model := &Model{}
275-
deleteModel := &Model{
276-
Model: db_repo.Model{
277-
Id: id1,
278-
Timestamps: db_repo.Timestamps{
279-
UpdatedAt: &time.Time{},
280-
CreatedAt: &time.Time{},
281-
},
282-
},
283-
Name: mdl.Box("foobar"),
284-
}
285-
286-
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(t))
287-
transformer := NewTransformer()
288-
transformer.Repo.On("Read", mock.AnythingOfType("context.backgroundCtx"), mock.AnythingOfType("*uint"), model).Run(func(args mock.Arguments) {
289-
model := args.Get(2).(*Model)
290-
model.Id = id1
291-
model.Name = mdl.Box("foobar")
292-
model.UpdatedAt = &time.Time{}
293-
model.CreatedAt = &time.Time{}
294-
}).Return(nil)
295-
transformer.Repo.On("Delete", mock.AnythingOfType("context.backgroundCtx"), deleteModel).Return(nil)
296-
297-
handler := crud.NewDeleteHandler(logger, transformer)
298-
299-
response := httpserver.HttpTest("DELETE", "/:id", "/1", "", handler)
300-
301-
assert.Equal(t, http.StatusOK, response.Code)
302-
assert.JSONEq(t, `{"id":1,"updatedAt":"0001-01-01T00:00:00Z","createdAt":"0001-01-01T00:00:00Z","name":"foobar"}`, response.Body.String())
303-
304-
transformer.Repo.AssertExpectations(t)
305-
}
306-
307-
func TestDeleteHandler_Handle_ValidationError(t *testing.T) {
308-
model := &Model{}
309-
deleteModel := &Model{
310-
Model: db_repo.Model{
311-
Id: id1,
312-
Timestamps: db_repo.Timestamps{
313-
UpdatedAt: &time.Time{},
314-
CreatedAt: &time.Time{},
315-
},
316-
},
317-
Name: mdl.Box("foobar"),
318-
}
319-
320-
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(t))
321-
transformer := NewTransformer()
322-
transformer.Repo.On("Read", mock.AnythingOfType("context.backgroundCtx"), mock.AnythingOfType("*uint"), model).Run(func(args mock.Arguments) {
323-
model := args.Get(2).(*Model)
324-
model.Id = id1
325-
model.Name = mdl.Box("foobar")
326-
model.UpdatedAt = &time.Time{}
327-
model.CreatedAt = &time.Time{}
328-
}).Return(nil)
329-
transformer.Repo.On("Delete", mock.AnythingOfType("context.backgroundCtx"), deleteModel).Return(&validation.Error{
330-
Errors: []error{fmt.Errorf("invalid foobar")},
331-
})
332-
333-
handler := crud.NewDeleteHandler(logger, transformer)
334-
335-
response := httpserver.HttpTest("DELETE", "/:id", "/1", "", handler)
336-
337-
assert.Equal(t, http.StatusBadRequest, response.Code)
338-
assert.JSONEq(t, `{"err":"validation: invalid foobar"}`, response.Body.String())
339-
340-
transformer.Repo.AssertExpectations(t)
341-
}
342-
343-
func TestListHandler_Handle(t *testing.T) {
344-
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(t))
345-
transformer := NewTransformer()
346-
handler := crud.NewListHandler(logger, transformer)
347-
348-
qb := db_repo.NewQueryBuilder()
349-
qb.Table("footable")
350-
qb.Where("(((name = ?)))", "foobar")
351-
qb.GroupBy("id")
352-
qb.OrderBy("name", "ASC")
353-
qb.Page(0, 2)
354-
355-
transformer.Repo.On("GetMetadata").Return(db_repo.Metadata{
356-
TableName: "footable",
357-
PrimaryKey: "id",
358-
Mappings: db_repo.FieldMappings{
359-
"id": db_repo.NewFieldMapping("id"),
360-
"name": db_repo.NewFieldMapping("name"),
361-
},
362-
})
363-
transformer.Repo.On("Count", mock.AnythingOfType("context.backgroundCtx"), qb, &Model{}).Return(1, nil)
364-
365-
body := `{"filter":{"matches":[{"values":["foobar"],"dimension":"name","operator":"="}],"bool":"and"},"order":[{"field":"name","direction":"ASC"}],"page":{"offset":0,"limit":2}}`
366-
response := httpserver.HttpTest("PUT", "/:id", "/1", body, handler)
367-
368-
assert.Equal(t, http.StatusOK, response.Code)
369-
assert.JSONEq(t, `{"total":1,"results":[{"Id":1,"UpdatedAt":"2006-01-02T15:04:05Z","CreatedAt":"2006-01-02T15:04:05Z","name":"foobar"}]}`, response.Body.String())
370-
371-
transformer.Repo.AssertExpectations(t)
372-
}

Diff for: ‎pkg/httpserver/crud/delete.go

+24-21
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package crud
33
import (
44
"context"
55
"errors"
6-
"net/http"
76

87
"github.com/gin-gonic/gin"
9-
"github.com/justtrackio/gosoline/pkg/db-repo"
8+
"github.com/justtrackio/gosoline/pkg/cfg"
9+
"github.com/justtrackio/gosoline/pkg/exec"
1010
"github.com/justtrackio/gosoline/pkg/httpserver"
1111
"github.com/justtrackio/gosoline/pkg/log"
1212
"github.com/justtrackio/gosoline/pkg/validation"
@@ -15,54 +15,57 @@ import (
1515
type deleteHandler struct {
1616
logger log.Logger
1717
transformer BaseHandler
18+
settings Settings
1819
}
1920

20-
func NewDeleteHandler(logger log.Logger, transformer BaseHandler) gin.HandlerFunc {
21+
func NewDeleteHandler(config cfg.Config, logger log.Logger, transformer BaseHandler) gin.HandlerFunc {
22+
settings := Settings{}
23+
config.UnmarshalKey(SettingsConfigKey, &settings)
24+
2125
dh := deleteHandler{
2226
transformer: transformer,
2327
logger: logger,
28+
settings: settings,
2429
}
2530

2631
return httpserver.CreateHandler(dh)
2732
}
2833

29-
func (dh deleteHandler) Handle(ctx context.Context, request *httpserver.Request) (*httpserver.Response, error) {
34+
func (dh deleteHandler) Handle(reqCtx context.Context, request *httpserver.Request) (*httpserver.Response, error) {
35+
// replace context with a new one to prevent cancellations from client side
36+
// include a new timeout to ensure that requests will be cancelled
37+
ctx, cancel := exec.WithDelayedCancelContext(reqCtx, dh.settings.WriteTimeout)
38+
defer cancel()
39+
40+
logger := dh.logger.WithContext(ctx)
41+
3042
id, valid := httpserver.GetUintFromRequest(request, "id")
3143

3244
if !valid {
33-
return nil, errors.New("no valid id provided")
45+
return handleErrorOnWrite(ctx, logger, &validation.Error{
46+
Errors: []error{
47+
errors.New("no valid id provided"),
48+
},
49+
})
3450
}
3551

3652
repo := dh.transformer.GetRepository()
3753
model := dh.transformer.GetModel()
3854

3955
err := repo.Read(ctx, id, model)
40-
41-
var notFound db_repo.RecordNotFoundError
42-
if errors.As(err, &notFound) {
43-
dh.logger.WithContext(ctx).Warn("failed to delete model: %s", err)
44-
45-
return httpserver.NewStatusResponse(http.StatusNotFound), nil
46-
}
47-
4856
if err != nil {
49-
return nil, err
57+
return handleErrorOnWrite(ctx, logger, err)
5058
}
5159

5260
err = repo.Delete(ctx, model)
53-
54-
if errors.Is(err, &validation.Error{}) {
55-
return httpserver.GetErrorHandler()(http.StatusBadRequest, err), nil
56-
}
57-
5861
if err != nil {
59-
return nil, err
62+
return handleErrorOnWrite(ctx, logger, err)
6063
}
6164

6265
apiView := GetApiViewFromHeader(request.Header)
6366
out, err := dh.transformer.TransformOutput(ctx, model, apiView)
6467
if err != nil {
65-
return nil, err
68+
return handleErrorOnWrite(ctx, logger, err)
6669
}
6770

6871
return httpserver.NewJsonResponse(out), nil

Diff for: ‎pkg/httpserver/crud/delete_test.go

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package crud_test
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"testing"
7+
"time"
8+
9+
"github.com/gin-gonic/gin"
10+
"github.com/justtrackio/gosoline/pkg/cfg"
11+
configMocks "github.com/justtrackio/gosoline/pkg/cfg/mocks"
12+
"github.com/justtrackio/gosoline/pkg/db-repo"
13+
"github.com/justtrackio/gosoline/pkg/httpserver"
14+
"github.com/justtrackio/gosoline/pkg/httpserver/crud"
15+
logMocks "github.com/justtrackio/gosoline/pkg/log/mocks"
16+
"github.com/justtrackio/gosoline/pkg/mdl"
17+
"github.com/justtrackio/gosoline/pkg/validation"
18+
"github.com/stretchr/testify/mock"
19+
"github.com/stretchr/testify/suite"
20+
)
21+
22+
type deleteTestSuite struct {
23+
suite.Suite
24+
25+
handler handler
26+
deleteHandler gin.HandlerFunc
27+
}
28+
29+
func Test_RunDeleteTestSuite(t *testing.T) {
30+
suite.Run(t, new(deleteTestSuite))
31+
}
32+
33+
func (s *deleteTestSuite) SetupTest() {
34+
config := configMocks.NewConfig(s.T())
35+
config.EXPECT().UnmarshalKey("crud", mock.AnythingOfType("*crud.Settings")).Run(func(key string, val any, additionalDefaults ...cfg.UnmarshalDefaults) {
36+
settings := val.(*crud.Settings)
37+
settings.WriteTimeout = time.Minute
38+
})
39+
40+
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(s.T()))
41+
42+
s.handler = newHandler(s.T())
43+
s.deleteHandler = crud.NewDeleteHandler(config, logger, s.handler)
44+
}
45+
46+
func (s *deleteTestSuite) TestDelete() {
47+
model := &Model{}
48+
deleteModel := &Model{
49+
Model: db_repo.Model{
50+
Id: mdl.Box(uint(1)),
51+
Timestamps: db_repo.Timestamps{
52+
UpdatedAt: &time.Time{},
53+
CreatedAt: &time.Time{},
54+
},
55+
},
56+
Name: mdl.Box("foobar"),
57+
}
58+
59+
s.handler.Repo.On("Read", mock.AnythingOfType("*exec.stoppableContext"), mock.AnythingOfType("*uint"), model).Run(func(args mock.Arguments) {
60+
model := args.Get(2).(*Model)
61+
model.Id = mdl.Box(uint(1))
62+
model.Name = mdl.Box("foobar")
63+
model.UpdatedAt = &time.Time{}
64+
model.CreatedAt = &time.Time{}
65+
}).Return(nil)
66+
s.handler.Repo.On("Delete", mock.AnythingOfType("*exec.stoppableContext"), deleteModel).Return(nil)
67+
68+
response := httpserver.HttpTest("DELETE", "/:id", "/1", "", s.deleteHandler)
69+
70+
s.Equal(http.StatusOK, response.Code)
71+
s.JSONEq(`{"id":1,"updatedAt":"0001-01-01T00:00:00Z","createdAt":"0001-01-01T00:00:00Z","name":"foobar"}`, response.Body.String())
72+
}
73+
74+
func (s *deleteTestSuite) TestDelete_ValidationError() {
75+
model := &Model{}
76+
deleteModel := &Model{
77+
Model: db_repo.Model{
78+
Id: mdl.Box(uint(1)),
79+
Timestamps: db_repo.Timestamps{
80+
UpdatedAt: &time.Time{},
81+
CreatedAt: &time.Time{},
82+
},
83+
},
84+
Name: mdl.Box("foobar"),
85+
}
86+
87+
s.handler.Repo.On("Read", mock.AnythingOfType("*exec.stoppableContext"), mock.AnythingOfType("*uint"), model).Run(func(args mock.Arguments) {
88+
model := args.Get(2).(*Model)
89+
model.Id = mdl.Box(uint(1))
90+
model.Name = mdl.Box("foobar")
91+
model.UpdatedAt = &time.Time{}
92+
model.CreatedAt = &time.Time{}
93+
}).Return(nil)
94+
s.handler.Repo.On("Delete", mock.AnythingOfType("*exec.stoppableContext"), deleteModel).Return(&validation.Error{
95+
Errors: []error{fmt.Errorf("invalid foobar")},
96+
})
97+
98+
response := httpserver.HttpTest("DELETE", "/:id", "/1", "", s.deleteHandler)
99+
100+
s.Equal(http.StatusBadRequest, response.Code)
101+
s.JSONEq(`{"err":"validation: invalid foobar"}`, response.Body.String())
102+
}

Diff for: ‎pkg/httpserver/crud/errors.go

+81-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,85 @@
11
package crud
22

3-
import "fmt"
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
9+
"github.com/justtrackio/gosoline/pkg/db"
10+
"github.com/justtrackio/gosoline/pkg/db-repo"
11+
"github.com/justtrackio/gosoline/pkg/exec"
12+
"github.com/justtrackio/gosoline/pkg/httpserver"
13+
"github.com/justtrackio/gosoline/pkg/log"
14+
"github.com/justtrackio/gosoline/pkg/validation"
15+
)
16+
17+
const HttpStatusClientClosedRequest = 499
418

519
var ErrModelNotChanged = fmt.Errorf("nothing has changed on model")
20+
21+
// handleErrorOnWrite handles errors for read operations.
22+
// Covers many default errors and responses like
23+
// - context.Canceled, context.DeadlineExceed -> HTTP 499
24+
// - db_repo.RecordNotFoundError | db_repo.NoQueryResultsError -> HTTP 404
25+
// - validation.Error -> HTTP 400
26+
func handleErrorOnRead(logger log.Logger, err error) (*httpserver.Response, error) {
27+
if exec.IsRequestCanceled(err) {
28+
logger.Info("read model(s) aborted: %s", err.Error())
29+
30+
return httpserver.NewStatusResponse(HttpStatusClientClosedRequest), nil
31+
}
32+
33+
if db_repo.IsRecordNotFoundError(err) || db_repo.IsNoQueryResultsError(err) {
34+
logger.Warn("failed to read model(s): %s", err.Error())
35+
36+
return httpserver.NewStatusResponse(http.StatusNotFound), nil
37+
}
38+
39+
if errors.Is(err, &validation.Error{}) {
40+
return httpserver.GetErrorHandler()(http.StatusBadRequest, err), nil
41+
}
42+
43+
// rely on the outside handling of access forbidden and HTTP 500
44+
return nil, err
45+
}
46+
47+
// handleErrorOnWrite handles errors for write operations.
48+
// Covers many default errors and responses like
49+
// - context.Canceled, context.DeadlineExceed -> HTTP 500
50+
// - db_repo.RecordNotFoundError | db_repo.NoQueryResultsError -> HTTP 404
51+
// - ErrModelNotChanged -> HTTP 304
52+
// - db.IsDuplicateEntryError -> HTTP 409
53+
// - validation.Error -> HTTP 400
54+
func handleErrorOnWrite(ctx context.Context, logger log.Logger, err error) (*httpserver.Response, error) {
55+
logger = logger.WithContext(ctx)
56+
57+
if exec.IsRequestCanceled(err) {
58+
logger.Error("failed to update model(s): %w", err)
59+
60+
return httpserver.NewStatusResponse(http.StatusInternalServerError), nil
61+
}
62+
63+
if db_repo.IsRecordNotFoundError(err) || db_repo.IsNoQueryResultsError(err) {
64+
logger.Warn("failed to fetch model(s): %s", err.Error())
65+
66+
return httpserver.NewStatusResponse(http.StatusNotFound), nil
67+
}
68+
69+
if errors.Is(err, ErrModelNotChanged) {
70+
logger.Info("model(s) unchanged, rejecting update")
71+
72+
return httpserver.NewStatusResponse(http.StatusNotModified), nil
73+
}
74+
75+
if db.IsDuplicateEntryError(err) {
76+
return httpserver.NewStatusResponse(http.StatusConflict), nil
77+
}
78+
79+
if errors.Is(err, &validation.Error{}) {
80+
return httpserver.GetErrorHandler()(http.StatusBadRequest, err), nil
81+
}
82+
83+
// rely on the outside handling of access forbidden and HTTP 500
84+
return nil, err
85+
}

Diff for: ‎pkg/httpserver/crud/handler.go

+36-24
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,34 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"time"
78

89
"github.com/jinzhu/inflection"
10+
"github.com/justtrackio/gosoline/pkg/cfg"
911
"github.com/justtrackio/gosoline/pkg/db-repo"
1012
"github.com/justtrackio/gosoline/pkg/httpserver"
1113
"github.com/justtrackio/gosoline/pkg/log"
1214
)
1315

14-
const DefaultApiView = "api"
16+
const (
17+
SettingsConfigKey = "crud"
18+
DefaultApiView = "api"
19+
)
20+
21+
// Settings structure for all CRUDL handler.
22+
type Settings struct {
23+
// Applies to create, update and delete handlers.
24+
// Write timeout is the maximum duration before canceling any write operation.
25+
WriteTimeout time.Duration `cfg:"write_timeout" default:"10min" validate:"min=1000000000"`
26+
}
1527

1628
//go:generate mockery --name Repository
1729
type Repository interface {
1830
Create(ctx context.Context, value db_repo.ModelBased) error
1931
Read(ctx context.Context, id *uint, out db_repo.ModelBased) error
2032
Update(ctx context.Context, value db_repo.ModelBased) error
2133
Delete(ctx context.Context, value db_repo.ModelBased) error
22-
Query(ctx context.Context, qb *db_repo.QueryBuilder, result interface{}) error
34+
Query(ctx context.Context, qb *db_repo.QueryBuilder, result any) error
2335
Count(ctx context.Context, qb *db_repo.QueryBuilder, model db_repo.ModelBased) (int, error)
2436
GetMetadata() db_repo.Metadata
2537
}
@@ -28,13 +40,13 @@ type Repository interface {
2840
type BaseHandler interface {
2941
GetRepository() Repository
3042
GetModel() db_repo.ModelBased
31-
TransformOutput(ctx context.Context, model db_repo.ModelBased, apiView string) (output interface{}, err error)
43+
TransformOutput(ctx context.Context, model db_repo.ModelBased, apiView string) (output any, err error)
3244
}
3345

3446
//go:generate mockery --name BaseCreateHandler
3547
type BaseCreateHandler interface {
36-
GetCreateInput() interface{}
37-
TransformCreate(ctx context.Context, input interface{}, model db_repo.ModelBased) (err error)
48+
GetCreateInput() any
49+
TransformCreate(ctx context.Context, input any, model db_repo.ModelBased) (err error)
3850
}
3951

4052
//go:generate mockery --name CreateHandler
@@ -45,8 +57,8 @@ type CreateHandler interface {
4557

4658
//go:generate mockery --name BaseUpdateHandler
4759
type BaseUpdateHandler interface {
48-
GetUpdateInput() interface{}
49-
TransformUpdate(ctx context.Context, input interface{}, model db_repo.ModelBased) (err error)
60+
GetUpdateInput() any
61+
TransformUpdate(ctx context.Context, input any, model db_repo.ModelBased) (err error)
5062
}
5163

5264
//go:generate mockery --name UpdateHandler
@@ -57,7 +69,7 @@ type UpdateHandler interface {
5769

5870
//go:generate mockery --name BaseListHandler
5971
type BaseListHandler interface {
60-
List(ctx context.Context, qb *db_repo.QueryBuilder, apiView string) (out interface{}, err error)
72+
List(ctx context.Context, qb *db_repo.QueryBuilder, apiView string) (out any, err error)
6173
}
6274

6375
//go:generate mockery --name ListHandler
@@ -74,42 +86,42 @@ type Handler interface {
7486
BaseListHandler
7587
}
7688

77-
func AddCrudHandlers(logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler Handler) {
78-
AddCreateHandler(logger, d, version, basePath, handler)
79-
AddReadHandler(logger, d, version, basePath, handler)
80-
AddUpdateHandler(logger, d, version, basePath, handler)
81-
AddDeleteHandler(logger, d, version, basePath, handler)
82-
AddListHandler(logger, d, version, basePath, handler)
89+
func AddCrudHandlers(config cfg.Config, logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler Handler) {
90+
AddCreateHandler(config, logger, d, version, basePath, handler)
91+
AddReadHandler(config, logger, d, version, basePath, handler)
92+
AddUpdateHandler(config, logger, d, version, basePath, handler)
93+
AddDeleteHandler(config, logger, d, version, basePath, handler)
94+
AddListHandler(config, logger, d, version, basePath, handler)
8395
}
8496

85-
func AddCreateHandler(logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler CreateHandler) {
97+
func AddCreateHandler(config cfg.Config, logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler CreateHandler) {
8698
path, _ := getHandlerPaths(version, basePath)
8799

88-
d.POST(path, NewCreateHandler(logger, handler))
100+
d.POST(path, NewCreateHandler(config, logger, handler))
89101
}
90102

91-
func AddReadHandler(logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler BaseHandler) {
103+
func AddReadHandler(config cfg.Config, logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler BaseHandler) {
92104
_, idPath := getHandlerPaths(version, basePath)
93105

94-
d.GET(idPath, NewReadHandler(logger, handler))
106+
d.GET(idPath, NewReadHandler(config, logger, handler))
95107
}
96108

97-
func AddUpdateHandler(logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler UpdateHandler) {
109+
func AddUpdateHandler(config cfg.Config, logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler UpdateHandler) {
98110
_, idPath := getHandlerPaths(version, basePath)
99111

100-
d.PUT(idPath, NewUpdateHandler(logger, handler))
112+
d.PUT(idPath, NewUpdateHandler(config, logger, handler))
101113
}
102114

103-
func AddDeleteHandler(logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler BaseHandler) {
115+
func AddDeleteHandler(config cfg.Config, logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler BaseHandler) {
104116
_, idPath := getHandlerPaths(version, basePath)
105117

106-
d.DELETE(idPath, NewDeleteHandler(logger, handler))
118+
d.DELETE(idPath, NewDeleteHandler(config, logger, handler))
107119
}
108120

109-
func AddListHandler(logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler ListHandler) {
121+
func AddListHandler(config cfg.Config, logger log.Logger, d *httpserver.Definitions, version int, basePath string, handler ListHandler) {
110122
plural := inflection.Plural(basePath)
111123
path := fmt.Sprintf("/v%d/%s", version, plural)
112-
d.POST(path, NewListHandler(logger, handler))
124+
d.POST(path, NewListHandler(config, logger, handler))
113125
}
114126

115127
func getHandlerPaths(version int, basePath string) (path string, idPath string) {

Diff for: ‎pkg/httpserver/crud/list.go

+13-7
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,24 @@ import (
44
"context"
55

66
"github.com/gin-gonic/gin"
7+
"github.com/justtrackio/gosoline/pkg/cfg"
78
"github.com/justtrackio/gosoline/pkg/httpserver"
89
"github.com/justtrackio/gosoline/pkg/httpserver/sql"
910
"github.com/justtrackio/gosoline/pkg/log"
11+
"github.com/justtrackio/gosoline/pkg/validation"
1012
)
1113

1214
type Output struct {
13-
Total int `json:"total"`
14-
Results interface{} `json:"results"`
15+
Total int `json:"total"`
16+
Results any `json:"results"`
1517
}
1618

1719
type listHandler struct {
1820
transformer ListHandler
1921
logger log.Logger
2022
}
2123

22-
func NewListHandler(logger log.Logger, transformer ListHandler) gin.HandlerFunc {
24+
func NewListHandler(_ cfg.Config, logger log.Logger, transformer ListHandler) gin.HandlerFunc {
2325
lh := listHandler{
2426
transformer: transformer,
2527
logger: logger,
@@ -28,32 +30,36 @@ func NewListHandler(logger log.Logger, transformer ListHandler) gin.HandlerFunc
2830
return httpserver.CreateJsonHandler(lh)
2931
}
3032

31-
func (lh listHandler) GetInput() interface{} {
33+
func (lh listHandler) GetInput() any {
3234
return sql.NewInput()
3335
}
3436

3537
func (lh listHandler) Handle(ctx context.Context, request *httpserver.Request) (*httpserver.Response, error) {
3638
inp := request.Body.(*sql.Input)
3739

40+
logger := lh.logger.WithContext(ctx)
41+
3842
repo := lh.transformer.GetRepository()
3943
metadata := repo.GetMetadata()
4044

4145
lqb := sql.NewOrmQueryBuilder(metadata)
4246
qb, err := lqb.Build(inp)
4347
if err != nil {
44-
return nil, err
48+
return handleErrorOnRead(logger, &validation.Error{
49+
Errors: []error{err},
50+
})
4551
}
4652

4753
apiView := GetApiViewFromHeader(request.Header)
4854
results, err := lh.transformer.List(ctx, qb, apiView)
4955
if err != nil {
50-
return nil, err
56+
return handleErrorOnRead(logger, err)
5157
}
5258

5359
model := lh.transformer.GetModel()
5460
total, err := repo.Count(ctx, qb, model)
5561
if err != nil {
56-
return nil, err
62+
return handleErrorOnRead(logger, err)
5763
}
5864

5965
out := Output{

Diff for: ‎pkg/httpserver/crud/list_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package crud_test
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
configMocks "github.com/justtrackio/gosoline/pkg/cfg/mocks"
8+
"github.com/justtrackio/gosoline/pkg/db-repo"
9+
"github.com/justtrackio/gosoline/pkg/httpserver"
10+
"github.com/justtrackio/gosoline/pkg/httpserver/crud"
11+
logMocks "github.com/justtrackio/gosoline/pkg/log/mocks"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/mock"
14+
)
15+
16+
func TestListHandler_Handle(t *testing.T) {
17+
config := configMocks.NewConfig(t)
18+
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(t))
19+
transformer := newHandler(t)
20+
handler := crud.NewListHandler(config, logger, transformer)
21+
22+
qb := db_repo.NewQueryBuilder()
23+
qb.Table("footable")
24+
qb.Where("(((name = ?)))", "foobar")
25+
qb.GroupBy("id")
26+
qb.OrderBy("name", "ASC")
27+
qb.Page(0, 2)
28+
29+
transformer.Repo.EXPECT().GetMetadata().Return(db_repo.Metadata{
30+
TableName: "footable",
31+
PrimaryKey: "id",
32+
Mappings: db_repo.FieldMappings{
33+
"id": db_repo.NewFieldMapping("id"),
34+
"name": db_repo.NewFieldMapping("name"),
35+
},
36+
})
37+
transformer.Repo.EXPECT().Count(mock.AnythingOfType("context.backgroundCtx"), qb, &Model{}).Return(1, nil)
38+
39+
body := `{"filter":{"matches":[{"values":["foobar"],"dimension":"name","operator":"="}],"bool":"and"},"order":[{"field":"name","direction":"ASC"}],"page":{"offset":0,"limit":2}}`
40+
response := httpserver.HttpTest("PUT", "/:id", "/1", body, handler)
41+
42+
assert.Equal(t, http.StatusOK, response.Code)
43+
assert.JSONEq(t, `{"total":1,"results":[{"Id":1,"UpdatedAt":"2006-01-02T15:04:05Z","CreatedAt":"2006-01-02T15:04:05Z","name":"foobar"}]}`, response.Body.String())
44+
45+
transformer.Repo.AssertExpectations(t)
46+
}

Diff for: ‎pkg/httpserver/crud/read.go

+16-14
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ package crud
22

33
import (
44
"context"
5-
"net/http"
65

76
"github.com/gin-gonic/gin"
8-
"github.com/justtrackio/gosoline/pkg/db-repo"
7+
"github.com/justtrackio/gosoline/pkg/cfg"
98
"github.com/justtrackio/gosoline/pkg/httpserver"
109
"github.com/justtrackio/gosoline/pkg/log"
10+
"github.com/justtrackio/gosoline/pkg/validation"
1111
"github.com/pkg/errors"
1212
)
1313

@@ -16,7 +16,7 @@ type readHandler struct {
1616
transformer BaseHandler
1717
}
1818

19-
func NewReadHandler(logger log.Logger, transformer BaseHandler) gin.HandlerFunc {
19+
func NewReadHandler(_ cfg.Config, logger log.Logger, transformer BaseHandler) gin.HandlerFunc {
2020
rh := readHandler{
2121
transformer: transformer,
2222
logger: logger,
@@ -26,31 +26,33 @@ func NewReadHandler(logger log.Logger, transformer BaseHandler) gin.HandlerFunc
2626
}
2727

2828
func (rh readHandler) Handle(ctx context.Context, request *httpserver.Request) (*httpserver.Response, error) {
29+
logger := rh.logger.WithContext(ctx)
30+
2931
id, valid := httpserver.GetUintFromRequest(request, "id")
3032

3133
if !valid {
32-
return nil, errors.New("no valid id provided")
34+
return handleErrorOnRead(logger, &validation.Error{
35+
Errors: []error{
36+
errors.New("no valid id provided"),
37+
},
38+
})
3339
}
3440

41+
logger = rh.logger.WithFields(log.Fields{
42+
"entity_id": id,
43+
})
44+
3545
repo := rh.transformer.GetRepository()
3646
model := rh.transformer.GetModel()
3747
err := repo.Read(ctx, id, model)
38-
39-
var notFound db_repo.RecordNotFoundError
40-
if errors.As(err, &notFound) {
41-
rh.logger.WithContext(ctx).Warn("failed to read model: %s", err)
42-
43-
return httpserver.NewStatusResponse(http.StatusNotFound), nil
44-
}
45-
4648
if err != nil {
47-
return nil, err
49+
return handleErrorOnRead(logger, err)
4850
}
4951

5052
apiView := GetApiViewFromHeader(request.Header)
5153
out, err := rh.transformer.TransformOutput(ctx, model, apiView)
5254
if err != nil {
53-
return nil, err
55+
return handleErrorOnRead(logger, err)
5456
}
5557

5658
return httpserver.NewJsonResponse(out), nil

Diff for: ‎pkg/httpserver/crud/read_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package crud_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"testing"
7+
"time"
8+
9+
configMocks "github.com/justtrackio/gosoline/pkg/cfg/mocks"
10+
"github.com/justtrackio/gosoline/pkg/db-repo"
11+
"github.com/justtrackio/gosoline/pkg/httpserver"
12+
"github.com/justtrackio/gosoline/pkg/httpserver/crud"
13+
logMocks "github.com/justtrackio/gosoline/pkg/log/mocks"
14+
"github.com/justtrackio/gosoline/pkg/mdl"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/mock"
17+
)
18+
19+
func TestReadHandler_Handle(t *testing.T) {
20+
model := &Model{}
21+
22+
config := configMocks.NewConfig(t)
23+
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(t))
24+
transformer := newHandler(t)
25+
transformer.Repo.EXPECT().Read(mock.AnythingOfType("context.backgroundCtx"), mdl.Box(uint(1)), model).Run(func(_ context.Context, _ *uint, out db_repo.ModelBased) {
26+
model := out.(*Model)
27+
model.Id = mdl.Box(uint(1))
28+
model.Name = mdl.Box("foobar")
29+
model.UpdatedAt = &time.Time{}
30+
model.CreatedAt = &time.Time{}
31+
}).Return(nil)
32+
33+
handler := crud.NewReadHandler(config, logger, transformer)
34+
35+
response := httpserver.HttpTest("GET", "/:id", "/1", "", handler)
36+
37+
assert.Equal(t, http.StatusOK, response.Code)
38+
assert.JSONEq(t, `{"id":1,"updatedAt":"0001-01-01T00:00:00Z","createdAt":"0001-01-01T00:00:00Z","name":"foobar"}`, response.Body.String())
39+
40+
transformer.Repo.AssertExpectations(t)
41+
}

Diff for: ‎pkg/httpserver/crud/update.go

+32-38
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ package crud
33
import (
44
"context"
55
"errors"
6-
"net/http"
76

87
"github.com/gin-gonic/gin"
9-
"github.com/justtrackio/gosoline/pkg/db"
10-
"github.com/justtrackio/gosoline/pkg/db-repo"
8+
"github.com/justtrackio/gosoline/pkg/cfg"
9+
"github.com/justtrackio/gosoline/pkg/exec"
1110
"github.com/justtrackio/gosoline/pkg/httpserver"
1211
"github.com/justtrackio/gosoline/pkg/log"
1312
"github.com/justtrackio/gosoline/pkg/validation"
@@ -16,82 +15,77 @@ import (
1615
type updateHandler struct {
1716
logger log.Logger
1817
transformer UpdateHandler
18+
settings Settings
1919
}
2020

21-
func NewUpdateHandler(logger log.Logger, transformer UpdateHandler) gin.HandlerFunc {
21+
func NewUpdateHandler(config cfg.Config, logger log.Logger, transformer UpdateHandler) gin.HandlerFunc {
22+
settings := Settings{}
23+
config.UnmarshalKey(SettingsConfigKey, &settings)
24+
2225
uh := updateHandler{
2326
transformer: transformer,
2427
logger: logger,
28+
settings: settings,
2529
}
2630

2731
return httpserver.CreateJsonHandler(uh)
2832
}
2933

30-
func (uh updateHandler) GetInput() interface{} {
34+
func (uh updateHandler) GetInput() any {
3135
return uh.transformer.GetUpdateInput()
3236
}
3337

34-
func (uh updateHandler) Handle(ctx context.Context, request *httpserver.Request) (*httpserver.Response, error) {
38+
func (uh updateHandler) Handle(reqCtx context.Context, request *httpserver.Request) (*httpserver.Response, error) {
39+
// replace context with a new one to prevent cancellations from client side
40+
// include a new timeout to ensure that requests will be cancelled
41+
ctx, cancel := exec.WithDelayedCancelContext(reqCtx, uh.settings.WriteTimeout)
42+
defer cancel()
43+
44+
logger := uh.logger.WithContext(ctx)
45+
3546
id, valid := httpserver.GetUintFromRequest(request, "id")
3647

3748
if !valid {
38-
return nil, errors.New("no valid id provided")
49+
return handleErrorOnWrite(ctx, logger, &validation.Error{
50+
Errors: []error{
51+
errors.New("no valid id provided"),
52+
},
53+
})
3954
}
4055

56+
logger = logger.WithFields(log.Fields{
57+
"entity_id": id,
58+
})
59+
4160
repo := uh.transformer.GetRepository()
4261
model := uh.transformer.GetModel()
43-
err := repo.Read(ctx, id, model)
44-
45-
var notFound db_repo.RecordNotFoundError
46-
if errors.As(err, &notFound) {
47-
uh.logger.WithContext(ctx).Warn("failed to update model: %s", err.Error())
48-
49-
return httpserver.NewStatusResponse(http.StatusNotFound), nil
50-
}
5162

63+
err := repo.Read(ctx, id, model)
5264
if err != nil {
53-
return nil, err
65+
return handleErrorOnWrite(ctx, logger, err)
5466
}
5567

5668
err = uh.transformer.TransformUpdate(ctx, request.Body, model)
57-
58-
if modelNotChanged(err) {
59-
return httpserver.NewStatusResponse(http.StatusNotModified), nil
60-
}
61-
6269
if err != nil {
63-
return nil, err
70+
return handleErrorOnWrite(ctx, logger, err)
6471
}
6572

6673
err = repo.Update(ctx, model)
67-
68-
if db.IsDuplicateEntryError(err) {
69-
return httpserver.NewStatusResponse(http.StatusConflict), nil
70-
}
71-
72-
if errors.Is(err, &validation.Error{}) {
73-
return httpserver.GetErrorHandler()(http.StatusBadRequest, err), nil
74-
}
75-
7674
if err != nil {
77-
return nil, err
75+
return handleErrorOnWrite(ctx, logger, err)
7876
}
7977

8078
reload := uh.transformer.GetModel()
8179
err = repo.Read(ctx, model.GetId(), reload)
8280
if err != nil {
83-
return nil, err
81+
return handleErrorOnWrite(ctx, logger, err)
8482
}
8583

8684
apiView := GetApiViewFromHeader(request.Header)
8785
out, err := uh.transformer.TransformOutput(ctx, reload, apiView)
8886
if err != nil {
89-
return nil, err
87+
return handleErrorOnWrite(ctx, logger, err)
9088
}
9189

9290
return httpserver.NewJsonResponse(out), nil
9391
}
94-
95-
func modelNotChanged(err error) bool {
96-
return errors.Is(err, ErrModelNotChanged)
97-
}

Diff for: ‎pkg/httpserver/crud/update_test.go

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package crud_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"testing"
8+
"time"
9+
10+
"github.com/gin-gonic/gin"
11+
"github.com/justtrackio/gosoline/pkg/cfg"
12+
configMocks "github.com/justtrackio/gosoline/pkg/cfg/mocks"
13+
"github.com/justtrackio/gosoline/pkg/db-repo"
14+
"github.com/justtrackio/gosoline/pkg/httpserver"
15+
"github.com/justtrackio/gosoline/pkg/httpserver/crud"
16+
logMocks "github.com/justtrackio/gosoline/pkg/log/mocks"
17+
"github.com/justtrackio/gosoline/pkg/mdl"
18+
"github.com/justtrackio/gosoline/pkg/validation"
19+
"github.com/stretchr/testify/mock"
20+
"github.com/stretchr/testify/suite"
21+
)
22+
23+
type updateTestSuite struct {
24+
suite.Suite
25+
26+
handler handler
27+
updateHandler gin.HandlerFunc
28+
}
29+
30+
func Test_RunUpdateTestSuite(t *testing.T) {
31+
suite.Run(t, new(updateTestSuite))
32+
}
33+
34+
func (s *updateTestSuite) SetupTest() {
35+
config := configMocks.NewConfig(s.T())
36+
config.EXPECT().UnmarshalKey("crud", mock.AnythingOfType("*crud.Settings")).Run(func(key string, val any, additionalDefaults ...cfg.UnmarshalDefaults) {
37+
settings := val.(*crud.Settings)
38+
settings.WriteTimeout = time.Minute
39+
})
40+
41+
logger := logMocks.NewLoggerMock(logMocks.WithMockAll, logMocks.WithTestingT(s.T()))
42+
43+
s.handler = newHandler(s.T())
44+
s.updateHandler = crud.NewUpdateHandler(config, logger, s.handler)
45+
}
46+
47+
func (s *updateTestSuite) TestUpdate() {
48+
readModel := &Model{}
49+
updateModel := &Model{
50+
Model: db_repo.Model{
51+
Id: mdl.Box(uint(1)),
52+
Timestamps: db_repo.Timestamps{
53+
UpdatedAt: &time.Time{},
54+
CreatedAt: &time.Time{},
55+
},
56+
},
57+
Name: mdl.Box("updated"),
58+
}
59+
60+
s.handler.Repo.On("Update", mock.AnythingOfType("*exec.stoppableContext"), updateModel).Return(nil)
61+
s.handler.Repo.On("Read", mock.AnythingOfType("*exec.stoppableContext"), mdl.Box(uint(1)), readModel).Run(func(args mock.Arguments) {
62+
model := args.Get(2).(*Model)
63+
model.Id = mdl.Box(uint(1))
64+
model.Name = mdl.Box("updated")
65+
model.UpdatedAt = &time.Time{}
66+
model.CreatedAt = &time.Time{}
67+
}).Return(nil)
68+
69+
body := `{"name": "updated"}`
70+
response := httpserver.HttpTest("PUT", "/:id", "/1", body, s.updateHandler)
71+
72+
s.Equal(http.StatusOK, response.Code)
73+
s.JSONEq(`{"id":1,"updatedAt":"0001-01-01T00:00:00Z","createdAt":"0001-01-01T00:00:00Z","name":"updated"}`, response.Body.String())
74+
}
75+
76+
func (s *updateTestSuite) TestUpdate_ValidationError() {
77+
readModel := &Model{}
78+
updateModel := &Model{
79+
Model: db_repo.Model{
80+
Id: mdl.Box(uint(1)),
81+
Timestamps: db_repo.Timestamps{
82+
UpdatedAt: &time.Time{},
83+
CreatedAt: &time.Time{},
84+
},
85+
},
86+
Name: mdl.Box("updated"),
87+
}
88+
89+
s.handler.Repo.EXPECT().Update(mock.AnythingOfType("*exec.stoppableContext"), updateModel).Return(&validation.Error{
90+
Errors: []error{fmt.Errorf("invalid foobar")},
91+
})
92+
s.handler.Repo.EXPECT().Read(mock.AnythingOfType("*exec.stoppableContext"), mdl.Box(uint(1)), readModel).Run(func(_ context.Context, _ *uint, out db_repo.ModelBased) {
93+
model := out.(*Model)
94+
model.Id = mdl.Box(uint(1))
95+
model.Name = mdl.Box("updated")
96+
model.UpdatedAt = &time.Time{}
97+
model.CreatedAt = &time.Time{}
98+
}).Return(nil)
99+
100+
body := `{"name": "updated"}`
101+
response := httpserver.HttpTest("PUT", "/:id", "/1", body, s.updateHandler)
102+
103+
s.Equal(http.StatusBadRequest, response.Code)
104+
s.JSONEq(`{"err":"validation: invalid foobar"}`, response.Body.String())
105+
}

0 commit comments

Comments
 (0)
Please sign in to comment.