Skip to content

Commit

Permalink
test: add tests for the openapi check route
Browse files Browse the repository at this point in the history
  • Loading branch information
hperl committed Jun 13, 2022
1 parent c0dffc0 commit 4ce02ef
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 75 deletions.
13 changes: 8 additions & 5 deletions internal/check/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ func NewHandler(d handlerDependencies) *Handler {
return &Handler{d: d}
}

const RouteBase = "/check"
const (
RouteBase = "/check"
OpenAPIRouteBase = "/check/openapi"
)

func (h *Handler) RegisterReadRoutes(r *x.ReadRouter) {
r.GET(RouteBase, h.getCheckMirrorStatus)
r.GET(RouteBase+"-sdk", h.getCheckNoStatus)
r.GET(OpenAPIRouteBase, h.getCheckNoStatus)
r.POST(RouteBase, h.postCheckMirrorStatus)
r.POST(RouteBase+"-sdk", h.postCheckNoStatus)
r.POST(OpenAPIRouteBase, h.postCheckNoStatus)
}

func (h *Handler) RegisterWriteRoutes(_ *x.WriteRouter) {}
Expand Down Expand Up @@ -73,7 +76,7 @@ type getCheckRequest struct {
MaxDepth int `json:"max-depth"`
}

// swagger:route GET /check-sdk read getCheck
// swagger:route GET /check/openapi read getCheck
//
// Check a relation tuple
//
Expand Down Expand Up @@ -130,7 +133,7 @@ func (h *Handler) getCheck(ctx context.Context, q url.Values) (bool, error) {
return h.d.PermissionEngine().SubjectIsAllowed(ctx, tuple, maxDepth)
}

// swagger:route POST /check-sdk read postCheck
// swagger:route POST /check/openapi read postCheck
//
// Check a relation tuple
//
Expand Down
157 changes: 91 additions & 66 deletions internal/check/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,26 @@ func assertAllowed(t *testing.T, resp *http.Response) {
assert.True(t, gjson.GetBytes(body, "allowed").Bool())
}

func assertDenied(t *testing.T, resp *http.Response) {
type responseAssertion func(t *testing.T, resp *http.Response)

func baseAssertDenied(t *testing.T, resp *http.Response) {
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

assert.Equal(t, http.StatusForbidden, resp.StatusCode, "%s", body)
assert.False(t, gjson.GetBytes(body, "allowed").Bool())
}

// For OpenAPI clients, we want to always regurn a 200 status code even if the
// check returned "denied".
func openAPIAssertDenied(t *testing.T, resp *http.Response) {
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

assert.Equal(t, http.StatusOK, resp.StatusCode, "%s", body)
assert.False(t, gjson.GetBytes(body, "allowed").Bool())
}

func TestRESTHandler(t *testing.T) {
nspaces := []*namespace.Namespace{{
Name: "check handler",
Expand All @@ -51,69 +63,82 @@ func TestRESTHandler(t *testing.T) {
ts := httptest.NewServer(r)
defer ts.Close()

t.Run("case=returns bad request on malformed int", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + check.RouteBase + "?max-depth=foo")
require.NoError(t, err)

assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Contains(t, string(body), "invalid syntax")
})

t.Run("case=returns bad request on malformed input", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + check.RouteBase + "?" + url.Values{
"subject": {"not#a valid userset rewrite"},
}.Encode())
require.NoError(t, err)

assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})

t.Run("case=returns bad request on missing subject", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + check.RouteBase)
require.NoError(t, err)

assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Contains(t, string(body), "Please provide a subject")
})

t.Run("case=returns denied on unknown namespace", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + check.RouteBase + "?" + url.Values{
"namespace": {"not " + nspaces[0].Name},
"subject_id": {"foo"},
}.Encode())
require.NoError(t, err)

assertDenied(t, resp)
})

