Skip to content

Commit

Permalink
Add tests for subgraph being offline
Browse files Browse the repository at this point in the history
  • Loading branch information
tobias-tengler committed Mar 4, 2024
1 parent ab3909e commit 5c8b87e
Show file tree
Hide file tree
Showing 3 changed files with 587 additions and 0 deletions.
98 changes: 98 additions & 0 deletions src/HotChocolate/Fusion/test/Core.Tests/ErrorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,104 @@ public class ErrorTests(ITestOutputHelper output)
{
private readonly Func<ICompositionLog> _logFactory = () => new TestCompositionLog(output);

[Fact]
public async Task Resolve_Parallel_Accounts_Offline_FieldNullable()
{
// arrange
using var demoProject = await DemoProject.CreateAsync();

var fusionGraph = await new FusionGraphComposer(logFactory: _logFactory).ComposeAsync(
new[]
{
demoProject.Accounts.ToConfiguration(AccountsExtensionSdl),
demoProject.Reviews2.ToConfiguration(Reviews2ExtensionSdl),
},
new FusionFeatureCollection(FusionFeatures.NodeField));

var executor = await new ServiceCollection()
.AddSingleton<IHttpClientFactory>(
new ErrorFactory(demoProject.HttpClientFactory, demoProject.Accounts.Name))
.AddSingleton(demoProject.WebSocketConnectionFactory)
.AddFusionGatewayServer()
.ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph))
.BuildRequestExecutorAsync();

var request = Parse(
"""
{
viewer {
user {
name
}
latestReview {
body
}
}
}
""");

// act
await using var result = await executor.ExecuteAsync(
QueryRequestBuilder
.New()
.SetQuery(request)
.Create());

// assert
var snapshot = new Snapshot();
CollectSnapshotData(snapshot, request, result, fusionGraph);
snapshot.MatchMarkdownSnapshot();
}

[Fact]
public async Task Resolve_Parallel_Accounts_Offline_FieldNonNull()
{
// arrange
using var demoProject = await DemoProject.CreateAsync();

var fusionGraph = await new FusionGraphComposer(logFactory: _logFactory).ComposeAsync(
new[]
{
demoProject.Accounts.ToConfiguration(AccountsExtensionSdl),
demoProject.Reviews2.ToConfiguration(Reviews2ExtensionSdl),
},
new FusionFeatureCollection(FusionFeatures.NodeField));

var executor = await new ServiceCollection()
.AddSingleton<IHttpClientFactory>(
new ErrorFactory(demoProject.HttpClientFactory, demoProject.Accounts.Name))
.AddSingleton(demoProject.WebSocketConnectionFactory)
.AddFusionGatewayServer()
.ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph))
.BuildRequestExecutorAsync();

var request = Parse(
"""
{
viewer? {
user! {
name
}
latestReview {
body
}
}
}
""");

// act
await using var result = await executor.ExecuteAsync(
QueryRequestBuilder
.New()
.SetQuery(request)
.Create());

// assert
var snapshot = new Snapshot();
CollectSnapshotData(snapshot, request, result, fusionGraph);
snapshot.MatchMarkdownSnapshot();
}

[Fact]
public async Task TopLevelResolveSubgraphError()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# Resolve_Parallel_Accounts_Offline_FieldNonNull

## User Request

```graphql
{
viewer? {
user! {
name
}
latestReview {
body
}
}
}
```

## Result

```json
{
"errors": [
{
"message": "Internal Execution Error"
}
],
"data": null
}
```

## QueryPlan

```json
{
"document": "{ viewer? { user! { name } latestReview { body } } }",
"rootNode": {
"type": "Sequence",
"nodes": [
{
"type": "Parallel",
"nodes": [
{
"type": "Resolve",
"subgraph": "Accounts",
"document": "query fetch_viewer_1 { viewer { user! { name } } }",
"selectionSetId": 0
},
{
"type": "Resolve",
"subgraph": "Reviews2",
"document": "query fetch_viewer_2 { viewer? { latestReview { body } } }",
"selectionSetId": 0
}
]
},
{
"type": "Compose",
"selectionSetIds": [
0
]
}
]
}
}
```

## QueryPlan Hash

```text
B1F8BD87F08DD7941DBB6AF4852DA3AA3EB10097
```

## Fusion Graph

