diff --git a/developers-italia.oas.yaml b/developers-italia.oas.yaml index 0531c81..bfcf830 100644 --- a/developers-italia.oas.yaml +++ b/developers-italia.oas.yaml @@ -1321,6 +1321,21 @@ components: software (fe. when the repository is empty or there's something malicious going on, but the problem is expected to be fixed in the near future) without removing it. default: true + vitality: + type: string + description: > + The vitality of the software repository, indicating + health, activity, or metrics relevant to its condition. + This is a opaque string, and users of the API can choose their + own format. + + The Italian catalog, for example, uses comma-separated values for + daily vitality scores (0-100) (see https://github.com/italia/publiccode-crawler/blob/main/vitality-ranges.yml). + minLength: 1 + maxLength: 99999 + pattern: '.*' + default: null + example: "90,100,94,12" createdAt: type: string format: date-time diff --git a/internal/common/requests.go b/internal/common/requests.go index 95fc904..231cc94 100644 --- a/internal/common/requests.go +++ b/internal/common/requests.go @@ -28,6 +28,7 @@ type SoftwarePost struct { Aliases []string `json:"aliases" validate:"dive,url"` PubliccodeYml string `json:"publiccodeYml" validate:"required"` Active *bool `json:"active"` + Vitality *string `json:"vitality"` } type SoftwarePatch struct { @@ -35,6 +36,7 @@ type SoftwarePatch struct { Aliases *[]string `json:"aliases" validate:"omitempty,dive,url"` PubliccodeYml *string `json:"publiccodeYml"` Active *bool `json:"active"` + Vitality *string `json:"vitality"` } type Log struct { diff --git a/internal/handlers/software.go b/internal/handlers/software.go index 8cafdd2..6fbf366 100644 --- a/internal/handlers/software.go +++ b/internal/handlers/software.go @@ -162,6 +162,7 @@ func (p *Software) PostSoftware(ctx *fiber.Ctx) error { Aliases: aliases, PubliccodeYml: softwareReq.PubliccodeYml, Active: softwareReq.Active, + Vitality: softwareReq.Vitality, } if err := p.db.Create(&software).Error; err != nil { diff --git a/internal/models/models.go b/internal/models/models.go index fa63869..8548480 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -76,6 +76,7 @@ type Software struct { PubliccodeYml string `json:"publiccodeYml"` Logs []Log `json:"-" gorm:"polymorphic:Entity;"` Active *bool `json:"active" gorm:"default:true;not null"` + Vitality *string `json:"vitality"` CreatedAt time.Time `json:"createdAt" gorm:"index"` UpdatedAt time.Time `json:"updatedAt"` } diff --git a/main_test.go b/main_test.go index c9614f2..5d922b9 100644 --- a/main_test.go +++ b/main_test.go @@ -1358,7 +1358,7 @@ func TestSoftwareEndpoints(t *testing.T) { assert.Equal(t, true, firstSoftware["active"]) for key := range firstSoftware { - assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active"}, key) + assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active", "vitality"}, key) } }, }, @@ -1401,7 +1401,7 @@ func TestSoftwareEndpoints(t *testing.T) { assert.Nil(t, err) for key := range firstSoftware { - assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active"}, key) + assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active", "vitality"}, key) } }, }, @@ -1442,7 +1442,7 @@ func TestSoftwareEndpoints(t *testing.T) { assert.Nil(t, err) for key := range firstSoftware { - assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active"}, key) + assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active", "vitality"}, key) } }, }, @@ -1479,7 +1479,7 @@ func TestSoftwareEndpoints(t *testing.T) { assert.Equal(t, "2014-05-01T00:00:00Z", firstSoftware["updatedAt"]) for key := range firstSoftware { - assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active"}, key) + assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active", "vitality"}, key) } }, }, @@ -1670,7 +1670,33 @@ func TestSoftwareEndpoints(t *testing.T) { assert.Nil(t, err) for key := range response { - assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active"}, key) + assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active", "vitality"}, key) + } + }, + }, { + description: "GET software with vitality field", + query: "GET /v1/software/9f135268-a37e-4ead-96ec-e4a24bb9344a", + expectedCode: 200, + expectedContentType: "application/json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.NotEmpty(t, response["publiccodeYml"]) + + assert.Equal(t, "https://2-a.example.org/code/repo", response["url"]) + + assert.IsType(t, []interface{}{}, response["aliases"]) + assert.Equal(t, 1, len(response["aliases"].([]interface{}))) + + match, err := regexp.MatchString(UUID_REGEXP, response["id"].(string)) + assert.Nil(t, err) + assert.True(t, match) + + _, err = time.Parse(time.RFC3339, response["createdAt"].(string)) + assert.Nil(t, err) + _, err = time.Parse(time.RFC3339, response["updatedAt"].(string)) + assert.Nil(t, err) + + for key := range response { + assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active", "vitality"}, key) } }, }, @@ -1705,7 +1731,7 @@ func TestSoftwareEndpoints(t *testing.T) { assert.Equal(t, true, response["active"]) for key := range response { - assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active"}, key) + assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active", "vitality"}, key) } // TODO: check the record was actually created in the database @@ -1745,14 +1771,51 @@ func TestSoftwareEndpoints(t *testing.T) { assert.Nil(t, err) for key := range response { - assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active"}, key) + assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active", "vitality"}, key) } // TODO: check the record was actually created in the database // TODO: check there are no dangling software_urls }, }, + { + description: "POST software with vitality field", + query: "POST /v1/software", + body: `{"publiccodeYml": "-", "url": "https://software.example.net", "vitality": "90,90,90"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 200, + expectedContentType: "application/json", + expectedBody: "x", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, "https://software.example.net", response["url"]) + assert.NotEmpty(t, response["publiccodeYml"]) + + assert.IsType(t, []interface{}{}, response["aliases"]) + assert.Empty(t, response["aliases"].([]interface{})) + + assert.Equal(t, "90,90,90", response["vitality"]) + + match, err := regexp.MatchString(UUID_REGEXP, response["id"].(string)) + assert.Nil(t, err) + assert.True(t, match) + + _, err = time.Parse(time.RFC3339, response["createdAt"].(string)) + assert.Nil(t, err) + + _, err = time.Parse(time.RFC3339, response["updatedAt"].(string)) + assert.Nil(t, err) + for key := range response { + assert.Contains(t, []string{"id", "createdAt", "updatedAt", "url", "aliases", "publiccodeYml", "active", "vitality"}, key) + } + + // TODO: check the record was actually created in the database + // TODO: check there are no dangling software_urls + }, + }, { description: "POST software with invalid payload", query: "POST /v1/software", @@ -2078,6 +2141,44 @@ func TestSoftwareEndpoints(t *testing.T) { assert.Greater(t, updated, created) }, }, + { + description: "PATCH software, vitality", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `{"vitality": "80,90,99"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/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, "-", response["publiccodeYml"]) + assert.Equal(t, "80,90,99", response["vitality"]) + + match, err := regexp.MatchString(UUID_REGEXP, response["id"].(string)) + assert.Nil(t, err) + assert.True(t, match) + + 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 - replace", query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", @@ -2112,6 +2213,42 @@ func TestSoftwareEndpoints(t *testing.T) { assert.Greater(t, updated, created) }, }, + { + description: "PATCH a software resource with JSON Patch - replace vitality", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `[{"op": "replace", "path": "/vitality", "value": "10,11"}]`, + 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.Equal(t, "10,11", response["vitality"]) + + 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, "-", 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", diff --git a/test/testdata/fixtures/software.yml b/test/testdata/fixtures/software.yml index bbc73c9..a42b85a 100644 --- a/test/testdata/fixtures/software.yml +++ b/test/testdata/fixtures/software.yml @@ -7,6 +7,7 @@ - id: 9f135268-a37e-4ead-96ec-e4a24bb9344a publiccode_yml: "-" software_url_id: f22d408f-93a5-411c-9c35-99039514afc4 + vitality: "90,85,90,90" created_at: '2014-05-16T00:00:00+00:00' updated_at: '2014-05-16T00:00:00+00:00' - id: 18348f13-1076-4a1e-b204-ed541b824d64