From 4ce02efea80ba1c19409e6de731e6fde36eb5f93 Mon Sep 17 00:00:00 2001 From: hperl <34397+hperl@users.noreply.github.com> Date: Mon, 13 Jun 2022 15:20:36 +0200 Subject: [PATCH] test: add tests for the openapi check route --- internal/check/handler.go | 13 +-- internal/check/handler_test.go | 157 +++++++++++++++++++-------------- spec/api.json | 4 +- spec/swagger.json | 4 +- 4 files changed, 103 insertions(+), 75 deletions(-) diff --git a/internal/check/handler.go b/internal/check/handler.go index ef0756005..e0341913d 100644 --- a/internal/check/handler.go +++ b/internal/check/handler.go @@ -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) {} @@ -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 // @@ -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 // diff --git a/internal/check/handler_test.go b/internal/check/handler_test.go index 759180ba4..bada52cae 100644 --- a/internal/check/handler_test.go +++ b/internal/check/handler_test.go @@ -30,7 +30,9 @@ 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) @@ -38,6 +40,16 @@ func assertDenied(t *testing.T, resp *http.Response) { 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", @@ -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) + }) + }) + } } diff --git a/spec/api.json b/spec/api.json index d5ff69492..125f568b0 100755 --- a/spec/api.json +++ b/spec/api.json @@ -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": { @@ -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", diff --git a/spec/swagger.json b/spec/swagger.json index a856ce333..47f8054a1 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -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"], @@ -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": {