```graphql
schema
@fusion(version: 1)
@transport(subgraph: "Accounts", location: "http:\/\/localhost:5000\/graphql", kind: "HTTP")
@transport(subgraph: "Accounts", location: "ws:\/\/localhost:5000\/graphql", kind: "WebSocket")
@transport(subgraph: "Reviews2", location: "http:\/\/localhost:5000\/graphql", kind: "HTTP")
@transport(subgraph: "Reviews2", location: "ws:\/\/localhost:5000\/graphql", kind: "WebSocket")
@node(subgraph: "Accounts", types: [ "User" ])
@node(subgraph: "Reviews2", types: [ "User", "Review" ]) {
query: Query
mutation: Mutation
subscription: Subscription
}

type Query {
errorField: String
@resolver(subgraph: "Accounts", select: "{ errorField }")
"Fetches an object given its ID."
node("ID of the object." id: ID!): Node
@variable(subgraph: "Accounts", name: "id", argument: "id")
@resolver(subgraph: "Accounts", select: "{ node(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
@variable(subgraph: "Reviews2", name: "id", argument: "id")
@resolver(subgraph: "Reviews2", select: "{ node(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
"Lookup nodes by a list of IDs."
nodes("The list of node IDs." ids: [ID!]!): [Node]!
@variable(subgraph: "Accounts", name: "ids", argument: "ids")
@resolver(subgraph: "Accounts", select: "{ nodes(ids: $ids) }", arguments: [ { name: "ids", type: "[ID!]!" } ])
@variable(subgraph: "Reviews2", name: "ids", argument: "ids")
@resolver(subgraph: "Reviews2", select: "{ nodes(ids: $ids) }", arguments: [ { name: "ids", type: "[ID!]!" } ])
productById(id: ID!): Product
@variable(subgraph: "Reviews2", name: "id", argument: "id")
@resolver(subgraph: "Reviews2", select: "{ productById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
reviewById(id: ID!): Review
@variable(subgraph: "Reviews2", name: "id", argument: "id")
@resolver(subgraph: "Reviews2", select: "{ reviewById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
reviewOrAuthor: ReviewOrAuthor!
@resolver(subgraph: "Reviews2", select: "{ reviewOrAuthor }")
reviews: [Review!]!
@resolver(subgraph: "Reviews2", select: "{ reviews }")
userById(id: ID!): User
@variable(subgraph: "Accounts", name: "id", argument: "id")
@resolver(subgraph: "Accounts", select: "{ userById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
@variable(subgraph: "Reviews2", name: "id", argument: "id")
@resolver(subgraph: "Reviews2", select: "{ authorById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
users: [User!]!
@resolver(subgraph: "Accounts", select: "{ users }")
usersById(ids: [ID!]!): [User!]!
@variable(subgraph: "Accounts", name: "ids", argument: "ids")
@resolver(subgraph: "Accounts", select: "{ usersById(ids: $ids) }", arguments: [ { name: "ids", type: "[ID!]!" } ])
viewer: Viewer!
@resolver(subgraph: "Accounts", select: "{ viewer }")
@resolver(subgraph: "Reviews2", select: "{ viewer }")
}

type Mutation {
addReview(input: AddReviewInput!): AddReviewPayload!
@variable(subgraph: "Reviews2", name: "input", argument: "input")
@resolver(subgraph: "Reviews2", select: "{ addReview(input: $input) }", arguments: [ { name: "input", type: "AddReviewInput!" } ])
addUser(input: AddUserInput!): AddUserPayload!
@variable(subgraph: "Accounts", name: "input", argument: "input")
@resolver(subgraph: "Accounts", select: "{ addUser(input: $input) }", arguments: [ { name: "input", type: "AddUserInput!" } ])
}

type Subscription {
onNewReview: Review!
@resolver(subgraph: "Reviews2", select: "{ onNewReview }", kind: "SUBSCRIBE")
}

type AddReviewPayload {
review: Review
@source(subgraph: "Reviews2")
}

type AddUserPayload {
user: User
@source(subgraph: "Accounts")
}

type Product
@variable(subgraph: "Reviews2", name: "Product_id", select: "id")
@resolver(subgraph: "Reviews2", select: "{ productById(id: $Product_id) }", arguments: [ { name: "Product_id", type: "ID!" } ]) {
id: ID!
@source(subgraph: "Reviews2")
reviews: [Review!]!
@source(subgraph: "Reviews2")
}

type Review implements Node
@variable(subgraph: "Reviews2", name: "Review_id", select: "id")
@resolver(subgraph: "Reviews2", select: "{ reviewById(id: $Review_id) }", arguments: [ { name: "Review_id", type: "ID!" } ])
@resolver(subgraph: "Reviews2", select: "{ nodes(ids: $Review_id) { ... on Review { ... Review } } }", arguments: [ { name: "Review_id", type: "[ID!]!" } ], kind: "BATCH") {
author: User!
@source(subgraph: "Reviews2")
body: String!
@source(subgraph: "Reviews2")
errorField: String
@source(subgraph: "Reviews2")
id: ID!
@source(subgraph: "Reviews2")
product: Product!
@source(subgraph: "Reviews2")
}

type SomeData {
accountValue: String!
@source(subgraph: "Accounts")
reviewsValue: String!
@source(subgraph: "Reviews2")
}

"The user who wrote the review."
type User implements Node
@variable(subgraph: "Accounts", name: "User_id", select: "id")
@variable(subgraph: "Reviews2", name: "User_id", select: "id")
@resolver(subgraph: "Accounts", select: "{ userById(id: $User_id) }", arguments: [ { name: "User_id", type: "ID!" } ])
@resolver(subgraph: "Accounts", select: "{ usersById(ids: $User_id) }", arguments: [ { name: "User_id", type: "[ID!]!" } ], kind: "BATCH")
@resolver(subgraph: "Reviews2", select: "{ authorById(id: $User_id) }", arguments: [ { name: "User_id", type: "ID!" } ])
@resolver(subgraph: "Reviews2", select: "{ nodes(ids: $User_id) { ... on User { ... User } } }", arguments: [ { name: "User_id", type: "[ID!]!" } ], kind: "BATCH") {
birthdate: Date!
@source(subgraph: "Accounts")
errorField: String
@source(subgraph: "Accounts")
id: ID!
@source(subgraph: "Accounts")
@source(subgraph: "Reviews2")
name: String!
@source(subgraph: "Accounts")
@source(subgraph: "Reviews2")
reviews: [Review!]!
@source(subgraph: "Reviews2")
username: String!
@source(subgraph: "Accounts")
}

type Viewer {
data: SomeData!
@source(subgraph: "Accounts")
@source(subgraph: "Reviews2")
latestReview: Review
@source(subgraph: "Reviews2")
user: User
@source(subgraph: "Accounts")
}

"The node interface is implemented by entities that have a global unique identifier."
interface Node {
id: ID!
}

union ReviewOrAuthor = User | Review

input AddReviewInput {
authorId: Int!
body: String!
upc: Int!
}

input AddUserInput {
birthdate: Date!
name: String!
username: String!
}

"The `Date` scalar represents an ISO-8601 compliant date type."
scalar Date
```

Loading

0 comments on commit 5c8b87e

Please sign in to comment.