mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-10 05:29:08 +08:00
chore(server): move server tests folder (#9614)
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import {
|
||||
applyDecorators,
|
||||
Controller,
|
||||
Get,
|
||||
HttpStatus,
|
||||
INestApplication,
|
||||
Logger,
|
||||
LoggerService,
|
||||
} from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import {
|
||||
SubscribeMessage as RawSubscribeMessage,
|
||||
WebSocketGateway,
|
||||
} from '@nestjs/websockets';
|
||||
import testFn, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
import request from 'supertest';
|
||||
|
||||
import {
|
||||
AccessDenied,
|
||||
GatewayErrorWrapper,
|
||||
UserFriendlyError,
|
||||
} from '../../base';
|
||||
import { Public } from '../../core/auth';
|
||||
import { createTestingApp } from '../utils';
|
||||
|
||||
@Public()
|
||||
@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 AccessDenied();
|
||||
}
|
||||
|
||||
@Query(() => String)
|
||||
unknownErrorQuery() {
|
||||
throw new Error('unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Controller('/')
|
||||
class TestController {
|
||||
@Get('/ok')
|
||||
ok() {
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
@Get('/throw-known-error')
|
||||
throwKnownError() {
|
||||
throw new AccessDenied();
|
||||
}
|
||||
|
||||
@Get('/throw-unknown-error')
|
||||
throwUnknownError() {
|
||||
throw new Error('Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
const SubscribeMessage = (event: string) =>
|
||||
applyDecorators(GatewayErrorWrapper(event), RawSubscribeMessage(event));
|
||||
|
||||
@WebSocketGateway({ transports: ['websocket'], path: '/ws' })
|
||||
class TestGateway {
|
||||
@SubscribeMessage('event:ok')
|
||||
async ok() {
|
||||
return {
|
||||
data: 'ok',
|
||||
};
|
||||
}
|
||||
|
||||
@SubscribeMessage('event:throw-known-error')
|
||||
async throwKnownError() {
|
||||
throw new AccessDenied();
|
||||
}
|
||||
|
||||
@SubscribeMessage('event:throw-unknown-error')
|
||||
async throwUnknownError() {
|
||||
throw new Error('Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
const test = testFn as TestFn<{
|
||||
app: INestApplication;
|
||||
logger: Sinon.SinonStubbedInstance<LoggerService>;
|
||||
}>;
|
||||
|
||||
function gql(app: INestApplication, query: string) {
|
||||
return request(app.getHttpServer())
|
||||
.post('/graphql')
|
||||
.send({ query })
|
||||
.expect(200);
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
const { app } = await createTestingApp({
|
||||
providers: [TestResolver, TestGateway],
|
||||
controllers: [TestController],
|
||||
});
|
||||
|
||||
context.logger = Sinon.stub(new Logger().localInstance);
|
||||
|
||||
context.app = app;
|
||||
});
|
||||
|
||||
test.afterEach.always(async ctx => {
|
||||
await ctx.context.app.close();
|
||||
});
|
||||
|
||||
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 handle known user error in graphql query', async t => {
|
||||
const res = await gql(t.context.app, `query { errorQuery }`);
|
||||
const err = res.body.errors[0];
|
||||
t.is(err.message, 'You do not have permission to access this resource.');
|
||||
t.is(err.extensions.status, HttpStatus.FORBIDDEN);
|
||||
t.is(err.extensions.name, 'ACCESS_DENIED');
|
||||
t.true(t.context.logger.error.notCalled);
|
||||
});
|
||||
|
||||
test('should be able to handle unknown internal error in graphql query', async t => {
|
||||
const res = await gql(t.context.app, `query { unknownErrorQuery }`);
|
||||
const err = res.body.errors[0];
|
||||
t.is(err.message, 'An internal error occurred.');
|
||||
t.is(err.extensions.status, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
t.is(err.extensions.name, 'INTERNAL_SERVER_ERROR');
|
||||
t.true(t.context.logger.error.calledOnceWith('Internal server error'));
|
||||
});
|
||||
|
||||
test('should be able to respond request', async t => {
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/ok')
|
||||
.expect(200);
|
||||
t.is(res.text, 'ok');
|
||||
});
|
||||
|
||||
test('should be able to handle known user error in http request', async t => {
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/throw-known-error')
|
||||
.expect(HttpStatus.FORBIDDEN);
|
||||
|
||||
t.is(res.body.message, 'You do not have permission to access this resource.');
|
||||
t.is(res.body.name, 'ACCESS_DENIED');
|
||||
t.true(t.context.logger.error.notCalled);
|
||||
});
|
||||
|
||||
test('should be able to handle unknown internal error in http request', async t => {
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/throw-unknown-error')
|
||||
.expect(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
t.is(res.body.message, 'An internal error occurred.');
|
||||
t.is(res.body.name, 'INTERNAL_SERVER_ERROR');
|
||||
t.true(t.context.logger.error.calledOnceWith('Internal server error'));
|
||||
});
|
||||
|
||||
// Hard to test through websocket, will call event handler directly
|
||||
test('should be able to response websocket event', async t => {
|
||||
const gateway = t.context.app.get(TestGateway);
|
||||
|
||||
const res = await gateway.ok();
|
||||
t.is(res.data, 'ok');
|
||||
});
|
||||
|
||||
test('should be able to handle known user error in websocket event', async t => {
|
||||
const gateway = t.context.app.get(TestGateway);
|
||||
|
||||
const { error } = (await gateway.throwKnownError()) as unknown as {
|
||||
error: UserFriendlyError;
|
||||
};
|
||||
t.is(error.message, 'You do not have permission to access this resource.');
|
||||
t.is(error.name, 'ACCESS_DENIED');
|
||||
t.true(t.context.logger.error.notCalled);
|
||||
});
|
||||
|
||||
test('should be able to handle unknown internal error in websocket event', async t => {
|
||||
const gateway = t.context.app.get(TestGateway);
|
||||
|
||||
const { error } = (await gateway.throwUnknownError()) as unknown as {
|
||||
error: UserFriendlyError;
|
||||
};
|
||||
t.is(error.message, 'An internal error occurred.');
|
||||
t.is(error.name, 'INTERNAL_SERVER_ERROR');
|
||||
t.true(t.context.logger.error.calledOnceWith('Internal server error'));
|
||||
});
|
||||
@@ -0,0 +1,361 @@
|
||||
import '../../plugins/config';
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpStatus,
|
||||
INestApplication,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
import request, { type Response } from 'supertest';
|
||||
|
||||
import { AppModule } from '../../app.module';
|
||||
import { ConfigModule } from '../../base/config';
|
||||
import {
|
||||
CloudThrottlerGuard,
|
||||
SkipThrottle,
|
||||
Throttle,
|
||||
ThrottlerStorage,
|
||||
} from '../../base/throttler';
|
||||
import { AuthService, Public } from '../../core/auth';
|
||||
import { createTestingApp, initTestingDB, internalSignIn } from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
storage: ThrottlerStorage;
|
||||
cookie: string;
|
||||
app: INestApplication;
|
||||
}>;
|
||||
|
||||
@UseGuards(CloudThrottlerGuard)
|
||||
@Throttle()
|
||||
@Controller('/throttled')
|
||||
class ThrottledController {
|
||||
@Get('/default')
|
||||
default() {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
@Get('/default2')
|
||||
default2() {
|
||||
return 'default2';
|
||||
}
|
||||
|
||||
@Get('/default3')
|
||||
@Throttle('default', { limit: 10 })
|
||||
default3() {
|
||||
return 'default3';
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('/authenticated')
|
||||
@Throttle('authenticated')
|
||||
none() {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
@Throttle('strict')
|
||||
@Get('/strict')
|
||||
strict() {
|
||||
return 'strict';
|
||||
}
|
||||
|
||||
@Public()
|
||||
@SkipThrottle()
|
||||
@Get('/skip')
|
||||
skip() {
|
||||
return 'skip';
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(CloudThrottlerGuard)
|
||||
@Controller('/nonthrottled')
|
||||
class NonThrottledController {
|
||||
@Public()
|
||||
@SkipThrottle()
|
||||
@Get('/skip')
|
||||
skip() {
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('/default')
|
||||
default() {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Throttle('strict')
|
||||
@Get('/strict')
|
||||
strict() {
|
||||
return 'strict';
|
||||
}
|
||||
}
|
||||
|
||||
test.before(async t => {
|
||||
const { app } = await createTestingApp({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
throttler: {
|
||||
default: {
|
||||
ttl: 60,
|
||||
limit: 120,
|
||||
},
|
||||
},
|
||||
}),
|
||||
AppModule,
|
||||
],
|
||||
controllers: [ThrottledController, NonThrottledController],
|
||||
});
|
||||
|
||||
t.context.storage = app.get(ThrottlerStorage);
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
test.beforeEach(async t => {
|
||||
await initTestingDB(t.context.app.get(PrismaClient));
|
||||
const { app } = t.context;
|
||||
const auth = app.get(AuthService);
|
||||
const u1 = await auth.signUp('u1@affine.pro', 'test');
|
||||
t.context.cookie = await internalSignIn(app, u1.id);
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
function rateLimitHeaders(res: Response) {
|
||||
return {
|
||||
limit: res.header['x-ratelimit-limit'],
|
||||
remaining: res.header['x-ratelimit-remaining'],
|
||||
reset: res.header['x-ratelimit-reset'],
|
||||
retryAfter: res.header['retry-after'],
|
||||
};
|
||||
}
|
||||
|
||||
test('should be able to prevent requests if limit is reached', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const stub = Sinon.stub(app.get(ThrottlerStorage), 'increment').resolves({
|
||||
timeToExpire: 10,
|
||||
totalHits: 21,
|
||||
isBlocked: true,
|
||||
timeToBlockExpire: 10,
|
||||
});
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/nonthrottled/strict')
|
||||
.expect(HttpStatus.TOO_MANY_REQUESTS);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.retryAfter, '10');
|
||||
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
// ====== unauthenticated user visits ======
|
||||
test('should use default throttler for unauthenticated user when not specified', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/nonthrottled/default')
|
||||
.expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, '120');
|
||||
t.is(headers.remaining, '119');
|
||||
});
|
||||
|
||||
test('should skip throttler for unauthenticated user when specified', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
let res = await request(app.getHttpServer())
|
||||
.get('/nonthrottled/skip')
|
||||
.expect(200);
|
||||
|
||||
let headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, undefined!);
|
||||
t.is(headers.remaining, undefined!);
|
||||
t.is(headers.reset, undefined!);
|
||||
|
||||
res = await request(app.getHttpServer()).get('/throttled/skip').expect(200);
|
||||
|
||||
headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, undefined!);
|
||||
t.is(headers.remaining, undefined!);
|
||||
t.is(headers.reset, undefined!);
|
||||
});
|
||||
|
||||
test('should use specified throttler for unauthenticated user', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/nonthrottled/strict')
|
||||
.expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, '20');
|
||||
t.is(headers.remaining, '19');
|
||||
});
|
||||
|
||||
// ==== authenticated user visits ====
|
||||
test('should not protect unspecified routes', async t => {
|
||||
const { app, cookie } = t.context;
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/nonthrottled/default')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, undefined!);
|
||||
t.is(headers.remaining, undefined!);
|
||||
t.is(headers.reset, undefined!);
|
||||
});
|
||||
|
||||
test('should use default throttler for authenticated user when not specified', async t => {
|
||||
const { app, cookie } = t.context;
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/throttled/default')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, '120');
|
||||
t.is(headers.remaining, '119');
|
||||
});
|
||||
|
||||
test('should use same throttler for multiple routes', async t => {
|
||||
const { app, cookie } = t.context;
|
||||
|
||||
let res = await request(app.getHttpServer())
|
||||
.get('/throttled/default')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
|
||||
let headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, '120');
|
||||
t.is(headers.remaining, '119');
|
||||
|
||||
res = await request(app.getHttpServer())
|
||||
.get('/throttled/default2')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
|
||||
headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, '120');
|
||||
t.is(headers.remaining, '118');
|
||||
});
|
||||
|
||||
test('should use different throttler if specified', async t => {
|
||||
const { app, cookie } = t.context;
|
||||
|
||||
let res = await request(app.getHttpServer())
|
||||
.get('/throttled/default')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
|
||||
let headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, '120');
|
||||
t.is(headers.remaining, '119');
|
||||
|
||||
res = await request(app.getHttpServer())
|
||||
.get('/throttled/default3')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
|
||||
headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, '10');
|
||||
t.is(headers.remaining, '9');
|
||||
});
|
||||
|
||||
test('should skip throttler for authenticated if `authenticated` throttler used', async t => {
|
||||
const { app, cookie } = t.context;
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/throttled/authenticated')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, undefined!);
|
||||
t.is(headers.remaining, undefined!);
|
||||
t.is(headers.reset, undefined!);
|
||||
});
|
||||
|
||||
test('should apply `default` throttler for authenticated user if `authenticated` throttler used', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/throttled/authenticated')
|
||||
.expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, '120');
|
||||
t.is(headers.remaining, '119');
|
||||
});
|
||||
|
||||
test('should skip throttler for authenticated user when specified', async t => {
|
||||
const { app, cookie } = t.context;
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/throttled/skip')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, undefined!);
|
||||
t.is(headers.remaining, undefined!);
|
||||
t.is(headers.reset, undefined!);
|
||||
});
|
||||
|
||||
test('should use specified throttler for authenticated user', async t => {
|
||||
const { app, cookie } = t.context;
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/throttled/strict')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
|
||||
t.is(headers.limit, '20');
|
||||
t.is(headers.remaining, '19');
|
||||
});
|
||||
|
||||
test('should separate anonymous and authenticated user throttlers', async t => {
|
||||
const { app, cookie } = t.context;
|
||||
|
||||
const authenticatedUserRes = await request(app.getHttpServer())
|
||||
.get('/throttled/default')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
const unauthenticatedUserRes = await request(app.getHttpServer())
|
||||
.get('/nonthrottled/default')
|
||||
.expect(200);
|
||||
|
||||
const authenticatedResHeaders = rateLimitHeaders(authenticatedUserRes);
|
||||
const unauthenticatedResHeaders = rateLimitHeaders(unauthenticatedUserRes);
|
||||
|
||||
t.is(authenticatedResHeaders.limit, '120');
|
||||
t.is(authenticatedResHeaders.remaining, '119');
|
||||
|
||||
t.is(unauthenticatedResHeaders.limit, '120');
|
||||
t.is(unauthenticatedResHeaders.remaining, '119');
|
||||
});
|
||||
Reference in New Issue
Block a user