Skip to content

Commit

Permalink
fix: load env from file
Browse files Browse the repository at this point in the history
  • Loading branch information
eddort committed Apr 26, 2023
1 parent 9c00b55 commit 0c2a4b9
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 68 deletions.
171 changes: 171 additions & 0 deletions src/common/config/config-loader.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Test } from '@nestjs/testing';
import { plainToClass } from 'class-transformer';
import { ConfigLoaderService } from './config-loader.service';
import { InMemoryConfiguration } from './in-memory-configuration';

const FAKE_FS = {
rabbit: 'rabbit',
wallet: 'wallet',
};

const DEFAULTS = {
RPC_URL: 'some-rpc-url',
RABBITMQ_URL: 'some-rabbit-url',
RABBITMQ_LOGIN: 'some-rabbit-login',
};

describe('ConfigLoaderService base spec', () => {
let configLoaderService: ConfigLoaderService;

beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [ConfigLoaderService],
}).compile();

configLoaderService = moduleRef.get(ConfigLoaderService);

const cb = (path: string) => {
if (FAKE_FS[path]) return FAKE_FS[path];

throw new Error('unknown path');
};
jest.spyOn(configLoaderService, 'readFile').mockImplementation(cb);
});

test('default behavior', async () => {
const prepConfig = plainToClass(InMemoryConfiguration, {
RABBITMQ_PASSCODE: 'some-rabbit-passcode',
...DEFAULTS,
});

await expect(() =>
configLoaderService.loadSecrets(prepConfig),
).not.toThrowError();
});

describe('rabbit mq', () => {
let configLoaderService: ConfigLoaderService;

beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [ConfigLoaderService],
}).compile();

configLoaderService = moduleRef.get(ConfigLoaderService);

const cb = (path: string) => {
if (FAKE_FS[path]) return FAKE_FS[path];

throw new Error('unknown path');
};
jest.spyOn(configLoaderService, 'readFile').mockImplementation(cb);
});

test('passcode in file negative', async () => {
const prepConfig = plainToClass(InMemoryConfiguration, {
RABBITMQ_PASSCODE_FILE: 'unreal path',
...DEFAULTS,
});

await expect(() =>
configLoaderService.loadSecrets(prepConfig),
).rejects.toThrow('unknown path');
});

test('passcode in env positive', async () => {
const prepConfig = plainToClass(InMemoryConfiguration, {
RABBITMQ_PASSCODE: 'env-rabbit',
...DEFAULTS,
});
const config = await configLoaderService.loadSecrets(prepConfig);

expect(config).toHaveProperty('RABBITMQ_PASSCODE', 'env-rabbit');
});

test('passcode in file positive', async () => {
const prepConfig = plainToClass(InMemoryConfiguration, {
RABBITMQ_PASSCODE_FILE: 'rabbit',
...DEFAULTS,
});
const config = await configLoaderService.loadSecrets(prepConfig);

expect(config).toHaveProperty('RABBITMQ_PASSCODE', 'rabbit');
});

test('passcode in file order _FILE', async () => {
const prepConfig = plainToClass(InMemoryConfiguration, {
RABBITMQ_PASSCODE_FILE: 'rabbit',
RABBITMQ_PASSCODE: 'some-rabbit-passcode',
...DEFAULTS,
});

const config = await configLoaderService.loadSecrets(prepConfig);
expect(config).toHaveProperty('RABBITMQ_PASSCODE', 'rabbit');
});
});

describe('wallet', () => {
let configLoaderService: ConfigLoaderService;
const DEFAULTS_WITH_RABBIT = {
...DEFAULTS,
RABBITMQ_PASSCODE: 'some-rabbit-passcode',
};

beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [ConfigLoaderService],
}).compile();

configLoaderService = moduleRef.get(ConfigLoaderService);

const cb = (path: string) => {
if (FAKE_FS[path]) return FAKE_FS[path];

throw new Error('unknown path');
};
jest.spyOn(configLoaderService, 'readFile').mockImplementation(cb);
});

test('passcode in file negative', async () => {
const prepConfig = plainToClass(InMemoryConfiguration, {
WALLET_PRIVATE_KEY_FILE: 'unreal path',
...DEFAULTS_WITH_RABBIT,
});

await expect(() =>
configLoaderService.loadSecrets(prepConfig),
).rejects.toThrow('unknown path');
});

test('passcode in env positive', async () => {
const prepConfig = plainToClass(InMemoryConfiguration, {
WALLET_PRIVATE_KEY: 'env-wallet',
...DEFAULTS_WITH_RABBIT,
});
const config = await configLoaderService.loadSecrets(prepConfig);

expect(config).toHaveProperty('WALLET_PRIVATE_KEY', 'env-wallet');
});

test('passcode in file positive', async () => {
const prepConfig = plainToClass(InMemoryConfiguration, {
WALLET_PRIVATE_KEY_FILE: 'wallet',
...DEFAULTS_WITH_RABBIT,
});
const config = await configLoaderService.loadSecrets(prepConfig);

expect(config).toHaveProperty('WALLET_PRIVATE_KEY', 'wallet');
});

