Skip to content
Merged
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
147 changes: 147 additions & 0 deletions events_actor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package main

import (
"io"
"net/http"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestEventActor covers the audit trail: every write leaves an event row
// carrying the subject of the token that authenticated the request.
func TestEventActor(t *testing.T) {
const (
deletablePublisherID = "15fda7c4-6bbf-4387-8f89-258c1e6fafb1"
deletableSoftwareID = "11e101c4-f989-4cc4-a665-63f9f34e83f6"
)

tests := []struct {
description string
method string
path string
body string
subject string
// entityID is the row the event is expected for. Left empty on a
// POST, which only learns the id from the response.
entityID string
expectedCode int
expectedActor string
}{
{
description: "POST publisher",
method: http.MethodPost,
path: "/v1/publishers",
body: `{"description":"actor test publisher","codeHosting":[{"url":"https://actor-test.example.org/repo"}]}`,
subject: "crawler",
expectedCode: 200,
expectedActor: "crawler",
},
{
description: "POST software",
method: http.MethodPost,
path: "/v1/software",
body: `{"publiccodeYml":"-","url":"https://actor-test.example.org/software"}`,
subject: "crawler",
expectedCode: 200,
expectedActor: "crawler",
},
{
description: "POST publisher in a catalog",
method: http.MethodPost,
path: "/v1/catalogs/" + italiaID + "/publishers",
body: `{"description":"actor test catalog publisher","codeHosting":[{"url":"https://actor-test.example.org/catalog-repo"}]}`,
subject: "curator",
expectedCode: 200,
expectedActor: "curator",
},
{
description: "PATCH publisher",
method: http.MethodPatch,
path: publisherPath + italiaPublisherID,
body: `{"description":"actor test patched description"}`,
subject: "editor",
entityID: italiaPublisherID,
expectedCode: 200,
expectedActor: "editor",
},
{
description: "PATCH software",
method: http.MethodPatch,
path: softwarePath + swissSoftwareID,
body: `{"vitality":"10,10,10"}`,
subject: "editor",
entityID: swissSoftwareID,
expectedCode: 200,
expectedActor: "editor",
},
{
description: "DELETE publisher",
method: http.MethodDelete,
path: publisherPath + deletablePublisherID,
subject: "janitor",
entityID: deletablePublisherID,
expectedCode: 204,
expectedActor: "janitor",
},
{
description: "DELETE software",
method: http.MethodDelete,
path: softwarePath + deletableSoftwareID,
subject: "janitor",
entityID: deletableSoftwareID,
expectedCode: 204,
expectedActor: "janitor",
},
{
description: "POST publisher with a token carrying no subject",
method: http.MethodPost,
path: "/v1/publishers",
body: `{"description":"actor test anonymous publisher","codeHosting":[{"url":"https://actor-test.example.org/anonymous"}]}`,
subject: "",
expectedCode: 200,
expectedActor: "",
},
}

for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
loadFixtures(t)

req, err := newTestRequest(test.method, test.path, strings.NewReader(test.body))
require.NoError(t, err)

req.Header = map[string][]string{
"Authorization": {bearerWithSubject(t, test.subject)},
"Content-Type": {"application/json"},
}

res, err := app.Test(req, -1)
require.NoError(t, err)
require.Equal(t, test.expectedCode, res.StatusCode)

entityID := test.entityID
if entityID == "" {
entityID = idFromResponse(t, res.Body)
}

require.Equal(t, 1, dbCount(t, "events", "entity_id", entityID))
assert.Equal(t, test.expectedActor, dbValue(t, "events", "actor", "entity_id", entityID))
})
}
}

