Repository: mercurius-js/mercurius
Pull request #747 (2022-03-07 16:11:31)

build(deps-dev): bump tap from 15.2.3 to 16.0.0
Bumps [tap](https://github.com/tapjs/node-tap) from 15.2.3 to 16.0.0.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/tapjs/node-tap/commit/8f2baa7eb5e1f4cd0fcf8b60da4fb4e41ea680f1"><code>8f2baa7</code></a> 16.0.0</li>
<li><a href="https://github.com/tapjs/node-tap/commit/65e599e8de3731ed7e4152a4ef8afde1afa27242"><code>65e599e</code></a> drop support for node v10</li>
<li><a href="https://github.com/tapjs/node-tap/commit/84fc6dfcd8c1f9077dd0f25f316fb214a3a6519c"><code>84fc6df</code></a> docs: changelog for v16</li>
<li><a href="https://github.com/tapjs/node-tap/commit/e6acf7b25ea02fea17759259f5c938e414a1499b"><code>e6acf7b</code></a> update coveralls documentation to say to install it</li>
<li><a href="https://github.com/tapjs/node-tap/commit/3c7b3ef6c8792558666676c0b8db5e757ba00ec8"><code>3c7b3ef</code></a> document coveralls removal in the cli help output</li>
<li><a href="https://github.com/tapjs/node-tap/commit/43bf1df7d83287ac419c9b1ad7e9c033fe3aded5"><code>43bf1df</code></a> undocument synonyms</li>
<li><a href="https://github.com/tapjs/node-tap/commit/1ff75a4e5cf2e123bcec87391f2cf316e32f13b0"><code>1ff75a4</code></a> make coveralls an optional peer dep</li>
<li><a href="https://github.com/tapjs/node-tap/commit/3f1ae47bc0050cfd97b0a5ac07054448ada5b69d"><code>3f1ae47</code></a> Add eslint for linting</li>
<li><a href="https://github.com/tapjs/node-tap/commit/162c1a8a92809591fce079ba9f9e1d854649aaa2"><code>162c1a8</code></a> Support Typescript for --before and --after</li>
<li><a href="https://github.com/tapjs/node-tap/commit/28d2d031eec1b3db537b236a879efe32109c3ee3"><code>28d2d03</code></a> Fix script on using node-tap with codecov</li>
<li>Additional commits viewable in <a href="https://github.com/tapjs/node-tap/compare/v15.2.3...v16.0.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tap&package-manager=npm_and_yarn&previous-version=15.2.3&new-version=16.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 912e478..dfa7fa6 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,16 +1,13 @@
 version: 2
 updates:
-- package-ecosystem: github-actions
-  directory: "/"
-  ignore:
-    - dependency-name: "actions/*"
-      update-types:
-        ["version-update:semver-minor", "version-update:semver-patch"]
-  schedule:
-    interval: daily
-  open-pull-requests-limit: 10
-- package-ecosystem: npm
-  directory: "/"
-  schedule:
-    interval: daily
-  open-pull-requests-limit: 10
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "monthly"
+    open-pull-requests-limit: 10
+
+  - package-ecosystem: "npm"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+    open-pull-requests-limit: 10
diff --git a/lib/errors.js b/lib/errors.js
index 0ca7138..08daee0 100644
--- a/lib/errors.js
+++ b/lib/errors.js
@@ -17,6 +17,9 @@ const FEDERATED_ERROR = Symbol('FEDERATED_ERROR')
 // to satisfy the `graphql` error handler
 class FederatedError extends Error {
   constructor (errors) {
+    if (errors && !Array.isArray(errors)) {
+      throw new TypeError('errors must be an Array')
+    }
     super(FEDERATED_ERROR.toString())
     this.extensions = { errors }
   }
diff --git a/lib/gateway/request.js b/lib/gateway/request.js
index f7a9d63..7ea9101 100644
--- a/lib/gateway/request.js
+++ b/lib/gateway/request.js
@@ -18,16 +18,18 @@ function agentOption (opts) {
 }
 
 function buildRequest (opts) {
-  let agent
-  if (Array.isArray(opts.url)) {
-    const upstreams = []
-    for (const url of opts.url) {
-      upstreams.push(new URL(url).origin)
-    }
+  let { agent } = opts
+  if (!agent) {
+    if (Array.isArray(opts.url)) {
+      const upstreams = []
+      for (const url of opts.url) {
+        upstreams.push(new URL(url).origin)
+      }
 
-    agent = new BalancedPool(upstreams, agentOption(opts))
-  } else {
-    agent = new Pool(new URL(opts.url).origin, agentOption(opts))
+      agent = new BalancedPool(upstreams, agentOption(opts))
+    } else {
+      agent = new Pool(new URL(opts.url).origin, agentOption(opts))
+    }
   }
 
   const rewriteHeaders = opts.rewriteHeaders || function () { return {} }
@@ -50,7 +52,7 @@ function buildRequest (opts) {
 
       return response
     } catch (err) {
-      throw new FederatedError(err)
+      throw new FederatedError([err])
     }
   }
 
@@ -93,7 +95,7 @@ function sendRequest (request, url, useSecureParse) {
       if (err instanceof FederatedError) {
         throw err
       }
-      throw new FederatedError(err)
+      throw new FederatedError([err])
     }
   }
 }
diff --git a/package.json b/package.json
index bf7b201..f38c45b 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
     "snazzy": "^9.0.0",
     "split2": "^4.0.0",
     "standard": "^16.0.3",
-    "tap": "^15.0.9",
+    "tap": "^16.0.0",
     "tsd": "^0.19.0",
     "typescript": "^4.3.5",
     "wait-on": "^6.0.0"


Test patch:
diff --git a/test/errors.js b/test/errors.js
index 6ab6e89..2e2649d 100644
--- a/test/errors.js
+++ b/test/errors.js
@@ -13,6 +13,21 @@ test('ErrorWithProps - support status code in the constructor', async (t) => {
   t.equal(error.statusCode, 500)
 })
 
+test('FederatedError - throws if given errors is not an array', async (t) => {
+  t.plan(1)
+  t.throws(() => new FederatedError({ message: 'Some error' }))
+})
+
+test('FederatedError - does not throw if given an array', async (t) => {
+  t.plan(1)
+  t.doesNotThrow(() => new FederatedError([{ message: 'Some error' }]))
+})
+
+test('FederatedError - does not throw if not given errors', async (t) => {
+  t.plan(1)
+  t.doesNotThrow(() => new FederatedError())
+})
+
 test('errors - multiple extended errors', async (t) => {
   const schema = `
     type Query {
diff --git a/test/gateway/errors-in-request.js b/test/gateway/errors-in-request.js
new file mode 100644
index 0000000..99c73ef
--- /dev/null
+++ b/test/gateway/errors-in-request.js
@@ -0,0 +1,94 @@
+const { test } = require('tap')
+const Fastify = require('fastify')
+const { MockAgent, setGlobalDispatcher } = require('undici')
+const GQL = require('../..')
+
+async function createTestGatewayServer (t, userServiceUrl, agent) {
+  // User service
+  const userServiceSchema = `
+  type Query @extends {
+    me: User
+  }
+
+  type Metadata {
+    info: String!
+  }
+
+  type User @key(fields: "id") {
+    id: ID!
+    name: String!
+    quote(input: String!): String!
+    metadata(input: String!): Metadata!
+  }`
+
+  const gateway = Fastify()
+  t.teardown(async () => {
+    await gateway.close()
+  })
+
+  gateway.register(GQL, {
+    gateway: {
+      services: [{
+        agent,
+        name: 'user',
+        schema: userServiceSchema,
+        url: userServiceUrl
+      }]
+    }
+  })
+
+  return gateway
+}
+
+test('it returns result object with error for exceptions connecting to federated service', async (t) => {
+  t.plan(1)
+  const userServiceHost = 'http://127.0.0.1:3333'
+
+  // Create mock undici agent and pool
+  const mockAgent = new MockAgent()
+  setGlobalDispatcher(mockAgent)
+  const mockPool = mockAgent.get(userServiceHost)
+
+  // Expected error from query request
+  const expectedErrorMessage = 'Request failed.'
+  const expectedResult = {
+    data: {
+      user: null
+    },
+    errors: [{
+      message: expectedErrorMessage
+    }]
+  }
+
+  // Intercept calls to the federated GraphQL service
+  mockPool.intercept({
+    path: '/graphql',
+    method: 'POST'
+  }).replyWithError(new Error(expectedErrorMessage))
+
+  // Create test gateway instance
+  const gateway = await createTestGatewayServer(t, `${userServiceHost}/graphql`, mockPool)
+
+  // GraphQL query to attempt
+  const query = `
+    query {
+      user: me {
+        id
+        name
+      }
+    }`
+
+  // Send the query
+  const result = await gateway.inject({
+    method: 'POST',
+    headers: { 'content-type': 'application/json' },
+    url: '/graphql',
+    body: JSON.stringify({ query })
+  })
+
+  // Clean up gateway instance
+  await gateway.close()
+
+  // Verify query responds correctly with the predefined error
+  t.same(JSON.parse(result.body), expectedResult)
+})
diff --git a/test/gateway/request.js b/test/gateway/request.js
index fb5d334..cc20440 100644
--- a/test/gateway/request.js
+++ b/test/gateway/request.js
@@ -22,6 +22,7 @@ test('sendRequest method rejects when request errs', t => {
 })
 
 test('sendRequest method rejects when response is not valid json', async (t) => {
+  t.plan(3)
   const app = fastify()
   app.post('/', async (request, reply) => {
     return 'response'
@@ -35,20 +36,27 @@ test('sendRequest method rejects when response is not valid json', async (t) =>
     close()
     return app.close()
   })
-  t.rejects(sendRequest(request, url)({
-    method: 'POST',
-    body: JSON.stringify({
-      query: `
-      query ServiceInfo {
-        _service {
-          sdl
+  try {
+    await sendRequest(request, url)({
+      method: 'POST',
+      body: JSON.stringify({
+        query: `
+        query ServiceInfo {
+          _service {
+            sdl
+          }
         }
-      }
-      `
+        `
+      })
     })
-  }))
+    t.fail('it must throw')
+  } catch (error) {
+    t.type(error, FederatedError)
+    t.type(error.extensions.errors, 'Array')
 
-  t.end()
+    // Full string on Node 17 is "Unexpected token r in JSON at position 0"
+    t.match(error.extensions.errors[0].message, 'Unexpected token')
+  }
 })
 
 test('sendRequest method rejects when response contains errors', async (t) => {