Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 67 additions & 4 deletions internal/handlers/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -12,6 +18,7 @@ import (
type PubliccodeymlValidatorHandler struct {
parser *publiccodeParser.Parser
parserExternalChecks *publiccodeParser.Parser
httpClient *http.Client
}

func NewPubliccodeymlValidatorHandler() *PubliccodeymlValidatorHandler {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
52 changes: 52 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"log"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
Expand Down Expand Up @@ -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{
{
Expand Down Expand Up @@ -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)
Expand Down