-
Notifications
You must be signed in to change notification settings - Fork 43
Ses s3 initmodules #213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Ses s3 initmodules #213
Changes from all commits
b3dacea
c649251
6705b1b
6611134
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { AWSS3Module } from './aws-s3.module'; | ||
|
|
||
| describe('AWSS3Module', () => { | ||
| let module: AWSS3Module; | ||
|
|
||
| beforeEach(() => { | ||
| process.env.AWS_ACCESS_KEY = 'test-access-key'; | ||
| process.env.AWS_SECRET_KEY = 'test-secret-key'; | ||
| module = new AWSS3Module(); | ||
| }); | ||
|
|
||
| it('should not throw when required env vars are set', () => { | ||
| expect(() => module.onModuleInit()).not.toThrow(); | ||
| }); | ||
|
|
||
| it('should throw if AWS_ACCESS_KEY is missing', () => { | ||
| delete process.env.AWS_ACCESS_KEY; | ||
|
|
||
| expect(() => module.onModuleInit()).toThrow( | ||
| 'Missing required environment variable: AWS_ACCESS_KEY', | ||
| ); | ||
| }); | ||
|
|
||
| it('should throw if AWS_SECRET_KEY is missing', () => { | ||
| delete process.env.AWS_SECRET_KEY; | ||
|
|
||
| expect(() => module.onModuleInit()).toThrow( | ||
| 'Missing required environment variable: AWS_SECRET_KEY', | ||
| ); | ||
| }); | ||
|
|
||
| it('should throw if an env var is whitespace-only', () => { | ||
| process.env.AWS_ACCESS_KEY = ' '; | ||
|
|
||
| expect(() => module.onModuleInit()).toThrow( | ||
| 'Missing required environment variable: AWS_ACCESS_KEY', | ||
| ); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,22 @@ | ||
| import { Global, Module } from '@nestjs/common'; | ||
| import { Global, Module, OnModuleInit } from '@nestjs/common'; | ||
| import { AWSS3Service } from './aws-s3.service'; | ||
|
|
||
| // Required s3 env values | ||
| const REQUIRED_ENV_VARS = ['AWS_ACCESS_KEY', 'AWS_SECRET_KEY'] as const; | ||
|
|
||
| @Global() | ||
| @Module({ | ||
| providers: [AWSS3Service], | ||
| exports: [AWSS3Service], | ||
| }) | ||
| export class AWSS3Module {} | ||
| export class AWSS3Module implements OnModuleInit { | ||
| onModuleInit(): void { | ||
| for (const name of REQUIRED_ENV_VARS) { | ||
| const value = process.env[name]; | ||
| // Treat unset and empty/whitespace-only values as missing. | ||
| if (!value || value.trim().length === 0) { | ||
| throw new Error(`Missing required environment variable: ${name}`); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,21 +12,19 @@ export const AmazonSESClientFactory: Provider<SESv2Client> = { | |
| provide: AMAZON_SES_CLIENT, | ||
| useFactory: () => { | ||
| // Create dummy client that is never used when email sending is set to false | ||
| if (process.env.SEND_AUTOMATED_EMAILS !== 'true') { | ||
| if (process.env.SEND_AUTOMATED_EMAILS.toLowerCase() !== 'true') { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to add an optional check here i think. the factory runs during dependency injection, so we could face issues here if this is unset. |
||
| return new SESv2Client({}); | ||
| } | ||
| const region = process.env.AWS_REGION; | ||
| const accessKeyId = process.env.AWS_ACCESS_KEY_ID; | ||
| const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; | ||
|
|
||
| if (!region) throw new Error('AWS_REGION is not defined'); | ||
| if (!accessKeyId) throw new Error('AWS_ACCESS_KEY_ID is not defined'); | ||
| if (!secretAccessKey) | ||
| throw new Error('AWS_SECRET_ACCESS_KEY is not defined'); | ||
|
|
||
| // If email sending is enabled, EmailsModule.onModuleInit() aborts startup | ||
| // when these env vars are missing, so a client built with empty-string | ||
| // fallbacks is never actually used to send mail. | ||
| return new SESv2Client({ | ||
| region, | ||
| credentials: { accessKeyId, secretAccessKey }, | ||
| region: process.env.AWS_REGION ?? '', | ||
| credentials: { | ||
| accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '', | ||
| secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '', | ||
| }, | ||
| }); | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { EmailsModule } from './email.module'; | ||
|
|
||
| describe('EmailsModule', () => { | ||
| const ENV_VARS = [ | ||
| 'SEND_AUTOMATED_EMAILS', | ||
| 'AWS_REGION', | ||
| 'AWS_ACCESS_KEY_ID', | ||
| 'AWS_SECRET_ACCESS_KEY', | ||
| 'AWS_SES_SENDER_EMAIL', | ||
| ] as const; | ||
|
|
||
| const REQUIRED_WHEN_ENABLED = [ | ||
| 'AWS_REGION', | ||
| 'AWS_ACCESS_KEY_ID', | ||
| 'AWS_SECRET_ACCESS_KEY', | ||
| 'AWS_SES_SENDER_EMAIL', | ||
| ] as const; | ||
|
|
||
| const originalEnv: Record<string, string | undefined> = {}; | ||
| let module: EmailsModule; | ||
|
|
||
| beforeEach(() => { | ||
| for (const name of ENV_VARS) { | ||
| originalEnv[name] = process.env[name]; | ||
| } | ||
|
|
||
| // Default to a fully-configured, enabled setup; individual tests override. | ||
| process.env.SEND_AUTOMATED_EMAILS = 'true'; | ||
| process.env.AWS_REGION = 'us-east-2'; | ||
| process.env.AWS_ACCESS_KEY_ID = 'test-access-key-id'; | ||
| process.env.AWS_SECRET_ACCESS_KEY = 'test-secret-access-key'; | ||
| process.env.AWS_SES_SENDER_EMAIL = 'sender@example.com'; | ||
|
|
||
| module = new EmailsModule(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| for (const name of ENV_VARS) { | ||
| if (originalEnv[name] === undefined) { | ||
| delete process.env[name]; | ||
| } else { | ||
| process.env[name] = originalEnv[name]; | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| describe('onModuleInit', () => { | ||
| it('does not throw when all required env vars are set and enabled', () => { | ||
| expect(() => module.onModuleInit()).not.toThrow(); | ||
| }); | ||
|
|
||
| it('does not throw when disabled, even if required vars are missing', () => { | ||
| process.env.SEND_AUTOMATED_EMAILS = 'false'; | ||
| for (const name of REQUIRED_WHEN_ENABLED) { | ||
| delete process.env[name]; | ||
| } | ||
| expect(() => module.onModuleInit()).not.toThrow(); | ||
| }); | ||
|
|
||
| it('does not throw when SEND_AUTOMATED_EMAILS is unset', () => { | ||
| delete process.env.SEND_AUTOMATED_EMAILS; | ||
| for (const name of REQUIRED_WHEN_ENABLED) { | ||
| delete process.env[name]; | ||
| } | ||
| expect(() => module.onModuleInit()).not.toThrow(); | ||
| }); | ||
|
|
||
| it.each(REQUIRED_WHEN_ENABLED)( | ||
| 'throws when enabled and %s is missing', | ||
| (name) => { | ||
| delete process.env[name]; | ||
| expect(() => module.onModuleInit()).toThrow( | ||
| `Missing required environment variable: ${name}`, | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| it('throws when enabled and a required var is empty/whitespace-only', () => { | ||
| process.env.AWS_SES_SENDER_EMAIL = ' '; | ||
| expect(() => module.onModuleInit()).toThrow( | ||
| 'Missing required environment variable: AWS_SES_SENDER_EMAIL', | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,34 @@ | ||
| import { Module } from '@nestjs/common'; | ||
| import { Module, OnModuleInit } from '@nestjs/common'; | ||
| import { EmailsService } from './email.service'; | ||
| import { AmazonSESWrapper } from './awsSes.wrapper'; | ||
| import { AmazonSESClientFactory } from './awsSesClient.factory'; | ||
|
|
||
| // Env vars required only when SES dispatch is enabled (SEND_AUTOMATED_EMAILS === 'true') | ||
| const REQUIRED_ENV_VARS_WHEN_ENABLED = [ | ||
| 'AWS_REGION', | ||
| 'AWS_ACCESS_KEY_ID', | ||
| 'AWS_SECRET_ACCESS_KEY', | ||
| 'AWS_SES_SENDER_EMAIL', | ||
| ] as const; | ||
|
|
||
| @Module({ | ||
| providers: [AmazonSESWrapper, AmazonSESClientFactory, EmailsService], | ||
| exports: [EmailsService], | ||
| }) | ||
| export class EmailsModule {} | ||
| export class EmailsModule implements OnModuleInit { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: for consistency, can we just name this AWSSESModule? |
||
| onModuleInit(): void { | ||
| // Email sending is disabled: skip validation so teams not using SES can | ||
| // boot without any AWS config. | ||
| if (process.env.SEND_AUTOMATED_EMAILS?.toLowerCase() !== 'true') { | ||
| return; | ||
| } | ||
|
|
||
| for (const name of REQUIRED_ENV_VARS_WHEN_ENABLED) { | ||
| const value = process.env[name]; | ||
| // Treat unset and empty/whitespace-only values as missing. | ||
| if (!value || value.trim().length === 0) { | ||
| throw new Error(`Missing required environment variable: ${name}`); | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
like the cognito module does, i think we should add a logger here, and just throw a warning not an error (same with ses). i know we were throwing an error beforehand, but i think that was not good in practice.