Skip to content

Commit

Permalink
feat(config): working config module + tests (#70)
Browse files Browse the repository at this point in the history
* fix(db): ensure migrations get run on boot, fix typeorm cli

* feat(config): start on config impl

* feat(config): inquirer works

* feat(config): working config module + tests (#61)
  • Loading branch information
avafloww authored May 19, 2022
1 parent eda2f33 commit 88e25a9
Show file tree
Hide file tree
Showing 26 changed files with 641 additions and 17 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = {
ignorePatterns: [
'.eslintrc.js',
'core/src/database/migrations/**/*.{ts,js}',
'build-scripts/**',
'**/test/**',
],
rules: {
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: yarn install
- name: Run tests
run: yarn test
- name: Release
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
Expand Down
26 changes: 26 additions & 0 deletions build-scripts/migration-generate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const child_process = require('child_process');
const path = require('path');

if (process.argv.length < 3) {
console.error('Usage: yarn migration:generate NameOfMigration');
console.error(
'Do not include the file extension or path in the migration name - they will be added automatically.',
);
process.exit(1);
}

const cliHelper = path.join(
__dirname,
'../core/dist/src/database/cli-helper.datasource.js',
);
const target = path.join(
__dirname,
'../core/src/database/migrations',
process.argv[2],
);
const args = process.argv.slice(3).join(' ');

child_process.spawnSync(
`yarn build && npx typeorm migration:generate -p -d ${cliHelper} ${target} ${args}`,
{ shell: true, stdio: 'inherit' },
);
2 changes: 1 addition & 1 deletion core/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ ROLE_WITHOUT_PLAYLIST_PERMISSION=YourRoleId
# Developer settings - do not touch unless you know what you are doing!
#
# Turns on TypeORM schema sync for easy development; you might lose data with this on, so be careful!
DATABASE_SYNCHRONIZE=true
#DATABASE_SYNCHRONIZE=true

# Turns on extra database logging
#DATABASE_LOGGING=true
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"migration:create": "yarn build && npx typeorm migration:generate -p -d ./dist/src/database/cli-helper.datasource.js"
"migration:generate": "node ../build-scripts/migration-generate.js"
},
"jest": {
"moduleFileExtensions": [
Expand Down
4 changes: 2 additions & 2 deletions core/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ConfigModule as NestConfigModule } from '@nestjs/config';

import { BotModule } from './bot/bot.module';
import { DatabaseModule } from './database/database.module';
Expand All @@ -9,7 +9,7 @@ import { SentryModule } from './sentry/sentry.module';

@Module({
imports: [
ConfigModule.forRoot(),
NestConfigModule.forRoot(),
SentryModule,
DatabaseModule,
UsersModule,
Expand Down
19 changes: 19 additions & 0 deletions core/src/config/config-subject-type.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export enum ConfigSubjectType {
/**
* Global configuration, to be used by the entire bot instance.
* This is typically used to store bot-wide settings, like API keys.
*/
Global = 'global',

/**
* Configuration for a specific guild.
* This may store settings like role selection, moderation settings, etc.
*/
Guild = 'guild',

/**
* Configuration for a specific user.
* This may store settings like a user's preferred language.
*/
User = 'user',
}
1 change: 1 addition & 0 deletions core/src/config/config.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const CONFIG_TARGET_MODULE = 'CONFIG_TARGET_MODULE';
42 changes: 42 additions & 0 deletions core/src/config/config.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { DynamicModule, Module, Type } from '@nestjs/common';
import { ConfigService } from './config.service';
import { MODULE_PACKAGE_NAME } from '../module-system/module.constants';
import { INQUIRER } from '@nestjs/core';
import { getDataSourceToken, TypeOrmModule } from '@nestjs/typeorm';
import { Config } from './entities/config.entity';
import { DataSource } from 'typeorm';

@Module({})
export class ConfigModule {
static forFeature(module?: Type<unknown>): DynamicModule {
return {
module: ConfigModule,
imports: [TypeOrmModule.forFeature([Config])],
providers: [
{
provide: ConfigService,
useFactory: (inquirer: Type<unknown>, dataSource: DataSource) => {
const target = module ?? inquirer;
if (!target) {
throw new Error(
'Unknown target; try ConfigModule.forFeature(ModuleTypeName)',
);
}

const packageName: string = Reflect.getMetadata(
MODULE_PACKAGE_NAME,
target.constructor,
);

return new ConfigService(
packageName,
dataSource.getRepository<Config>(Config),
);
},
inject: [INQUIRER, getDataSourceToken()],
},
],
exports: [ConfigService],
};
}
}
187 changes: 187 additions & 0 deletions core/src/config/config.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { Test } from '@nestjs/testing';
import {
CONFIG_SUBJECT,
CONFIG_SUBJECT_TYPE,
ConfigService,
} from './config.service';
import { MODULE_PACKAGE_NAME } from '../module';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Config } from './entities/config.entity';
import { Repository } from 'typeorm';
import { ConfigSubjectType } from './config-subject-type.enum';

type TestConfigType = {
testOne: string;
testTwo: number;
};

describe('ConfigService', () => {
let service: ConfigService;
let testConfig: TestConfigType;
const mockRepository = {
findOne: jest.fn(),
save: jest.fn(),
};

beforeEach(async () => {
await Test.createTestingModule({
providers: [
{
provide: MODULE_PACKAGE_NAME,
useValue: 'test',
},
{
provide: getRepositoryToken(Config),
useValue: mockRepository,
},
],
}).compile();

service = new ConfigService(
'test',
mockRepository as unknown as Repository<Config>,
);

testConfig = {
testOne: 'testOne',
testTwo: 2,
};
});

afterEach(() => {
jest.clearAllMocks();
});

it('should be defined', () => {
expect(service).toBeDefined();
});

it('should get config without defaults', async () => {
const spy = jest
.spyOn(mockRepository, 'findOne')
.mockResolvedValueOnce(undefined);
const result = await service.getGlobalConfig<TestConfigType>();

expect(spy).toHaveBeenCalledTimes(1);
expect(result).toStrictEqual({});
});

it('should get config with defaults', async () => {
const spy = jest
.spyOn(mockRepository, 'findOne')
.mockResolvedValueOnce(undefined);
const result = await service.getGlobalConfig<TestConfigType>(testConfig);

expect(spy).toHaveBeenCalledTimes(1);
expect(result).toStrictEqual(testConfig);
});

it('should return a global config with correct metadata', async () => {
const spy = jest
.spyOn(mockRepository, 'findOne')
.mockResolvedValueOnce(undefined);
const result = await service.getGlobalConfig<TestConfigType>(testConfig);

expect(spy).toHaveBeenCalledTimes(1);
expect(Reflect.getMetadata(CONFIG_SUBJECT_TYPE, result)).toBe(
ConfigSubjectType.Global,
);
expect(Reflect.hasMetadata(CONFIG_SUBJECT, result)).toBe(false);
});

it('should return a guild config with correct metadata', async () => {
const spy = jest
.spyOn(mockRepository, 'findOne')
.mockResolvedValueOnce(undefined);
const result = await service.getGuildConfig<TestConfigType>(
1234,
testConfig,
);

expect(spy).toHaveBeenCalledTimes(1);
expect(Reflect.getMetadata(CONFIG_SUBJECT_TYPE, result)).toBe(
ConfigSubjectType.Guild,
);
expect(Reflect.getMetadata(CONFIG_SUBJECT, result)).toBe(1234);
});

it('should return a user config with correct metadata', async () => {
const spy = jest
.spyOn(mockRepository, 'findOne')
.mockResolvedValueOnce(undefined);
const result = await service.getUserConfig<TestConfigType>(
1234,
testConfig,
);

expect(spy).toHaveBeenCalledTimes(1);
expect(Reflect.getMetadata(CONFIG_SUBJECT_TYPE, result)).toBe(
ConfigSubjectType.User,
);
expect(Reflect.getMetadata(CONFIG_SUBJECT, result)).toBe(1234);
});

it('should save a global config', async () => {
const spy = jest.spyOn(mockRepository, 'save');
const config = await service.getGlobalConfig<TestConfigType>();

await service.saveConfig(config);

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith({
module: 'test',
subjectType: ConfigSubjectType.Global,
data: config,
});
});

it('should save a guild config', async () => {
const spy = jest.spyOn(mockRepository, 'save');
const config = await service.getGuildConfig<TestConfigType>(
1234,
testConfig,
);

await service.saveConfig(config);

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith({
module: 'test',
subjectType: ConfigSubjectType.Guild,
subject: 1234,
data: config,
});
});

it('should save a user config', async () => {
const spy = jest.spyOn(mockRepository, 'save');
const config = await service.getUserConfig<TestConfigType>(
1234,
testConfig,
);

await service.saveConfig(config);

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith({
module: 'test',
subjectType: ConfigSubjectType.User,
subject: 1234,
data: config,
});
});

it('should not throw when saving a config object with no metadata', async () => {
expect(async () => {
await service.saveConfig({});
}).not.toThrow();
});

it('should not throw when the save call fails', async () => {
jest.spyOn(mockRepository, 'save').mockRejectedValueOnce(new Error('test'));

expect(async () => {
await service.saveConfig(testConfig);
}).not.toThrow();
});
});
Loading

0 comments on commit 88e25a9

Please sign in to comment.