test('passcode in file order _FILE', async () => {
const prepConfig = plainToClass(InMemoryConfiguration, {
WALLET_PRIVATE_KEY_FILE: 'wallet',
WALLET_PRIVATE_KEY: 'some-wallet-passcode',
...DEFAULTS_WITH_RABBIT,
});

const config = await configLoaderService.loadSecrets(prepConfig);
expect(config).toHaveProperty('WALLET_PRIVATE_KEY', 'wallet');
});
});
});
71 changes: 71 additions & 0 deletions src/common/config/config-loader.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import { readFile } from 'fs/promises';

import { InMemoryConfiguration } from './in-memory-configuration';
import { validateOrReject } from 'class-validator';

@Injectable()
export class ConfigLoaderService {
public async readFile(filePath: string) {
return await readFile(filePath, 'utf-8');
}

public async loadFile(filePath: string, envVarFile: string) {
try {
const fileContent = (await this.readFile(filePath))
.toString()
.replace(/(\r\n|\n|\r)/gm, '');
return fileContent;
} catch (error) {
const errorCode = (error as any).code;

switch (errorCode) {
case 'ENOENT':
throw new Error(`Failed to load ENV variable from the ${envVarFile}`);
case 'EACCES':
throw new Error(
`Permission denied when trying to read the file specified by ${envVarFile}`,
);
case 'EMFILE':
throw new Error(
`Too many open files in the system when trying to read the file specified by ${envVarFile}`,
);
default:
throw error;
}
}
}

public async loadEnvOrFile(
config: InMemoryConfiguration,
envName: string,
): Promise<string> {
const envVarFile = envName + '_FILE';
const filePath = config[envVarFile];

if (filePath) {
return await this.loadFile(filePath, envVarFile);
}

return config[envName];
}

public async loadSecrets(
config: InMemoryConfiguration,
): Promise<InMemoryConfiguration> {
config.RABBITMQ_PASSCODE = await this.loadEnvOrFile(
config,
'RABBITMQ_PASSCODE',
);
config.WALLET_PRIVATE_KEY = await this.loadEnvOrFile(
config,
'WALLET_PRIVATE_KEY',
);

await validateOrReject(config, {
validationError: { target: false, value: false },
});

return config;
}
}
73 changes: 5 additions & 68 deletions src/common/config/config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,84 +6,21 @@ import { InMemoryConfiguration } from './in-memory-configuration';
import { Configuration } from './configuration';
import { validateOrReject, ValidationError } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { readFile } from 'fs/promises';
import { ConfigLoaderService } from './config-loader.service';

dotenv.config({ path: resolve(appRoot.path, '.env') });

@Module({})
export class ConfigModule {
static async readFile(
filePath: string,
envVarFile: string,
config: InMemoryConfiguration,
) {
try {
const fileContent = (await readFile(filePath, 'utf-8'))
.toString()
.replace(/(\r\n|\n|\r)/gm, '');

delete config[envVarFile];
return fileContent;
} catch (error) {
const errorCode = (error as any).code;

switch (errorCode) {
case 'ENOENT':
throw new Error(`Failed to load ENV variable from the ${envVarFile}`);
case 'EACCES':
throw new Error(
`Permission denied when trying to read the file specified by ${envVarFile}`,
);
case 'EMFILE':
throw new Error(
`Too many open files in the system when trying to read the file specified by ${envVarFile}`,
);
default:
throw error;
}
}
}
static async loadEnvOrFile(
config: InMemoryConfiguration,
envName: string,
): Promise<string> {
const envVarFile = envName + '_FILE';
const filePath = config[envVarFile];

if (filePath) {
return await this.readFile(filePath, envVarFile, config);
}

return config[envName];
}

static async loadSecrets(
config: InMemoryConfiguration,
): Promise<InMemoryConfiguration> {
config.RABBITMQ_PASSCODE = await this.loadEnvOrFile(
config,
'RABBITMQ_PASSCODE',
);
config.WALLET_PRIVATE_KEY = await this.loadEnvOrFile(
config,
'WALLET_PRIVATE_KEY',
);

await validateOrReject(config, {
validationError: { target: false, value: false },
});

return config;
}

static forRoot(): DynamicModule {
return {
module: ConfigModule,
global: true,
providers: [
ConfigLoaderService,
{
provide: Configuration,
useFactory: async () => {
useFactory: async (configLoaderService: ConfigLoaderService) => {
const prepConfig = plainToClass(InMemoryConfiguration, process.env);
try {
if (prepConfig.NODE_ENV === 'test') {
Expand All @@ -94,7 +31,7 @@ export class ConfigModule {
validationError: { target: false, value: false },
});

return await this.loadSecrets(prepConfig);
return await configLoaderService.loadSecrets(prepConfig);
} catch (error) {
// handling the validation error of the configs
if (
Expand All @@ -116,7 +53,7 @@ export class ConfigModule {
throw error;
}
},
inject: [],
inject: [ConfigLoaderService],
},
],
exports: [Configuration],
Expand Down

0 comments on commit 0c2a4b9

Please sign in to comment.