From 4b0ee5d0897857038a0b2c6335a99892ac1d134e Mon Sep 17 00:00:00 2001 From: pasibun Date: Wed, 11 Mar 2026 10:14:38 +0100 Subject: [PATCH] feat: support validating publiccode.yml via url query param --- README.md | 7 ++++ internal/handlers/validate.go | 71 +++++++++++++++++++++++++++++++++-- main_test.go | 52 +++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ba06d1f..6dff1aa 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,13 @@ curl -X QUERY "http://localhost:3000/v1/validate" \ --data-binary $'publiccodeYmlVersion: "0.5"\ndevelopmentStatus: stable\n [...]\n' ``` +or by URL query parameter: + +```console +curl -G -X QUERY "http://localhost:3000/v1/validate" \ +--data-urlencode "url=https://example.com/publiccode.yml" +``` + ### Example response (valid publiccode.yml) ```json diff --git a/internal/handlers/validate.go b/internal/handlers/validate.go index e595f7a..9892bc0 100644 --- a/internal/handlers/validate.go +++ b/internal/handlers/validate.go @@ -3,6 +3,12 @@ package handlers import ( "bytes" "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" "github.com/gofiber/fiber/v3" publiccodeParser "github.com/italia/publiccode-parser-go/v5" @@ -12,6 +18,7 @@ import ( type PubliccodeymlValidatorHandler struct { parser *publiccodeParser.Parser parserExternalChecks *publiccodeParser.Parser + httpClient *http.Client } func NewPubliccodeymlValidatorHandler() *PubliccodeymlValidatorHandler { @@ -25,7 +32,11 @@ func NewPubliccodeymlValidatorHandler() *PubliccodeymlValidatorHandler { panic("can't create a publiccode.yml parser: " + err.Error()) } - return &PubliccodeymlValidatorHandler{parser: parser, parserExternalChecks: parserExternalChecks} + return &PubliccodeymlValidatorHandler{ + parser: parser, + parserExternalChecks: parserExternalChecks, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } } func (vh *PubliccodeymlValidatorHandler) Query(ctx fiber.Ctx) error { @@ -38,13 +49,24 @@ func (vh *PubliccodeymlValidatorHandler) Query(ctx fiber.Ctx) error { parser = vh.parserExternalChecks } - if len(ctx.Body()) == 0 { - return common.Error(fiber.StatusBadRequest, "empty body", "need a body to validate") + input := ctx.Body() + if len(input) == 0 { + rawURL := strings.TrimSpace(fiber.Query[string](ctx, "url", "")) + if rawURL == "" { + return common.Error(fiber.StatusBadRequest, "empty body", "need a body to validate") + } + + content, err := vh.fetchURL(rawURL) + if err != nil { + return err + } + + input = content } results := make(publiccodeParser.ValidationResults, 0) - reader := bytes.NewReader(ctx.Body()) + reader := bytes.NewReader(input) parsed, err := parser.ParseStream(reader) if err != nil { @@ -72,3 +94,44 @@ func (vh *PubliccodeymlValidatorHandler) Query(ctx fiber.Ctx) error { //nolint:wrapcheck return ctx.JSON(fiber.Map{"valid": valid, "results": results, "normalized": normalized}) } + +func (vh *PubliccodeymlValidatorHandler) fetchURL(rawURL string) ([]byte, error) { + parsedURL, err := url.ParseRequestURI(rawURL) + if err != nil || parsedURL.Host == "" { + return nil, common.Error(fiber.StatusBadRequest, "invalid url", "query parameter 'url' must be a valid http(s) URL") + } + + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + return nil, common.Error(fiber.StatusBadRequest, "invalid url", "query parameter 'url' must use http or https") + } + + req, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil) + if err != nil { + return nil, common.Error(fiber.StatusBadRequest, "invalid url", "query parameter 'url' is invalid") + } + + resp, err := vh.httpClient.Do(req) + if err != nil { + return nil, common.Error(fiber.StatusBadRequest, "url fetch failed", fmt.Sprintf("failed to fetch URL: %v", err)) + } + defer resp.Body.Close() + + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return nil, common.Error( + fiber.StatusBadRequest, + "url fetch failed", + fmt.Sprintf("failed to fetch URL: HTTP %d", resp.StatusCode), + ) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, common.Error(fiber.StatusBadRequest, "url fetch failed", "failed to read response body") + } + + if len(body) == 0 { + return nil, common.Error(fiber.StatusBadRequest, "empty body", "the URL response is empty") + } + + return body, nil +} diff --git a/main_test.go b/main_test.go index 4d049b0..52d9569 100644 --- a/main_test.go +++ b/main_test.go @@ -5,6 +5,7 @@ import ( "io" "log" "net/http" + "net/http/httptest" "net/url" "os" "strings" @@ -129,6 +130,23 @@ func TestApi(t *testing.T) { func TestValidateEndpoint(t *testing.T) { validYml := loadTestdata(t, "valid.publiccode.yml") invalidYml := loadTestdata(t, "invalid.publiccode.yml") + sourceServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/valid.publiccode.yml": + _, _ = w.Write([]byte(validYml)) + case "/invalid.publiccode.yml": + _, _ = w.Write([]byte(invalidYml)) + case "/empty.publiccode.yml": + // Return 200 with an empty response body + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer sourceServer.Close() + + validYmlURL := url.QueryEscape(sourceServer.URL + "/valid.publiccode.yml") + emptyYmlURL := url.QueryEscape(sourceServer.URL + "/empty.publiccode.yml") + notFoundYmlURL := url.QueryEscape(sourceServer.URL + "/missing.publiccode.yml") tests := []TestCase{ { @@ -167,6 +185,40 @@ func TestValidateEndpoint(t *testing.T) { assert.Nil(t, response["normalized"]) }, }, + { + description: "validate: valid file from URL query parameter", + query: "QUERY /v1/validate?url=" + validYmlURL, + expectedCode: 200, + expectedContentType: "application/json; charset=utf-8", + validateFunc: func(t *testing.T, response map[string]any) { + assert.Equal(t, true, response["valid"]) + results, ok := response["results"].([]any) + assert.True(t, ok) + assert.Len(t, results, 0) + assert.NotNil(t, response["normalized"]) + }, + }, + { + description: "validate: invalid URL query parameter", + query: "QUERY /v1/validate?url=not-a-url", + expectedCode: 400, + expectedBody: `{"title":"invalid url","detail":"query parameter 'url' must be a valid http(s) URL","status":400}`, + expectedContentType: "application/problem+json", + }, + { + description: "validate: URL query parameter returns empty body", + query: "QUERY /v1/validate?url=" + emptyYmlURL, + expectedCode: 400, + expectedBody: `{"title":"empty body","detail":"the URL response is empty","status":400}`, + expectedContentType: "application/problem+json", + }, + { + description: "validate: URL query parameter returns non-2xx", + query: "QUERY /v1/validate?url=" + notFoundYmlURL, + expectedCode: 400, + expectedBody: `{"title":"url fetch failed","detail":"failed to fetch URL: HTTP 404","status":400}`, + expectedContentType: "application/problem+json", + }, } runTestCases(t, tests)