From 50965686116103a349ac677c44c9c0a425399077 Mon Sep 17 00:00:00 2001 From: Fabio Bonelli Date: Tue, 5 Dec 2023 23:56:42 +0100 Subject: [PATCH] feat: implement JSON Patch for Software resource --- internal/handlers/software.go | 39 +++++++++++----- main_test.go | 85 +++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 11 deletions(-) diff --git a/internal/handlers/software.go b/internal/handlers/software.go index cb83a92..a5940d8 100644 --- a/internal/handlers/software.go +++ b/internal/handlers/software.go @@ -30,8 +30,9 @@ type Software struct { } var ( - errLoadNotFound = errors.New("Software was not found") - errLoad = errors.New("error while loading Software") + errLoadNotFound = errors.New("Software was not found") + errLoad = errors.New("error while loading Software") + errMalformedJsonPatch = errors.New("malformed JSON Patch") ) func NewSoftware(db *gorm.DB) *Software { @@ -174,7 +175,6 @@ func (p *Software) PostSoftware(ctx *fiber.Ctx) error { func (p *Software) PatchSoftware(ctx *fiber.Ctx) error { //nolint:funlen,cyclop const errMsg = "can't update Software" - softwareReq := common.SoftwarePatch{} software := models.Software{} if err := loadSoftware(p.db, &software, ctx.Params("id")); err != nil { @@ -185,20 +185,37 @@ func (p *Software) PatchSoftware(ctx *fiber.Ctx) error { //nolint:funlen,cyclop return common.Error(fiber.StatusInternalServerError, errMsg, err.Error()) } - if err := common.ValidateRequestEntity(ctx, &softwareReq, errMsg); err != nil { - return err //nolint:wrapcheck - } - softwareJSON, err := json.Marshal(&software) if err != nil { return common.Error(fiber.StatusInternalServerError, errMsg, err.Error()) } - updatedJSON, err := jsonpatch.MergePatch(softwareJSON, ctx.Body()) - if err != nil { - return common.Error(fiber.StatusInternalServerError, errMsg, err.Error()) - } + var updatedJSON []byte + switch ctx.Get(fiber.HeaderContentType) { + case "application/json-patch+json": + patch, err := jsonpatch.DecodePatch(ctx.Body()) + if err != nil { + return common.Error(fiber.StatusBadRequest, errMsg, errMalformedJsonPatch.Error()) + } + updatedJSON, err = patch.Apply(softwareJSON) + if err != nil { + return common.Error(fiber.StatusUnprocessableEntity, errMsg, err.Error()) + } + + // application/merge-patch+json by default + default: + softwareReq := common.SoftwarePatch{} + if err := common.ValidateRequestEntity(ctx, &softwareReq, errMsg); err != nil { + return err //nolint:wrapcheck + } + + updatedJSON, err = jsonpatch.MergePatch(softwareJSON, ctx.Body()) + if err != nil { + return common.Error(fiber.StatusInternalServerError, errMsg, err.Error()) + } + } + var updatedSoftware models.Software err = json.Unmarshal(updatedJSON, &updatedSoftware) diff --git a/main_test.go b/main_test.go index 32ec52a..5ff010d 100644 --- a/main_test.go +++ b/main_test.go @@ -2078,6 +2078,91 @@ func TestSoftwareEndpoints(t *testing.T) { assert.Greater(t, updated, created) }, }, + { + description: "PATCH a software resource with JSON Patch - replace", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `[{"op": "replace", "path": "/publiccodeYml", "value": "new publiccode data"}]`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json-patch+json"}, + }, + + expectedCode: 200, + expectedContentType: "application/json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, true, response["active"]) + assert.Equal(t, "https://18-a.example.org/code/repo", response["url"]) + + assert.IsType(t, []interface{}{}, response["aliases"]) + + aliases := response["aliases"].([]interface{}) + assert.Equal(t, 1, len(aliases)) + + assert.Equal(t, "https://18-b.example.org/code/repo", aliases[0]) + + assert.Equal(t, "new publiccode data", response["publiccodeYml"]) + assert.Equal(t, "59803fb7-8eec-4fe5-a354-8926009c364a", response["id"]) + + created, err := time.Parse(time.RFC3339, response["createdAt"].(string)) + assert.Nil(t, err) + + updated, err := time.Parse(time.RFC3339, response["updatedAt"].(string)) + assert.Nil(t, err) + + assert.Greater(t, updated, created) + }, + }, + // { + // description: "PATCH a software resource with JSON Patch - add", + // query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + // body: `[{"op": "add", "path": "/aliases", "value": "https://18-c.example.org"]`, + // headers: map[string][]string{ + // "Authorization": {goodToken}, + // "Content-Type": {"application/json-patch+json"}, + // }, + + // expectedCode: 200, + // expectedContentType: "application/json", + // validateFunc: func(t *testing.T, response map[string]interface{}) { + // assert.Equal(t, true, response["active"]) + // assert.Equal(t, "https://18-a.example.org/code/repo", response["url"]) + + // assert.IsType(t, []interface{}{}, response["aliases"]) + + // aliases := response["aliases"].([]interface{}) + // assert.Equal(t, 2, len(aliases)) + + // assert.Equal(t, "https://18-b.example.org/code/repo", aliases[0]) + // assert.Equal(t, "https://18-c.example.org/code/repo", aliases[1]) + + // assert.Equal(t, "publiccodedata", response["publiccodeYml"]) + // assert.Equal(t, "59803fb7-8eec-4fe5-a354-8926009c364a", response["id"]) + + // created, err := time.Parse(time.RFC3339, response["createdAt"].(string)) + // assert.Nil(t, err) + + // updated, err := time.Parse(time.RFC3339, response["updatedAt"].(string)) + // assert.Nil(t, err) + + // assert.Greater(t, updated, created) + // }, + // }, + { + description: "PATCH a software resource with JSON Patch as Content-Type, but non JSON Patch payload", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `{"publiccodeYml": "publiccodedata", "url": "https://software-new.example.org"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json-patch+json"}, + }, + + expectedCode: 400, + expectedContentType: "application/problem+json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, `can't update Software`, response["title"]) + assert.Equal(t, "malformed JSON Patch", response["detail"]) + }, + }, { description: "PATCH software using an already taken URL as url", query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a",