AFFiNE/packages/backend/server/tests/graphql.spec.ts
liuyi 26db1d436d
refactor(server): server errors (#5741)
standardize the error raising in both GraphQL Resolvers and Controllers.

Now, All user aware errors should be throwed with `HttpException`'s variants, for example `NotFoundException`.

> Directly throwing `GraphQLError` are forbidden.
The GraphQL errorFormatter will handle it automatically and set `code`, `status` in error extensions.

At the same time, the frontend `GraphQLError` should be imported from `@affine/graphql`, which introduce a better error extensions type.

----
controller example:
```js
@Get('/docs/${id}')
doc() {
  // ...
  // imported from '@nestjs/common'
  throw new NotFoundException('Doc is not found.');
  // ...
}
```
the above will response as:
```
status: 404 Not Found
{
  "message": "Doc is not found.",
  "statusCode": 404,
  "error": "Not Found"
}
```

resolver example:
```js
@Mutation()
invite() {
  // ...
  throw new PayloadTooLargeException('Workspace seats is full.')
  // ...
}
```

the above will response as:
```
status: 200 Ok
{
  "data": null,
  "errors": [
    {
      "message": "Workspace seats is full.",
      "extensions": {
        "code": 404,
        "status": "Not Found"
      }
    }
  ]
}
```

for frontend GraphQLError user-friend, a helper function introduced:

```js
import { findGraphQLError } from '@affine/graphql'

fetch(query)
  .catch(errOrArr => {
    const e = findGraphQLError(errOrArr, e => e.extensions.code === 404)
    if (e) {
      // handle
    }
})
```
2024-01-31 08:43:03 +00:00

91 lines
2.4 KiB
TypeScript

import {
ForbiddenException,
HttpStatus,
INestApplication,
} from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Test } from '@nestjs/testing';
import testFn, { TestFn } from 'ava';
import request from 'supertest';
import { ConfigModule } from '../src/fundamentals/config';
import { GqlModule } from '../src/fundamentals/graphql';
@Resolver(() => String)
class TestResolver {
greating = 'hello world';
@Query(() => String)
hello() {
return this.greating;
}
@Mutation(() => String)
update(@Args('greating') greating: string) {
this.greating = greating;
return this.greating;
}
@Query(() => String)
errorQuery() {
throw new ForbiddenException('forbidden query');
}
@Query(() => String)
unknownErrorQuery() {
throw new Error('unknown error');
}
}
const test = testFn as TestFn<{ app: INestApplication }>;
function gql(app: INestApplication, query: string) {
return request(app.getHttpServer())
.post('/graphql')
.send({ query })
.expect(200);
}
test.beforeEach(async ctx => {
const module = await Test.createTestingModule({
imports: [ConfigModule.forRoot(), GqlModule],
providers: [TestResolver],
}).compile();
ctx.context.app = await module
.createNestApplication({
logger: false,
})
.init();
});
test('should be able to execute query', async t => {
const res = await gql(t.context.app, `query { hello }`);
t.is(res.body.data.hello, 'hello world');
});
test('should be able to execute mutation', async t => {
const res = await gql(t.context.app, `mutation { update(greating: "hi") }`);
t.is(res.body.data.update, 'hi');
const newRes = await gql(t.context.app, `query { hello }`);
t.is(newRes.body.data.hello, 'hi');
});
test('should be able to handle known http exception', async t => {
const res = await gql(t.context.app, `query { errorQuery }`);
const err = res.body.errors[0];
t.is(err.message, 'forbidden query');
t.is(err.extensions.code, HttpStatus.FORBIDDEN);
t.is(err.extensions.status, HttpStatus[HttpStatus.FORBIDDEN]);
});
test('should be able to handle unknown internal error', async t => {
const res = await gql(t.context.app, `query { unknownErrorQuery }`);
const err = res.body.errors[0];
t.is(err.message, 'Internal Server Error');
t.is(err.extensions.code, HttpStatus.INTERNAL_SERVER_ERROR);
t.is(err.extensions.status, HttpStatus[HttpStatus.INTERNAL_SERVER_ERROR]);
});