diff --git a/acp/README.md b/acp/README.md index 3fedb5a274..0f73662dd9 100644 --- a/acp/README.md +++ b/acp/README.md @@ -631,7 +631,8 @@ Result: Error: document not found or not authorized to access ``` -Sometimes we might want to give a specific access (form a relationship) not just to one identity, but any identity. +Sometimes we might want to give a specific access (i.e. form a relationship) not just with one identity, but with +any identity (includes even requests with no-identity). In that case we can specify "*" instead of specifying an explicit `actor`: ```sh defradb client acp relationship add \ diff --git a/http/handler_ccip_test.go b/http/handler_ccip_test.go index 43797b622d..1888effe37 100644 --- a/http/handler_ccip_test.go +++ b/http/handler_ccip_test.go @@ -67,7 +67,7 @@ func TestCCIPGet_WithValidData(t *testing.T) { resHex, err := hex.DecodeString(strings.TrimPrefix(ccipRes.Data, "0x")) require.NoError(t, err) - assert.JSONEq(t, `{"data": {"User": [{"name": "bob"}]}}`, string(resHex)) + assert.JSONEq(t, `{"data": {"User": [{"name": "bob"}, {"name": "adam"}]}}`, string(resHex)) } func TestCCIPGet_WithSubscription(t *testing.T) { @@ -153,7 +153,7 @@ func TestCCIPPost_WithValidData(t *testing.T) { resHex, err := hex.DecodeString(strings.TrimPrefix(ccipRes.Data, "0x")) require.NoError(t, err) - assert.JSONEq(t, `{"data": {"User": [{"name": "bob"}]}}`, string(resHex)) + assert.JSONEq(t, `{"data": {"User": [{"name": "bob"}, {"name": "adam"}]}}`, string(resHex)) } func TestCCIPPost_WithInvalidGraphQLRequest(t *testing.T) { @@ -210,5 +210,11 @@ func setupDatabase(t *testing.T) client.DB { err = col.Create(ctx, doc) require.NoError(t, err) + doc2, err := client.NewDocFromJSON([]byte(`{"name": "adam"}`), col.Definition()) + require.NoError(t, err) + + err = col.Create(ctx, doc2) + require.NoError(t, err) + return cdb } diff --git a/http/handler_store.go b/http/handler_store.go index 35436f3762..3d2cef63de 100644 --- a/http/handler_store.go +++ b/http/handler_store.go @@ -284,7 +284,21 @@ func (s *storeHandler) ExecRequest(rw http.ResponseWriter, req *http.Request) { var request GraphQLRequest switch { case req.URL.Query().Get("query") != "": + request.Query = req.URL.Query().Get("query") + + request.OperationName = req.URL.Query().Get("operationName") + + variablesFromQuery := req.URL.Query().Get("variables") + if variablesFromQuery != "" { + var variables map[string]any + if err := json.Unmarshal([]byte(variablesFromQuery), &variables); err != nil { + responseJSON(rw, http.StatusBadRequest, errorResponse{err}) + return + } + request.Variables = variables + } + case req.Body != nil: if err := requestJSON(req, &request); err != nil { responseJSON(rw, http.StatusBadRequest, errorResponse{err}) @@ -294,7 +308,6 @@ func (s *storeHandler) ExecRequest(rw http.ResponseWriter, req *http.Request) { responseJSON(rw, http.StatusBadRequest, errorResponse{ErrMissingRequest}) return } - var options []client.RequestOption if request.OperationName != "" { options = append(options, client.WithOperationName(request.OperationName)) diff --git a/http/handler_store_test.go b/http/handler_store_test.go index dabf9648bd..7d39c4be07 100644 --- a/http/handler_store_test.go +++ b/http/handler_store_test.go @@ -16,6 +16,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "testing" "github.com/stretchr/testify/assert" @@ -93,3 +94,100 @@ func TestExecRequest_WithInvalidQuery_HasSpecCompliantErrors(t *testing.T) { "message": "Cannot query field \"invalid\" on type \"User\".", }}) } + +func TestExecRequest_HttpGet_WithOperationName(t *testing.T) { + cdb := setupDatabase(t) + + query := ` + query UserQuery { + User { + name + } + } + query UserQueryWithDocID { + User { + _docID + name + } + } + ` + operationName := "UserQuery" + + encodedQuery := url.QueryEscape(query) + encodedOperationName := url.QueryEscape(operationName) + + endpointURL := "http://localhost:9181/api/v0/graphql?query=" + encodedQuery + "&operationName=" + encodedOperationName + + req := httptest.NewRequest(http.MethodGet, endpointURL, nil) + rec := httptest.NewRecorder() + + handler, err := NewHandler(cdb) + require.NoError(t, err) + handler.ServeHTTP(rec, req) + + res := rec.Result() + require.NotNil(t, res.Body) + + resData, err := io.ReadAll(res.Body) + require.NoError(t, err) + + var gqlResponse map[string]any + err = json.Unmarshal(resData, &gqlResponse) + require.NoError(t, err) + + // Ensure the response data contains names, but not the _docID field + expectedJSON := `{ + "data": { + "User": [ + {"name": "bob"}, + {"name": "adam"} + ] + } + }` + assert.JSONEq(t, expectedJSON, string(resData)) +} + +func TestExecRequest_HttpGet_WithVariables(t *testing.T) { + cdb := setupDatabase(t) + + query := `query getUser($filter: UserFilterArg) { + User(filter: $filter) { + name + } + }` + operationName := "getUser" + variables := `{"filter":{"name":{"_eq":"bob"}}}` + + encodedQuery := url.QueryEscape(query) + encodedOperationName := url.QueryEscape(operationName) + encodedVariables := url.QueryEscape(variables) + + endpointURL := "http://localhost:9181/api/v0/graphql?query=" + encodedQuery + "&operationName=" + encodedOperationName + "&variables=" + encodedVariables + + req := httptest.NewRequest(http.MethodGet, endpointURL, nil) + rec := httptest.NewRecorder() + + handler, err := NewHandler(cdb) + require.NoError(t, err) + handler.ServeHTTP(rec, req) + + res := rec.Result() + require.NotNil(t, res.Body) + + resData, err := io.ReadAll(res.Body) + require.NoError(t, err) + + var gqlResponse map[string]any + err = json.Unmarshal(resData, &gqlResponse) + require.NoError(t, err) + + // Ensure only bob is returned, because of the filter variable + expectedJSON := `{ + "data": { + "User": [ + {"name": "bob"} + ] + } + }` + assert.JSONEq(t, expectedJSON, string(resData)) +} diff --git a/internal/db/base/compare.go b/internal/db/base/compare.go index c5636f9e15..63bf27bf0b 100644 --- a/internal/db/base/compare.go +++ b/internal/db/base/compare.go @@ -34,7 +34,7 @@ func Compare(a, b any) int { case bool: return compareBool(v, b.(bool)) case int: - return compareInt(int64(v), b.(int64)) + return compareInt(int64(v), int64(b.(int))) case int64: return compareInt(v, b.(int64)) case uint64: diff --git a/internal/db/permission/check.go b/internal/db/permission/check.go index ce111bccaf..599329855b 100644 --- a/internal/db/permission/check.go +++ b/internal/db/permission/check.go @@ -67,18 +67,22 @@ func CheckAccessOfDocOnCollectionWithACP( return true, nil } - // At this point if the request is not signatured, then it has no access, because: - // the collection has a policy on it, and the acp is enabled/available, - // and the document is not public (is registered with acp). + var identityValue string if !identity.HasValue() { - return false, nil + // We can't assume that there is no-access just because there is no identity even if the document + // is registered with acp, this is because it is possible that acp has a registered relation targeting + // "*" (any) actor which would mean that even a request without an identity might be able to access + // a document registered with acp. So we pass an empty `did` to accommodate that case. + identityValue = "" + } else { + identityValue = identity.Value().DID } // Now actually check using the signature if this identity has access or not. hasAccess, err := acpSystem.CheckDocAccess( ctx, permission, - identity.Value().DID, + identityValue, policyID, resourceName, docID, diff --git a/internal/planner/average.go b/internal/planner/average.go index c5274b5b6f..76bbfc107d 100644 --- a/internal/planner/average.go +++ b/internal/planner/average.go @@ -28,6 +28,8 @@ type averageNode struct { virtualFieldIndex int execInfo averageExecInfo + + aggregateFilter *mapper.Filter } type averageExecInfo struct { @@ -37,6 +39,7 @@ type averageExecInfo struct { func (p *Planner) Average( field *mapper.Aggregate, + filter *mapper.Filter, ) (*averageNode, error) { var sumField *mapper.Aggregate var countField *mapper.Aggregate @@ -57,6 +60,7 @@ func (p *Planner) Average( countFieldIndex: countField.Index, virtualFieldIndex: field.Index, docMapper: docMapper{field.DocumentMapping}, + aggregateFilter: filter, }, nil } @@ -102,7 +106,7 @@ func (n *averageNode) Next() (bool, error) { return false, client.NewErrUnhandledType("sum", sumProp) } - return true, nil + return mapper.RunFilter(n.currentValue, n.aggregateFilter) } func (n *averageNode) SetPlan(p planNode) { n.plan = p } diff --git a/internal/planner/count.go b/internal/planner/count.go index b71fcab1e5..1b58109749 100644 --- a/internal/planner/count.go +++ b/internal/planner/count.go @@ -35,6 +35,7 @@ type countNode struct { virtualFieldIndex int aggregateMapping []mapper.AggregateTarget + aggregateFilter *mapper.Filter execInfo countExecInfo } @@ -44,11 +45,12 @@ type countExecInfo struct { iterations uint64 } -func (p *Planner) Count(field *mapper.Aggregate, host *mapper.Select) (*countNode, error) { +func (p *Planner) Count(field *mapper.Aggregate, host *mapper.Select, filter *mapper.Filter) (*countNode, error) { return &countNode{ p: p, virtualFieldIndex: field.Index, aggregateMapping: field.AggregateTargets, + aggregateFilter: filter, docMapper: docMapper{field.DocumentMapping}, }, nil } @@ -181,7 +183,7 @@ func (n *countNode) Next() (bool, error) { } n.currentValue.Fields[n.virtualFieldIndex] = count - return true, nil + return mapper.RunFilter(n.currentValue, n.aggregateFilter) } // countDocs counts the number of documents in a slice, skipping over hidden items diff --git a/internal/planner/max.go b/internal/planner/max.go index c3eb6b488e..530e60e25e 100644 --- a/internal/planner/max.go +++ b/internal/planner/max.go @@ -33,6 +33,7 @@ type maxNode struct { // that contains the result of the aggregate. virtualFieldIndex int aggregateMapping []mapper.AggregateTarget + aggregateFilter *mapper.Filter execInfo maxExecInfo } @@ -45,11 +46,13 @@ type maxExecInfo struct { func (p *Planner) Max( field *mapper.Aggregate, parent *mapper.Select, + filter *mapper.Filter, ) (*maxNode, error) { return &maxNode{ p: p, parent: parent, aggregateMapping: field.AggregateTargets, + aggregateFilter: filter, virtualFieldIndex: field.Index, docMapper: docMapper{field.DocumentMapping}, }, nil @@ -252,5 +255,5 @@ func (n *maxNode) Next() (bool, error) { res, _ := max.Int64() n.currentValue.Fields[n.virtualFieldIndex] = res } - return true, nil + return mapper.RunFilter(n.currentValue, n.aggregateFilter) } diff --git a/internal/planner/min.go b/internal/planner/min.go index 99278785bc..be70a8ccb9 100644 --- a/internal/planner/min.go +++ b/internal/planner/min.go @@ -33,6 +33,7 @@ type minNode struct { // that contains the result of the aggregate. virtualFieldIndex int aggregateMapping []mapper.AggregateTarget + aggregateFilter *mapper.Filter execInfo minExecInfo } @@ -45,11 +46,13 @@ type minExecInfo struct { func (p *Planner) Min( field *mapper.Aggregate, parent *mapper.Select, + filter *mapper.Filter, ) (*minNode, error) { return &minNode{ p: p, parent: parent, aggregateMapping: field.AggregateTargets, + aggregateFilter: filter, virtualFieldIndex: field.Index, docMapper: docMapper{field.DocumentMapping}, }, nil @@ -252,5 +255,5 @@ func (n *minNode) Next() (bool, error) { res, _ := min.Int64() n.currentValue.Fields[n.virtualFieldIndex] = res } - return true, nil + return mapper.RunFilter(n.currentValue, n.aggregateFilter) } diff --git a/internal/planner/select.go b/internal/planner/select.go index d0e816cfb9..d3bcbb910d 100644 --- a/internal/planner/select.go +++ b/internal/planner/select.go @@ -19,6 +19,7 @@ import ( "github.com/sourcenetwork/defradb/internal/core" "github.com/sourcenetwork/defradb/internal/db/base" "github.com/sourcenetwork/defradb/internal/keys" + "github.com/sourcenetwork/defradb/internal/planner/filter" "github.com/sourcenetwork/defradb/internal/planner/mapper" ) @@ -344,18 +345,21 @@ func (n *selectNode) initFields(selectReq *mapper.Select) ([]aggregateNode, erro case *mapper.Aggregate: var plan aggregateNode var aggregateError error + var aggregateFilter *mapper.Filter + // extract aggregate filters from the select + selectReq.Filter, aggregateFilter = filter.SplitByFields(selectReq.Filter, f.Field) switch f.Name { case request.CountFieldName: - plan, aggregateError = n.planner.Count(f, selectReq) + plan, aggregateError = n.planner.Count(f, selectReq, aggregateFilter) case request.SumFieldName: - plan, aggregateError = n.planner.Sum(f, selectReq) + plan, aggregateError = n.planner.Sum(f, selectReq, aggregateFilter) case request.AverageFieldName: - plan, aggregateError = n.planner.Average(f) + plan, aggregateError = n.planner.Average(f, aggregateFilter) case request.MaxFieldName: - plan, aggregateError = n.planner.Max(f, selectReq) + plan, aggregateError = n.planner.Max(f, selectReq, aggregateFilter) case request.MinFieldName: - plan, aggregateError = n.planner.Min(f, selectReq) + plan, aggregateError = n.planner.Min(f, selectReq, aggregateFilter) } if aggregateError != nil { diff --git a/internal/planner/sum.go b/internal/planner/sum.go index c790cba60d..a77e56da3d 100644 --- a/internal/planner/sum.go +++ b/internal/planner/sum.go @@ -30,6 +30,7 @@ type sumNode struct { isFloat bool virtualFieldIndex int aggregateMapping []mapper.AggregateTarget + aggregateFilter *mapper.Filter execInfo sumExecInfo } @@ -42,6 +43,7 @@ type sumExecInfo struct { func (p *Planner) Sum( field *mapper.Aggregate, parent *mapper.Select, + filter *mapper.Filter, ) (*sumNode, error) { isFloat := false for _, target := range field.AggregateTargets { @@ -60,6 +62,7 @@ func (p *Planner) Sum( p: p, isFloat: isFloat, aggregateMapping: field.AggregateTargets, + aggregateFilter: filter, virtualFieldIndex: field.Index, docMapper: docMapper{field.DocumentMapping}, }, nil @@ -310,8 +313,7 @@ func (n *sumNode) Next() (bool, error) { typedSum = int64(sum) } n.currentValue.Fields[n.virtualFieldIndex] = typedSum - - return true, nil + return mapper.RunFilter(n.currentValue, n.aggregateFilter) } func (n *sumNode) SetPlan(p planNode) { n.plan = p } diff --git a/internal/planner/top.go b/internal/planner/top.go index 6224b6d62d..658dc66dd8 100644 --- a/internal/planner/top.go +++ b/internal/planner/top.go @@ -199,15 +199,15 @@ func (p *Planner) Top(m *mapper.Select) (*topLevelNode, error) { var err error switch field.GetName() { case request.CountFieldName: - child, err = p.Count(f, m) + child, err = p.Count(f, m, nil) case request.SumFieldName: - child, err = p.Sum(f, m) + child, err = p.Sum(f, m, nil) case request.AverageFieldName: - child, err = p.Average(f) + child, err = p.Average(f, nil) case request.MaxFieldName: - child, err = p.Max(f, m) + child, err = p.Max(f, m, nil) case request.MinFieldName: - child, err = p.Min(f, m) + child, err = p.Min(f, m, nil) } if err != nil { return nil, err diff --git a/tests/integration/acp/relationship/doc_actor/add/with_manager_gql_test.go b/tests/integration/acp/relationship/doc_actor/add/with_manager_gql_test.go index 757053f365..813294a7fd 100644 --- a/tests/integration/acp/relationship/doc_actor/add/with_manager_gql_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_manager_gql_test.go @@ -26,10 +26,13 @@ func TestACP_OwnerMakesAManagerThatGivesItSelfReadAndWriteAccess_GQL_ManagerCanR Description: "Test acp, owner makes a manager that gives itself read and write access", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - // GQL mutation will return no error when wrong identity is used so test that separately. - testUtils.GQLRequestMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used (only for update requests), + // so test that separately. + testUtils.GQLRequestMutationType, + }, + ), Actions: []any{ testUtils.AddPolicy{ @@ -274,10 +277,13 @@ func TestACP_OwnerMakesManagerButManagerCanNotPerformOperations_GQL_ManagerCantR Description: "Test acp, owner makes a manager, manager can't read or write", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - // GQL mutation will return no error when wrong identity is used so test that separately. - testUtils.GQLRequestMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used (only for update requests), + // so test that separately. + testUtils.GQLRequestMutationType, + }, + ), Actions: []any{ testUtils.AddPolicy{ @@ -442,10 +448,13 @@ func TestACP_ManagerAddsRelationshipWithRelationItDoesNotManageAccordingToPolicy Description: "Test acp, manager adds relationship with relation it does not manage according to policy, error", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - // GQL mutation will return no error when wrong identity is used so test that separately. - testUtils.GQLRequestMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used (only for update requests), + // so test that separately. + testUtils.GQLRequestMutationType, + }, + ), Actions: []any{ testUtils.AddPolicy{ diff --git a/tests/integration/acp/relationship/doc_actor/add/with_manager_test.go b/tests/integration/acp/relationship/doc_actor/add/with_manager_test.go index 485c130805..0b972acf8e 100644 --- a/tests/integration/acp/relationship/doc_actor/add/with_manager_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_manager_test.go @@ -601,10 +601,12 @@ func TestACP_OwnerMakesAManagerThatGivesItSelfReadAndWriteAccess_ManagerCanReadA Description: "Test acp, owner makes a manager that gives itself read and write access", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - testUtils.CollectionNamedMutationType, - testUtils.CollectionSaveMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used with gql (only for update requests), + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), Actions: []any{ testUtils.AddPolicy{ @@ -849,10 +851,12 @@ func TestACP_ManagerAddsRelationshipWithRelationItDoesNotManageAccordingToPolicy Description: "Test acp, manager adds relationship with relation it does not manage according to policy, error", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - testUtils.CollectionNamedMutationType, - testUtils.CollectionSaveMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used with gql (only for update requests), + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), Actions: []any{ testUtils.AddPolicy{ @@ -1017,10 +1021,12 @@ func TestACP_OwnerMakesManagerButManagerCanNotPerformOperations_ManagerCantReadO Description: "Test acp, owner makes a manager, manager can't read or write", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - testUtils.CollectionNamedMutationType, - testUtils.CollectionSaveMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used with gql (only for update requests), + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), Actions: []any{ testUtils.AddPolicy{ diff --git a/tests/integration/acp/relationship/doc_actor/add/with_only_write_gql_test.go b/tests/integration/acp/relationship/doc_actor/add/with_only_write_gql_test.go index 6a3f02f4ba..9391bb5b4d 100644 --- a/tests/integration/acp/relationship/doc_actor/add/with_only_write_gql_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_only_write_gql_test.go @@ -26,10 +26,13 @@ func TestACP_OwnerGivesUpdateWriteAccessToAnotherActorWithoutExplicitReadPerm_GQ Description: "Test acp, owner gives write(update) access without explicit read permission, can still update", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - // GQL mutation will return no error when wrong identity is used so test that separately. - testUtils.GQLRequestMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used (only for update requests), + // so test that separately. + testUtils.GQLRequestMutationType, + }, + ), Actions: []any{ testUtils.AddPolicy{ diff --git a/tests/integration/acp/relationship/doc_actor/add/with_only_write_test.go b/tests/integration/acp/relationship/doc_actor/add/with_only_write_test.go index ccac9cd232..dba024e1a4 100644 --- a/tests/integration/acp/relationship/doc_actor/add/with_only_write_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_only_write_test.go @@ -26,10 +26,12 @@ func TestACP_OwnerGivesUpdateWriteAccessToAnotherActorWithoutExplicitReadPerm_Ot Description: "Test acp, owner gives write(update) access without explicit read permission, can still update", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - testUtils.CollectionNamedMutationType, - testUtils.CollectionSaveMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used with gql (only for update requests), + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), Actions: []any{ testUtils.AddPolicy{ diff --git a/tests/integration/acp/relationship/doc_actor/add/with_reader_gql_test.go b/tests/integration/acp/relationship/doc_actor/add/with_reader_gql_test.go index f51861ec5c..7ca1c30e09 100644 --- a/tests/integration/acp/relationship/doc_actor/add/with_reader_gql_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_reader_gql_test.go @@ -26,10 +26,13 @@ func TestACP_OwnerGivesOnlyReadAccessToAnotherActor_GQL_OtherActorCanReadButNotU Description: "Test acp, owner gives read access to another actor, but the other actor can't update", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - // GQL mutation will return no error when wrong identity is used so test that separately. - testUtils.GQLRequestMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used (only for update requests), + // so test that separately. + testUtils.GQLRequestMutationType, + }, + ), Actions: []any{ testUtils.AddPolicy{ diff --git a/tests/integration/acp/relationship/doc_actor/add/with_reader_test.go b/tests/integration/acp/relationship/doc_actor/add/with_reader_test.go index fd452c2d7d..541b40977e 100644 --- a/tests/integration/acp/relationship/doc_actor/add/with_reader_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_reader_test.go @@ -465,10 +465,11 @@ func TestACP_OwnerGivesOnlyReadAccessToAnotherActor_OtherActorCanReadButNotUpdat Description: "Test acp, owner gives read access to another actor, but the other actor can't update", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - testUtils.CollectionNamedMutationType, - testUtils.CollectionSaveMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), Actions: []any{ testUtils.AddPolicy{ diff --git a/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_gql_test.go b/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_gql_test.go index c05380d8e0..2a421dc74d 100644 --- a/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_gql_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_gql_test.go @@ -28,7 +28,8 @@ func TestACP_OwnerGivesOnlyReadAccessToAllActors_GQL_AllActorsCanReadButNotUpdat SupportedMutationTypes: immutable.Some( []testUtils.MutationType{ - // GQL mutation will return no error when wrong identity is used so test that separately. + // GQL mutation will return no error when wrong identity is used (only for update requests), + // so test that separately. testUtils.GQLRequestMutationType, }, ), @@ -248,3 +249,210 @@ func TestACP_OwnerGivesOnlyReadAccessToAllActors_GQL_AllActorsCanReadButNotUpdat testUtils.ExecuteTestCase(t, test) } + +func TestACP_OwnerGivesOnlyReadAccessToAllActors_GQL_CanReadEvenWithoutIdentityButNotUpdateOrDelete(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives read access to all actors (gql), can read without an identity but can't update or delete", + + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used (only for update requests), + // so test that separately. + testUtils.GQLRequestMutationType, + }, + ), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: testUtils.ClientIdentity(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: testUtils.ClientIdentity(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: testUtils.NoIdentity(), // Can not read without an identity. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.DeleteDoc{ // Since can't read without identity, can't delete either. + CollectionID: 0, + + Identity: testUtils.NoIdentity(), + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.UpdateDoc{ // Since can't read without identity, can't update either. + CollectionID: 0, + + Identity: testUtils.NoIdentity(), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + SkipLocalUpdateEvent: true, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.AllClientIdentities(), + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: testUtils.NoIdentity(), // Now any identity can read, even if there is no identity + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.UpdateDoc{ // But doesn't mean they can update. + CollectionID: 0, + + Identity: testUtils.NoIdentity(), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.DeleteDoc{ // But doesn't mean they can delete. + CollectionID: 0, + + Identity: testUtils.NoIdentity(), + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_test.go b/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_test.go index 4ee858345b..ad587232f9 100644 --- a/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_target_all_actors_test.go @@ -28,6 +28,7 @@ func TestACP_OwnerGivesOnlyReadAccessToAllActors_AllActorsCanReadButNotUpdateOrD SupportedMutationTypes: immutable.Some( []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used with gql (only for update requests), testUtils.CollectionNamedMutationType, testUtils.CollectionSaveMutationType, }, @@ -248,3 +249,210 @@ func TestACP_OwnerGivesOnlyReadAccessToAllActors_AllActorsCanReadButNotUpdateOrD testUtils.ExecuteTestCase(t, test) } + +func TestACP_OwnerGivesOnlyReadAccessToAllActors_CanReadEvenWithoutIdentityButNotUpdateOrDelete(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner gives read access to all actors, can read without an identity but can't update or delete", + + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used with gql (only for update requests), + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }, + ), + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: testUtils.ClientIdentity(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: testUtils.ClientIdentity(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.Request{ + Identity: testUtils.NoIdentity(), // Can not read without an identity. + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents yet + }, + }, + + testUtils.DeleteDoc{ // Since can't read without identity, can't delete either. + CollectionID: 0, + + Identity: testUtils.NoIdentity(), + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.UpdateDoc{ // Since can't read without identity, can't update either. + CollectionID: 0, + + Identity: testUtils.NoIdentity(), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.AllClientIdentities(), + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: testUtils.NoIdentity(), // Now any identity can read, even if there is no identity + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.UpdateDoc{ // But doesn't mean they can update. + CollectionID: 0, + + Identity: testUtils.NoIdentity(), + + DocID: 0, + + Doc: ` + { + "name": "Shahzad Lone" + } + `, + + ExpectedError: "document not found or not authorized to access", + }, + + testUtils.DeleteDoc{ // But doesn't mean they can delete. + CollectionID: 0, + + Identity: testUtils.NoIdentity(), + + DocID: 0, + + ExpectedError: "document not found or not authorized to access", + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/acp/relationship/doc_actor/add/with_update_gql_test.go b/tests/integration/acp/relationship/doc_actor/add/with_update_gql_test.go index eff2be0f7d..6c17ee792b 100644 --- a/tests/integration/acp/relationship/doc_actor/add/with_update_gql_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_update_gql_test.go @@ -26,10 +26,11 @@ func TestACP_OwnerGivesUpdateWriteAccessToAnotherActorTwice_GQL_ShowThatTheRelat Description: "Test acp, owner gives write(update) access to another actor twice, no-op", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - // GQL mutation will return no error when wrong identity is used so test that separately. - testUtils.GQLRequestMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used so test that separately. + testUtils.GQLRequestMutationType, + }), Actions: []any{ testUtils.AddPolicy{ @@ -184,10 +185,11 @@ func TestACP_OwnerGivesUpdateWriteAccessToAnotherActor_GQL_OtherActorCanUpdate(t Description: "Test acp, owner gives write(update) access to another actor", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - // GQL mutation will return no error when wrong identity is used so test that separately. - testUtils.GQLRequestMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used so test that separately. + testUtils.GQLRequestMutationType, + }), Actions: []any{ testUtils.AddPolicy{ diff --git a/tests/integration/acp/relationship/doc_actor/add/with_update_test.go b/tests/integration/acp/relationship/doc_actor/add/with_update_test.go index f6bf553356..582f42af01 100644 --- a/tests/integration/acp/relationship/doc_actor/add/with_update_test.go +++ b/tests/integration/acp/relationship/doc_actor/add/with_update_test.go @@ -26,10 +26,12 @@ func TestACP_OwnerGivesUpdateWriteAccessToAnotherActorTwice_ShowThatTheRelations Description: "Test acp, owner gives write(update) access to another actor twice, no-op", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - testUtils.CollectionNamedMutationType, - testUtils.CollectionSaveMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used with gql (only for update requests), + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), Actions: []any{ testUtils.AddPolicy{ @@ -184,10 +186,12 @@ func TestACP_OwnerGivesUpdateWriteAccessToAnotherActor_OtherActorCanUpdate(t *te Description: "Test acp, owner gives write(update) access to another actor", - SupportedMutationTypes: immutable.Some([]testUtils.MutationType{ - testUtils.CollectionNamedMutationType, - testUtils.CollectionSaveMutationType, - }), + SupportedMutationTypes: immutable.Some( + []testUtils.MutationType{ + // GQL mutation will return no error when wrong identity is used with gql (only for update requests), + testUtils.CollectionNamedMutationType, + testUtils.CollectionSaveMutationType, + }), Actions: []any{ testUtils.AddPolicy{ diff --git a/tests/integration/acp/relationship/doc_actor/delete/with_target_all_actors_test.go b/tests/integration/acp/relationship/doc_actor/delete/with_target_all_actors_test.go index 14c0121a41..9db120ce91 100644 --- a/tests/integration/acp/relationship/doc_actor/delete/with_target_all_actors_test.go +++ b/tests/integration/acp/relationship/doc_actor/delete/with_target_all_actors_test.go @@ -165,7 +165,7 @@ func TestACP_OwnerRevokesAccessFromAllNonExplicitActors_ActorsCanNotReadAnymore( }, }, - testUtils.DeleteDocActorRelationship{ // Revoke access from all actors, not explictly allowed. + testUtils.DeleteDocActorRelationship{ // Revoke access from all actors, (ones given access through * implicitly). RequestorIdentity: testUtils.ClientIdentity(1), TargetIdentity: testUtils.AllClientIdentities(), @@ -444,7 +444,7 @@ func TestACP_OwnerRevokesAccessFromAllNonExplicitActors_ExplicitActorsCanStillRe }, }, - testUtils.DeleteDocActorRelationship{ // Revoke access from all actors, not explictly allowed. + testUtils.DeleteDocActorRelationship{ // Revoke access from all actors, (ones given access through * implicitly). RequestorIdentity: testUtils.ClientIdentity(1), TargetIdentity: testUtils.AllClientIdentities(), @@ -546,3 +546,164 @@ func TestACP_OwnerRevokesAccessFromAllNonExplicitActors_ExplicitActorsCanStillRe testUtils.ExecuteTestCase(t, test) } + +func TestACP_OwnerRevokesAccessFromAllNonExplicitActors_NonIdentityRequestsCanNotReadAnymore(t *testing.T) { + expectedPolicyID := "fc56b7509c20ac8ce682b3b9b4fdaad868a9c70dda6ec16720298be64f16e9a4" + + test := testUtils.TestCase{ + + Description: "Test acp, owner revokes read access from actors that were given read access implicitly, non-identity actors can't read anymore", + + Actions: []any{ + testUtils.AddPolicy{ + + Identity: testUtils.ClientIdentity(1), + + Policy: ` + name: Test Policy + + description: A Policy + + actor: + name: actor + + resources: + users: + permissions: + read: + expr: owner + reader + writer + + write: + expr: owner + writer + + nothing: + expr: dummy + + relations: + owner: + types: + - actor + + reader: + types: + - actor + + writer: + types: + - actor + + admin: + manages: + - reader + types: + - actor + + dummy: + types: + - actor + `, + + ExpectedPolicyID: expectedPolicyID, + }, + + testUtils.SchemaUpdate{ + Schema: fmt.Sprintf(` + type Users @policy( + id: "%s", + resource: "users" + ) { + name: String + age: Int + } + `, + expectedPolicyID, + ), + }, + + testUtils.CreateDoc{ + Identity: testUtils.ClientIdentity(1), + + CollectionID: 0, + + Doc: ` + { + "name": "Shahzad", + "age": 28 + } + `, + }, + + testUtils.AddDocActorRelationship{ + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.AllClientIdentities(), // Give implicit access to all identities. + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedExistence: false, + }, + + testUtils.Request{ + Identity: testUtils.NoIdentity(), // Can read even without identity + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{ + { + "_docID": "bae-9d443d0c-52f6-568b-8f74-e8ff0825697b", + "name": "Shahzad", + "age": int64(28), + }, + }, + }, + }, + + testUtils.DeleteDocActorRelationship{ // Revoke access from all actors, (ones given access through * implicitly). + RequestorIdentity: testUtils.ClientIdentity(1), + + TargetIdentity: testUtils.AllClientIdentities(), + + CollectionID: 0, + + DocID: 0, + + Relation: "reader", + + ExpectedRecordFound: true, + }, + + testUtils.Request{ + Identity: testUtils.NoIdentity(), // Can not read anymore + + Request: ` + query { + Users { + _docID + name + age + } + } + `, + + Results: map[string]any{ + "Users": []map[string]any{}, // Can't see the documents now + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} diff --git a/tests/integration/query/one_to_many/with_count_order_test.go b/tests/integration/query/one_to_many/with_count_order_test.go new file mode 100644 index 0000000000..11b26ae396 --- /dev/null +++ b/tests/integration/query/one_to_many/with_count_order_test.go @@ -0,0 +1,87 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package one_to_many + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryOneToMany_WithCountAliasOrder_ShouldOrderResults(t *testing.T) { + test := testUtils.TestCase{ + Description: "One-to-many relation query from many side with order alias", + Actions: []any{ + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Painted House", + "rating": 4.9, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "A Time for Mercy", + "rating": 4.5, + "author_id": testUtils.NewDocIndex(1, 0), + }, + }, + testUtils.CreateDoc{ + CollectionID: 0, + DocMap: map[string]any{ + "name": "Theif Lord", + "rating": 4.8, + "author_id": testUtils.NewDocIndex(1, 1), + }, + }, + testUtils.Request{ + Request: `query { + Author(order: {_alias: {publishedCount: DESC}}) { + name + publishedCount: _count(published: {}) + } + }`, + Results: map[string]any{ + "Author": []map[string]any{ + { + "name": "John Grisham", + "publishedCount": int64(2), + }, + { + "name": "Cornelia Funke", + "publishedCount": int64(1), + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/query/one_to_many/with_count_test.go b/tests/integration/query/one_to_many/with_count_test.go index 77d4e754f3..77905ed748 100644 --- a/tests/integration/query/one_to_many/with_count_test.go +++ b/tests/integration/query/one_to_many/with_count_test.go @@ -119,11 +119,9 @@ func TestQueryOneToManyWithCount(t *testing.T) { } } -// This test documents the behavior of aggregate alias targeting which is not yet implemented. -// https://github.com/sourcenetwork/defradb/issues/3195 -func TestQueryOneToMany_WithCountAliasFilter_ShouldFilterAll(t *testing.T) { +func TestQueryOneToMany_WithCountAliasFilter_ShouldMatchAll(t *testing.T) { test := testUtils.TestCase{ - Description: "One-to-many relation query from many side with count", + Description: "One-to-many relation query from many side with count alias", Actions: []any{ testUtils.CreateDoc{ CollectionID: 1, @@ -173,7 +171,16 @@ func TestQueryOneToMany_WithCountAliasFilter_ShouldFilterAll(t *testing.T) { } }`, Results: map[string]any{ - "Author": []map[string]any{}, + "Author": []map[string]any{ + { + "name": "Cornelia Funke", + "publishedCount": 1, + }, + { + "name": "John Grisham", + "publishedCount": 2, + }, + }, }, }, }, diff --git a/tests/integration/query/one_to_many/with_sum_order_test.go b/tests/integration/query/one_to_many/with_sum_order_test.go new file mode 100644 index 0000000000..4c6c7aa0bf --- /dev/null +++ b/tests/integration/query/one_to_many/with_sum_order_test.go @@ -0,0 +1,95 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package one_to_many + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQueryOneToMany_WithSumWithAliasOrder_ShouldOrderResults(t *testing.T) { + test := testUtils.TestCase{ + Description: "One-to-many relation query from many side with sum with order alias", + Actions: []any{ + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "name": "Painted House", + "rating": 4.9, + "author_id": "bae-e1ea288f-09fa-55fa-b0b5-0ac8941ea35b" + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "name": "A Time for Mercy", + "rating": 4.5, + "author_id": "bae-e1ea288f-09fa-55fa-b0b5-0ac8941ea35b" + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "name": "The Associate", + "rating": 4.2, + "author_id": "bae-e1ea288f-09fa-55fa-b0b5-0ac8941ea35b" + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: `{ + "name": "Theif Lord", + "rating": 4.8, + "author_id": "bae-72e8c691-9f20-55e7-9228-8af1cf54cace" + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "John Grisham", + "age": 65, + "verified": true + }`, + }, + testUtils.CreateDoc{ + CollectionID: 1, + Doc: `{ + "name": "Cornelia Funke", + "age": 62, + "verified": false + }`, + }, + testUtils.Request{ + Request: `query { + Author(order: {_alias: {totalRating: DESC}}) { + name + totalRating: _sum(published: {field: rating}) + } + }`, + Results: map[string]any{ + "Author": []map[string]any{ + { + "name": "John Grisham", + "totalRating": 13.600000000000001, + }, + { + "name": "Cornelia Funke", + "totalRating": 4.8, + }, + }, + }, + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/query/simple/with_group_aggregate_alias_filter_test.go b/tests/integration/query/simple/with_group_aggregate_alias_filter_test.go new file mode 100644 index 0000000000..037a187cad --- /dev/null +++ b/tests/integration/query/simple/with_group_aggregate_alias_filter_test.go @@ -0,0 +1,303 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package simple + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +func TestQuerySimple_WithGroupAverageAliasFilter_FiltersResults(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with group average alias filter", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: `type Users { + Name: String + Score: Int + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Score": 10 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Score": 20 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "Score": 40 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "Score": 0 + }`, + }, + testUtils.Request{ + Request: `query { + Users(groupBy: [Name], filter: {_alias: {averageScore: {_eq: 20}}}) { + Name + averageScore: _avg(_group: {field: Score}) + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "Alice", + "averageScore": float64(20), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQuerySimple_WithGroupSumAliasFilter_FiltersResults(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with group sum alias filter", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: `type Users { + Name: String + Score: Int + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Score": 10 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Score": 20 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "Score": 40 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "Score": 0 + }`, + }, + testUtils.Request{ + Request: `query { + Users(groupBy: [Name], filter: {_alias: {totalScore: {_eq: 40}}}) { + Name + totalScore: _sum(_group: {field: Score}) + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "Alice", + "totalScore": float64(40), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQuerySimple_WithGroupMinAliasFilter_FiltersResults(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with group min alias filter", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: `type Users { + Name: String + Score: Int + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Score": 10 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Score": 20 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "Score": 40 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "Score": 0 + }`, + }, + testUtils.Request{ + Request: `query { + Users(groupBy: [Name], filter: {_alias: {minScore: {_eq: 0}}}) { + Name + minScore: _min(_group: {field: Score}) + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "Alice", + "minScore": int64(0), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQuerySimple_WithGroupMaxAliasFilter_FiltersResults(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with group max alias filter", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: `type Users { + Name: String + Score: Int + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Score": 10 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Score": 20 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "Score": 40 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "Score": 0 + }`, + }, + testUtils.Request{ + Request: `query { + Users(groupBy: [Name], filter: {_alias: {maxScore: {_eq: 40}}}) { + Name + maxScore: _max(_group: {field: Score}) + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "Alice", + "maxScore": int64(40), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +} + +func TestQuerySimple_WithGroupCountAliasFilter_FiltersResults(t *testing.T) { + test := testUtils.TestCase{ + Description: "Simple query with group count alias filter", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: `type Users { + Name: String + Score: Int + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Score": 10 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Bob", + "Score": 20 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "Score": 40 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "Score": 0 + }`, + }, + testUtils.CreateDoc{ + Doc: `{ + "Name": "Alice", + "Score": 5 + }`, + }, + testUtils.Request{ + Request: `query { + Users(groupBy: [Name], filter: {_alias: {scores: {_eq: 3}}}) { + Name + scores: _count(_group: {}) + } + }`, + Results: map[string]any{ + "Users": []map[string]any{ + { + "Name": "Alice", + "scores": int64(3), + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +}