-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(config): working config module + tests (#70)
* 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
Showing
26 changed files
with
641 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' }, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const CONFIG_TARGET_MODULE = 'CONFIG_TARGET_MODULE'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.