t.Run("case=returns allowed", func(t *testing.T) {
rt := &relationtuple.InternalRelationTuple{
Namespace: nspaces[0].Name,
Object: "o",
Relation: "r",
Subject: &relationtuple.SubjectID{ID: "s"},
}
require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), rt))

q, err := rt.ToURLQuery()
require.NoError(t, err)
resp, err := ts.Client().Get(ts.URL + check.RouteBase + "?" + q.Encode())
require.NoError(t, err)

assertAllowed(t, resp)
})

t.Run("case=returns denied", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + check.RouteBase + "?" + url.Values{
"namespace": {nspaces[0].Name},
"subject_id": {"foo"},
}.Encode())
require.NoError(t, err)

assertDenied(t, resp)
})
for _, suite := range []struct {
name string
base string
assertDenied responseAssertion
}{
{name: "base", base: check.RouteBase, assertDenied: baseAssertDenied},
{name: "openapi", base: check.OpenAPIRouteBase, assertDenied: openAPIAssertDenied},
} {
t.Run("suite="+suite.name, func(t *testing.T) {
assertDenied := suite.assertDenied

t.Run("case=returns bad request on malformed int", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + suite.base + "?max-depth=foo")
require.NoError(t, err)

assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Contains(t, string(body), "invalid syntax")
})

t.Run("case=returns bad request on malformed input", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + suite.base + "?" + url.Values{
"subject": {"not#a valid userset rewrite"},
}.Encode())
require.NoError(t, err)

assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})

t.Run("case=returns bad request on missing subject", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + suite.base)
require.NoError(t, err)

assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Contains(t, string(body), "Please provide a subject")
})

t.Run("case=returns denied on unknown namespace", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + suite.base + "?" + url.Values{
"namespace": {"not " + nspaces[0].Name},
"subject_id": {"foo"},
}.Encode())
require.NoError(t, err)

assertDenied(t, resp)
})

t.Run("case=returns allowed", func(t *testing.T) {
rt := &relationtuple.InternalRelationTuple{
Namespace: nspaces[0].Name,
Object: "o",
Relation: "r",
Subject: &relationtuple.SubjectID{ID: "s"},
}
require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), rt))

q, err := rt.ToURLQuery()
require.NoError(t, err)
resp, err := ts.Client().Get(ts.URL + suite.base + "?" + q.Encode())
require.NoError(t, err)

assertAllowed(t, resp)
})

t.Run("case=returns denied", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + suite.base + "?" + url.Values{
"namespace": {nspaces[0].Name},
"subject_id": {"foo"},
}.Encode())
require.NoError(t, err)

assertDenied(t, resp)
})
})
}
}
4 changes: 2 additions & 2 deletions spec/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
}
},
"required": ["allowed"],
"title": "Represents the response for a check request.",
"title": "RESTResponse represents the response for a check request.",
"type": "object"
},
"getRelationTuplesResponse": {
Expand Down Expand Up @@ -215,7 +215,7 @@
},
"openapi": "3.0.3",
"paths": {
"/check": {
"/check/openapi": {
"get": {
"description": "To learn how relation tuples and the check works, head over to [the documentation](../concepts/relation-tuples.mdx).",
"operationId": "getCheck",
Expand Down
4 changes: 2 additions & 2 deletions spec/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"basePath": "/",
"paths": {
"/check": {
"/check/openapi": {
"get": {
"description": "To learn how relation tuples and the check works, head over to [the documentation](../concepts/relation-tuples.mdx).",
"consumes": ["application/x-www-form-urlencoded"],
Expand Down Expand Up @@ -658,7 +658,7 @@
"getCheckResponse": {
"description": "The content of the allowed field is mirrored in the HTTP status code.",
"type": "object",
"title": "Represents the response for a check request.",
"title": "RESTResponse represents the response for a check request.",
"required": ["allowed"],
"properties": {
"allowed": {
Expand Down

0 comments on commit 4ce02ef

Please sign in to comment.