// idFromResponse reads the id of the entity a POST created.
func idFromResponse(t *testing.T, body io.Reader) string {
t.Helper()

raw, err := io.ReadAll(body)
require.NoError(t, err)

id, ok := decodeJSON(t, raw)["id"].(string)
require.True(t, ok, "no id in the response: %s", raw)

return id
}
241 changes: 241 additions & 0 deletions events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package main

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEventsEndpoints(t *testing.T) {
const (
eventWithActorID = "0ab7b216-d819-4a2a-8258-65c7dbe3af4d"
eventWithoutActorID = "d37d1082-528e-449d-a626-445561368d6b"
eventCount = 8
)

authHeaders := map[string][]string{"Authorization": {goodToken}}

// The actor of every fixture event, empty for the rows the fixtures
// leave without one.
actors := map[string]string{
"d5e6f708-91a2-4bde-8f30-6b7c8d9e0f75": "crawler",
"c4d5e6f7-8091-4cad-9e2f-5a6b7c8d9e74": "",
"b3c4d5e6-7f80-4b9c-8d1e-4f5a6b7c8d73": "crawler",
"a2b3c4d5-6e7f-4a8b-9c0d-3e4f5a6b7c72": "editor",
eventWithActorID: "crawler",
eventWithoutActorID: "",
"9b1c2d34-4e5f-4a6b-8c7d-2e3f4a5b6c71": "editor",
"8e0a1f56-3a70-4b6e-9a2d-1f3b4c5d6e70": "crawler",
}

tests := []TestCase{
// GET /events
{
description: "GET events without a token",
query: "GET /v1/events",
expectedCode: 401,
expectedBody: `{"title":"token authentication failed","status":401}`,
expectedContentType: "application/problem+json",
},
{
description: "GET events",
query: "GET /v1/events",
headers: authHeaders,
expectedCode: 200,
expectedContentType: "application/json",
validateFunc: func(t *testing.T, response map[string]any) {
data := assertListResponse(t, response)

assert.Equal(t, eventCount, len(data))

// The whole fixture fits into the default page of 25.
assertPaginationLinks(t, response, nil, nil)

var prevCreatedAt *time.Time

for _, event := range data {
assertUUID(t, event["id"])

assert.Contains(t, []any{"create", "update", "delete"}, event["type"])
assert.Contains(t, []any{"software", "publishers"}, event["entityType"])
assertUUID(t, event["entityId"])

createdAt := assertRFC3339(t, event["createdAt"])
assertRFC3339(t, event["updatedAt"])

id, ok := event["id"].(string)
require.True(t, ok)

expectedActor, known := actors[id]
require.True(t, known, "unexpected event %q in the response", id)
assertActor(t, event, expectedActor)

assertOnlyKeys(t, event, "id", "type", "entityType", "entityId", "actor", "createdAt", "updatedAt")

if prevCreatedAt != nil {
assert.GreaterOrEqual(t, *prevCreatedAt, createdAt)
}

prevCreatedAt = &createdAt
}
},
},
{
description: "GET events with page[size] query param",
query: "GET /v1/events?page[size]=3",
headers: authHeaders,
expectedCode: 200,
expectedContentType: "application/json",
validateFunc: func(t *testing.T, response map[string]any) {
data := assertListResponse(t, response)

assert.Equal(t, 3, len(data))

assertPaginationLinks(t, response, nil, "?page[after]=WyIyMDE5LTA5LTE1VDAwOjAwOjAwWiIsImIzYzRkNWU2LTdmODAtNGI5Yy04ZDFlLTRmNWE2YjdjOGQ3MyJd&page[size]=3")
},
},
{
description: "GET events with page[after] query param",
query: "GET /v1/events?page[after]=WyIyMDE5LTA5LTE1VDAwOjAwOjAwWiIsImIzYzRkNWU2LTdmODAtNGI5Yy04ZDFlLTRmNWE2YjdjOGQ3MyJd",
headers: authHeaders,
expectedCode: 200,
expectedContentType: "application/json",
validateFunc: func(t *testing.T, response map[string]any) {
data := assertListResponse(t, response)

assert.Equal(t, 5, len(data))

assertPaginationLinks(t, response, "?page[before]=WyIyMDE4LTExLTMwVDAwOjAwOjAwWiIsImEyYjNjNGQ1LTZlN2YtNGE4Yi05YzBkLTNlNGY1YTZiN2M3MiJd", nil)
},
},
{
description: `GET events with "from" query param`,
query: "GET /v1/events?from=2019-01-01T00:00:00Z",
headers: authHeaders,
expectedCode: 200,
expectedContentType: "application/json",
validateFunc: func(t *testing.T, response map[string]any) {
data := assertListResponse(t, response)

assert.Equal(t, 3, len(data))
},
},
{
description: `GET events with invalid "from" query param`,
query: "GET /v1/events?from=3",
headers: authHeaders,
expectedCode: 422,
expectedContentType: "application/problem+json",
validateFunc: func(t *testing.T, response map[string]any) {
assert.Equal(t, `can't get Events`, response["title"])
assert.Equal(t, "invalid date time format (RFC 3339 needed)", response["detail"])
},
},
{
description: `GET events with "to" query param`,
query: "GET /v1/events?to=2019-01-01T00:00:00Z",
headers: authHeaders,
expectedCode: 200,
expectedContentType: "application/json",
validateFunc: func(t *testing.T, response map[string]any) {
data := assertListResponse(t, response)

assert.Equal(t, 5, len(data))
},
},
{
description: `GET events with invalid "to" query param`,
query: "GET /v1/events?to=3",
headers: authHeaders,
expectedCode: 422,
expectedContentType: "application/problem+json",
validateFunc: func(t *testing.T, response map[string]any) {
assert.Equal(t, `can't get Events`, response["title"])
assert.Equal(t, "invalid date time format (RFC 3339 needed)", response["detail"])
},
},
{
description: `GET events with "from" and "to" query params`,
query: "GET /v1/events?from=2016-01-01T00:00:00Z&to=2019-01-01T00:00:00Z",
headers: authHeaders,
expectedCode: 200,
expectedContentType: "application/json",
validateFunc: func(t *testing.T, response map[string]any) {
data := assertListResponse(t, response)

assert.Equal(t, 4, len(data))
},
},

// GET /events/:id
{
description: "GET event without a token",
query: "GET /v1/events/" + eventWithActorID,
expectedCode: 401,
expectedBody: `{"title":"token authentication failed","status":401}`,
expectedContentType: "application/problem+json",
},
{
description: "GET event with an actor",
query: "GET /v1/events/" + eventWithActorID,
headers: authHeaders,
expectedCode: 200,
expectedContentType: "application/json",
validateFunc: func(t *testing.T, response map[string]any) {
assert.Equal(t, eventWithActorID, response["id"])
assert.Equal(t, "update", response["type"])
assert.Equal(t, "software", response["entityType"])
assert.Equal(t, "c5dec6fa-8a01-4881-9e7d-132770d4214d", response["entityId"])
assert.Equal(t, "crawler", response["actor"])

assertTimestamps(t, response)
assertOnlyKeys(t, response, "id", "type", "entityType", "entityId", "actor", "createdAt", "updatedAt")
},
},
{
description: "GET event with no actor",
query: "GET /v1/events/" + eventWithoutActorID,
headers: authHeaders,
setupFunc: func(t *testing.T) {
assert.True(t, dbNull(t, "events", "actor", "id", eventWithoutActorID))
},
expectedCode: 200,
expectedContentType: "application/json",
validateFunc: func(t *testing.T, response map[string]any) {
assert.Equal(t, eventWithoutActorID, response["id"])
assert.Equal(t, "create", response["type"])

assertActor(t, response, "")
assertOnlyKeys(t, response, "id", "type", "entityType", "entityId", "createdAt", "updatedAt")
},
},
{
description: "GET non-existent event",
query: "GET /v1/events/eea19c82-0449-11ed-bd84-d8bbc146d165",
headers: authHeaders,
expectedCode: 404,
expectedBody: `{"title":"can't get Event","detail":"Event was not found","status":404}`,
expectedContentType: "application/problem+json",
},
}

runTestCases(t, tests)
}

// assertActor checks the actor of an event, which is absent from the
// response when the token that made the write carried no subject.
func assertActor(t *testing.T, event map[string]any, expected string) {
t.Helper()

actor, present := event["actor"]

if expected == "" {
assert.False(t, present, "expected no actor, got %v", actor)

return
}

assert.Equal(t, expected, actor)
}
Loading
Loading