зеркало из https://github.com/mozilla/fxa.git
feat(admin-server): convert to NestJS
Because: * We're standardizing our applications on NestJS. This commit: * Converts the admin-server to NestJS. Closes #6262
This commit is contained in:
Родитель
0007318e83
Коммит
69f44d78e1
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "tsc-watch",
|
||||
"command": "npm",
|
||||
"args": ["run", "watch"],
|
||||
"type": "shell",
|
||||
"isBackground": true,
|
||||
"group": "build",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run Current Test",
|
||||
"type": "shell",
|
||||
"command": "./node_modules/mocha/bin/mocha",
|
||||
"args": ["-r", "ts-node/register", "${relativeFile}"],
|
||||
"group": "test",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -4,7 +4,7 @@ This is the GraphQL server for an internal resource for FxA Admins to access a s
|
|||
|
||||
## Generate test email bounces
|
||||
|
||||
If you need to create a handful of test email bounces in development you can use `npm run email-bounce`.
|
||||
If you need to create a handful of test email bounces in development you can use `yarn email-bounce`.
|
||||
|
||||
By default this will create a new email bounce for a newly-created dummy account.
|
||||
|
||||
|
@ -12,24 +12,22 @@ Use the `--email` flag to create a bounce for an existing account.
|
|||
|
||||
Use the `--count` flag to create X number of bounces in a single command.
|
||||
|
||||
Example: `npm run email-bounce -- --email=test@example.com --count=3`
|
||||
Example: `yarn email-bounce --email test@example.com --count 3`
|
||||
|
||||
## Testing
|
||||
|
||||
This package uses [Mocha](https://mochajs.org/) to test its code. By default `npm test` will test all files ending in `.spec.ts` under `src/test/` and uses `ts-node` so it can process TypeScript files.
|
||||
This package uses [Jest](https://mochajs.org/) to test its code. By default `yarn test` will test all files ending in `.spec.ts`.
|
||||
|
||||
Test specific tests with the following commands:
|
||||
Test commands:
|
||||
|
||||
```bash
|
||||
# Test only src/test/lib/sentry.spec.ts
|
||||
npx mocha -r ts-node/register src/test/lib/sentry.spec.ts
|
||||
# Test with coverage
|
||||
yarn test:cov
|
||||
|
||||
# Grep for "returns lbheartbeat"
|
||||
npx mocha -r ts-node/register src/test/lib/** -g "returns lbheartbeat"
|
||||
# Test on file change
|
||||
yarn test:watch
|
||||
```
|
||||
|
||||
Refer to Mocha's [CLI documentation](https://mochajs.org/#command-line-usage) for more advanced test configuration.
|
||||
|
||||
## License
|
||||
|
||||
MPL-2.0
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
|
@ -3,14 +3,19 @@
|
|||
"version": "1.188.1",
|
||||
"description": "FxA GraphQL Admin Server",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "nest build",
|
||||
"lint": "eslint *",
|
||||
"audit": "npm audit --json | audit-filter --nsp-config=.nsprc --audit=-",
|
||||
"watch": "tsc -w",
|
||||
"start": "pm2 start pm2.config.js",
|
||||
"stop": "pm2 stop pm2.config.js",
|
||||
"start:prod": "node dist/main",
|
||||
"restart": "pm2 restart pm2.config.js",
|
||||
"test": "mocha -r ts-node/register src/test/**/*.spec.ts src/test/**/**/*.spec.ts src/test/**/**/**/*.spec.ts",
|
||||
"test": "jest && yarn test:e2e",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"email-bounce": "ts-node ./scripts/email-bounce.ts"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -28,49 +33,70 @@
|
|||
"homepage": "https://github.com/mozilla/fxa#readme",
|
||||
"readmeFilename": "README.md",
|
||||
"dependencies": {
|
||||
"@sentry/integrations": "^5.21.1",
|
||||
"@sentry/node": "^5.21.1",
|
||||
"@nestjs/common": "^7.4.4",
|
||||
"@nestjs/config": "^0.5.0",
|
||||
"@nestjs/core": "^7.4.4",
|
||||
"@nestjs/graphql": "^7.6.0",
|
||||
"@nestjs/mapped-types": "^0.1.0",
|
||||
"@nestjs/platform-express": "^7.4.4",
|
||||
"@sentry/integrations": "^5.23.0",
|
||||
"@sentry/node": "^5.23.0",
|
||||
"apollo-server": "^2.16.1",
|
||||
"apollo-server-express": "^2.16.1",
|
||||
"class-transformer": "^0.3.1",
|
||||
"class-validator": "^0.12.2",
|
||||
"convict": "^6.0.0",
|
||||
"convict-format-with-moment": "^6.0.0",
|
||||
"convict-format-with-validator": "^6.0.0",
|
||||
"express": "^4.17.1",
|
||||
"fxa-shared": "workspace:*",
|
||||
"graphql": "^14.6.0",
|
||||
"graphql-tools": "^4.0.8",
|
||||
"helmet": "^4.1.1",
|
||||
"knex": "^0.21.4",
|
||||
"mozlog": "^3.0.1",
|
||||
"mysql": "^2.18.1",
|
||||
"objection": "^2.2.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"tslib": "^2.0.1",
|
||||
"type-graphql": "^0.17.6",
|
||||
"typedi": "^0.8.0"
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^6.5.5",
|
||||
"tslib": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.12",
|
||||
"@types/chance": "^1.1.0",
|
||||
"@types/convict": "^5.2.1",
|
||||
"@types/eslint": "7.2.0",
|
||||
"@types/graphql": "^14.5.0",
|
||||
"@types/mocha": "^8.0.2",
|
||||
"@types/node": "^13.9.1",
|
||||
"@types/proxyquire": "^1.3.28",
|
||||
"@types/sinon": "9.0.5",
|
||||
"@types/rimraf": "3.0.0",
|
||||
"@types/supertest": "^2.0.10",
|
||||
"@types/yargs": "^15.0.5",
|
||||
"audit-filter": "^0.5.0",
|
||||
"chai": "^4.2.0",
|
||||
"chance": "^1.1.6",
|
||||
"eslint": "^7.6.0",
|
||||
"fxa-shared": "workspace:*",
|
||||
"mocha": "^8.1.1",
|
||||
"jest": "26.4.2",
|
||||
"pm2": "^4.4.1",
|
||||
"prettier": "^2.0.5",
|
||||
"proxyquire": "^2.1.3",
|
||||
"sinon": "^9.0.3",
|
||||
"supertest": "^4.0.2",
|
||||
"ts-node": "^8.10.2",
|
||||
"ts-sinon": "^1.2.0",
|
||||
"ts-jest": "26.1.0",
|
||||
"ts-node": "^8.6.2",
|
||||
"tsconfig-paths": "^3.9.0",
|
||||
"typescript": "3.9.7",
|
||||
"yargs": "^15.4.1"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,17 +10,16 @@ module.exports = {
|
|||
apps: [
|
||||
{
|
||||
name: 'admin-server',
|
||||
script: 'node -r ts-node/register src/bin/main.ts',
|
||||
script: 'nest start --debug=9150 --watch',
|
||||
cwd: __dirname,
|
||||
max_restarts: '1',
|
||||
min_uptime: '2m',
|
||||
env: {
|
||||
PATH,
|
||||
NODE_ENV: 'development',
|
||||
NODE_OPTIONS: '--inspect=9150',
|
||||
TS_NODE_TRANSPILE_ONLY: 'true',
|
||||
TS_NODE_FILES: 'true',
|
||||
PORT: '8095', // TODO: this needs to get added to src/config.ts
|
||||
PORT: '8095',
|
||||
},
|
||||
filter_env: ['npm_'],
|
||||
watch: ['src'],
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
import { argv } from 'yargs';
|
||||
import { EmailBounces } from '../src/lib/db/models/email-bounces';
|
||||
import { Account } from '../src/lib/db/models/account';
|
||||
import Config from '../src/config';
|
||||
import { setupDatabase } from '../src/lib/db';
|
||||
import Knex from 'knex';
|
||||
import { Model } from 'objection';
|
||||
import yargs from 'yargs';
|
||||
|
||||
import Config from '../src/config';
|
||||
import { Account, EmailBounces } from '../src/database/model';
|
||||
import {
|
||||
AccountIsh,
|
||||
BounceIsh,
|
||||
randomAccount,
|
||||
randomEmail,
|
||||
randomEmailBounce,
|
||||
AccountIsh,
|
||||
BounceIsh
|
||||
} from '../src/test/lib/db/models/helpers';
|
||||
} from '../src/database/model/helpers';
|
||||
|
||||
const config = Config.getProperties();
|
||||
|
||||
const argv = yargs.options({
|
||||
count: { type: 'number', default: 1 },
|
||||
email: { type: 'string' },
|
||||
}).argv;
|
||||
|
||||
async function addBounceToDB() {
|
||||
const knex = setupDatabase(config.database);
|
||||
const count = argv.count || 1;
|
||||
const knex = Knex({ client: 'mysql', connection: config.database });
|
||||
Model.knex(knex);
|
||||
const count = argv.count;
|
||||
|
||||
let bounce: BounceIsh;
|
||||
let account: AccountIsh;
|
||||
|
@ -34,13 +40,20 @@ async function addBounceToDB() {
|
|||
}
|
||||
|
||||
await EmailBounces.query().insertGraph(bounce);
|
||||
if (!argv.email) {
|
||||
console.log(`=> Created 1 email bounce for ${bounce.email}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (argv.email) {
|
||||
console.log(
|
||||
`=> Created ${count} email ${count === 1 ? 'bounce' : 'bounces'} for ${
|
||||
argv.email
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
await knex.destroy();
|
||||
|
||||
console.log(
|
||||
`=> Created ${count} email ${count === 1 ? 'bounce' : 'bounces'} for ${bounce.email}`
|
||||
);
|
||||
}
|
||||
|
||||
addBounceToDB();
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { GraphQLModule } from '@nestjs/graphql';
|
||||
import { HealthModule } from 'fxa-shared/nestjs/health/health.module';
|
||||
import { LoggerModule } from 'fxa-shared/nestjs/logger/logger.module';
|
||||
import { SentryModule } from 'fxa-shared/nestjs/sentry/sentry.module';
|
||||
import { getVersionInfo } from 'fxa-shared/nestjs/version';
|
||||
import { join } from 'path';
|
||||
|
||||
import Config, { AppConfig } from './config';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
import { DatabaseService } from './database/database.service';
|
||||
import { GqlModule } from './gql/gql.module';
|
||||
|
||||
const version = getVersionInfo(__dirname);
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
load: [(): AppConfig => Config.getProperties()],
|
||||
isGlobal: true,
|
||||
}),
|
||||
DatabaseModule,
|
||||
GqlModule,
|
||||
GraphQLModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
path: '/graphql',
|
||||
useGlobalPrefix: true,
|
||||
debug: configService.get<string>('env') !== 'production',
|
||||
playground: configService.get<string>('env') !== 'production',
|
||||
autoSchemaFile: join(__dirname, './schema.gql'),
|
||||
context: ({ req }) => ({ req }),
|
||||
}),
|
||||
}),
|
||||
HealthModule.forRootAsync({
|
||||
imports: [DatabaseModule],
|
||||
inject: [DatabaseService],
|
||||
useFactory: async (db: DatabaseService) => ({
|
||||
version,
|
||||
extraHealthData: () => db.dbHealthCheck(),
|
||||
}),
|
||||
}),
|
||||
LoggerModule,
|
||||
SentryModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService<AppConfig>) => ({
|
||||
dsn: configService.get('sentryDsn'),
|
||||
environment: configService.get('env'),
|
||||
version: version.version,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
})
|
||||
export class AppModule {}
|
|
@ -0,0 +1,9 @@
|
|||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: unknown, context: ExecutionContext) => {
|
||||
const ctx = GqlExecutionContext.create(context);
|
||||
return ctx.getContext().req.user;
|
||||
}
|
||||
);
|
|
@ -0,0 +1,10 @@
|
|||
import { GqlAuthHeaderGuard } from './auth-header.guard';
|
||||
|
||||
describe('AuthHeaderGuard', () => {
|
||||
it('should be defined', () => {
|
||||
const MockConfig = {
|
||||
get: jest.fn().mockReturnValue({ authHeader: 'test' }),
|
||||
};
|
||||
expect(new GqlAuthHeaderGuard(MockConfig as any)).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import { Request } from 'express';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { AppConfig } from '../config';
|
||||
|
||||
@Injectable()
|
||||
export class GqlAuthHeaderGuard implements CanActivate {
|
||||
private authHeader: string;
|
||||
|
||||
constructor(configService: ConfigService<AppConfig>) {
|
||||
this.authHeader = configService.get('authHeader') as string;
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const ctx = GqlExecutionContext.create(context);
|
||||
const request = ctx.getContext().req as Request;
|
||||
const username = request.get(this.authHeader);
|
||||
if (username) {
|
||||
(request as any).user = username;
|
||||
}
|
||||
return !!username;
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import 'reflect-metadata';
|
||||
|
||||
import express from 'express';
|
||||
import mozlog from 'mozlog';
|
||||
|
||||
import Config from '../config';
|
||||
import { dbHealthCheck } from '../lib/db';
|
||||
import { loadBalancerRoutes, strictTransportSecurity } from '../lib/middleware';
|
||||
import { configureSentry } from '../lib/sentry';
|
||||
import { createServer } from '../lib/server';
|
||||
import { version } from '../lib/version';
|
||||
|
||||
const logger = mozlog(Config.get('logging'))('supportPanel');
|
||||
configureSentry({ dsn: Config.getProperties().sentryDsn, release: version.version });
|
||||
|
||||
async function run() {
|
||||
const app = express();
|
||||
app.use(strictTransportSecurity);
|
||||
|
||||
const server = await createServer(Config.getProperties(), logger);
|
||||
server.applyMiddleware({ app });
|
||||
app.use(loadBalancerRoutes(dbHealthCheck));
|
||||
|
||||
app.listen({ port: 8090 }, () => {
|
||||
logger.info('startup', {
|
||||
message: `Server is running, GraphQL Playground available at http://localhost:8090${server.graphqlPath}`
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
run().catch(err => {
|
||||
logger.error('startup', { err });
|
||||
});
|
|
@ -1,7 +1,6 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import convict from 'convict';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
@ -54,7 +53,7 @@ const conf = convict({
|
|||
env: 'NODE_ENV',
|
||||
format: ['development', 'test', 'stage', 'production'],
|
||||
},
|
||||
logging: {
|
||||
log: {
|
||||
app: { default: 'fxa-user-admin-server' },
|
||||
fmt: {
|
||||
default: 'heka',
|
||||
|
@ -65,17 +64,12 @@ const conf = convict({
|
|||
default: 'info',
|
||||
env: 'LOG_LEVEL',
|
||||
},
|
||||
routes: {
|
||||
enabled: {
|
||||
default: true,
|
||||
doc: 'Enable route logging. Set to false to trimming CI logs.',
|
||||
env: 'ENABLE_ROUTE_LOGGING',
|
||||
},
|
||||
format: {
|
||||
default: 'default_fxa',
|
||||
format: ['default_fxa', 'dev_fxa', 'default', 'dev', 'short', 'tiny'],
|
||||
},
|
||||
},
|
||||
},
|
||||
port: {
|
||||
default: 8095,
|
||||
doc: 'Default port to listen on',
|
||||
env: 'PORT',
|
||||
format: Number,
|
||||
},
|
||||
sentryDsn: {
|
||||
default: '',
|
||||
|
@ -110,4 +104,5 @@ conf.loadFile(files);
|
|||
conf.validate({ allowed: 'strict' });
|
||||
const Config = conf;
|
||||
|
||||
export type AppConfig = ReturnType<typeof Config['getProperties']>;
|
||||
export default Config;
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { DatabaseService } from './database.service';
|
||||
|
||||
@Module({
|
||||
providers: [DatabaseService],
|
||||
exports: [DatabaseService],
|
||||
})
|
||||
export class DatabaseModule {}
|
|
@ -0,0 +1,38 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { DatabaseService } from './database.service';
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
describe('DatabaseService', () => {
|
||||
let service: DatabaseService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const MockConfig: Provider = {
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: jest.fn().mockReturnValue({
|
||||
database: {
|
||||
database: 'testAdmin',
|
||||
host: 'localhost',
|
||||
password: '',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [DatabaseService, MockConfig],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DatabaseService>(DatabaseService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Knex from 'knex';
|
||||
|
||||
import { AppConfig } from '../config';
|
||||
import { Account, EmailBounces, Emails } from './model';
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseService {
|
||||
public knex: Knex;
|
||||
public account: typeof Account;
|
||||
public emails: typeof Emails;
|
||||
public emailBounces: typeof EmailBounces;
|
||||
|
||||
constructor(configService: ConfigService<AppConfig>) {
|
||||
const dbConfig = configService.get('database') as AppConfig['database'];
|
||||
this.knex = Knex({ connection: dbConfig, client: 'mysql' });
|
||||
this.account = Account.bindKnex(this.knex);
|
||||
this.emails = Emails.bindKnex(this.knex);
|
||||
this.emailBounces = EmailBounces.bindKnex(this.knex);
|
||||
}
|
||||
|
||||
async dbHealthCheck(): Promise<Record<string, any>> {
|
||||
let status = 'ok';
|
||||
try {
|
||||
await this.account.query().limit(1);
|
||||
} catch (err) {
|
||||
status = 'error';
|
||||
}
|
||||
return {
|
||||
db: { status },
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import Knex from 'knex';
|
||||
|
||||
import { uuidTransformer } from '../transformers';
|
||||
import { Account } from './account.model';
|
||||
import { chance, randomAccount, testDatabaseSetup } from './helpers';
|
||||
|
||||
const USER_1 = randomAccount();
|
||||
|
||||
describe('account model', () => {
|
||||
let knex: Knex;
|
||||
|
||||
beforeAll(async () => {
|
||||
knex = await testDatabaseSetup('testAdminAccount');
|
||||
// Load the user in
|
||||
await Account.bindKnex(knex).query().insertGraph(USER_1);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
it('looks up the user by email', async () => {
|
||||
const result = await Account.bindKnex(knex).query().findOne({
|
||||
normalizedEmail: USER_1.normalizedEmail,
|
||||
});
|
||||
expect(result.uid).toBe(USER_1.uid);
|
||||
expect(result.email).toBe(USER_1.email);
|
||||
});
|
||||
|
||||
it('looks up the user by uid buffer', async () => {
|
||||
const uidBuffer = uuidTransformer.to(USER_1.uid);
|
||||
const result = await Account.bindKnex(knex)
|
||||
.query()
|
||||
.findOne({ uid: uidBuffer });
|
||||
expect(result.uid).toBe(USER_1.uid);
|
||||
expect(result.email).toBe(USER_1.email);
|
||||
});
|
||||
|
||||
it('does not find a non-existent user', async () => {
|
||||
let result = await Account.bindKnex(knex).query().findOne({
|
||||
normalizedEmail: chance.email(),
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
const uid = chance.guid({ version: 4 }).replace(/-/g, '');
|
||||
const uidBuffer = uuidTransformer.to(uid);
|
||||
result = await Account.bindKnex(knex).query().findOne({ uid: uidBuffer });
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -1,11 +1,10 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { Model } from 'objection';
|
||||
|
||||
import { intBoolTransformer, uuidTransformer } from '../transformers';
|
||||
import { Emails } from './emails';
|
||||
import { Emails } from './emails.model';
|
||||
|
||||
export class Account extends Model {
|
||||
public static tableName = 'accounts';
|
||||
|
@ -15,11 +14,11 @@ export class Account extends Model {
|
|||
emails: {
|
||||
join: {
|
||||
from: 'accounts.uid',
|
||||
to: 'emails.uid'
|
||||
to: 'emails.uid',
|
||||
},
|
||||
modelClass: Emails,
|
||||
relation: Model.HasManyRelation
|
||||
}
|
||||
relation: Model.HasManyRelation,
|
||||
},
|
||||
};
|
||||
|
||||
public uid!: string;
|
|
@ -0,0 +1,44 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import Knex from 'knex';
|
||||
|
||||
import { EmailBounces } from './email-bounces.model';
|
||||
import {
|
||||
chance,
|
||||
randomAccount,
|
||||
randomEmailBounce,
|
||||
testDatabaseSetup,
|
||||
} from './helpers';
|
||||
|
||||
const USER_1 = randomAccount();
|
||||
const EMAIL_BOUNCE = randomEmailBounce(USER_1.email);
|
||||
|
||||
describe('emailBounces model', () => {
|
||||
let knex: Knex;
|
||||
|
||||
beforeAll(async () => {
|
||||
knex = await testDatabaseSetup('testAdminEmail');
|
||||
// Load the emailBounce in
|
||||
await EmailBounces.bindKnex(knex).query().insertGraph(EMAIL_BOUNCE);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
it('looks up the email bounce', async () => {
|
||||
const result = await EmailBounces.bindKnex(knex).query().findOne({
|
||||
email: EMAIL_BOUNCE.email,
|
||||
});
|
||||
expect(result.email).toBe(EMAIL_BOUNCE.email);
|
||||
expect(result.bounceType).toBe(EMAIL_BOUNCE.bounceType);
|
||||
});
|
||||
|
||||
it('does not find a non-existent email bounce', async () => {
|
||||
const result = await EmailBounces.bindKnex(knex).query().findOne({
|
||||
email: chance.email(),
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -1,7 +1,6 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { Model } from 'objection';
|
||||
|
||||
export class EmailBounces extends Model {
|
|
@ -1,7 +1,6 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { Model } from 'objection';
|
||||
|
||||
import { intBoolTransformer, uuidTransformer } from '../transformers';
|
|
@ -1,16 +1,13 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Chance from 'chance';
|
||||
import fs from 'fs';
|
||||
import Knex from 'knex';
|
||||
import path from 'path';
|
||||
|
||||
import Chance from 'chance';
|
||||
import Knex from 'knex';
|
||||
|
||||
import { setupDatabase } from '../../../../lib/db';
|
||||
import { Account } from '../../../../lib/db/models/account';
|
||||
import { EmailBounces } from '../../../../lib/db/models/email-bounces';
|
||||
import { Account, EmailBounces } from '.';
|
||||
import { Model } from 'objection';
|
||||
|
||||
export type AccountIsh = Pick<
|
||||
Account,
|
||||
|
@ -80,7 +77,7 @@ export function randomEmail(account: AccountIsh, createSecondaryEmail = false) {
|
|||
};
|
||||
}
|
||||
|
||||
export async function testDatabaseSetup(): Promise<Knex> {
|
||||
export async function testDatabaseSetup(dbname: string): Promise<Knex> {
|
||||
// Create the db if it doesn't exist
|
||||
let knex = Knex({
|
||||
client: 'mysql',
|
||||
|
@ -93,16 +90,18 @@ export async function testDatabaseSetup(): Promise<Knex> {
|
|||
},
|
||||
});
|
||||
|
||||
await knex.raw('DROP DATABASE IF EXISTS testAdmin');
|
||||
await knex.raw('CREATE DATABASE testAdmin');
|
||||
await knex.raw(`DROP DATABASE IF EXISTS ${dbname}`);
|
||||
await knex.raw(`CREATE DATABASE ${dbname}`);
|
||||
await knex.destroy();
|
||||
|
||||
knex = setupDatabase({
|
||||
database: 'testAdmin',
|
||||
host: 'localhost',
|
||||
password: '',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
knex = Knex({
|
||||
connection: {
|
||||
database: dbname,
|
||||
host: 'localhost',
|
||||
password: '',
|
||||
port: 3306,
|
||||
user: 'root',
|
||||
},
|
||||
client: 'mysql',
|
||||
});
|
||||
|
||||
await knex.raw(accountTable);
|
|
@ -1,9 +1,8 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { Account } from './account';
|
||||
import { EmailBounces } from './email-bounces';
|
||||
import { Emails } from './emails';
|
||||
import { Account } from './account.model';
|
||||
import { EmailBounces } from './email-bounces.model';
|
||||
import { Emails } from './emails.model';
|
||||
|
||||
export { Account, Emails, EmailBounces };
|
|
@ -20,6 +20,12 @@ function buffer(value: string | Buffer) {
|
|||
return value;
|
||||
}
|
||||
|
||||
export const uuidTransformer = { to: (v: any) => buffer(v), from: (v: any) => unbuffer(v) };
|
||||
export const uuidTransformer = {
|
||||
to: (v: any) => buffer(v),
|
||||
from: (v: any) => unbuffer(v),
|
||||
};
|
||||
|
||||
export const intBoolTransformer = { to: (v: any) => (v ? 1 : 0), from: (v: any) => !!v };
|
||||
export const intBoolTransformer = {
|
||||
to: (v: any) => (v ? 1 : 0),
|
||||
from: (v: any) => !!v,
|
||||
};
|
|
@ -0,0 +1,131 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
|
||||
import Knex from 'knex';
|
||||
|
||||
import { Account, EmailBounces, Emails } from '../../database/model';
|
||||
import {
|
||||
randomAccount,
|
||||
randomEmail,
|
||||
randomEmailBounce,
|
||||
testDatabaseSetup,
|
||||
} from '../../database/model/helpers';
|
||||
import { AccountResolver } from './account.resolver';
|
||||
import { DatabaseService } from 'fxa-admin-server/src/database/database.service';
|
||||
|
||||
const USER_1 = randomAccount();
|
||||
const EMAIL_1 = randomEmail(USER_1);
|
||||
const EMAIL_2 = randomEmail(USER_1, true);
|
||||
const EMAIL_BOUNCE_1 = randomEmailBounce(USER_1.email);
|
||||
|
||||
const USER_2 = randomAccount();
|
||||
|
||||
describe('AccountResolver', () => {
|
||||
let resolver: AccountResolver;
|
||||
let logger: any;
|
||||
let knex: Knex;
|
||||
let db = {
|
||||
account: Account,
|
||||
emails: Emails,
|
||||
emailBounces: EmailBounces,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
knex = await testDatabaseSetup('testAdminAccountResolver');
|
||||
// Load the users in
|
||||
db.account = Account.bindKnex(knex);
|
||||
db.emails = Emails.bindKnex(knex);
|
||||
db.emailBounces = EmailBounces.bindKnex(knex);
|
||||
await (db.account as any).query().insertGraph({
|
||||
...USER_1,
|
||||
emails: [EMAIL_1, EMAIL_2],
|
||||
});
|
||||
await db.emailBounces.query().insert(EMAIL_BOUNCE_1);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
logger = { debug: jest.fn(), error: jest.fn(), info: jest.fn() };
|
||||
const MockMozLogger: Provider = {
|
||||
provide: MozLoggerService,
|
||||
useValue: logger,
|
||||
};
|
||||
const MockConfig: Provider = {
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: jest.fn().mockReturnValue({ authHeader: 'test' }),
|
||||
},
|
||||
};
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AccountResolver,
|
||||
MockMozLogger,
|
||||
MockConfig,
|
||||
{ provide: DatabaseService, useValue: db },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
resolver = module.get<AccountResolver>(AccountResolver);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(resolver).toBeDefined();
|
||||
});
|
||||
|
||||
it('locates the user by uid', async () => {
|
||||
const result = (await resolver.accountByUid(USER_1.uid, 'joe')) as Account;
|
||||
expect(result).toBeDefined();
|
||||
expect(result.email).toBe(USER_1.email);
|
||||
expect(logger.info).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not locate non-existent users by uid', async () => {
|
||||
const result = await resolver.accountByUid(USER_2.uid, 'joe');
|
||||
expect(result).toBeUndefined();
|
||||
expect(logger.info).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('locates the user by email', async () => {
|
||||
const result = (await resolver.accountByEmail(
|
||||
USER_1.email,
|
||||
'joe'
|
||||
)) as Account;
|
||||
expect(result).toBeDefined();
|
||||
expect(result.email).toBe(USER_1.email);
|
||||
expect(logger.info).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not locate non-existent users by email', async () => {
|
||||
const result = await resolver.accountByEmail(USER_2.email, 'joe');
|
||||
expect(result).toBeUndefined();
|
||||
expect(logger.info).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('loads emailBounces', async () => {
|
||||
const user = (await resolver.accountByEmail(
|
||||
USER_1.email,
|
||||
'joe'
|
||||
)) as Account;
|
||||
const result = await resolver.emailBounces(user);
|
||||
expect(result).toBeDefined();
|
||||
const bounce = result[0];
|
||||
expect(bounce).toEqual(EMAIL_BOUNCE_1);
|
||||
});
|
||||
|
||||
it('loads emails', async () => {
|
||||
const user = (await resolver.accountByEmail(
|
||||
USER_1.email,
|
||||
'joe'
|
||||
)) as Account;
|
||||
const result = await resolver.emails(user);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
|
@ -1,13 +1,16 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, Query, ResolveField, Resolver, Root } from '@nestjs/graphql';
|
||||
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
|
||||
|
||||
import { Arg, Ctx, FieldResolver, Query, Resolver, Root } from 'type-graphql';
|
||||
|
||||
import { Account, EmailBounces, Emails } from '../db/models';
|
||||
import { uuidTransformer } from '../db/transformers';
|
||||
import { Context } from '../server';
|
||||
import { Account as AccountType } from './types/account';
|
||||
import { CurrentUser } from '../../auth/auth-header.decorator';
|
||||
import { GqlAuthHeaderGuard } from '../../auth/auth-header.guard';
|
||||
import { DatabaseService } from '../../database/database.service';
|
||||
import { Account } from '../../database/model';
|
||||
import { uuidTransformer } from '../../database/transformers';
|
||||
import { Account as AccountType } from '../../gql/model/account.model';
|
||||
|
||||
const ACCOUNT_COLUMNS = ['uid', 'email', 'emailVerified', 'createdAt'];
|
||||
const EMAIL_COLUMNS = [
|
||||
|
@ -21,12 +24,15 @@ const EMAIL_COLUMNS = [
|
|||
'verifiedAt',
|
||||
];
|
||||
|
||||
@Resolver((of) => AccountType)
|
||||
@UseGuards(GqlAuthHeaderGuard)
|
||||
@Resolver((of: any) => AccountType)
|
||||
export class AccountResolver {
|
||||
constructor(private log: MozLoggerService, private db: DatabaseService) {}
|
||||
|
||||
@Query((returns) => AccountType, { nullable: true })
|
||||
public accountByUid(
|
||||
@Arg('uid', { nullable: false }) uid: string,
|
||||
@Ctx() context: Context
|
||||
@Args('uid', { nullable: false }) uid: string,
|
||||
@CurrentUser() user: string
|
||||
) {
|
||||
let uidBuffer;
|
||||
try {
|
||||
|
@ -34,31 +40,36 @@ export class AccountResolver {
|
|||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
context.logAction('accountByUid', { uid });
|
||||
return Account.query().select(ACCOUNT_COLUMNS).findOne({ uid: uidBuffer });
|
||||
this.log.info('accountByUid', { uid, user });
|
||||
return this.db.account
|
||||
.query()
|
||||
.select(ACCOUNT_COLUMNS)
|
||||
.findOne({ uid: uidBuffer });
|
||||
}
|
||||
|
||||
@Query((returns) => AccountType, { nullable: true })
|
||||
public accountByEmail(
|
||||
@Arg('email', { nullable: false }) email: string,
|
||||
@Ctx() context: Context
|
||||
@Args('email', { nullable: false }) email: string,
|
||||
@CurrentUser() user: string
|
||||
) {
|
||||
context.logAction('accountByEmail', { email });
|
||||
return Account.query()
|
||||
this.log.info('accountByEmail', { email, user });
|
||||
return this.db.account
|
||||
.query()
|
||||
.select(ACCOUNT_COLUMNS.map((c) => 'accounts.' + c))
|
||||
.innerJoin('emails', 'emails.uid', 'accounts.uid')
|
||||
.where('emails.normalizedEmail', email)
|
||||
.first();
|
||||
}
|
||||
|
||||
@FieldResolver()
|
||||
@ResolveField()
|
||||
public async emailBounces(@Root() account: Account) {
|
||||
const uidBuffer = uuidTransformer.to(account.uid);
|
||||
// MySQL Query optimizer does weird things, use separate queries to force index use
|
||||
const emails = await Emails.query()
|
||||
const emails = await this.db.emails
|
||||
.query()
|
||||
.select('emails.normalizedEmail')
|
||||
.where('emails.uid', uidBuffer);
|
||||
const result = await EmailBounces.query().where(
|
||||
const result = await this.db.emailBounces.query().where(
|
||||
'emailBounces.email',
|
||||
'in',
|
||||
emails.map((x) => x.normalizedEmail)
|
||||
|
@ -66,9 +77,12 @@ export class AccountResolver {
|
|||
return result;
|
||||
}
|
||||
|
||||
@FieldResolver()
|
||||
@ResolveField()
|
||||
public async emails(@Root() account: Account) {
|
||||
const uidBuffer = uuidTransformer.to(account.uid);
|
||||
return await Emails.query().select(EMAIL_COLUMNS).where('uid', uidBuffer);
|
||||
return await this.db.emails
|
||||
.query()
|
||||
.select(EMAIL_COLUMNS)
|
||||
.where('uid', uidBuffer);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
|
||||
import Knex from 'knex';
|
||||
|
||||
import { Account, EmailBounces } from '../../database/model';
|
||||
import {
|
||||
randomAccount,
|
||||
randomEmail,
|
||||
randomEmailBounce,
|
||||
testDatabaseSetup,
|
||||
} from '../../database/model/helpers';
|
||||
import { EmailBounceResolver } from './email-bounce.resolver';
|
||||
import { DatabaseService } from 'fxa-admin-server/src/database/database.service';
|
||||
|
||||
const USER_1 = randomAccount();
|
||||
const EMAIL_1 = randomEmail(USER_1);
|
||||
const EMAIL_2 = randomEmail(USER_1, true);
|
||||
const EMAIL_BOUNCE_1 = randomEmailBounce(USER_1.email);
|
||||
|
||||
describe('EmailBounceResolver', () => {
|
||||
let resolver: EmailBounceResolver;
|
||||
let logger: any;
|
||||
let knex: Knex;
|
||||
let db = {
|
||||
account: Account,
|
||||
emailBounces: EmailBounces,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
knex = await testDatabaseSetup('testAdminEmailResolver');
|
||||
db.emailBounces = EmailBounces.bindKnex(knex);
|
||||
db.account = Account.bindKnex(knex);
|
||||
|
||||
// Load the users in
|
||||
await (db.account as any).query().insertGraph({
|
||||
...USER_1,
|
||||
emails: [EMAIL_1, EMAIL_2],
|
||||
});
|
||||
await db.emailBounces.query().insert(EMAIL_BOUNCE_1);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
logger = { debug: jest.fn(), error: jest.fn(), info: jest.fn() };
|
||||
const MockMozLogger: Provider = {
|
||||
provide: MozLoggerService,
|
||||
useValue: logger,
|
||||
};
|
||||
const MockConfig: Provider = {
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: jest.fn().mockReturnValue({ authHeader: 'test' }),
|
||||
},
|
||||
};
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
EmailBounceResolver,
|
||||
MockMozLogger,
|
||||
MockConfig,
|
||||
{ provide: DatabaseService, useValue: db },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
resolver = module.get<EmailBounceResolver>(EmailBounceResolver);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(resolver).toBeDefined();
|
||||
});
|
||||
|
||||
it('should clear email bounces', async () => {
|
||||
const result = await resolver.clearEmailBounce(USER_1.email, 'test');
|
||||
expect(result).toBeTruthy();
|
||||
expect(logger.info).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
|
||||
|
||||
import { CurrentUser } from '../../auth/auth-header.decorator';
|
||||
import { GqlAuthHeaderGuard } from '../../auth/auth-header.guard';
|
||||
import { DatabaseService } from '../../database/database.service';
|
||||
import { EmailBounce as EmailBounceType } from '../../gql/model/email-bounces.model';
|
||||
|
||||
@UseGuards(GqlAuthHeaderGuard)
|
||||
@Resolver((of: any) => EmailBounceType)
|
||||
export class EmailBounceResolver {
|
||||
constructor(private log: MozLoggerService, private db: DatabaseService) {}
|
||||
|
||||
@Mutation((returns) => Boolean)
|
||||
public async clearEmailBounce(
|
||||
@Args('email') email: string,
|
||||
@CurrentUser() user: string
|
||||
) {
|
||||
const result = await this.db.emailBounces
|
||||
.query()
|
||||
.delete()
|
||||
.where('email', email);
|
||||
this.log.info('clearEmailBounce', { user, email, success: result });
|
||||
return !!result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AccountResolver } from './account/account.resolver';
|
||||
import { EmailBounceResolver } from './email-bounce/email-bounce.resolver';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule],
|
||||
providers: [AccountResolver, EmailBounceResolver],
|
||||
})
|
||||
export class GqlModule {}
|
|
@ -1,11 +1,10 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { Field, ID, ObjectType } from 'type-graphql';
|
||||
|
||||
import { EmailBounce } from './email-bounces';
|
||||
import { Email } from './emails';
|
||||
import { EmailBounce } from './email-bounces.model';
|
||||
import { Email } from './emails.model';
|
||||
|
||||
@ObjectType()
|
||||
export class Account {
|
|
@ -1,14 +1,13 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { Field, ObjectType, registerEnumType } from 'type-graphql';
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum BounceType {
|
||||
unmapped,
|
||||
Permanent,
|
||||
Transient,
|
||||
Complaint
|
||||
Complaint,
|
||||
}
|
||||
|
||||
export enum BounceSubType {
|
||||
|
@ -26,14 +25,14 @@ export enum BounceSubType {
|
|||
Fraud,
|
||||
NotSpam,
|
||||
Other,
|
||||
Virus
|
||||
Virus,
|
||||
}
|
||||
|
||||
registerEnumType(BounceType, {
|
||||
name: 'BounceType'
|
||||
name: 'BounceType',
|
||||
});
|
||||
registerEnumType(BounceSubType, {
|
||||
name: 'BounceSubType'
|
||||
name: 'BounceSubType',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
|
@ -41,10 +40,10 @@ export class EmailBounce {
|
|||
@Field()
|
||||
public email!: string;
|
||||
|
||||
@Field(type => BounceType)
|
||||
@Field((type) => BounceType)
|
||||
public bounceType!: string;
|
||||
|
||||
@Field(type => BounceSubType)
|
||||
@Field((type) => BounceSubType)
|
||||
public bounceSubType!: string;
|
||||
|
||||
@Field()
|
|
@ -1,8 +1,7 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { Field, ObjectType } from 'type-graphql';
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class Email {
|
|
@ -1,35 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Knex from 'knex';
|
||||
import { Model } from 'objection';
|
||||
|
||||
import { HealthExtras } from '../middleware';
|
||||
import { Account } from './models/account';
|
||||
|
||||
export type DatabaseConfig = {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
password: string;
|
||||
database: string;
|
||||
};
|
||||
|
||||
export async function dbHealthCheck(): Promise<HealthExtras> {
|
||||
let status = 'ok';
|
||||
try {
|
||||
await Account.query().limit(1);
|
||||
} catch (err) {
|
||||
status = 'error';
|
||||
}
|
||||
return {
|
||||
db: { status }
|
||||
};
|
||||
}
|
||||
|
||||
export function setupDatabase(opts: DatabaseConfig): Knex {
|
||||
const knex = Knex({ connection: opts, client: 'mysql' });
|
||||
Model.knex(knex);
|
||||
return knex;
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Config from '../config';
|
||||
import { Request, Response, Router } from 'express';
|
||||
|
||||
import { version } from './version';
|
||||
|
||||
export type HealthExtras = {
|
||||
[key: string]: string | number | boolean | HealthExtras;
|
||||
};
|
||||
|
||||
export function loadBalancerRoutes(healthExtras?: () => Promise<HealthExtras>): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get('/__version__', (_: Request, res: Response) => {
|
||||
res.json(version).status(200);
|
||||
});
|
||||
|
||||
router.get('/__lbheartbeat__', (_: Request, res: Response) => {
|
||||
res.json({}).status(200);
|
||||
});
|
||||
|
||||
router.get('/__heartbeat__', async (_: Request, res: Response) => {
|
||||
let healthExtra: HealthExtras;
|
||||
let status = 200;
|
||||
try {
|
||||
healthExtra = (await healthExtras?.()) ?? {};
|
||||
} catch (err) {
|
||||
status = 500;
|
||||
healthExtra = typeof err.healthExtra === 'object' ? err.healthExtra : {};
|
||||
}
|
||||
res.json({ status: status === 200 ? 'ok' : 'error', ...healthExtra }).status(status);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
export function strictTransportSecurity(_: Request, res: Response, next: () => void) {
|
||||
if (Config.get('hstsEnabled')) {
|
||||
res.header(
|
||||
'Strict-Transport-Security',
|
||||
`max-age=${Config.get('hstsMaxAge')}; includeSubDomains`
|
||||
);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { Arg, Ctx, Mutation, Resolver } from 'type-graphql';
|
||||
|
||||
import { EmailBounces } from '../db/models';
|
||||
import { Context } from '../server';
|
||||
import { EmailBounce } from './types/email-bounces';
|
||||
|
||||
@Resolver(of => EmailBounce)
|
||||
export class EmailBounceResolver {
|
||||
@Mutation(returns => Boolean)
|
||||
public async clearEmailBounce(@Arg('email') email: string, @Ctx() context: Context) {
|
||||
const result = await EmailBounces.query()
|
||||
.delete()
|
||||
.where('email', email);
|
||||
context.logAction('clearEmailBounce', { email, success: result });
|
||||
return !!result;
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import path from 'path';
|
||||
|
||||
import { RewriteFrames } from '@sentry/integrations';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { Logger } from 'mozlog';
|
||||
|
||||
// Matches uid, session, oauth and other common tokens which we would
|
||||
// prefer not to include in Sentry reports.
|
||||
const TOKENREGEX = /[a-fA-F0-9]{32,}/gi;
|
||||
const FILTERED = '[Filtered]';
|
||||
|
||||
/**
|
||||
* Filter a sentry event for PII in addition to the default filters.
|
||||
*
|
||||
* Current replacements:
|
||||
* - A 32-char hex string that typically is a FxA user-id.
|
||||
*
|
||||
* Data Removed:
|
||||
* - Request body.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
function filterSentryEvent(event: Sentry.Event, hint: unknown) {
|
||||
if (event.message) {
|
||||
event.message = event.message.replace(TOKENREGEX, FILTERED);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Sentry with additional Sentry event filtering.
|
||||
*
|
||||
* @param options Sentry options to include.
|
||||
*/
|
||||
export function configureSentry(options?: Sentry.NodeOptions) {
|
||||
Sentry.init({
|
||||
...options,
|
||||
beforeSend(event, hint) {
|
||||
return filterSentryEvent(event, hint);
|
||||
},
|
||||
integrations: [
|
||||
new RewriteFrames({
|
||||
root: path.dirname(path.dirname(__dirname))
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a GraphQL error to Sentry if in production mode, otherwise log it out.
|
||||
*
|
||||
* @param debug Debug mode or not
|
||||
* @param error
|
||||
*/
|
||||
export function reportGraphQLError(debug: boolean, logger: Logger, error: GraphQLError) {
|
||||
if (debug) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error.name === 'ValidationError') {
|
||||
return new Error('Request error');
|
||||
}
|
||||
|
||||
const graphPath = error.path?.join('.');
|
||||
|
||||
logger.error('graphql', { path: graphPath, error: error.originalError?.message });
|
||||
|
||||
Sentry.withScope(scope => {
|
||||
scope.setContext('graphql', {
|
||||
path: graphPath
|
||||
});
|
||||
Sentry.captureException(error.originalError);
|
||||
});
|
||||
|
||||
return new Error('Internal server error');
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { ApolloServer } from 'apollo-server-express';
|
||||
import { Logger } from 'mozlog';
|
||||
import * as TypeGraphQL from 'type-graphql';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
import { DatabaseConfig, setupDatabase } from './db';
|
||||
import { AccountResolver } from './resolvers/account-resolver';
|
||||
import { EmailBounceResolver } from './resolvers/email-bounce-resolver';
|
||||
import { reportGraphQLError } from './sentry';
|
||||
|
||||
type ServerConfig = {
|
||||
authHeader: string;
|
||||
database: DatabaseConfig;
|
||||
env: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Context available to resolvers
|
||||
*/
|
||||
export type Context = {
|
||||
authUser: string | undefined;
|
||||
logger: Logger;
|
||||
logAction: (action: string, options?: object) => {};
|
||||
};
|
||||
|
||||
export async function createServer(
|
||||
config: ServerConfig,
|
||||
logger: Logger,
|
||||
context?: () => object
|
||||
): Promise<ApolloServer> {
|
||||
setupDatabase(config.database);
|
||||
const schema = await TypeGraphQL.buildSchema({
|
||||
container: Container,
|
||||
resolvers: [AccountResolver, EmailBounceResolver]
|
||||
});
|
||||
const debugMode = config.env !== 'production';
|
||||
const defaultContext = ({ req }: any) => {
|
||||
const authUser = req.headers[config.authHeader.toLowerCase()];
|
||||
return {
|
||||
authUser,
|
||||
logAction: (action: string, options?: object) => {
|
||||
logger.info(action, { authUser, ...options });
|
||||
},
|
||||
logger
|
||||
};
|
||||
};
|
||||
|
||||
return new ApolloServer({
|
||||
context: context ?? defaultContext,
|
||||
formatError: err => reportGraphQLError(debugMode, logger, err),
|
||||
schema
|
||||
});
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/*
|
||||
* Return version info based on package.json, the git sha, and source repo
|
||||
*
|
||||
* Try to statically determine commitHash and sourceRepo at startup.
|
||||
*
|
||||
* If commitHash cannot be found from `version.json` (i.e., this is not
|
||||
* production or stage), then an attempt will be made to determine commitHash
|
||||
* and sourceRepo dynamically from `git`. If it cannot be found with `git`,
|
||||
* just show 'unknown' for commitHash and sourceRepo.
|
||||
*
|
||||
* This module may be called as ./dist/lib/version.js, or when testing,
|
||||
* ./lib/version.ts so we need to look for `package.json` and `version.json`
|
||||
* in two possible locations.
|
||||
*/
|
||||
|
||||
import cp from 'child_process';
|
||||
import path from 'path';
|
||||
|
||||
function readJson(filepath: string) {
|
||||
try {
|
||||
return require(filepath);
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function getValue(name: string, command: string): string {
|
||||
const value =
|
||||
readJson(path.resolve(__dirname, '..', 'version.json')) ||
|
||||
readJson(path.resolve(__dirname, '..', '..', 'version.json'));
|
||||
|
||||
if (value?.version?.[name]) {
|
||||
return value.version[name];
|
||||
}
|
||||
|
||||
let stdout = 'unknown';
|
||||
try {
|
||||
stdout = cp.execSync(command, { cwd: __dirname }).toString('utf8');
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
return stdout?.toString().trim();
|
||||
}
|
||||
|
||||
interface Version {
|
||||
commit: string;
|
||||
source: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
function getVersionInfo(): Version {
|
||||
const commit = getValue('hash', 'git rev-parse HEAD');
|
||||
const source = getValue('source', 'git config --get remote.origin.url');
|
||||
|
||||
const packageInfo =
|
||||
readJson(path.resolve(__dirname, '..', '..', 'package.json')) ||
|
||||
readJson(path.resolve(__dirname, '..', '..', '..', 'package.json'));
|
||||
|
||||
return {
|
||||
commit,
|
||||
source,
|
||||
version: packageInfo?.version
|
||||
};
|
||||
}
|
||||
|
||||
export const version = getVersionInfo();
|
|
@ -0,0 +1,35 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { NestApplicationOptions } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { SentryInterceptor } from 'fxa-shared/nestjs/sentry/sentry.interceptor';
|
||||
import helmet from 'helmet';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
import Config, { AppConfig } from './config';
|
||||
|
||||
async function bootstrap() {
|
||||
const nestConfig: NestApplicationOptions = {};
|
||||
if (Config.getProperties().env !== 'development') {
|
||||
nestConfig.logger = false;
|
||||
}
|
||||
const app = await NestFactory.create(AppModule, nestConfig);
|
||||
const config: ConfigService<AppConfig> = app.get(ConfigService);
|
||||
const port = config.get('port') as number;
|
||||
|
||||
if (config.get<boolean>('hstsEnabled')) {
|
||||
const maxAge = config.get<number>('hstsMaxAge');
|
||||
app.use(helmet.hsts({ includeSubDomains: true, maxAge }));
|
||||
}
|
||||
|
||||
// Add sentry as error reporter
|
||||
app.useGlobalInterceptors(new SentryInterceptor());
|
||||
|
||||
// Starts listening for shutdown hooks
|
||||
app.enableShutdownHooks();
|
||||
|
||||
await app.listen(port);
|
||||
}
|
||||
bootstrap();
|
|
@ -1,25 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { assert } from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
import { dbHealthCheck } from '../../../lib/db';
|
||||
import { testDatabaseSetup } from './models/helpers';
|
||||
|
||||
describe('db', () => {
|
||||
describe('dbHealthCheck', () => {
|
||||
it('returns ok when db is configured', async () => {
|
||||
const knex = await testDatabaseSetup();
|
||||
const result = await dbHealthCheck();
|
||||
assert.deepEqual(result, { db: { status: 'ok' } });
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
it('returns error when db is not configured', async () => {
|
||||
const result = await dbHealthCheck();
|
||||
assert.deepEqual(result, { db: { status: 'error' } });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,53 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { assert } from 'chai';
|
||||
import Knex from 'knex';
|
||||
import 'mocha';
|
||||
|
||||
import { chance, randomAccount, testDatabaseSetup } from './helpers';
|
||||
|
||||
import { Account } from '../../../../lib/db/models/account';
|
||||
import { uuidTransformer } from '../../../../lib/db/transformers';
|
||||
|
||||
const USER_1 = randomAccount();
|
||||
|
||||
describe('account model', () => {
|
||||
let knex: Knex;
|
||||
|
||||
before(async () => {
|
||||
knex = await testDatabaseSetup();
|
||||
// Load the user in
|
||||
await Account.query().insertGraph(USER_1);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
it('looks up the user by email', async () => {
|
||||
const result = await Account.query().findOne({ normalizedEmail: USER_1.normalizedEmail });
|
||||
assert.equal(result.uid, USER_1.uid);
|
||||
assert.equal(result.email, USER_1.email);
|
||||
});
|
||||
|
||||
it('looks up the user by uid buffer', async () => {
|
||||
const uidBuffer = uuidTransformer.to(USER_1.uid);
|
||||
const result = await Account.query().findOne({ uid: uidBuffer });
|
||||
assert.equal(result.uid, USER_1.uid);
|
||||
assert.equal(result.email, USER_1.email);
|
||||
});
|
||||
|
||||
it('does not find a non-existent user', async () => {
|
||||
let result = await Account.query().findOne({ normalizedEmail: chance.email() });
|
||||
assert.isUndefined(result);
|
||||
|
||||
const uid = chance.guid({ version: 4 }).replace(/-/g, '');
|
||||
const uidBuffer = uuidTransformer.to(uid);
|
||||
result = await Account.query().findOne({ uid: uidBuffer });
|
||||
assert.isUndefined(result);
|
||||
});
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { assert } from 'chai';
|
||||
import Knex from 'knex';
|
||||
import 'mocha';
|
||||
|
||||
import { chance, randomAccount, randomEmailBounce, testDatabaseSetup } from './helpers';
|
||||
|
||||
import { EmailBounces } from '../../../../lib/db/models/email-bounces';
|
||||
|
||||
const USER_1 = randomAccount();
|
||||
const EMAIL_BOUNCE = randomEmailBounce(USER_1.email);
|
||||
|
||||
describe('emailBounces model', () => {
|
||||
let knex: Knex;
|
||||
|
||||
before(async () => {
|
||||
knex = await testDatabaseSetup();
|
||||
// Load the emailBounce in
|
||||
await EmailBounces.query().insertGraph(EMAIL_BOUNCE);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
it('looks up the email bounce', async () => {
|
||||
const result = await EmailBounces.query().findOne({ email: EMAIL_BOUNCE.email });
|
||||
assert.equal(result.email, EMAIL_BOUNCE.email);
|
||||
assert.equal(result.bounceType, EMAIL_BOUNCE.bounceType);
|
||||
});
|
||||
|
||||
it('does not find a non-existent email bounce', async () => {
|
||||
const result = await EmailBounces.query().findOne({ email: chance.email() });
|
||||
assert.isUndefined(result);
|
||||
});
|
||||
});
|
|
@ -1,69 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import express from 'express';
|
||||
import 'mocha';
|
||||
import request from 'supertest';
|
||||
|
||||
import { loadBalancerRoutes, strictTransportSecurity } from '../../lib/middleware';
|
||||
import { version } from '../../lib/version';
|
||||
|
||||
describe('loadBalancerRoutes', () => {
|
||||
const app = express();
|
||||
app.use(loadBalancerRoutes());
|
||||
|
||||
it('returns version', () => {
|
||||
return request(app)
|
||||
.get('/__version__')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, version);
|
||||
});
|
||||
|
||||
it('returns lbheartbeat', () => {
|
||||
return request(app)
|
||||
.get('/__lbheartbeat__')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, {});
|
||||
});
|
||||
|
||||
it('returns heartbeat', () => {
|
||||
return request(app)
|
||||
.get('/__heartbeat__')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, { status: 'ok' });
|
||||
});
|
||||
|
||||
it('returns heartbeat with healthchecks', () => {
|
||||
const app2 = express();
|
||||
app2.use(loadBalancerRoutes(async () => ({ additional: true })));
|
||||
return request(app2)
|
||||
.get('/__heartbeat__')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, { status: 'ok', additional: true });
|
||||
});
|
||||
|
||||
it('returns heartbeat with erroring healthcheck', () => {
|
||||
const app2 = express();
|
||||
app2.use(
|
||||
loadBalancerRoutes(async () => {
|
||||
throw new Error('boom');
|
||||
})
|
||||
);
|
||||
return request(app2)
|
||||
.get('/__heartbeat__')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200, { status: 'error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('strictTransportSecurity', () => {
|
||||
const app = express();
|
||||
app.use(strictTransportSecurity);
|
||||
|
||||
it('sets Strict-Transport-Header', () => {
|
||||
return request(app)
|
||||
.get('/')
|
||||
.expect('Strict-Transport-Security', /max-age=\d*; includeSubDomains/);
|
||||
});
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { Logger } from 'mozlog';
|
||||
import sinon from 'sinon';
|
||||
import { stubInterface } from 'ts-sinon';
|
||||
|
||||
export function mockContext() {
|
||||
return {
|
||||
authUser: sinon.stub(),
|
||||
logAction: sinon.stub(),
|
||||
logger: stubInterface<Logger>()
|
||||
};
|
||||
}
|
|
@ -1,185 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { graphql, GraphQLSchema } from 'graphql';
|
||||
import Chance from 'chance';
|
||||
import Knex from 'knex';
|
||||
import 'mocha';
|
||||
import { buildSchema } from 'type-graphql';
|
||||
|
||||
import {
|
||||
randomAccount,
|
||||
randomEmail,
|
||||
randomEmailBounce,
|
||||
testDatabaseSetup,
|
||||
} from '../db/models/helpers';
|
||||
import { mockContext } from '../mocks';
|
||||
|
||||
import { Account, EmailBounces } from '../../../lib/db/models';
|
||||
import { AccountResolver } from '../../../lib/resolvers/account-resolver';
|
||||
import { EmailBounceResolver } from '../../../lib/resolvers/email-bounce-resolver';
|
||||
import {
|
||||
BounceSubType,
|
||||
BounceType,
|
||||
} from '../../../lib/resolvers/types/email-bounces';
|
||||
|
||||
const USER_1 = randomAccount();
|
||||
const EMAIL_1 = randomEmail(USER_1);
|
||||
const EMAIL_2 = randomEmail(USER_1, true);
|
||||
const EMAIL_BOUNCE_1 = randomEmailBounce(USER_1.email);
|
||||
|
||||
const USER_2 = randomAccount();
|
||||
|
||||
describe('accountResolver', () => {
|
||||
let knex: Knex;
|
||||
let schema: GraphQLSchema;
|
||||
let context: ReturnType<typeof mockContext>;
|
||||
|
||||
before(async () => {
|
||||
knex = await testDatabaseSetup();
|
||||
// Load the users in
|
||||
await (Account as any)
|
||||
.query()
|
||||
.insertGraph({ ...USER_1, emails: [EMAIL_1, EMAIL_2] });
|
||||
await EmailBounces.query().insert(EMAIL_BOUNCE_1);
|
||||
schema = await buildSchema({
|
||||
resolvers: [AccountResolver, EmailBounceResolver],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
context = mockContext();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
it('locates the user by uid', async () => {
|
||||
const query = `query {
|
||||
accountByUid(uid: "${USER_1.uid}") {
|
||||
uid
|
||||
email
|
||||
}
|
||||
}`;
|
||||
const result = (await graphql(schema, query, undefined, context)) as any;
|
||||
assert.isDefined(result.data);
|
||||
assert.isDefined(result.data.accountByUid);
|
||||
assert.deepEqual(result.data.accountByUid, {
|
||||
email: USER_1.email,
|
||||
uid: USER_1.uid,
|
||||
});
|
||||
assert.isTrue(context.logAction.calledOnce);
|
||||
});
|
||||
|
||||
it('does not locate non-existent users by uid', async () => {
|
||||
const query = `query {
|
||||
accountByUid(uid: "${USER_2.uid}") {
|
||||
uid
|
||||
email
|
||||
}
|
||||
}`;
|
||||
const result = (await graphql(schema, query, undefined, context)) as any;
|
||||
assert.isDefined(result.data);
|
||||
assert.isNull(result.data.accountByUid);
|
||||
assert.isTrue(context.logAction.calledOnce);
|
||||
});
|
||||
|
||||
it('locates the user by email', async () => {
|
||||
const query = `query {
|
||||
accountByEmail(email: "${USER_1.email}") {
|
||||
uid
|
||||
email
|
||||
}
|
||||
}`;
|
||||
const result = (await graphql(schema, query, undefined, context)) as any;
|
||||
assert.isDefined(result.data);
|
||||
assert.isDefined(result.data.accountByEmail);
|
||||
assert.deepEqual(result.data.accountByEmail, {
|
||||
email: USER_1.email,
|
||||
uid: USER_1.uid,
|
||||
});
|
||||
assert.isTrue(context.logAction.calledOnce);
|
||||
});
|
||||
|
||||
it('does not locate non-existent users by email', async () => {
|
||||
const query = `query {
|
||||
accountByEmail(email: "${USER_2.email}") {
|
||||
uid
|
||||
email
|
||||
}
|
||||
}`;
|
||||
const result = (await graphql(schema, query, undefined, context)) as any;
|
||||
assert.isDefined(result.data);
|
||||
assert.isNull(result.data.accountByEmail);
|
||||
assert.isTrue(context.logAction.calledOnce);
|
||||
});
|
||||
|
||||
it('loads all emails with field resolver', async () => {
|
||||
const query = `query {
|
||||
accountByEmail(email: "${USER_1.email}") {
|
||||
emails {
|
||||
email
|
||||
isPrimary
|
||||
isVerified
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}`;
|
||||
const result = (await graphql(schema, query, undefined, context)) as any;
|
||||
assert.isDefined(result.data);
|
||||
assert.isDefined(result.data.accountByEmail);
|
||||
assert.deepEqual(result.data.accountByEmail, {
|
||||
emails: [
|
||||
{
|
||||
email: USER_1.email,
|
||||
isPrimary: true,
|
||||
isVerified: true,
|
||||
createdAt: EMAIL_1.createdAt,
|
||||
},
|
||||
{
|
||||
email: EMAIL_2.email,
|
||||
isPrimary: false,
|
||||
isVerified: true,
|
||||
createdAt: EMAIL_2.createdAt,
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.isTrue(context.logAction.calledOnce);
|
||||
});
|
||||
|
||||
it('loads emailBounces with field resolver', async () => {
|
||||
const query = `query {
|
||||
accountByEmail(email: "${USER_1.email}") {
|
||||
uid
|
||||
email
|
||||
emailBounces {
|
||||
email
|
||||
bounceType
|
||||
bounceSubType
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}`;
|
||||
const result = (await graphql(schema, query, undefined, context)) as any;
|
||||
assert.isDefined(result.data);
|
||||
assert.isDefined(result.data.accountByEmail);
|
||||
assert.deepEqual(result.data.accountByEmail, {
|
||||
email: USER_1.email,
|
||||
emailBounces: [
|
||||
{
|
||||
bounceSubType: BounceSubType[EMAIL_BOUNCE_1.bounceSubType],
|
||||
bounceType: BounceType[EMAIL_BOUNCE_1.bounceType],
|
||||
createdAt: EMAIL_BOUNCE_1.createdAt,
|
||||
email: EMAIL_BOUNCE_1.email,
|
||||
},
|
||||
],
|
||||
uid: USER_1.uid,
|
||||
});
|
||||
assert.isTrue(context.logAction.calledOnce);
|
||||
});
|
||||
});
|
|
@ -1,95 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { graphql, GraphQLSchema } from 'graphql';
|
||||
import Knex from 'knex';
|
||||
import 'mocha';
|
||||
import { buildSchema } from 'type-graphql';
|
||||
|
||||
import {
|
||||
randomAccount,
|
||||
randomEmail,
|
||||
randomEmailBounce,
|
||||
testDatabaseSetup
|
||||
} from '../db/models/helpers';
|
||||
import { mockContext } from '../mocks';
|
||||
|
||||
import { Account, EmailBounces } from '../../../lib/db/models';
|
||||
import { AccountResolver } from '../../../lib/resolvers/account-resolver';
|
||||
import { EmailBounceResolver } from '../../../lib/resolvers/email-bounce-resolver';
|
||||
|
||||
const USER_1 = randomAccount();
|
||||
const EMAIL_1 = randomEmail(USER_1);
|
||||
const EMAIL_BOUNCE_1 = randomEmailBounce(USER_1.email);
|
||||
|
||||
const USER_2 = randomAccount();
|
||||
|
||||
describe('emailBounceResolver', () => {
|
||||
let knex: Knex;
|
||||
let schema: GraphQLSchema;
|
||||
let context: ReturnType<typeof mockContext>;
|
||||
|
||||
before(async () => {
|
||||
knex = await testDatabaseSetup();
|
||||
// Load the users in
|
||||
await (Account as any).query().insertGraph({ ...USER_1, emails: [EMAIL_1] });
|
||||
await EmailBounces.query().insert(EMAIL_BOUNCE_1);
|
||||
schema = await buildSchema({ resolvers: [AccountResolver, EmailBounceResolver] });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
context = mockContext();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await knex.destroy();
|
||||
});
|
||||
|
||||
it('clears an email bounce', async () => {
|
||||
const query = `query {
|
||||
accountByEmail(email: "${USER_1.email}") {
|
||||
uid
|
||||
email
|
||||
emailBounces {
|
||||
email
|
||||
bounceType
|
||||
bounceSubType
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}`;
|
||||
let result = (await graphql(schema, query, undefined, context)) as any;
|
||||
assert.isDefined(result.data);
|
||||
assert.isDefined(result.data.accountByEmail);
|
||||
assert.lengthOf(result.data.accountByEmail.emailBounces, 1);
|
||||
assert.isTrue(context.logAction.calledOnce);
|
||||
|
||||
const mutation = `mutation {
|
||||
clearEmailBounce(email: "${USER_1.email}")
|
||||
}`;
|
||||
result = (await graphql(schema, mutation, undefined, context)) as any;
|
||||
assert.isDefined(result.data);
|
||||
assert.isTrue(result.data.clearEmailBounce);
|
||||
assert.isTrue(context.logAction.calledTwice);
|
||||
|
||||
result = (await graphql(schema, query, undefined, context)) as any;
|
||||
assert.isDefined(result.data);
|
||||
assert.isDefined(result.data.accountByEmail);
|
||||
assert.lengthOf(result.data.accountByEmail.emailBounces, 0);
|
||||
assert.isTrue(context.logAction.calledThrice);
|
||||
});
|
||||
|
||||
it('fails to clear a non-existent bounce', async () => {
|
||||
const mutation = `mutation {
|
||||
clearEmailBounce(email: "${USER_2.email}")
|
||||
}`;
|
||||
const result = (await graphql(schema, mutation, undefined, context)) as any;
|
||||
assert.isDefined(result.data);
|
||||
assert.isFalse(result.data.clearEmailBounce);
|
||||
assert.isTrue(context.logAction.calledOnce);
|
||||
});
|
||||
});
|
|
@ -1,127 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { assert } from 'chai';
|
||||
import {
|
||||
getIntrospectionQuery,
|
||||
graphql,
|
||||
IntrospectionEnumType,
|
||||
IntrospectionObjectType,
|
||||
IntrospectionSchema,
|
||||
TypeKind,
|
||||
} from 'graphql';
|
||||
import 'mocha';
|
||||
import { buildSchema } from 'type-graphql';
|
||||
|
||||
import { AccountResolver } from '../../lib/resolvers/account-resolver';
|
||||
import { EmailBounceResolver } from '../../lib/resolvers/email-bounce-resolver';
|
||||
|
||||
describe('Schema', () => {
|
||||
let builtSchema;
|
||||
let schemaIntrospection: IntrospectionSchema;
|
||||
let queryType: IntrospectionObjectType;
|
||||
let mutationType: IntrospectionObjectType;
|
||||
|
||||
beforeEach(async () => {
|
||||
const schema = await buildSchema({
|
||||
resolvers: [AccountResolver, EmailBounceResolver],
|
||||
});
|
||||
builtSchema = await graphql(schema, getIntrospectionQuery());
|
||||
schemaIntrospection = builtSchema.data!.__schema as IntrospectionSchema;
|
||||
assert.isDefined(schemaIntrospection);
|
||||
queryType = schemaIntrospection.types.find(
|
||||
(type) =>
|
||||
type.name ===
|
||||
(schemaIntrospection as IntrospectionSchema).queryType.name
|
||||
) as IntrospectionObjectType;
|
||||
|
||||
const mutationTypeNameRef = schemaIntrospection.mutationType;
|
||||
if (mutationTypeNameRef) {
|
||||
mutationType = schemaIntrospection.types.find(
|
||||
(type) => type.name === mutationTypeNameRef.name
|
||||
) as IntrospectionObjectType;
|
||||
}
|
||||
});
|
||||
|
||||
function findTypeByName(name: string) {
|
||||
return schemaIntrospection.types.find(
|
||||
(type) => type.kind === TypeKind.OBJECT && type.name === name
|
||||
) as IntrospectionObjectType;
|
||||
}
|
||||
|
||||
function findEnumByName(name: string) {
|
||||
return schemaIntrospection.types.find(
|
||||
(type) => type.kind === TypeKind.ENUM && type.name === name
|
||||
) as IntrospectionEnumType;
|
||||
}
|
||||
|
||||
it('is created with expected types', async () => {
|
||||
const queryNames = queryType.fields.map((it) => it.name);
|
||||
assert.sameMembers(queryNames, ['accountByUid', 'accountByEmail']);
|
||||
|
||||
const mutationNames = mutationType.fields.map((it) => it.name);
|
||||
assert.sameMembers(mutationNames, ['clearEmailBounce']);
|
||||
|
||||
const accountType = findTypeByName('Account');
|
||||
assert.isDefined(accountType);
|
||||
assert.lengthOf(accountType.fields, 6);
|
||||
const accountTypeNames = accountType.fields.map((it) => it.name);
|
||||
assert.sameMembers(accountTypeNames, [
|
||||
'uid',
|
||||
'email',
|
||||
'emails',
|
||||
'emailVerified',
|
||||
'createdAt',
|
||||
'emailBounces',
|
||||
]);
|
||||
|
||||
const emailBounceType = findTypeByName('EmailBounce');
|
||||
assert.isDefined(emailBounceType);
|
||||
assert.lengthOf(emailBounceType.fields, 4);
|
||||
const emailBounceTypeNames = emailBounceType.fields.map((it) => it.name);
|
||||
assert.sameMembers(emailBounceTypeNames, [
|
||||
'email',
|
||||
'bounceType',
|
||||
'bounceSubType',
|
||||
'createdAt',
|
||||
]);
|
||||
|
||||
const bounceTypeEnum = findEnumByName('BounceType');
|
||||
assert.isDefined(bounceTypeEnum);
|
||||
assert.lengthOf(bounceTypeEnum.enumValues, 4);
|
||||
const bounceTypeValues = bounceTypeEnum.enumValues.map((it) => it.name);
|
||||
assert.sameOrderedMembers(bounceTypeValues, [
|
||||
'unmapped',
|
||||
'Permanent',
|
||||
'Transient',
|
||||
'Complaint',
|
||||
]);
|
||||
|
||||
const bounceSubTypeEnum = findEnumByName('BounceSubType');
|
||||
assert.isDefined(bounceSubTypeEnum);
|
||||
assert.lengthOf(bounceSubTypeEnum.enumValues, 15);
|
||||
const bounceSubTypeValues = bounceSubTypeEnum.enumValues.map(
|
||||
(it) => it.name
|
||||
);
|
||||
assert.sameOrderedMembers(bounceSubTypeValues, [
|
||||
'unmapped',
|
||||
'Undetermined',
|
||||
'General',
|
||||
'NoEmail',
|
||||
'Suppressed',
|
||||
'MailboxFull',
|
||||
'MessageTooLarge',
|
||||
'ContentRejected',
|
||||
'AttachmentRejected',
|
||||
'Abuse',
|
||||
'AuthFailure',
|
||||
'Fraud',
|
||||
'NotSpam',
|
||||
'Other',
|
||||
'Virus',
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,78 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { ValidationError } from 'apollo-server';
|
||||
import { assert } from 'chai';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import 'mocha';
|
||||
import { Logger } from 'mozlog';
|
||||
import proxyquire from 'proxyquire';
|
||||
import sinon, { SinonSpy } from 'sinon';
|
||||
import { stubInterface } from 'ts-sinon';
|
||||
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
describe('sentry', () => {
|
||||
let logger: Logger;
|
||||
let mockCaptureException: any;
|
||||
let mockScope: any;
|
||||
let reportGraphQLError: any;
|
||||
|
||||
const originalError = new Error('boom');
|
||||
const err = new GraphQLError(
|
||||
'Internal server error',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
['resolver', 'field'],
|
||||
originalError
|
||||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
mockScope = { setContext: sinon.stub() };
|
||||
const mockCapture = (func: any) => {
|
||||
func(mockScope);
|
||||
};
|
||||
mockCaptureException = sinon.stub();
|
||||
reportGraphQLError = proxyquire('../../lib/sentry.ts', {
|
||||
'@sentry/node': { withScope: mockCapture, captureException: mockCaptureException }
|
||||
}).reportGraphQLError;
|
||||
logger = stubInterface<Logger>();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.reset();
|
||||
});
|
||||
|
||||
it('captures an error', async () => {
|
||||
reportGraphQLError(false, logger, err);
|
||||
const errResult = mockCaptureException.args[0][0];
|
||||
assert.equal(errResult.message, 'boom');
|
||||
const logSpy = (logger.error as unknown) as SinonSpy;
|
||||
assert.isTrue(
|
||||
(logger.error as SinonSpy).calledOnceWith('graphql', {
|
||||
error: 'boom',
|
||||
path: 'resolver.field'
|
||||
})
|
||||
);
|
||||
assert.isTrue(mockScope.setContext.calledOnceWith('graphql', { path: 'resolver.field' }));
|
||||
});
|
||||
|
||||
it('skips error capture in debug mode', () => {
|
||||
const result = reportGraphQLError(true, logger, err);
|
||||
assert.equal(result, err);
|
||||
assert.isTrue((logger.error as SinonSpy).notCalled);
|
||||
});
|
||||
|
||||
it('changes error capture on validation bugs', () => {
|
||||
const validationErr = new ValidationError('Bad form');
|
||||
const result = reportGraphQLError(false, logger, validationErr);
|
||||
const logSpy = (logger.error as unknown) as SinonSpy;
|
||||
assert.notEqual(result, err);
|
||||
assert.equal(result.message, 'Request error');
|
||||
assert.isTrue(logSpy.notCalled);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/__version__ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/__lbheartbeat__')
|
||||
.expect(200)
|
||||
.expect('{}');
|
||||
});
|
||||
|
||||
it('/graphql (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/graphql')
|
||||
.send({
|
||||
operationName: null,
|
||||
variables: {},
|
||||
query: '{accountByEmail(email:"test@test.com"){uid}}',
|
||||
})
|
||||
.expect(200);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
},
|
||||
"references": [{ "path": "../fxa-shared" }],
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
|
@ -5,8 +5,8 @@
|
|||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"noEmitHelpers": true,
|
||||
"importHelpers": true,
|
||||
"types": ["mocha", "mozlog"]
|
||||
"importHelpers": true
|
||||
},
|
||||
"include": ["./src"]
|
||||
"references": [{ "path": "../fxa-shared" }],
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
"noEmitHelpers": true,
|
||||
"importHelpers": true
|
||||
},
|
||||
"references": [{ "path": "../fxa-shared" }],
|
||||
"include": ["src"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { DynamicModule, Module } from '@nestjs/common';
|
||||
import { DynamicModule, Module, ModuleMetadata } from '@nestjs/common';
|
||||
|
||||
import { Version } from '../version';
|
||||
import { HEALTH_CONFIG } from './health.constants';
|
||||
|
@ -12,6 +12,14 @@ export interface HealthControllerConfigParams {
|
|||
extraHealthData?: () => Promise<Record<string, any>>;
|
||||
}
|
||||
|
||||
export interface HealthModuleAsyncParams
|
||||
extends Pick<ModuleMetadata, 'imports' | 'providers'> {
|
||||
useFactory: (
|
||||
...args: any[]
|
||||
) => HealthControllerConfigParams | Promise<HealthControllerConfigParams>;
|
||||
inject?: any[];
|
||||
}
|
||||
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
})
|
||||
|
@ -22,4 +30,18 @@ export class HealthModule {
|
|||
providers: [{ provide: HEALTH_CONFIG, useValue: options }],
|
||||
};
|
||||
}
|
||||
|
||||
static forRootAsync(options: HealthModuleAsyncParams): DynamicModule {
|
||||
return {
|
||||
module: HealthModule,
|
||||
imports: options.imports,
|
||||
providers: [
|
||||
{
|
||||
provide: HEALTH_CONFIG,
|
||||
useFactory: options.useFactory,
|
||||
inject: options.inject || [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
53
yarn.lock
53
yarn.lock
|
@ -16776,17 +16776,19 @@ fsevents@^1.2.7:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "fxa-admin-server@workspace:packages/fxa-admin-server"
|
||||
dependencies:
|
||||
"@sentry/integrations": ^5.21.1
|
||||
"@sentry/node": ^5.21.1
|
||||
"@types/chai": ^4.2.12
|
||||
"@nestjs/common": ^7.4.4
|
||||
"@nestjs/config": ^0.5.0
|
||||
"@nestjs/core": ^7.4.4
|
||||
"@nestjs/graphql": ^7.6.0
|
||||
"@nestjs/mapped-types": ^0.1.0
|
||||
"@nestjs/platform-express": ^7.4.4
|
||||
"@sentry/integrations": ^5.23.0
|
||||
"@sentry/node": ^5.23.0
|
||||
"@types/chance": ^1.1.0
|
||||
"@types/convict": ^5.2.1
|
||||
"@types/eslint": 7.2.0
|
||||
"@types/graphql": ^14.5.0
|
||||
"@types/mocha": ^8.0.2
|
||||
"@types/node": ^13.9.1
|
||||
"@types/proxyquire": ^1.3.28
|
||||
"@types/sinon": 9.0.5
|
||||
"@types/rimraf": 3.0.0
|
||||
"@types/supertest": ^2.0.10
|
||||
"@types/yargs": ^15.0.5
|
||||
apollo-server: ^2.16.1
|
||||
|
@ -16794,14 +16796,19 @@ fsevents@^1.2.7:
|
|||
audit-filter: ^0.5.0
|
||||
chai: ^4.2.0
|
||||
chance: ^1.1.6
|
||||
class-transformer: ^0.3.1
|
||||
class-validator: ^0.12.2
|
||||
convict: ^6.0.0
|
||||
convict-format-with-moment: ^6.0.0
|
||||
convict-format-with-validator: ^6.0.0
|
||||
eslint: ^7.6.0
|
||||
express: ^4.17.1
|
||||
fxa-shared: "workspace:*"
|
||||
graphql: ^14.6.0
|
||||
graphql-tools: ^4.0.8
|
||||
helmet: ^4.1.1
|
||||
jest: 26.4.2
|
||||
knex: ^0.21.4
|
||||
mocha: ^8.1.1
|
||||
mozlog: ^3.0.1
|
||||
mysql: ^2.18.1
|
||||
objection: ^2.2.2
|
||||
|
@ -16809,13 +16816,13 @@ fsevents@^1.2.7:
|
|||
prettier: ^2.0.5
|
||||
proxyquire: ^2.1.3
|
||||
reflect-metadata: ^0.1.13
|
||||
sinon: ^9.0.3
|
||||
rimraf: ^3.0.2
|
||||
rxjs: ^6.5.5
|
||||
supertest: ^4.0.2
|
||||
ts-node: ^8.10.2
|
||||
ts-sinon: ^1.2.0
|
||||
ts-jest: 26.1.0
|
||||
ts-node: ^8.6.2
|
||||
tsconfig-paths: ^3.9.0
|
||||
tslib: ^2.0.1
|
||||
type-graphql: ^0.17.6
|
||||
typedi: ^0.8.0
|
||||
typescript: 3.9.7
|
||||
yargs: ^15.4.1
|
||||
languageName: unknown
|
||||
|
@ -18649,7 +18656,7 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graphql-tag@npm:^2.11.0, graphql-tag@npm:^2.4.2, graphql-tag@npm:^2.9.2":
|
||||
"graphql-tag@npm:^2.11.0":
|
||||
version: 2.11.0
|
||||
resolution: "graphql-tag@npm:2.11.0"
|
||||
peerDependencies:
|
||||
|
@ -18658,7 +18665,16 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graphql-tools@npm:^4.0.0":
|
||||
"graphql-tag@npm:^2.4.2, graphql-tag@npm:^2.9.2":
|
||||
version: 2.10.3
|
||||
resolution: "graphql-tag@npm:2.10.3"
|
||||
peerDependencies:
|
||||
graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0
|
||||
checksum: ea7746a7ed6f166754bba089c76595a2e9c0fbb4f64421be48974d00baf91be533296a953f700ca96b04fec469af029f199c35ce3bf819a48eb3794f8190fcd0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graphql-tools@npm:^4.0.0, graphql-tools@npm:^4.0.8":
|
||||
version: 4.0.8
|
||||
resolution: "graphql-tools@npm:4.0.8"
|
||||
dependencies:
|
||||
|
@ -19365,6 +19381,13 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"helmet@npm:^4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "helmet@npm:4.1.1"
|
||||
checksum: 7d4371e3a4b5cb60c9d0b4e1341cade3836bff9734b837e04c6571ebe1237e6c52b928a4c9e308d80d6996bdf8561105e86911d04cff2f8957d355436191a19e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hex-color-regex@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "hex-color-regex@npm:1.1.0"
|
||||
|
|
Загрузка…
Ссылка в новой задаче