diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..118f159d0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,55 @@ +# AGENTS.md + +GraphZen is a code-first GraphQL SDK for .NET. + +## Commands + +- **Build:** `dotnet build` +- **Test:** `dotnet test` +- **Filter tests:** `dotnet test --filter "FullyQualifiedName~TestName"` +- **Code gen:** `dotnet run --project src/GraphZen.DevCli/GraphZen.DevCli.csproj gen` + +## Architecture + +Layered packages, each building on the previous: + +1. **Abstractions** - Data annotations for code-first GraphQL development +2. **Infrastructure** - Internal shared utilities +3. **LanguageModel** - GraphQL parser, AST, and printer (uses Superpower) +4. **TypeSystem** - Schema definition and type system object model +5. **QueryEngine** - Query execution engine +6. **AspNetCore.Server** - ASP.NET Core integration for hosting GraphQL APIs + +Key patterns: +- `SchemaBuilder` fluent API builds `SchemaDefinition` (mutable) -> `Schema` (immutable) +- Type system uses `I*Definition` / `I*` interface pairs (mutable builder vs immutable runtime) +- AST nodes in `Syntax/` with visitor/walker pattern; T4-generated code (`*.Generated.cs`) +- Tests follow `*.Tests`, `*.IntegrationTests`, `*.FunctionalTests` conventions + +## Related Repositories + +- **Superpower** (parsing library) - Local clone at `~/Code/datalust/superpower` (upstream: https://github.com/datalust/superpower). Used by `GraphZen.LanguageModel` for GraphQL parsing. + +## ReSharper CLI (`dotnet jb`) + +Inspection excludes (e.g. `TestResults/`) are configured in `GraphZen.slnx.DotSettings`. Cleanup requires `--exclude` (not supported via `.DotSettings`). + +- **Inspect:** `dotnet jb inspectcode GraphZen.slnx -f Text --stdout` +- **Inspect (warnings+):** `dotnet jb inspectcode GraphZen.slnx -e WARNING -f Text --stdout` +- **Inspect single project:** `dotnet jb inspectcode GraphZen.slnx --project "GraphZen.TypeSystem" -f Text --stdout` +- **Cleanup:** `dotnet jb cleanupcode GraphZen.slnx --exclude="**/TestResults/**"` +- **Cleanup scoped:** `dotnet jb cleanupcode GraphZen.slnx --include "src/GraphZen.TypeSystem/**/*.cs"` +- **Reformat only:** `dotnet jb cleanupcode GraphZen.slnx --exclude="**/TestResults/**" --profile "Built-in: Reformat Code"` + +## Code Style + +Nullable enabled, warnings as errors, latest C# language version. + +## Related Repositories + +- **Superpower** (parsing library): `~/Code/datalust/superpower` | [github.com/datalust/superpower](https://github.com/datalust/superpower) +- **graphql-spec** (GraphQL specification): `~/Code/graphql/graphql-spec` | [github.com/graphql/graphql-spec](https://github.com/graphql/graphql-spec) +- **graphql-js** (reference implementation): `~/Code/graphql/graphql-js` | [github.com/graphql/graphql-js](https://github.com/graphql/graphql-js) +- **graphiql** (in-browser GraphQL IDE): `~/Code/graphql/graphiql` | [github.com/graphql/graphiql](https://github.com/graphql/graphiql) +- **dataloader** (batching/caching utility): `~/Code/graphql/dataloader` | [github.com/graphql/dataloader](https://github.com/graphql/dataloader) +- **foundation** (GraphQL Foundation): `~/Code/graphql/foundation` | [github.com/graphql/foundation](https://github.com/graphql/foundation) diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 6264e484d..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,55 +0,0 @@ -# CLAUDE.md - -GraphZen is a code-first GraphQL SDK for .NET. - -## Commands - -- **Build:** `dotnet build` -- **Test:** `dotnet test` -- **Filter tests:** `dotnet test --filter "FullyQualifiedName~TestName"` -- **Code gen:** `dotnet run --project src/GraphZen.DevCli/GraphZen.DevCli.csproj gen` - -## Architecture - -Layered packages, each building on the previous: - -1. **Abstractions** - Data annotations for code-first GraphQL development -2. **Infrastructure** - Internal shared utilities -3. **LanguageModel** - GraphQL parser, AST, and printer (uses Superpower) -4. **TypeSystem** - Schema definition and type system object model -5. **QueryEngine** - Query execution engine -6. **AspNetCore.Server** - ASP.NET Core integration for hosting GraphQL APIs - -Key patterns: -- `SchemaBuilder` fluent API builds `SchemaDefinition` (mutable) -> `Schema` (immutable) -- Type system uses `I*Definition` / `I*` interface pairs (mutable builder vs immutable runtime) -- AST nodes in `Syntax/` with visitor/walker pattern; T4-generated code (`*.Generated.cs`) -- Tests follow `*.Tests`, `*.IntegrationTests`, `*.FunctionalTests` conventions - -## Related Repositories - -- **Superpower** (parsing library) - Local clone at `~/Code/datalust/superpower` (upstream: https://github.com/datalust/superpower). Used by `GraphZen.LanguageModel` for GraphQL parsing. - -## ReSharper CLI (`dotnet jb`) - -Inspection excludes (e.g. `TestResults/`) are configured in `GraphZen.slnx.DotSettings`. Cleanup requires `--exclude` (not supported via `.DotSettings`). - -- **Inspect:** `dotnet jb inspectcode GraphZen.slnx -f Text --stdout` -- **Inspect (warnings+):** `dotnet jb inspectcode GraphZen.slnx -e WARNING -f Text --stdout` -- **Inspect single project:** `dotnet jb inspectcode GraphZen.slnx --project "GraphZen.TypeSystem" -f Text --stdout` -- **Cleanup:** `dotnet jb cleanupcode GraphZen.slnx --exclude="**/TestResults/**"` -- **Cleanup scoped:** `dotnet jb cleanupcode GraphZen.slnx --include "src/GraphZen.TypeSystem/**/*.cs"` -- **Reformat only:** `dotnet jb cleanupcode GraphZen.slnx --exclude="**/TestResults/**" --profile "Built-in: Reformat Code"` - -## Code Style - -Nullable enabled, warnings as errors, latest C# language version. - -## Related Repositories - -- **Superpower** (parsing library): `~/Code/datalust/superpower` | [github.com/datalust/superpower](https://github.com/datalust/superpower) -- **graphql-spec** (GraphQL specification): `~/Code/graphql/graphql-spec` | [github.com/graphql/graphql-spec](https://github.com/graphql/graphql-spec) -- **graphql-js** (reference implementation): `~/Code/graphql/graphql-js` | [github.com/graphql/graphql-js](https://github.com/graphql/graphql-js) -- **graphiql** (in-browser GraphQL IDE): `~/Code/graphql/graphiql` | [github.com/graphql/graphiql](https://github.com/graphql/graphiql) -- **dataloader** (batching/caching utility): `~/Code/graphql/dataloader` | [github.com/graphql/dataloader](https://github.com/graphql/dataloader) -- **foundation** (GraphQL Foundation): `~/Code/graphql/foundation` | [github.com/graphql/foundation](https://github.com/graphql/foundation) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/GraphZen.slnx b/GraphZen.slnx index 74a2fd49b..52e772908 100644 --- a/GraphZen.slnx +++ b/GraphZen.slnx @@ -37,6 +37,7 @@ + diff --git a/test/GraphZen.SpecConformance.Tests/AGENTS.md b/test/GraphZen.SpecConformance.Tests/AGENTS.md new file mode 100644 index 000000000..5d668b918 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/AGENTS.md @@ -0,0 +1,279 @@ +# AGENTS.md + +GraphZen's specification conformance suite -- an executable conformance statement mapping the GraphQL spec to test coverage. Each class maps to one spec subsection, each method proves one normative statement, and gaps are always explicit. The directory should read like an executable appendix to the GraphQL spec. + +## Coverage Principles + +The spec is the sole source of truth for what this suite covers. graphql-js is a supplement for example test cases, not a coverage guide. + +- **Every spec test case gets written** with its query and expected outcome, regardless of whether GraphZen can pass it today. The test body should contain the actual GraphQL query and assertion call. +- **Skip the test, not the work.** When GraphZen lacks an implementation, the test is skipped with a reason pointing at the implementation gap. The skip message describes what GraphZen needs to implement, not that the test is deferred. +- **Empty placeholders are temporary.** A skipped `[Fact]` with an empty body is acceptable only while actively porting test cases. The goal state is a fully-written test that's skipped because the implementation isn't there yet — not a placeholder that says "we'll write this later." +- **The manifest is driven by the spec**, not by GraphZen's capabilities. Every validation subsection in the spec gets a manifest entry. +- **The suite is agnostic of GraphZen internals.** Conformance classes should not reference GraphZen implementation details. The harness provides `ExpectValid`, `ExpectErrors`, and `ToDeepEqual` — that is the full API surface for test authors. + +## Running Tests + +```sh +dotnet test --project test/GraphZen.SpecConformance.Tests/ # all conformance tests +dotnet test --project test/GraphZen.SpecConformance.Tests/ --filter "SpecSection=5" # Chapter 5 +dotnet test --project test/GraphZen.SpecConformance.Tests/ --filter "SpecSection=5.3" # Fields (5.3.x) +dotnet test --project test/GraphZen.SpecConformance.Tests/ --filter "SpecSection=5.3.3" # one subsection +``` + +Hierarchical filtering works because `SpecSectionDiscoverer` expands `"5.3.3"` into traits for `"5"`, `"5.3"`, and `"5.3.3"`. + +## Adding a Conformance Class + +1. **Verify the heading** in the local spec source (`~/Code/graphql/graphql-spec/spec/Section 5 -- Validation.md`). Do not invent headings from memory. +2. **Derive the section number** from `https://spec.graphql.org/draft/`. The source markdown has no explicit numbers -- count headings to derive numbers like `5.3.3`. +3. **Check graphql-js** in `~/Code/graphql/graphql-js/src/validation/` for upstream test cases and intent. This is a supplement, not the source of truth. +4. **Add the section number** to `SpecCoverageManifest.ValidationSections` if not already present. +5. **Create the class** in the correct folder and namespace (see Structure below). +6. **Add test cases** as individual `[Fact]` methods -- one method per distinct spec scenario (see Test Patterns below). +7. **Run the section tests**: `dotnet test --filter "SpecSection=X.Y.Z"` +8. **Run the coverage test**: `dotnet test --filter "FullyQualifiedName~ValidationCoverageTests"` + +## Specification Sources + +This suite tracks the **working draft** of the GraphQL specification, not a published edition. + +| Source | Use for | +|---|---| +| **Spec website:** `https://spec.graphql.org/draft/` | Canonical section numbers, headings, deep-link URLs | +| **Local spec clone:** `~/Code/graphql/graphql-spec` | Exact wording, markdown source | +| **Local graphql-js clone:** `~/Code/graphql/graphql-js` | Reference implementation behavior, upstream tests | +| **Upstream repos:** `github.com/graphql/graphql-spec`, `github.com/graphql/graphql-js` | Linking in PRs | + +**Section numbering:** The spec source markdown has no explicit section numbers. Numbers like `5.3.3` are derived from heading order (first `##` = X.1, first `###` under X.3 = X.3.1, etc.). Always verify against the website or by counting headings -- do not guess. + +## Structure + +The mapping from specification to code is explicit and predictable: + +| Artifact | Maps to | +|---|---| +| folder | spec chapter or subsection group | +| namespace | same as folder | +| file | one class, named to match the class it contains | +| class | one exact spec subsection | +| method | one normative statement, allowance, prohibition, or example | + +If the file path, namespace, class name, and attribute disagree about which subsection is represented, fix the structure. Do not put multiple conformance classes in one file. + +### Example + +For spec subsection `5.3.3 Leaf Field Selections`: + +| Artifact | Value | +|---|---| +| file path | `Section5_Validation/Fields/LeafFieldSelectionsConformanceTests.cs` | +| namespace | `GraphZen.SpecConformance.Tests.Section5_Validation.Fields` | +| class name | `LeafFieldSelectionsConformanceTests` | +| XML doc comment | `/// ` | +| attribute | `[SpecSection("5.3.3", "Leaf Field Selections")]` | + +### Naming + +- Chapter folders: `Section2_Language`, `Section3_TypeSystem`, `Section4_Introspection`, `Section5_Validation`, `Section6_Execution`, `Section7_Response` +- Subsection folders: spec-oriented group names (`Fields/`, `Arguments/`), not implementation names +- Classes: named after the subsection heading, suffixed with `ConformanceTests` +- Methods: snake_case, read like executable spec prose (`object_field_selection_is_valid()`) + +### `SpecSection` Attribute + +```csharp +[SpecSection("5.3.3", "Leaf Field Selections")] +``` + +First parameter is the section number, second is the spec heading. Each class must have exactly one `[SpecSection]` attribute -- do not use multiple attributes on a single class. + +### XML Doc Comments + +Use XML doc comments for spec traceability. Unlike `//` comments, they are semantically bound to their declaration and won't be separated by formatters. + +**Class level** -- always include a `` with the spec deep link: + +```csharp +/// +[SpecSection("5.3.3", "Leaf Field Selections")] +public class LeafFieldSelectionsConformanceTests +``` + +Do not add a `` that restates the class name or attribute. + +**Method level** -- only when the comment adds information the method name and query don't already carry: + +```csharp +/// +/// The spec explicitly allows fields defined on implementors to appear +/// in inline fragments on the interface, even though the interface +/// itself does not declare the field. +/// +[Fact] +public void valid_field_in_inline_fragment_on_implementor() +``` + +Do not use `//` comments for spec traceability (URLs, version references, graphql-js paths). The spec version is in `SpecMetadata.cs`. + +#### Spec URL Format + +``` +https://spec.graphql.org/draft/#sec-{Heading-With-Hyphens} +``` + +Spaces become hyphens. Ambiguous headings are prefixed with the parent section using a dot separator: + +| Heading | URL | +|---|---| +| Leaf Field Selections | `...#sec-Leaf-Field-Selections` | +| Fragments (under Language) | `...#sec-Language.Fragments` | +| Input Coercion (under Input Objects) | `...#sec-Input-Objects.Input-Coercion` | + +When in doubt, navigate to the heading on the spec website and copy the anchor. + +### Sub-Subsection Handling + +Some subsections contain further children (e.g., 5.5.2.3 "Fragment Spread Is Possible" has 5.5.2.3.1 through 5.5.2.3.5). The manifest lists only the parent subsection, and a single conformance class covers it including all children. Do not create separate manifest entries or classes for sub-subsections. + +## Test Patterns + +### Conformance Class + +Each test case is an individual `[Fact]` method. The method name reads like an executable spec statement, and the body contains the query and assertion. + +```csharp +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Fields; + +/// +[SpecSection("5.3.3", "Leaf Field Selections")] +public class LeafFieldSelectionsConformanceTests +{ + [Fact] + public void valid_scalar_selection() + { + ExpectValid(ScalarLeafs, """ + fragment scalarSelection on Dog { + barks + } + """); + } + + [Fact(Skip = "GraphZen does not reject queries selecting sub-fields on scalar types.")] + public void object_type_missing_selection_is_rejected() + { + ExpectErrors(ScalarLeafs, """ + query directQueryOnObjectWithoutSubFields { + human + } + """).ToDeepEqual( + new("Field \"human\" of type \"Human\" must have a selection of subfields. Did you mean \"human { ... }\"?", + Line: 2, Column: 15)); + } +} +``` + +Conventions: + +- One `[Fact]` per distinct spec scenario -- do not group unrelated cases into `TheoryData` +- Use `[Theory]` only for genuine parametric variations of the same scenario +- Keep GraphQL documents as formatted raw string literals +- Every skipped test gets its own `Skip` message describing the specific gap + +### Implementation Gaps + +When GraphZen lacks an implementation, write the test case with the query and expected behavior, then throw `NotImplementedException`: + +```csharp +[Fact] +public void query_type_must_be_defined() +{ + var query = """ + { field } + """; + throw new NotImplementedException( + "GraphZen does not implement Operation Type Existence validation."); +} +``` + +When the implementation lands, replace the throw with the assertion and the test is ready to run. + +### Assertion Helpers + +`SpecValidation` (imported via `using static`) provides: + +| Method | Use when | +|---|---| +| `ExpectValid(rule, query)` | Query should produce zero errors from this rule | +| `ExpectValid(schema, rule, query)` | Same, against a custom schema | +| `ExpectErrors(rule, query)` | Returns errors from a specific rule against TestSchema -- chain with `.ToDeepEqual(...)` | +| `ExpectErrors(schema, rule, query)` | Same, against a custom schema | + +Every test explicitly names the rule it exercises. `ExpectValid` is sugar for `ExpectErrors(rule, query).ToDeepEqual()` (no args = empty error list). This mirrors graphql-js's per-file `expectValid`/`expectErrors` harness pattern. + +Do not use `QueryShouldPass` or `QueryShouldFail` -- they don't bind to a specific rule and `QueryShouldFail` only checks error count. + +#### `ToDeepEqual` and `ExpectedError` + +`.ToDeepEqual(...)` asserts the exact error list -- message, line, and column for each error: + +```csharp +ExpectErrors(FieldsOnCorrectType, """ + fragment typeKnownAgain on Pet { + unknown_pet_field { + ... on Cat { + unknown_cat_field + } + } + } + """).ToDeepEqual( + new("Cannot query field \"unknown_pet_field\" on type \"Pet\".", Line: 3, Column: 7), + new("Cannot query field \"unknown_cat_field\" on type \"Cat\".", Line: 5, Column: 11)); +``` + +`ExpectedError` is a positional record: `new(message, Line: line, Column: column)`. Always assert both the message and the location -- never assert only the error count. Calling `.ToDeepEqual()` with no arguments asserts zero errors (this is what `ExpectValid` does). + +### TestSchema + +Most tests use the shared `TestSchema` provided by the harness. Introduce a section-local schema when a test needs a smaller or clearer setup. + +- **Interfaces**: Being, Pet, Canine, Intelligent +- **Objects**: Dog, Cat, Human, Alien, ComplicatedArgs, QueryRoot +- **Unions**: CatOrDog, DogOrHuman, HumanOrAlien +- **Enums**: DogCommand, FurColor +- **Input Objects**: ComplexInput +- **Custom Scalars**: Invalid, Any +- **Directives**: onQuery, onMutation, onSubscription, onField, onFragmentDefinition, onFragmentSpread, onInlineFragment + +QueryRoot exposes: `human`, `alien`, `cat`, `pet`, `catOrDog`, `dogOrHuman`, `humanOrAlien`, `complicatedArgs`, `invalidArg`, `anyArg`. + +## Coverage Manifest + +`Infrastructure/SpecCoverageManifest.cs` lists every spec subsection that should have a conformance class. `Infrastructure/ValidationCoverageTests.cs` uses reflection to verify every manifest entry has a corresponding `[SpecSection]` class. + +When adding a new subsection: + +1. Add the section number to `SpecCoverageManifest.ValidationSections` +2. Create the conformance class (or gap placeholder) +3. Run `dotnet test --filter "FullyQualifiedName~ValidationCoverageTests"` + +## Quality Standards + +- One test proves one thing +- Tests read like the spec, not framework plumbing +- Assert observable GraphQL behavior, not internal implementation +- Gaps are always explicit -- never silently absent +- Keep GraphQL inputs visible in the test -- avoid helpers that hide the query +- Repeat simple setup if it aids readability + +### Porting from graphql-js + +- Preserve the original intent before adapting style +- Keep one C# test close to one upstream case +- Keep case names recognizable +- Do not port reference implementation quirks not required by the spec +- Mark cases that are reference-only or not applicable to GraphZen diff --git a/test/GraphZen.SpecConformance.Tests/CLAUDE.md b/test/GraphZen.SpecConformance.Tests/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/test/GraphZen.SpecConformance.Tests/GraphZen.SpecConformance.Tests.csproj b/test/GraphZen.SpecConformance.Tests/GraphZen.SpecConformance.Tests.csproj new file mode 100644 index 000000000..03fa67041 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/GraphZen.SpecConformance.Tests.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecCoverageManifest.cs b/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecCoverageManifest.cs new file mode 100644 index 000000000..c6beece58 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecCoverageManifest.cs @@ -0,0 +1,40 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +namespace GraphZen.SpecConformance.Tests.Infrastructure; + +public static class SpecCoverageManifest +{ + public static IReadOnlyList ValidationSections { get; } = + [ + "5.1.1", + "5.2.2.1", + "5.2.3.1", + "5.2.4.1", + "5.3.1", + "5.3.2", + "5.3.3", + "5.4.1", + "5.4.2", + "5.4.3", + "5.5.1.1", + "5.5.1.2", + "5.5.1.3", + "5.5.1.4", + "5.5.2.1", + "5.5.2.2", + "5.5.2.3", + "5.6.1", + "5.6.2", + "5.6.3", + "5.6.4", + "5.7.1", + "5.7.2", + "5.7.3", + "5.8.1", + "5.8.2", + "5.8.3", + "5.8.4", + "5.8.5", + ]; +} diff --git a/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecMetadata.cs b/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecMetadata.cs new file mode 100644 index 000000000..1f0d88a2d --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecMetadata.cs @@ -0,0 +1,9 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +namespace GraphZen.SpecConformance.Tests.Infrastructure; + +public static class SpecMetadata +{ + public const string Version = "draft-2026-04-02"; +} diff --git a/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecSectionAttribute.cs b/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecSectionAttribute.cs new file mode 100644 index 000000000..714ee5e02 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecSectionAttribute.cs @@ -0,0 +1,17 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using Xunit.Sdk; + +namespace GraphZen.SpecConformance.Tests.Infrastructure; + +[TraitDiscoverer( + "GraphZen.SpecConformance.Tests.Infrastructure.SpecSectionDiscoverer", + "GraphZen.SpecConformance.Tests")] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public sealed class SpecSectionAttribute(string section, string? rule = null) : Attribute, ITraitAttribute +{ + public string Section { get; } = section; + + public string? Rule { get; } = rule; +} diff --git a/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecSectionDiscoverer.cs b/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecSectionDiscoverer.cs new file mode 100644 index 000000000..369a91e36 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecSectionDiscoverer.cs @@ -0,0 +1,38 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace GraphZen.SpecConformance.Tests.Infrastructure; + +public sealed class SpecSectionDiscoverer : ITraitDiscoverer +{ + public IEnumerable> GetTraits(IAttributeInfo traitAttribute) + { + var arguments = traitAttribute.GetConstructorArguments().ToArray(); + var section = (string)arguments[0]; + var rule = arguments.Length > 1 ? arguments[1] as string : null; + + foreach (var parentSection in ExpandSectionHierarchy(section)) + { + yield return new KeyValuePair("SpecSection", parentSection); + } + + yield return new KeyValuePair("SpecVersion", SpecMetadata.Version); + + if (!string.IsNullOrWhiteSpace(rule)) + { + yield return new KeyValuePair("SpecRule", rule!); + } + } + + private static IEnumerable ExpandSectionHierarchy(string section) + { + var parts = section.Split('.'); + for (var i = 1; i <= parts.Length; i++) + { + yield return string.Join(".", parts.Take(i)); + } + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecValidation.cs b/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecValidation.cs new file mode 100644 index 000000000..c4af13d6e --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Infrastructure/SpecValidation.cs @@ -0,0 +1,54 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.LanguageModel.Internal; +using GraphZen.LanguageModel.Validation; +using GraphZen.QueryEngine.Validation; +using GraphZen.Tests.Validation.Rules; +using GraphZen.TypeSystem; + +namespace GraphZen.SpecConformance.Tests.Infrastructure; + +public record ExpectedError(string Message, int Line, int Column); + +public class ValidationResult +{ + private readonly IReadOnlyCollection _errors; + + public ValidationResult(IReadOnlyCollection errors) => _errors = errors; + + public void ToDeepEqual(params ExpectedError[] expected) + { + Assert.Equal(expected.Length, _errors.Count); + var errorsList = _errors.ToList(); + for (var i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i].Message, errorsList[i].Message); + Assert.NotNull(errorsList[i].Locations); + var location = errorsList[i].Locations![0]; + Assert.Equal(expected[i].Line, location.Line); + Assert.Equal(expected[i].Column, location.Column); + } + } +} + +public static class SpecValidation +{ + public static Schema TestSchema => ValidationRuleHarness.TestSchema; + + public static ValidationResult ExpectErrors(ValidationRule rule, string query) => + ExpectErrors(TestSchema, rule, query); + + public static ValidationResult ExpectErrors(Schema schema, ValidationRule rule, string query) + { + var document = Parser.ParseDocument(query); + var errors = new QueryValidator([rule]).Validate(schema, document); + return new ValidationResult(errors); + } + + public static void ExpectValid(ValidationRule rule, string query) => + ExpectErrors(rule, query).ToDeepEqual(); + + public static void ExpectValid(Schema schema, ValidationRule rule, string query) => + ExpectErrors(schema, rule, query).ToDeepEqual(); +} diff --git a/test/GraphZen.SpecConformance.Tests/Infrastructure/ValidationCoverageTests.cs b/test/GraphZen.SpecConformance.Tests/Infrastructure/ValidationCoverageTests.cs new file mode 100644 index 000000000..78af20902 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Infrastructure/ValidationCoverageTests.cs @@ -0,0 +1,27 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using System.Reflection; + +namespace GraphZen.SpecConformance.Tests.Infrastructure; + +public class ValidationCoverageTests +{ + [Fact] + public void validation_manifest_sections_have_a_conformance_class() + { + var discoveredSections = Assembly + .GetExecutingAssembly() + .GetTypes() + .SelectMany(type => type.GetCustomAttributes()) + .Select(attribute => attribute.Section) + .ToHashSet(StringComparer.Ordinal); + + var missingSections = SpecCoverageManifest.ValidationSections + .Where(section => !discoveredSections.Contains(section)) + .ToArray(); + + Assert.True(missingSections.Length == 0, + "Missing conformance classes for sections: " + string.Join(", ", missingSections)); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Arguments/ArgumentNamesConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Arguments/ArgumentNamesConformanceTests.cs new file mode 100644 index 000000000..601c2c4bb --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Arguments/ArgumentNamesConformanceTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Arguments; + +/// +[SpecSection("5.4.1", "Argument Names")] +public class ArgumentNamesConformanceTests +{ + [Fact] + public void single_arg_is_known() => + ExpectValid(KnownArgumentNames, """ + fragment argOnRequiredArg on Dog { + doesKnowCommand(dogCommand: SIT) + } + """); + + [Fact] + public void multiple_args_are_known() => + ExpectValid(KnownArgumentNames, """ + fragment multipleArgs on ComplicatedArgs { + multipleReqs(req1: 1, req2: 2) + } + """); + + [Fact] + public void directive_args_are_known() => + ExpectValid(KnownArgumentNames, """ + { + cat @skip(if: true) { + nickname + } + } + """); + + [Fact(Skip = "GraphZen does not reject unknown argument names on fields and directives.")] + public void directive_without_args_reports_unknown_arg() + { + _ = """ + { + cat @onField(if: true) { + nickname + } + } + """; + throw new NotImplementedException( + "Expected error assertions for KnownArgumentNames rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject unknown argument names on fields and directives.")] + public void invalid_field_argument_name() + { + _ = """ + fragment invalidArgName on Dog { + doesKnowCommand(unknown: true) + } + """; + throw new NotImplementedException( + "Expected error assertions for KnownArgumentNames rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject unknown argument names on fields and directives.")] + public void unknown_args_amongst_known_args() + { + _ = """ + fragment oneGoodArgOneInvalidArg on Dog { + doesKnowCommand(whoKnows: 1, dogCommand: SIT, unknown: true) + } + """; + throw new NotImplementedException( + "Expected error assertions for KnownArgumentNames rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Arguments/ArgumentUniquenessConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Arguments/ArgumentUniquenessConformanceTests.cs new file mode 100644 index 000000000..b14ad549c --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Arguments/ArgumentUniquenessConformanceTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Arguments; + +/// +[SpecSection("5.4.2", "Argument Uniqueness")] +public class ArgumentUniquenessConformanceTests +{ + [Fact] + public void argument_on_field() => + ExpectValid(UniqueArgumentNames, """ + { + field(arg: "value") + } + """); + + [Fact] + public void argument_on_directive() => + ExpectValid(UniqueArgumentNames, """ + { + field @directive(arg: "value") + } + """); + + [Fact] + public void multiple_field_arguments() => + ExpectValid(UniqueArgumentNames, """ + { + field(arg1: "value", arg2: "value", arg3: "value") + } + """); + + [Fact(Skip = "GraphZen does not reject duplicate argument names.")] + public void duplicate_field_arguments() + { + _ = """ + { + field(arg1: "value", arg1: "value") + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueArgumentNames rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject duplicate argument names.")] + public void many_duplicate_field_arguments() + { + _ = """ + { + field(arg1: "value", arg1: "value", arg1: "value") + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueArgumentNames rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject duplicate argument names.")] + public void duplicate_directive_arguments() + { + _ = """ + { + field @directive(arg1: "value", arg1: "value") + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueArgumentNames rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Arguments/RequiredArgumentsConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Arguments/RequiredArgumentsConformanceTests.cs new file mode 100644 index 000000000..865ea6c4b --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Arguments/RequiredArgumentsConformanceTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Arguments; + +/// +[SpecSection("5.4.3", "Required Arguments")] +public class RequiredArgumentsConformanceTests +{ + [Fact] + public void unknown_arguments_are_ignored() => + ExpectValid(ProvidedRequiredArguments, """ + fragment ignoresUnknownArguments on Dog { + isHouseTrained(unknownArgument: true) + } + """); + + [Fact] + public void no_arg_on_optional_arg() => + ExpectValid(ProvidedRequiredArguments, """ + fragment noArgOnOptionalArg on Dog { + isHouseTrained + } + """); + + [Fact] + public void no_arg_on_non_null_field_with_default() => + ExpectValid(ProvidedRequiredArguments, """ + { + complicatedArgs { + nonNullFieldWithDefault + } + } + """); + + [Fact] + public void multiple_required_args() => + ExpectValid(ProvidedRequiredArguments, """ + { + complicatedArgs { + multipleReqs(req1: 1, req2: 2) + } + } + """); + + [Fact] + public void directive_with_required_arg() => + ExpectValid(ProvidedRequiredArguments, """ + { + cat @include(if: true) { + nickname + } + } + """); + + [Fact(Skip = "GraphZen does not reject missing required arguments.")] + public void missing_one_non_nullable_argument() + { + _ = """ + { + complicatedArgs { + multipleReqs(req2: 2) + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ProvidedRequiredArguments rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject missing required arguments.")] + public void missing_multiple_non_nullable_arguments() + { + _ = """ + { + complicatedArgs { + multipleReqs + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ProvidedRequiredArguments rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject missing required arguments.")] + public void directive_with_missing_required_arg() + { + _ = """ + { + cat @include { + nickname @skip + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ProvidedRequiredArguments rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Directives/DirectivesAreDefinedConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Directives/DirectivesAreDefinedConformanceTests.cs new file mode 100644 index 000000000..50023de79 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Directives/DirectivesAreDefinedConformanceTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Directives; + +/// +[SpecSection("5.7.1", "Directives Are Defined")] +public class DirectivesAreDefinedConformanceTests +{ + [Fact] + public void no_directives() => + ExpectValid(KnownDirectives, """ + query Foo { + name + ...Frag + } + + fragment Frag on Dog { + name + } + """); + + [Fact] + public void with_known_directives() => + ExpectValid(KnownDirectives, """ + { + dog @include(if: true) { + name + } + human @skip(if: false) { + name + } + } + """); + + [Fact(Skip = "GraphZen does not reject unknown directives.")] + public void with_unknown_directive() + { + _ = """ + { + dog @unknown(directive: "value") { + name + } + } + """; + throw new NotImplementedException( + "Expected error assertions for KnownDirectives rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject unknown directives.")] + public void with_many_unknown_directives() + { + _ = """ + { + dog @unknown(directive: "value") { + name + } + human @unknown(directive: "value") { + name + pets @unknown(directive: "value") { + name + } + } + } + """; + throw new NotImplementedException( + "Expected error assertions for KnownDirectives rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Directives/DirectivesAreInValidLocationsConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Directives/DirectivesAreInValidLocationsConformanceTests.cs new file mode 100644 index 000000000..a8f78f9d0 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Directives/DirectivesAreInValidLocationsConformanceTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Directives; + +/// +[SpecSection("5.7.2", "Directives Are in Valid Locations")] +public class DirectivesAreInValidLocationsConformanceTests +{ + [Fact] + public void well_placed_directives() => + ExpectValid(KnownDirectives, """ + query Foo($var: Boolean) @onQuery { + name @include(if: $var) + ...Frag @include(if: true) + skippedField @skip(if: true) + ...SkippedFrag @skip(if: true) + } + + mutation Bar @onMutation { + someField + } + """); + + [Fact(Skip = "GraphZen does not reject directives in invalid locations.")] + public void with_misplaced_directives() + { + _ = """ + query Foo($var: Boolean) @include(if: true) { + name @onQuery @include(if: $var) + ...Frag @onQuery + } + + mutation Bar @onQuery { + someField + } + """; + throw new NotImplementedException( + "Expected error assertions for KnownDirectives rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Directives/DirectivesAreUniquePerLocationConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Directives/DirectivesAreUniquePerLocationConformanceTests.cs new file mode 100644 index 000000000..f4a65a189 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Directives/DirectivesAreUniquePerLocationConformanceTests.cs @@ -0,0 +1,61 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Directives; + +/// +[SpecSection("5.7.3", "Directives Are Unique per Location")] +public class DirectivesAreUniquePerLocationConformanceTests +{ + [Fact] + public void same_directives_in_different_locations() => + ExpectValid(UniqueDirectivesPerLocation, """ + { + cat @skip(if: false) { + nickname @skip(if: true) + } + } + """); + + [Fact] + public void unknown_directives_are_ignored() => + ExpectValid(UniqueDirectivesPerLocation, """ + { + cat @unknown { + nickname @unknown + } + } + """); + + [Fact(Skip = "GraphZen does not reject duplicate directives per location.")] + public void duplicate_directives_in_one_location() + { + _ = """ + { + cat @skip(if: false) @skip(if: true) { + nickname + } + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueDirectivesPerLocation rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject duplicate directives per location.")] + public void different_duplicate_directives_in_one_location() + { + _ = """ + { + cat @skip(if: false) @skip(if: true) @include(if: true) @include(if: false) { + nickname + } + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueDirectivesPerLocation rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Documents/ExecutableDefinitionsConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Documents/ExecutableDefinitionsConformanceTests.cs new file mode 100644 index 000000000..f4be2f231 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Documents/ExecutableDefinitionsConformanceTests.cs @@ -0,0 +1,75 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Documents; + +/// +[SpecSection("5.1.1", "Executable Definitions")] +public class ExecutableDefinitionsConformanceTests +{ + [Fact] + public void with_only_operation() => + ExpectValid(ExecutableDefinitions, """ + query Foo { + dog { + name + } + } + """); + + [Fact] + public void with_operation_and_fragment() => + ExpectValid(ExecutableDefinitions, """ + query Foo { + dog { + name + ...Frag + } + } + + fragment Frag on Dog { + name + } + """); + + [Fact(Skip = "GraphZen does not reject non-executable definitions in query documents.")] + public void with_type_definition() => + ExpectErrors(ExecutableDefinitions, """ + query Foo { + dog { + name + } + } + + type Cow { + name: String + } + + extend type Dog { + color: String + } + """).ToDeepEqual( + new ExpectedError("The \"Cow\" definition is not executable.", 7, 1), + new ExpectedError("The \"Dog\" definition is not executable.", 11, 1)); + + [Fact(Skip = "GraphZen does not reject non-executable definitions in query documents.")] + public void with_schema_definition() => + ExpectErrors(ExecutableDefinitions, """ + schema { + query: Query + } + + type Query { + test: String + } + + extend schema @directive + """).ToDeepEqual( + new ExpectedError("The schema definition is not executable.", 1, 1), + new ExpectedError("The \"Query\" definition is not executable.", 5, 1), + new ExpectedError("The schema definition is not executable.", 9, 1)); +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fields/FieldSelectionMergingConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fields/FieldSelectionMergingConformanceTests.cs new file mode 100644 index 000000000..392a4bfa7 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fields/FieldSelectionMergingConformanceTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Fields; + +/// +[SpecSection("5.3.2", "Field Selection Merging")] +public class FieldSelectionMergingConformanceTests +{ + [Fact] + public void unique_fields() => + ExpectValid(OverlappingFieldsCanBeMerged, """ + fragment uniqueFields on Dog { + name + nickname + } + """); + + [Fact] + public void identical_fields() => + ExpectValid(OverlappingFieldsCanBeMerged, """ + fragment mergeIdenticalFields on Dog { + name + name + } + """); + + [Fact] + public void identical_fields_with_identical_args() => + ExpectValid(OverlappingFieldsCanBeMerged, """ + fragment mergeIdenticalFieldsWithIdenticalArgs on Dog { + doesKnowCommand(dogCommand: SIT) + doesKnowCommand(dogCommand: SIT) + } + """); + + [Fact] + public void different_args_with_different_aliases() => + ExpectValid(OverlappingFieldsCanBeMerged, """ + fragment differentArgsWithDifferentAliases on Dog { + knowsSit: doesKnowCommand(dogCommand: SIT) + knowsDown: doesKnowCommand(dogCommand: DOWN) + } + """); + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void same_aliases_with_different_field_targets() + { + _ = """ + fragment sameAliasesWithDifferentFieldTargets on Dog { + fido: name + fido: nickname + } + """; + throw new NotImplementedException( + "Expected error assertions for OverlappingFieldsCanBeMerged rule need to be ported from graphql-js."); + } + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void conflicting_args() + { + _ = """ + fragment conflictingArgs on Dog { + doesKnowCommand(dogCommand: SIT) + doesKnowCommand(dogCommand: HEEL) + } + """; + throw new NotImplementedException( + "Expected error assertions for OverlappingFieldsCanBeMerged rule need to be ported from graphql-js."); + } + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void deep_conflict() + { + _ = """ + { + field { + x: a + } + field { + x: b + } + } + """; + throw new NotImplementedException( + "Expected error assertions for OverlappingFieldsCanBeMerged rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fields/FieldSelectionsConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fields/FieldSelectionsConformanceTests.cs new file mode 100644 index 000000000..12e1bd6e0 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fields/FieldSelectionsConformanceTests.cs @@ -0,0 +1,203 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Fields; + +/// +[SpecSection("5.3.1", "Field Selections")] +public class FieldSelectionsConformanceTests +{ + [Fact] + public void object_field_selection() => + ExpectValid(FieldsOnCorrectType, """ + fragment objectFieldSelection on Dog { + __typename + name + } + """); + + [Fact] + public void aliased_object_field_selection() => + ExpectValid(FieldsOnCorrectType, """ + fragment aliasedObjectFieldSelection on Dog { + tn : __typename + otherName : name + } + """); + + [Fact] + public void interface_field_selection() => + ExpectValid(FieldsOnCorrectType, """ + fragment interfaceFieldSelection on Pet { + __typename + name + } + """); + + [Fact] + public void aliased_interface_field_selection() => + ExpectValid(FieldsOnCorrectType, """ + fragment interfaceFieldSelection on Pet { + otherName : name + } + """); + + [Fact] + public void lying_alias_selection() => + ExpectValid(FieldsOnCorrectType, """ + fragment lyingAliasSelection on Dog { + name : nickname + } + """); + + [Fact] + public void ignores_fields_on_unknown_type() => + ExpectValid(FieldsOnCorrectType, """ + fragment unknownSelection on UnknownType { + unknownField + } + """); + + [Fact] + public void meta_field_selection_on_union() => + ExpectValid(FieldsOnCorrectType, """ + fragment directFieldSelectionOnUnion on CatOrDog { + __typename + } + """); + + [Fact] + public void valid_field_in_inline_fragment() => + ExpectValid(FieldsOnCorrectType, """ + fragment objectFieldSelection on Pet { + ... on Dog { + name + } + ... { + name + } + } + """); + + [Fact(Skip = "GraphZen does not reject fields that are not defined on the target type.")] + public void reports_errors_when_type_is_known_again() + { + _ = """ + fragment typeKnownAgain on Pet { + unknown_pet_field { + ... on Cat { + unknown_cat_field + } + } + } + """; + throw new NotImplementedException( + "Expected error assertions for FieldsOnCorrectType rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject fields that are not defined on the target type.")] + public void ignores_deeply_unknown_field() + { + _ = """ + fragment deepFieldNotDefined on Dog { + unknown_field { + deeper_unknown_field + } + } + """; + throw new NotImplementedException( + "Expected error assertions for FieldsOnCorrectType rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject fields that are not defined on the target type.")] + public void sub_field_not_defined() + { + _ = """ + fragment subFieldNotDefined on Human { + pets { + unknown_field + } + } + """; + throw new NotImplementedException( + "Expected error assertions for FieldsOnCorrectType rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject fields that are not defined on the target type.")] + public void field_not_defined_on_inline_fragment() + { + _ = """ + fragment fieldNotDefined on Pet { + ... on Dog { + meowVolume + } + } + """; + throw new NotImplementedException( + "Expected error assertions for FieldsOnCorrectType rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject fields that are not defined on the target type.")] + public void aliased_field_target_not_defined() + { + _ = """ + fragment aliasedFieldTargetNotDefined on Dog { + volume : mooVolume + } + """; + throw new NotImplementedException( + "Expected error assertions for FieldsOnCorrectType rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject fields that are not defined on the target type.")] + public void not_defined_on_interface() + { + _ = """ + fragment notDefinedOnInterface on Pet { + tailLength + } + """; + throw new NotImplementedException( + "Expected error assertions for FieldsOnCorrectType rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject fields that are not defined on the target type.")] + public void defined_on_implementors_but_not_on_interface() + { + _ = """ + fragment definedOnImplementorsButNotInterface on Pet { + nickname + } + """; + throw new NotImplementedException( + "Expected error assertions for FieldsOnCorrectType rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject fields that are not defined on the target type.")] + public void direct_field_selection_on_union() + { + _ = """ + fragment directFieldSelectionOnUnion on CatOrDog { + directField + } + """; + throw new NotImplementedException( + "Expected error assertions for FieldsOnCorrectType rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject fields that are not defined on the target type.")] + public void defined_on_implementors_queried_on_union() + { + _ = """ + fragment definedOnImplementorsQueriedOnUnion on CatOrDog { + name + } + """; + throw new NotImplementedException( + "Expected error assertions for FieldsOnCorrectType rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fields/LeafFieldSelectionsConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fields/LeafFieldSelectionsConformanceTests.cs new file mode 100644 index 000000000..7141765d4 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fields/LeafFieldSelectionsConformanceTests.cs @@ -0,0 +1,102 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Fields; + +/// +[SpecSection("5.3.3", "Leaf Field Selections")] +public class LeafFieldSelectionsConformanceTests +{ + [Fact] + public void valid_scalar_selection() => + ExpectValid(ScalarLeafs, """ + fragment scalarSelection on Dog { + barks + } + """); + + [Fact] + public void valid_scalar_selection_with_args() => + ExpectValid(ScalarLeafs, """ + fragment scalarSelectionWithArgs on Dog { + doesKnowCommand(dogCommand: SIT) + } + """); + + [Fact(Skip = + "GraphZen does not reject sub-selections on scalar/enum types or missing selections on composite types.")] + public void object_type_missing_selection() + { + _ = """ + query directQueryOnObjectWithoutSubFields { + human + } + """; + throw new NotImplementedException( + "Expected error assertions for ScalarLeafs rule need to be ported from graphql-js."); + } + + [Fact(Skip = + "GraphZen does not reject sub-selections on scalar/enum types or missing selections on composite types.")] + public void interface_type_missing_selection() + { + _ = """ + { + human { + pets + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ScalarLeafs rule need to be ported from graphql-js."); + } + + [Fact(Skip = + "GraphZen does not reject sub-selections on scalar/enum types or missing selections on composite types.")] + public void scalar_selection_not_allowed_on_boolean() + { + _ = """ + fragment scalarSelectionsNotAllowedOnBoolean on Dog { + barks { + sinceWhen + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ScalarLeafs rule need to be ported from graphql-js."); + } + + [Fact(Skip = + "GraphZen does not reject sub-selections on scalar/enum types or missing selections on composite types.")] + public void scalar_selection_not_allowed_on_enum() + { + _ = """ + fragment scalarSelectionsNotAllowedOnEnum on Cat { + furColor { + inHexDec + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ScalarLeafs rule need to be ported from graphql-js."); + } + + [Fact(Skip = + "GraphZen does not reject sub-selections on scalar/enum types or missing selections on composite types.")] + public void scalar_selection_not_allowed_with_args() + { + _ = """ + fragment scalarSelectionsNotAllowedWithArgs on Dog { + doesKnowCommand(dogCommand: SIT) { + sinceWhen + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ScalarLeafs rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentNameUniquenessConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentNameUniquenessConformanceTests.cs new file mode 100644 index 000000000..d1a39359a --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentNameUniquenessConformanceTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Fragments; + +/// +[SpecSection("5.5.1.1", "Fragment Name Uniqueness")] +public class FragmentNameUniquenessConformanceTests +{ + [Fact] + public void many_fragments() => + ExpectValid(UniqueFragmentNames, """ + { + dogOrHuman { + __typename + } + } + + fragment one on Dog { + name + } + + fragment two on Cat { + name + } + """); + + [Fact] + public void fragment_and_operation_named_the_same() => + ExpectValid(UniqueFragmentNames, """ + query dog { + cat { + name + } + } + + fragment dog on Dog { + name + } + """); + + [Fact(Skip = "GraphZen does not reject duplicate fragment names.")] + public void fragments_named_the_same() + { + _ = """ + fragment fragmentOne on Dog { + name + } + + fragment fragmentOne on Cat { + name + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueFragmentNames rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject duplicate fragment names.")] + public void duplicate_fragment_name_without_reference() + { + _ = """ + fragment fragmentOne on Dog { + name + } + + fragment fragmentOne on Cat { + name + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueFragmentNames rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadIsPossibleConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadIsPossibleConformanceTests.cs new file mode 100644 index 000000000..39e012216 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadIsPossibleConformanceTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Fragments; + +/// +[SpecSection("5.5.2.3", "Fragment Spread Is Possible")] +public class FragmentSpreadIsPossibleConformanceTests +{ + [Fact] + public void object_into_same_object() => + ExpectValid(PossibleFragmentSpreads, """ + fragment objectWithinObject on Dog { ...dogFragment } + fragment dogFragment on Dog { barkVolume } + """); + + [Fact] + public void object_into_implemented_interface() => + ExpectValid(PossibleFragmentSpreads, """ + fragment objectWithinInterface on Pet { ...dogFragment } + fragment dogFragment on Dog { barkVolume } + """); + + [Fact] + public void object_into_containing_union() => + ExpectValid(PossibleFragmentSpreads, """ + fragment objectWithinUnion on CatOrDog { ...dogFragment } + fragment dogFragment on Dog { barkVolume } + """); + + [Fact] + public void union_into_contained_object() => + ExpectValid(PossibleFragmentSpreads, """ + fragment unionWithinObject on Dog { ...catOrDogFragment } + fragment catOrDogFragment on CatOrDog { __typename } + """); + + [Fact] + public void interface_into_implementing_object() => + ExpectValid(PossibleFragmentSpreads, """ + fragment interfaceWithinObject on Dog { ...petFragment } + fragment petFragment on Pet { name } + """); + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void different_object_into_object() + { + _ = """ + fragment invalidObjectWithinObject on Cat { ...dogFragment } + fragment dogFragment on Dog { barkVolume } + """; + throw new NotImplementedException( + "Expected error assertions for PossibleFragmentSpreads rule need to be ported from graphql-js."); + } + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void object_not_implementing_interface() + { + _ = """ + fragment invalidObjectWithinInterface on Pet { ...humanFragment } + fragment humanFragment on Human { pets { name } } + """; + throw new NotImplementedException( + "Expected error assertions for PossibleFragmentSpreads rule need to be ported from graphql-js."); + } + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void object_not_in_union() + { + _ = """ + fragment invalidObjectWithinUnion on CatOrDog { ...humanFragment } + fragment humanFragment on Human { pets { name } } + """; + throw new NotImplementedException( + "Expected error assertions for PossibleFragmentSpreads rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadTargetDefinedConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadTargetDefinedConformanceTests.cs new file mode 100644 index 000000000..76b5accd8 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadTargetDefinedConformanceTests.cs @@ -0,0 +1,61 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Fragments; + +/// +[SpecSection("5.5.2.1", "Fragment Spread Target Defined")] +public class FragmentSpreadTargetDefinedConformanceTests +{ + [Fact] + public void known_fragment_names_are_valid() => + ExpectValid(KnownFragmentNames, """ + { + human(id: 4) { + ...HumanFields1 + ... on Human { + ...HumanFields2 + } + ... { + name + } + } + } + fragment HumanFields1 on Human { + name + ...HumanFields3 + } + fragment HumanFields2 on Human { + name + } + fragment HumanFields3 on Human { + name + } + """); + + [Fact(Skip = "GraphZen does not reject spreads targeting undefined fragments.")] + public void unknown_fragment_names() + { + _ = """ + { + human(id: 4) { + ...UnknownFragment1 + ... on Human { + ...UnknownFragment2 + } + } + } + + fragment HumanFields on Human { + name + ...UnknownFragment3 + } + """; + throw new NotImplementedException( + "Expected error assertions for KnownFragmentNames rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadTypeExistenceConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadTypeExistenceConformanceTests.cs new file mode 100644 index 000000000..12788db26 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadTypeExistenceConformanceTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Fragments; + +/// +[SpecSection("5.5.1.2", "Fragment Spread Type Existence")] +public class FragmentSpreadTypeExistenceConformanceTests +{ + [Fact] + public void known_type_names_are_valid() => + ExpectValid(KnownTypeNames, """ + query Foo($var: String, $required: [String!]!) { + user(id: 4) { + pets { ... on Pet { name }, ...PetFields, ... { name } } + } + } + + fragment PetFields on Pet { + name + } + """); + + [Fact(Skip = "GraphZen does not reject spreads on unknown type names.")] + public void unknown_type_names_are_invalid() + { + _ = """ + query Foo($var: JumbledUpLetters) { + user(id: 4) { + name + pets { ... on Badger { name }, ...PetFields } + } + } + + fragment PetFields on Peettt { + name + } + """; + throw new NotImplementedException( + "Expected error assertions for KnownTypeNames rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject spreads on unknown type names.")] + public void ignores_type_definitions() + { + _ = """ + type NotInTheSchema { + field: FooBar + } + interface FooBar { + field: NotInTheSchema + } + union U = A | B + input Blob { + field: UnknownType + } + query Foo($var: NotInTheSchema) { + user(id: $var) { + id + } + } + """; + throw new NotImplementedException( + "Expected error assertions for KnownTypeNames rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadsMustNotFormCyclesConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadsMustNotFormCyclesConformanceTests.cs new file mode 100644 index 000000000..3a34961c0 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentSpreadsMustNotFormCyclesConformanceTests.cs @@ -0,0 +1,86 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Fragments; + +/// +[SpecSection("5.5.2.2", "Fragment Spreads Must Not Form Cycles")] +public class FragmentSpreadsMustNotFormCyclesConformanceTests +{ + [Fact] + public void single_reference_is_valid() => + ExpectValid(NoFragmentCycles, """ + fragment fragA on Type { + ...fragB + } + + fragment fragB on Type { + field + } + """); + + [Fact] + public void spreading_twice_is_not_circular() => + ExpectValid(NoFragmentCycles, """ + fragment fragA on Type { + ...fragB + ...fragB + } + + fragment fragB on Type { + field + } + """); + + [Fact(Skip = "GraphZen does not reject cyclic fragment spreads.")] + public void no_spreading_itself_directly() + { + _ = """ + fragment fragA on Type { + ...fragA + } + """; + throw new NotImplementedException( + "Expected error assertions for NoFragmentCycles rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject cyclic fragment spreads.")] + public void no_spreading_itself_indirectly() + { + _ = """ + fragment fragA on Type { + ...fragB + } + + fragment fragB on Type { + ...fragA + } + """; + throw new NotImplementedException( + "Expected error assertions for NoFragmentCycles rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject cyclic fragment spreads.")] + public void no_spreading_itself_deeply() + { + _ = """ + fragment fragA on Type { + ...fragB + } + + fragment fragB on Type { + ...fragC + } + + fragment fragC on Type { + ...fragA + } + """; + throw new NotImplementedException( + "Expected error assertions for NoFragmentCycles rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentsMustBeUsedConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentsMustBeUsedConformanceTests.cs new file mode 100644 index 000000000..270b9c86d --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentsMustBeUsedConformanceTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Fragments; + +/// +[SpecSection("5.5.1.4", "Fragments Must Be Used")] +public class FragmentsMustBeUsedConformanceTests +{ + [Fact] + public void all_fragment_names_are_used() => + ExpectValid(NoUnusedFragments, """ + { + ...FragA + } + + fragment FragA on Type { + ...FragB + } + + fragment FragB on Type { + field + } + """); + + [Fact] + public void unknown_fragments_are_ignored() => + ExpectValid(NoUnusedFragments, """ + { + ...UnknownFragment + } + """); + + [Fact(Skip = "GraphZen does not reject unused fragment definitions.")] + public void contains_unknown_fragments() + { + _ = """ + { + ...FragA + } + + fragment FragA on Type { + field + } + + fragment FragB on Type { + field + } + """; + throw new NotImplementedException( + "Expected error assertions for NoUnusedFragments rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject unused fragment definitions.")] + public void contains_unknown_and_undefined_fragments() + { + _ = """ + { + ...FragA + } + + fragment FragA on Type { + ...FragB + } + + fragment FragB on Type { + field + } + + fragment FragC on Type { + field + } + """; + throw new NotImplementedException( + "Expected error assertions for NoUnusedFragments rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentsOnObjectInterfaceOrUnionTypesConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentsOnObjectInterfaceOrUnionTypesConformanceTests.cs new file mode 100644 index 000000000..1e9365f04 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Fragments/FragmentsOnObjectInterfaceOrUnionTypesConformanceTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Fragments; + +/// +[SpecSection("5.5.1.3", "Fragments on Object, Interface or Union Types")] +public class FragmentsOnObjectInterfaceOrUnionTypesConformanceTests +{ + [Fact] + public void object_is_valid_fragment_type() => + ExpectValid(FragmentsOnCompositeTypes, """ + fragment validFragment on Dog { + barks + } + """); + + [Fact] + public void interface_is_valid_fragment_type() => + ExpectValid(FragmentsOnCompositeTypes, """ + fragment validFragment on Pet { + name + } + """); + + [Fact] + public void object_is_valid_inline_fragment_type() => + ExpectValid(FragmentsOnCompositeTypes, """ + fragment validFragment on Pet { + ... on Dog { + barks + } + } + """); + + [Fact] + public void inline_fragment_without_type_is_valid() => + ExpectValid(FragmentsOnCompositeTypes, """ + fragment validFragment on Pet { + ... { + name + } + } + """); + + [Fact] + public void union_is_valid_fragment_type() => + ExpectValid(FragmentsOnCompositeTypes, """ + fragment validFragment on CatOrDog { + __typename + } + """); + + [Fact(Skip = "GraphZen does not reject fragments on non-composite types.")] + public void scalar_is_invalid_fragment_type() + { + _ = """ + fragment scalarFragment on Boolean { + bad + } + """; + throw new NotImplementedException( + "Expected error assertions for FragmentsOnCompositeTypes rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject fragments on non-composite types.")] + public void enum_is_invalid_fragment_type() + { + _ = """ + fragment scalarFragment on FurColor { + bad + } + """; + throw new NotImplementedException( + "Expected error assertions for FragmentsOnCompositeTypes rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject fragments on non-composite types.")] + public void input_object_is_invalid_fragment_type() + { + _ = """ + fragment inputFragment on ComplexInput { + stringField + } + """; + throw new NotImplementedException( + "Expected error assertions for FragmentsOnCompositeTypes rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject fragments on non-composite types.")] + public void scalar_is_invalid_inline_fragment_type() + { + _ = """ + fragment invalidFragment on Pet { + ... on String { + barks + } + } + """; + throw new NotImplementedException( + "Expected error assertions for FragmentsOnCompositeTypes rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Operations/LoneAnonymousOperationConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Operations/LoneAnonymousOperationConformanceTests.cs new file mode 100644 index 000000000..ffbeaa305 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Operations/LoneAnonymousOperationConformanceTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Operations; + +/// +[SpecSection("5.2.3.1", "Lone Anonymous Operation")] +public class LoneAnonymousOperationConformanceTests +{ + [Fact] + public void no_operations() => + ExpectValid(LoneAnonymousOperation, """ + fragment fragA on Type { + field + } + """); + + [Fact] + public void one_anonymous_operation() => + ExpectValid(LoneAnonymousOperation, """ + { + field + } + """); + + [Fact] + public void multiple_named_operations() => + ExpectValid(LoneAnonymousOperation, """ + query Foo { + field + } + + query Bar { + field + } + """); + + [Fact(Skip = + "GraphZen does not reject multiple anonymous operations or anonymous operations alongside named ones.")] + public void multiple_anonymous_operations() + { + _ = """ + { + fieldA + } + { + fieldB + } + """; + throw new NotImplementedException( + "Expected error assertions for LoneAnonymousOperation rule need to be ported from graphql-js."); + } + + [Fact(Skip = + "GraphZen does not reject multiple anonymous operations or anonymous operations alongside named ones.")] + public void anonymous_operation_with_a_mutation() + { + _ = """ + { + fieldA + } + mutation Foo { + fieldB + } + """; + throw new NotImplementedException( + "Expected error assertions for LoneAnonymousOperation rule need to be ported from graphql-js."); + } + + [Fact(Skip = + "GraphZen does not reject multiple anonymous operations or anonymous operations alongside named ones.")] + public void anonymous_operation_with_a_subscription() + { + _ = """ + { + fieldA + } + subscription Foo { + fieldB + } + """; + throw new NotImplementedException( + "Expected error assertions for LoneAnonymousOperation rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Operations/OperationNameUniquenessConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Operations/OperationNameUniquenessConformanceTests.cs new file mode 100644 index 000000000..8fb4e3fb5 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Operations/OperationNameUniquenessConformanceTests.cs @@ -0,0 +1,103 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Operations; + +/// +[SpecSection("5.2.2.1", "Operation Name Uniqueness")] +public class OperationNameUniquenessConformanceTests +{ + [Fact] + public void no_operations() => + ExpectValid(UniqueOperationNames, """ + fragment fragA on Type { + field + } + """); + + [Fact] + public void one_anonymous_operation() => + ExpectValid(UniqueOperationNames, """ + { + field + } + """); + + [Fact] + public void one_named_operation() => + ExpectValid(UniqueOperationNames, """ + query Foo { + field + } + """); + + [Fact] + public void multiple_operations() => + ExpectValid(UniqueOperationNames, """ + query Foo { + field + } + query Bar { + field + } + """); + + [Fact] + public void multiple_operations_of_different_types() => + ExpectValid(UniqueOperationNames, """ + query Foo { + field + } + mutation Bar { + field + } + subscription Baz { + field + } + """); + + [Fact] + public void fragment_and_operation_named_the_same() => + ExpectValid(UniqueOperationNames, """ + query Foo { + ...Foo + } + fragment Foo on Type { + field + } + """); + + [Fact(Skip = "GraphZen does not reject duplicate operation names.")] + public void multiple_operations_of_same_name_mutation() + { + _ = """ + query Foo { + fieldA + } + query Foo { + fieldB + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueOperationNames rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject duplicate operation names.")] + public void multiple_operations_of_same_name_subscription() + { + _ = """ + query Foo { + fieldA + } + subscription Foo { + fieldB + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueOperationNames rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Operations/SingleRootFieldConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Operations/SingleRootFieldConformanceTests.cs new file mode 100644 index 000000000..879420ebb --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Operations/SingleRootFieldConformanceTests.cs @@ -0,0 +1,86 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using GraphZen.TypeSystem; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Operations; + +/// +[SpecSection("5.2.4.1", "Single Root Field")] +public class SingleRootFieldConformanceTests +{ + private static readonly Schema SubscriptionSchema = Schema.Create(sb => + { + sb.Object("Message") + .Field("body", "String") + .Field("sender", "String"); + + sb.Object("QueryRoot") + .Field("ping", "String"); + + sb.Object("SubscriptionRoot") + .Field("newMessage", "Message") + .Field("otherMessage", "Message"); + + sb.QueryType("QueryRoot"); + sb.SubscriptionType("SubscriptionRoot"); + }); + + [Fact] + public void valid_subscription_passes() => + ExpectValid(SubscriptionSchema, SingleFieldSubscriptions, """ + subscription sub { + newMessage { + body + sender + } + } + """); + + [Fact] + public void valid_subscription_with_fragment_passes() => + ExpectValid(SubscriptionSchema, SingleFieldSubscriptions, """ + subscription sub { + ...newMessageFields + } + + fragment newMessageFields on SubscriptionRoot { + newMessage { + body + sender + } + } + """); + + [Fact(Skip = "GraphZen does not validate single root field in subscriptions.")] + public void multiple_root_fields_fail() + { + _ = """ + subscription sub { + newMessage { + body + } + otherMessage { + body + } + } + """; + throw new NotImplementedException( + "Expected error assertions for SingleFieldSubscriptions rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not validate single root field in subscriptions.")] + public void introspection_root_field_fails() + { + _ = """ + subscription sub { + __typename + } + """; + throw new NotImplementedException( + "Expected error assertions for SingleFieldSubscriptions rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/InputObjectFieldNamesConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/InputObjectFieldNamesConformanceTests.cs new file mode 100644 index 000000000..262d93968 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/InputObjectFieldNamesConformanceTests.cs @@ -0,0 +1,67 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Values; + +/// +[SpecSection("5.6.2", "Input Object Field Names")] +public class InputObjectFieldNamesConformanceTests +{ + [Fact] + public void known_input_object_fields() => + ExpectValid(ValuesOfCorrectType, """ + { + complicatedArgs { + complexArgField(complexArg: { requiredField: true, intField: 4 }) + } + } + """); + + [Fact] + public void partial_object_only_required() => + ExpectValid(ValuesOfCorrectType, """ + { + complicatedArgs { + complexArgField(complexArg: { requiredField: true }) + } + } + """); + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void unknown_input_object_field() + { + _ = """ + { + complicatedArgs { + complexArgField(complexArg: { + requiredField: true, + invalidField: "value" + }) + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ValuesOfCorrectType rule need to be ported from graphql-js."); + } + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void misspelled_input_object_field() + { + _ = """ + { + complicatedArgs { + complexArgField(complexArg: { + requiredField: true, + intFeild: 4 + }) + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ValuesOfCorrectType rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/InputObjectFieldUniquenessConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/InputObjectFieldUniquenessConformanceTests.cs new file mode 100644 index 000000000..5769393b4 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/InputObjectFieldUniquenessConformanceTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Values; + +/// +[SpecSection("5.6.3", "Input Object Field Uniqueness")] +public class InputObjectFieldUniquenessConformanceTests +{ + [Fact] + public void multiple_input_object_fields() => + ExpectValid(UniqueInputFieldNames, """ + { + field(arg: { f1: "value", f2: "value", f3: "value" }) + } + """); + + [Fact] + public void same_input_object_within_two_args() => + ExpectValid(UniqueInputFieldNames, """ + { + field(arg1: { f: true }, arg2: { f: true }) + } + """); + + [Fact(Skip = "GraphZen does not reject duplicate input object field names.")] + public void duplicate_input_object_fields() + { + _ = """ + { + field(arg: { f1: "value", f1: "value" }) + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueInputFieldNames rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject duplicate input object field names.")] + public void many_duplicate_input_object_fields() + { + _ = """ + { + field(arg: { f1: "value", f1: "value", f1: "value" }) + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueInputFieldNames rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject duplicate input object field names.")] + public void nested_duplicate_input_object_fields() + { + _ = """ + { + field(arg: { f1: { f2: "value", f2: "value" } }) + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueInputFieldNames rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/InputObjectRequiredFieldsConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/InputObjectRequiredFieldsConformanceTests.cs new file mode 100644 index 000000000..eda412517 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/InputObjectRequiredFieldsConformanceTests.cs @@ -0,0 +1,61 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Values; + +/// +[SpecSection("5.6.4", "Input Object Required Fields")] +public class InputObjectRequiredFieldsConformanceTests +{ + [Fact] + public void all_required_fields_provided() => + ExpectValid(ProvidedRequiredArguments, """ + { + complicatedArgs { + complexArgField(complexArg: { requiredField: true }) + } + } + """); + + [Fact] + public void required_and_optional_fields_provided() => + ExpectValid(ProvidedRequiredArguments, """ + { + complicatedArgs { + complexArgField(complexArg: { requiredField: true, intField: 4 }) + } + } + """); + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void missing_required_field() + { + _ = """ + { + complicatedArgs { + complexArgField(complexArg: { intField: 4 }) + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ProvidedRequiredArguments rule need to be ported from graphql-js."); + } + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void missing_required_field_with_null() + { + _ = """ + { + complicatedArgs { + complexArgField(complexArg: { requiredField: null, intField: 4 }) + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ProvidedRequiredArguments rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/ValuesOfCorrectTypeConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/ValuesOfCorrectTypeConformanceTests.cs new file mode 100644 index 000000000..9edb9df0a --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Values/ValuesOfCorrectTypeConformanceTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Values; + +/// +[SpecSection("5.6.1", "Values of Correct Type")] +public class ValuesOfCorrectTypeConformanceTests +{ + [Fact] + public void good_int_value() => + ExpectValid(ValuesOfCorrectType, """ + { + complicatedArgs { + intArgField(intArg: 2) + } + } + """); + + [Fact] + public void good_boolean_value() => + ExpectValid(ValuesOfCorrectType, """ + { + complicatedArgs { + booleanArgField(booleanArg: true) + } + } + """); + + [Fact] + public void good_string_value() => + ExpectValid(ValuesOfCorrectType, """ + { + complicatedArgs { + stringArgField(stringArg: "foo") + } + } + """); + + [Fact] + public void good_enum_value() => + ExpectValid(ValuesOfCorrectType, """ + { + dog { + doesKnowCommand(dogCommand: SIT) + } + } + """); + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void string_into_int() + { + _ = """ + { + complicatedArgs { + intArgField(intArg: "3") + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ValuesOfCorrectType rule need to be ported from graphql-js."); + } + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void float_into_int() + { + _ = """ + { + complicatedArgs { + intArgField(intArg: 3.333) + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ValuesOfCorrectType rule need to be ported from graphql-js."); + } + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void int_into_string() + { + _ = """ + { + complicatedArgs { + stringArgField(stringArg: 1) + } + } + """; + throw new NotImplementedException( + "Expected error assertions for ValuesOfCorrectType rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/AllVariableUsagesAreAllowedConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/AllVariableUsagesAreAllowedConformanceTests.cs new file mode 100644 index 000000000..644a94434 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/AllVariableUsagesAreAllowedConformanceTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Variables; + +/// +[SpecSection("5.8.5", "All Variable Usages Are Allowed")] +public class AllVariableUsagesAreAllowedConformanceTests +{ + [Fact] + public void boolean_to_boolean() => + ExpectValid(VariablesInAllowedPosition, """ + query Query($booleanArg: Boolean) { + complicatedArgs { + booleanArgField(booleanArg: $booleanArg) + } + } + """); + + [Fact] + public void boolean_non_null_to_boolean() => + ExpectValid(VariablesInAllowedPosition, """ + query Query($nonNullBooleanArg: Boolean!) { + complicatedArgs { + booleanArgField(booleanArg: $nonNullBooleanArg) + } + } + """); + + [Fact] + public void list_to_list() => + ExpectValid(VariablesInAllowedPosition, """ + query Query($stringListVar: [String]) { + complicatedArgs { + stringListArgField(stringListArg: $stringListVar) + } + } + """); + + [Fact(Skip = "GraphZen does not reject variables used in incompatible type positions.")] + public void int_to_non_null_int() + { + _ = """ + query Query($intArg: Int) { + complicatedArgs { + nonNullIntArgField(nonNullIntArg: $intArg) + } + } + """; + throw new NotImplementedException( + "Expected error assertions for VariablesInAllowedPosition rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject variables used in incompatible type positions.")] + public void string_over_boolean() + { + _ = """ + query Query($stringVar: String) { + complicatedArgs { + booleanArgField(booleanArg: $stringVar) + } + } + """; + throw new NotImplementedException( + "Expected error assertions for VariablesInAllowedPosition rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject variables used in incompatible type positions.")] + public void string_to_list() + { + _ = """ + query Query($stringVar: String) { + complicatedArgs { + stringListArgField(stringListArg: $stringVar) + } + } + """; + throw new NotImplementedException( + "Expected error assertions for VariablesInAllowedPosition rule need to be ported from graphql-js."); + } + + [Fact(Skip = "Error assertions need to be ported from graphql-js.")] + public void nullable_variable_in_oneof_position() + { + _ = """ + query ($string: String) { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: $string }) + } + } + """; + throw new NotImplementedException( + "Expected error assertions for VariablesInAllowedPosition rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/AllVariableUsesDefinedConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/AllVariableUsesDefinedConformanceTests.cs new file mode 100644 index 000000000..0662ce0aa --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/AllVariableUsesDefinedConformanceTests.cs @@ -0,0 +1,97 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Variables; + +/// +[SpecSection("5.8.3", "All Variable Uses Defined")] +public class AllVariableUsesDefinedConformanceTests +{ + [Fact] + public void all_variables_defined() => + ExpectValid(NoUndefinedVariables, """ + query Foo($a: String, $b: String, $c: String) { + field(a: $a, b: $b, c: $c) + } + """); + + [Fact] + public void all_variables_in_fragments_defined() => + ExpectValid(NoUndefinedVariables, """ + query Foo($a: String, $b: String, $c: String) { + ...FragA + } + + fragment FragA on Type { + field(a: $a) { + ...FragB + } + } + + fragment FragB on Type { + field(b: $b) { + ...FragC + } + } + + fragment FragC on Type { + field(c: $c) + } + """); + + [Fact(Skip = "GraphZen does not reject uses of undefined variables.")] + public void variable_not_defined() + { + _ = """ + query Foo($a: String, $b: String, $c: String) { + field(a: $a, b: $b, c: $c, d: $d) + } + """; + throw new NotImplementedException( + "Expected error assertions for NoUndefinedVariables rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject uses of undefined variables.")] + public void multiple_variables_not_defined() + { + _ = """ + query Foo($b: String) { + field(a: $a, b: $b, c: $c) + } + """; + throw new NotImplementedException( + "Expected error assertions for NoUndefinedVariables rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject uses of undefined variables.")] + public void variable_in_fragment_not_defined_by_operation() + { + _ = """ + query Foo($a: String, $b: String) { + ...FragA + } + + fragment FragA on Type { + field(a: $a) { + ...FragB + } + } + + fragment FragB on Type { + field(b: $b) { + ...FragC + } + } + + fragment FragC on Type { + field(c: $c) + } + """; + throw new NotImplementedException( + "Expected error assertions for NoUndefinedVariables rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/AllVariablesUsedConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/AllVariablesUsedConformanceTests.cs new file mode 100644 index 000000000..1dc47f418 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/AllVariablesUsedConformanceTests.cs @@ -0,0 +1,97 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Variables; + +/// +[SpecSection("5.8.4", "All Variables Used")] +public class AllVariablesUsedConformanceTests +{ + [Fact] + public void uses_all_variables() => + ExpectValid(NoUnusedVariables, """ + query ($a: String, $b: String, $c: String) { + field(a: $a, b: $b, c: $c) + } + """); + + [Fact] + public void uses_all_variables_in_fragments() => + ExpectValid(NoUnusedVariables, """ + query Foo($a: String, $b: String, $c: String) { + ...FragA + } + + fragment FragA on Type { + field(a: $a) { + ...FragB + } + } + + fragment FragB on Type { + field(b: $b) { + ...FragC + } + } + + fragment FragC on Type { + field(c: $c) + } + """); + + [Fact(Skip = "GraphZen does not reject unused variable definitions.")] + public void variable_not_used() + { + _ = """ + query ($a: String, $b: String, $c: String) { + field(a: $a, b: $b) + } + """; + throw new NotImplementedException( + "Expected error assertions for NoUnusedVariables rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject unused variable definitions.")] + public void multiple_variables_not_used() + { + _ = """ + query Foo($a: String, $b: String, $c: String) { + field(b: $b) + } + """; + throw new NotImplementedException( + "Expected error assertions for NoUnusedVariables rule need to be ported from graphql-js."); + } + + [Fact(Skip = "GraphZen does not reject unused variable definitions.")] + public void variables_not_used_in_fragments() + { + _ = """ + query Foo($a: String, $b: String, $c: String) { + ...FragA + } + + fragment FragA on Type { + field { + ...FragB + } + } + + fragment FragB on Type { + field(b: $b) { + ...FragC + } + } + + fragment FragC on Type { + field + } + """; + throw new NotImplementedException( + "Expected error assertions for NoUnusedVariables rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/VariableUniquenessConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/VariableUniquenessConformanceTests.cs new file mode 100644 index 000000000..e5ff2b1fb --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/VariableUniquenessConformanceTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Variables; + +/// +[SpecSection("5.8.1", "Variable Uniqueness")] +public class VariableUniquenessConformanceTests +{ + [Fact] + public void unique_variable_names_pass() => + ExpectValid(UniqueVariableNames, """ + query A($x: Int, $y: String) { + __typename + } + + query B($x: String, $y: Int) { + __typename + } + """); + + [Fact(Skip = "GraphZen does not reject duplicate variable names.")] + public void duplicate_variable_names_fail() + { + _ = """ + query A($x: Int, $x: Int, $x: String) { + __typename + } + + query B($x: String, $x: Int) { + __typename + } + + query C($x: Int, $x: Int) { + __typename + } + """; + throw new NotImplementedException( + "Expected error assertions for UniqueVariableNames rule need to be ported from graphql-js."); + } +} diff --git a/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/VariablesAreInputTypesConformanceTests.cs b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/VariablesAreInputTypesConformanceTests.cs new file mode 100644 index 000000000..5a23edf45 --- /dev/null +++ b/test/GraphZen.SpecConformance.Tests/Section5_Validation/Variables/VariablesAreInputTypesConformanceTests.cs @@ -0,0 +1,41 @@ +// Copyright (c) GraphZen LLC. All rights reserved. +// Licensed under the GraphZen Community License. See the LICENSE file in the project root for license information. + +using GraphZen.SpecConformance.Tests.Infrastructure; +using static GraphZen.QueryEngine.Validation.QueryValidationRules; +using static GraphZen.SpecConformance.Tests.Infrastructure.SpecValidation; + +namespace GraphZen.SpecConformance.Tests.Section5_Validation.Variables; + +/// +[SpecSection("5.8.2", "Variables Are Input Types")] +public class VariablesAreInputTypesConformanceTests +{ + [Fact] + public void unknown_types_are_ignored() => + ExpectValid(VariablesAreInputTypes, """ + query Foo($a: Unknown, $b: [[Unknown!]]!) { + field(a: $a, b: $b) + } + """); + + [Fact] + public void input_types_are_valid() => + ExpectValid(VariablesAreInputTypes, """ + query Foo($a: String, $b: [Boolean!]!, $c: ComplexInput) { + field(a: $a, b: $b, c: $c) + } + """); + + [Fact(Skip = "GraphZen does not reject output types used as variable types.")] + public void output_types_are_invalid() + { + _ = """ + query Foo($a: Dog, $b: [[CatOrDog!]]!, $c: Pet) { + field(a: $a, b: $b, c: $c) + } + """; + throw new NotImplementedException( + "Expected error assertions for VariablesAreInputTypes rule need to be ported from graphql-js."); + } +}