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 @@
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 @@
type PubliccodeymlValidatorHandler struct {
parser *publiccodeParser.Parser
parserExternalChecks *publiccodeParser.Parser
httpClient *http.Client
}

func NewPubliccodeymlValidatorHandler() *PubliccodeymlValidatorHandler {
Expand All @@ -25,10 +32,14 @@
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 {

Check failure on line 42 in internal/handlers/validate.go

View workflow job for this annotation

GitHub Actions / linters

calculated cyclomatic complexity for function Query is 12, max is 10 (cyclop)
var normalized *string

valid := true
Expand All @@ -38,13 +49,24 @@
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 @@
//nolint:wrapcheck
return ctx.JSON(fiber.Map{"valid": valid, "results": results, "normalized": normalized})
}

func (vh *PubliccodeymlValidatorHandler) fetchURL(rawURL string) ([]byte, error) {

Check failure on line 98 in internal/handlers/validate.go

View workflow job for this annotation

GitHub Actions / linters

calculated cyclomatic complexity for function fetchURL is 11, max is 10 (cyclop)
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)

Check failure on line 108 in internal/handlers/validate.go

View workflow job for this annotation

GitHub Actions / linters

net/http.NewRequest must not be called. use net/http.NewRequestWithContext (noctx)
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
Loading