Skip to content

Commit

Permalink
feat: implement JSON Patch for Software resource
Browse files Browse the repository at this point in the history
SECURITY NOTE:
This doesn't implement any authorization on resources, which is good
*for now* as you either have the ability to write to resources or not
have it.

That MUST be implemented when there will be multiple write tokens with
different scopes.
  • Loading branch information
bfabio committed Mar 7, 2024
1 parent fbc80f7 commit b728447
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 10 deletions.
38 changes: 28 additions & 10 deletions internal/handlers/software.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -185,18 +185,36 @@ 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
Expand Down
85 changes: 85 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit b728447

Please sign in to comment.