diff --git a/go.mod b/go.mod index 6d57c97..48278a3 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,6 @@ require ( github.com/gorilla/mux v1.7.3 github.com/italia/publiccode-parser-go v0.0.0-20190819165358-31bf64802d87 github.com/sebbalex/go-vcsurl v0.0.0-20190821183029-e2c014ec6077 + github.com/stretchr/testify v1.3.0 gopkg.in/yaml.v2 v2.2.3 ) diff --git a/go.sum b/go.sum index 6e5011b..c70628e 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ github.com/alranel/go-spdx v0.0.5/go.mod h1:lcAlhyAoH9rjd33PkIx+2gsCIYwDpL4H0WX7 github.com/alranel/go-vcsurl v0.0.0-20190819164520-88a614c7acb4 h1:a+SmZVgzBVoCfNK1LPRyy/rXDn8JiA/8SpMwcaBRRO0= github.com/alranel/go-vcsurl v0.0.0-20190819164520-88a614c7acb4/go.mod h1:tp+e312yiwgu8H4/Ly26J8MevK9lz7BucU4PPlEv0ag= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deadcheat/goblet v1.3.1 h1:OPOYQjHlbPmG8fFCt8GNNGECLVhaJ0n7F7cP5Mh7G/A= github.com/deadcheat/goblet v1.3.1/go.mod h1:IrMNyAwyrVgB30HsND2WgleTUM4wHTS9m40yNY6NJQg= @@ -20,11 +21,13 @@ github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/italia/publiccode-parser-go v0.0.0-20190819165358-31bf64802d87 h1:7KH+erTBwZAvxnwuSHMmNQSyppY1Ol77hOYYQuhKqCw= github.com/italia/publiccode-parser-go v0.0.0-20190819165358-31bf64802d87/go.mod h1:q18a0jHIXgRVNJ12lI5mBHnVlhqLcUdQ2a3888PhTqY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sebbalex/go-vcsurl v0.0.0-20190821183029-e2c014ec6077 h1:G6sZr8RQ9Sfph8umeO+K4Z63lI0VkE/6e320pZExppQ= github.com/sebbalex/go-vcsurl v0.0.0-20190821183029-e2c014ec6077/go.mod h1:kjPbS94KyBQo2epWbw3fSBSUz+pbpODf9QYPWlShy1A= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/thoas/go-funk v0.4.0 h1:KBaa5NL7NMtsFlQaD8nQMbDt1wuM+OOaNQyYNYQFhVo= github.com/thoas/go-funk v0.4.0/go.mod h1:mlR+dHGb+4YgXkf13rkQTuzrneeHANxOm6+ZnEV9HsA= diff --git a/src/errors.go b/src/errors.go new file mode 100644 index 0000000..11a4229 --- /dev/null +++ b/src/errors.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "strings" +) + +// ErrorInvalidKey represents an error caused by an invalid key. +type ErrorInvalidKey struct { + Key string +} + +func (e ErrorInvalidKey) Error() string { + return fmt.Sprintf("invalid key: %s", e.Key) +} + +// ErrorInvalidValue represents an error caused by an invalid value. +type ErrorInvalidValue struct { + Key string + Reason string +} + +func (e ErrorInvalidValue) Error() string { + return fmt.Sprintf("%s: %s", e.Key, e.Reason) +} + +func newErrorInvalidValue(key string, reason string, args ...interface{}) ErrorInvalidValue { + return ErrorInvalidValue{Key: key, Reason: fmt.Sprintf(reason, args...)} +} + +// ErrorParseMulti represents an error caused by a multivalue key. +type ErrorParseMulti []error + +func (es ErrorParseMulti) Error() string { + var ss []string + for _, e := range es { + ss = append(ss, e.Error()) + } + return strings.Join(ss, "\n") +} diff --git a/src/main.go b/src/main.go index 224761c..ce69dbc 100644 --- a/src/main.go +++ b/src/main.go @@ -195,10 +195,13 @@ func (app *App) validate(w http.ResponseWriter, r *http.Request) { log.Printf("Error converting: %v", errConverting) } if errParse != nil { - log.Printf("Error parsing: %v", errParse) + // log.Printf("Error parsing: %v", errParse) w.Header().Set("Content-type", "application/json") w.WriteHeader(http.StatusUnprocessableEntity) - json.NewEncoder(w).Encode(errParse) + + json, _ := json.Marshal(errParse) + w.Write(json) + // json.NewEncoder(w).Encode(errParse) } else { //set response CT based on client accept header //and return respectively content diff --git a/src/main_test.go b/src/main_test.go index cda58c2..1a37dac 100644 --- a/src/main_test.go +++ b/src/main_test.go @@ -1,22 +1,42 @@ package main import ( + "encoding/json" "io/ioutil" "log" + "net/http" + "net/http/httptest" "net/url" "os" - "reflect" - "runtime" + "sort" + "strconv" "strings" "testing" + + "github.com/stretchr/testify/assert" ) +var app App + +type Es []ErrorInvalidValue + +func (a Es) Len() int { return len(a) } +func (a Es) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a Es) Less(i, j int) bool { return a[i].Key < a[j].Key } + +func TestMain(m *testing.M) { + app = App{} + app.init() + code := m.Run() + os.Exit(code) +} + func TestGitHub(t *testing.T) { url, _ := url.Parse("https://github.com/sebbalex/pc-web-validator") - AssertEqual(t, getRawURL(url), "https://raw.githubusercontent.com/sebbalex/pc-web-validator/master/") + assert.Equal(t, getRawURL(url), "https://raw.githubusercontent.com/sebbalex/pc-web-validator/master/") url, _ = url.Parse("https://github.com/sebbalex/pc-web-validator.git") - AssertEqual(t, getRawURL(url), "https://raw.githubusercontent.com/sebbalex/pc-web-validator/master/") + assert.Equal(t, getRawURL(url), "https://raw.githubusercontent.com/sebbalex/pc-web-validator/master/") } func TestConversion(t *testing.T) { @@ -27,25 +47,87 @@ func TestConversion(t *testing.T) { } yml, err := ioutil.ReadAll(fileYML) json, err := ioutil.ReadAll(fileJSON) - AssertEqual(t, string(yaml2json(yml)), strings.TrimSpace(string(json))) + assert.Equal(t, string(yaml2json(yml)), strings.TrimSpace(string(json))) +} + +func TestValidationZeroPayload(t *testing.T) { + req, _ := http.NewRequest("POST", "/pc/validate?disableNetwork=true", nil) + response := executeRequest(req) + checkResponseCode(t, http.StatusOK, response.Code) + if body := response.Body.String(); body != "" { + t.Errorf("Expected an message. Got %s", body) + } +} + +func TestValidationErrWithNoNetwork(t *testing.T) { + var errs []ErrorInvalidValue //[]map[string]interface{} + var errOut []ErrorInvalidValue //[]map[string]interface{} + + fileYML, err := os.Open("../tests/missing_maintenance_contacts.yml") + if err != nil { + log.Fatal(err) + } + out, err := ioutil.ReadFile("../tests/invalid_out.log") + if err != nil { + log.Fatal(err) + } + req, _ := http.NewRequest("POST", "/pc/validate?disableNetwork=false", fileYML) + response := executeRequest(req) + err = json.Unmarshal(out, &errOut) + if err != nil { + log.Fatal(err) + } + err = json.Unmarshal(response.Body.Bytes(), &errs) + if err != nil { + log.Fatal(err) + } + sort.Sort(Es(errs)) + sort.Sort(Es(errOut)) + assert.Equal(t, errs, errOut) } -// AssertEqual checks if values are equal -func AssertEqual(t *testing.T, a interface{}, b interface{}) { - if a == b { - return +func TestValidationWithNoNetwork(t *testing.T) { + checks := []bool{true, false} + for _, check := range checks { + fileYML, err := os.Open("../tests/valid.minimal.yml") + if err != nil { + log.Fatal(err) + } + out, err := ioutil.ReadFile("../tests/valid.minimal.out.yml") + if err != nil { + log.Fatal(err) + } + req, _ := http.NewRequest("POST", "/pc/validate?disableNetwork="+strconv.FormatBool(check), fileYML) + response := executeRequest(req) + checkResponseCode(t, http.StatusOK, response.Code) + assert.Equal(t, string(out), response.Body.String()) } - // debug.PrintStack() - _, fn, line, _ := runtime.Caller(1) - t.Errorf("%s:%d: Received %v (type %v), expected %v (type %v)", fn, line, a, reflect.TypeOf(a), b, reflect.TypeOf(b)) } -// AssertNil checks if a value is nil -func AssertNil(t *testing.T, a interface{}) { - if reflect.ValueOf(a).IsNil() { - return +func TestValidationWithNetwork(t *testing.T) { + fileYML, err := os.Open("../tests/valid.minimal.yml") + if err != nil { + log.Fatal(err) + } + out, err := ioutil.ReadFile("../tests/valid.minimal.out.yml") + if err != nil { + log.Fatal(err) + } + req, _ := http.NewRequest("POST", "/pc/validate", fileYML) + response := executeRequest(req) + checkResponseCode(t, http.StatusOK, response.Code) + assert.Equal(t, string(out), response.Body.String()) +} + +// Utility functions to make mock request and check response +func executeRequest(req *http.Request) *httptest.ResponseRecorder { + rr := httptest.NewRecorder() + app.Router.ServeHTTP(rr, req) + + return rr +} +func checkResponseCode(t *testing.T, expected, actual int) { + if expected != actual { + t.Errorf("Expected response code %d. Got %d\n", expected, actual) } - //debug.PrintStack() - _, fn, line, _ := runtime.Caller(1) - t.Errorf("%s:%d: Received %v (type %v), expected nil", fn, line, a, reflect.TypeOf(a)) } diff --git a/tests/invalid_legal_license.yml b/tests/invalid_legal_license.yml new file mode 100644 index 0000000..8a018ba --- /dev/null +++ b/tests/invalid_legal_license.yml @@ -0,0 +1,166 @@ +publiccodeYmlVersion: "0.1" + +name: Medusa +applicationSuite: MegaProductivitySuite +url: "https://github.com/italia/developers.italia.it.git" # URL of this repository +landingURL: "https://developers.italia.it" + +isBasedOn: "https://github.com/italia/developers.italia.it.git" # The original repository, "Otello" +softwareVersion: "1.0" # Last stable version +releaseDate: 2017-04-15 # Date of last stable software release +logo: tests/img/logo.png +monochromeLogo: tests/img/logo-mono.svg + +inputTypes: + - text/plain +outputTypes: + - text/plain + +platforms: # or Windows, Mac, Linux, etc. + - android + - ios + +categories: + - cloud-management + +usedBy: + - Comune di Firenze + - Comune di Roma + +roadmap: "https://designers.italia.it/roadmap/" + +developmentStatus: development + +softwareType: "standalone" + +intendedAudience: + onlyFor: + - cities + - health-services + - it-ag-agricolo + countries: + - it + - de + unsupportedCountries: # Explicitly unsupported countries + - us + +description: + eng: + localisedName: Medusa + genericName: "Text Editor" + shortDescription: "A really interesting software." + longDescription: > + Very long description of this software, also split + on multiple rows. You should note what the software + is and why one should need it. This is 158 characters. + Very long description of this software, also split + on multiple rows. You should note what the software + is and why one should need it. This is 316 characters. + Very long description of this software, also split + on multiple rows. You should note what the software + is and why one should need it. This is 474 characters. + Very long description of this software, also split + on multiple rows. You should note what the software + is and why one should need it. This is 632 characters. + + documentation: "https://docs.developers.italia.it" + apiDocumentation: "https://developers.italia.it/it/api" + + features: + - Very important feature + - Will run without a problem + - Has zero bugs + - Solves all the problems of the world + screenshots: + - tests/img/sshot1.png + - tests/img/sshot2.png + - tests/img/sshot3.png + videos: # Demo videos of the software + - https://www.youtube.com/watch?v=RaHmGbBOP84 + awards: + - 1st Price Software of the year + freeTags: + - freeTag + - FreeoloTag + +legal: + license: AGPLicense-3.0 # SPDX expression of license + mainCopyrightOwner: City of Chicago + repoOwner: City of Chicago + authorsFile: tests/AUTHORS # file listing copyright information + +maintenance: + type: "contract" + + contractors: + - name: "Fornitore Privato SPA" # if maintainance is a contract + website: "https://developers.italia.it" + until: "2019-01-01" + + contacts: + - name: Francesco Rossi + email: "francesco.rossi@comune.reggioemilia.it" + affiliation: Comune di Reggio Emilia + phone: +39 231 13215112 + - name: Dario Bianchi + email: "dario.bianchi@fornitore.it" + affiliation: Fornitore Privato S.P.A. + phone: +39 16 24231322 + - name: Giancarlo Verdi + email: "dario.bianchi@fornitore.it" + affiliation: Fornitore Privato S.P.A. + phone: +39 16 24231322 + +localisation: + # Does the software support, at least by design, multiple languages? + localisationReady: yes + # Languages already available + availableLanguages: + - eng + - ita + - fra + - deu + +dependsOn: # List of dependencies. The only mandatory list is the proprietary one + open: # List of open dependencies. Optional + - name: MySQL + versionMin: "1.1" + versionMax: "1.3" + optional: yes + - name: PostgreSQL + version: "3.2" + optional: yes + # List of proprietary software which is a dependency for using this product. This includes runtime dependencies + proprietary: + - name: Oracle + versionMin: "11.4" + - name: IBM SoftLayer + hardware: # List of special hardware required. Optional. + - name: NFC Reader + optional: yes + +# IT: Italian extension. +it: + conforme: + accessibile: yes + interoperabile: yes + sicuro: yes + privacy: yes + + riuso: + # Codice IPA della PA che ha pubblicato questo repo (repo-owner) + codiceIPA: c_h501 + + spid: yes + pagopa: yes + cie: yes + anpr: yes + ecosistemi: + - scuola + + + designKit: + seo: no + ui: yes + web: yes + content: no diff --git a/tests/invalid_out.log b/tests/invalid_out.log new file mode 100644 index 0000000..bdd89f6 --- /dev/null +++ b/tests/invalid_out.log @@ -0,0 +1 @@ +[{"Key":"logo","Reason":"HTTP GET returned 404 for https://raw.githubusercontent.com/italia/developers.italia.it/master/tests/img/logo.png; 200 was expected"},{"Key":"monochromeLogo","Reason":"HTTP GET returned 404 for https://raw.githubusercontent.com/italia/developers.italia.it/master/tests/img/logo-mono.svg; 200 was expected"},{"Key":"description/eng/screenshots","Reason":"HTTP GET returned 404 for https://raw.githubusercontent.com/italia/developers.italia.it/master/tests/img/sshot1.png; 200 was expected"},{"Key":"legal/authorsFile","Reason":"HTTP GET returned 404 for https://raw.githubusercontent.com/italia/developers.italia.it/master/tests/AUTHORS; 200 was expected"},{"Key":"Unexpected boolean key: it/conforme/accessibile"},{"Key":"Unexpected boolean key: it/conforme/interoperabile"},{"Key":"Unexpected boolean key: it/conforme/sicuro"},{"Key":"Unexpected boolean key: it/conforme/privacy"},{"Key":"Unexpected boolean key: it/spid"},{"Key":"Unexpected boolean key: it/pagopa"},{"Key":"Unexpected boolean key: it/cie"},{"Key":"Unexpected boolean key: it/anpr"},{"Key":"Unexpected array key: it/ecosistemi"},{"Key":"Unexpected boolean key: it/designKit/content"},{"Key":"Unexpected boolean key: it/designKit/seo"},{"Key":"Unexpected boolean key: it/designKit/ui"},{"Key":"Unexpected boolean key: it/designKit/web"},{"Key":"Unexpected array key: intendedAudience/onlyFor"}] \ No newline at end of file diff --git a/tests/missing_maintenance_contacts.yml b/tests/missing_maintenance_contacts.yml new file mode 100644 index 0000000..51afec7 --- /dev/null +++ b/tests/missing_maintenance_contacts.yml @@ -0,0 +1,147 @@ +publiccodeYmlVersion: "0.1" + +name: Medusa +applicationSuite: MegaProductivitySuite +url: "https://github.com/italia/developers.italia.it.git" # URL of this repository +landingURL: "https://developers.italia.it" + +isBasedOn: "https://github.com/italia/developers.italia.it.git" # The original repository, "Otello" +softwareVersion: "1.0" # Last stable version +releaseDate: 2017-04-15 # Date of last stable software release +logo: tests/img/logo.png +monochromeLogo: tests/img/logo-mono.svg + +inputTypes: + - text/plain +outputTypes: + - text/plain + +platforms: # or Windows, Mac, Linux, etc. + - android + - ios + +categories: + - cloud-management + +usedBy: + - Comune di Firenze + - Comune di Roma + +roadmap: "https://designers.italia.it/roadmap/" + +developmentStatus: development + +softwareType: "standalone" + +intendedAudience: + onlyFor: + - cities + - health-services + - it-ag-agricolo + countries: + - it + - de + unsupportedCountries: # Explicitly unsupported countries + - us + +description: + eng: + localisedName: Medusa + genericName: "Text Editor" + shortDescription: "A really interesting software." + longDescription: > + Very long description of this software, also split + on multiple rows. You should note what the software + is and why one should need it. This is 158 characters. + Very long description of this software, also split + on multiple rows. You should note what the software + is and why one should need it. This is 316 characters. + Very long description of this software, also split + on multiple rows. You should note what the software + is and why one should need it. This is 474 characters. + Very long description of this software, also split + on multiple rows. You should note what the software + is and why one should need it. This is 632 characters. + + documentation: "https://docs.developers.italia.it" + apiDocumentation: "https://developers.italia.it/it/api" + + features: + - Very important feature + - Will run without a problem + - Has zero bugs + - Solves all the problems of the world + screenshots: + - tests/img/sshot1.png + - tests/img/sshot2.png + - tests/img/sshot3.png + videos: # Demo videos of the software + - https://www.youtube.com/watch?v=RaHmGbBOP84 + awards: + - 1st Price Software of the year + freeTags: + - freeTag + - FreeoloTag + +legal: + license: AGPL-3.0-or-later # SPDX expression of license + mainCopyrightOwner: City of Chicago + repoOwner: City of Chicago + authorsFile: tests/AUTHORS # file listing copyright information + +maintenance: + type: "internal" + +localisation: + # Does the software support, at least by design, multiple languages? + localisationReady: yes + # Languages already available + availableLanguages: + - eng + - ita + - fra + - deu + +dependsOn: # List of dependencies. The only mandatory list is the proprietary one + open: # List of open dependencies. Optional + - name: MySQL + versionMin: "1.1" + versionMax: "1.3" + optional: yes + - name: PostgreSQL + version: "3.2" + optional: yes + # List of proprietary software which is a dependency for using this product. This includes runtime dependencies + proprietary: + - name: Oracle + versionMin: "11.4" + - name: IBM SoftLayer + hardware: # List of special hardware required. Optional. + - name: NFC Reader + optional: yes + +# IT: Italian extension. +it: + conforme: + accessibile: yes + interoperabile: yes + sicuro: yes + privacy: yes + + riuso: + # Codice IPA della PA che ha pubblicato questo repo (repo-owner) + codiceIPA: c_h501 + + spid: yes + pagopa: yes + cie: yes + anpr: yes + ecosistemi: + - scuola + + + designKit: + seo: no + ui: yes + web: yes + content: no diff --git a/tests/valid.minimal.out.yml b/tests/valid.minimal.out.yml new file mode 100644 index 0000000..69f61fc --- /dev/null +++ b/tests/valid.minimal.out.yml @@ -0,0 +1,49 @@ +publiccodeYmlVersion: "0.2" +name: Medusa +url: https://github.com/italia/developers.italia.it.git +softwareVersion: dev +releaseDate: "2017-04-15" +inputTypes: +- application/x.empty +outputTypes: +- application/x.empty +platforms: +- web +categories: +- cloud-management +developmentStatus: development +softwareType: standalone/other +intendedAudience: {} +description: + en: + localisedName: Medusa + genericName: Text Editor + shortDescription: | + A rather short description which is probably useless + longDescription: | + Very long description of this software, also split on multiple rows. You should note what the software is and why one should need it. This is 158 characters. Very long description of this software, also split on multiple rows. You should note what the software is and why one should need it. This is 316 characters. Very long description of this software, also split on multiple rows. You should note what the software is and why one should need it. This is 474 characters. Very long description of this software, also split on multiple rows. You should note what the software is and why one should need it. This is 632 characters. + features: + - Just one feature +legal: + license: AGPL-3.0-or-later +maintenance: + type: community + contacts: + - name: Francesco Rossi +localisation: + localisationReady: true + availableLanguages: + - en +it: + countryExtensionVersion: "0.2" + conforme: + lineeGuidaDesign: false + modelloInteroperabilita: false + misureMinimeSicurezza: false + gdpr: false + riuso: {} + piattaforme: + spid: false + pagopa: false + cie: false + anpr: false