From b7734139de3ed3896c8ce3fed39b9a0df03effd0 Mon Sep 17 00:00:00 2001 From: Jordan Welch Date: Tue, 29 Aug 2023 23:49:51 -0500 Subject: [PATCH] Migrate to using config --- __tests__/config/config.spec.ts | 8 --- __tests__/config/firestore.spec.ts | 3 +- __tests__/controllers/cacheController.spec.ts | 8 ++- __tests__/controllers/rsvpController.spec.ts | 6 +- __tests__/controllers/weekController.spec.ts | 5 +- .../data/firestore/firestoreAdapter.spec.ts | 5 +- __tests__/data/notion/notionAdapter.spec.ts | 59 +++---------------- __tests__/data/tmdb/tmdbAdapter.spec.ts | 3 +- __tests__/support/mockConfig.ts | 15 +++++ src/config/firestore.ts | 5 +- src/config/mail.ts | 9 --- src/controllers/rsvpController.ts | 3 +- src/data/firestore/firestoreAdapter.ts | 23 +++++--- src/data/notion/notionAdapter.ts | 24 ++------ src/data/tmdb/tmdbAdapter.ts | 9 ++- src/index.ts | 9 ++- 16 files changed, 79 insertions(+), 115 deletions(-) create mode 100644 __tests__/support/mockConfig.ts delete mode 100644 src/config/mail.ts diff --git a/__tests__/config/config.spec.ts b/__tests__/config/config.spec.ts index cdbc9cd7..5d4b6f63 100644 --- a/__tests__/config/config.spec.ts +++ b/__tests__/config/config.spec.ts @@ -1,11 +1,3 @@ -/* - -Authorization: `Bearer ${process.env.TMDB_READ_KEY}`, - -src="<%= process.env.CALENDAR_URL %>" - -*/ - import { beforeEach, describe, expect, it, jest } from '@jest/globals' import Config from '../../src/config/config' diff --git a/__tests__/config/firestore.spec.ts b/__tests__/config/firestore.spec.ts index b4f53d3a..34694586 100644 --- a/__tests__/config/firestore.spec.ts +++ b/__tests__/config/firestore.spec.ts @@ -3,10 +3,11 @@ import { initializeApp } from 'firebase/app' import { applicationDefault } from 'firebase-admin/app' import { getFirestore } from 'firebase/firestore' import setupFirestore from '../../src/config/firestore' +import { mockConfig } from '../support/mockConfig' describe('setupFirestore', () => { it('initializes the firestore', () => { - const firestore = setupFirestore() + const firestore = setupFirestore(mockConfig()) expect (applicationDefault).toHaveBeenCalledTimes(1) expect (initializeApp).toHaveBeenCalledTimes(1) diff --git a/__tests__/controllers/cacheController.spec.ts b/__tests__/controllers/cacheController.spec.ts index 4a429b1c..2c33e393 100644 --- a/__tests__/controllers/cacheController.spec.ts +++ b/__tests__/controllers/cacheController.spec.ts @@ -20,15 +20,17 @@ import { TmdbMock } from '../support/tmdbMock' import { mockFetch } from '../support/fetchMock' import Movie from '../../src/models/movie' import TmdbAdapter from '../../src/data/tmdb/tmdbAdapter' +import { mockConfig } from '../support/mockConfig' let notionMock: NotionMock const { res, mockClear } = getMockRes() const newCacheController = () => { - const firestore = new FirestoreAdapter() - const notion = new NotionAdapter() - const tmdbAdapter = new TmdbAdapter() + const config = mockConfig() + const firestore = new FirestoreAdapter(config) + const notion = new NotionAdapter(config) + const tmdbAdapter = new TmdbAdapter(config) return new CacheController(firestore, notion, tmdbAdapter) } diff --git a/__tests__/controllers/rsvpController.spec.ts b/__tests__/controllers/rsvpController.spec.ts index 1802422d..91ab557a 100644 --- a/__tests__/controllers/rsvpController.spec.ts +++ b/__tests__/controllers/rsvpController.spec.ts @@ -4,11 +4,11 @@ import { getMockReq, getMockRes } from '@jest-mock/express' import { Timestamp, addDoc } from 'firebase/firestore' import { FirebaseMock } from '../support/firebaseMock' import FirestoreAdapter from '../../src/data/firestore/firestoreAdapter' +import { mockConfig } from '../support/mockConfig' const { res, mockClear } = getMockRes() beforeEach(() => { - process.env.ADMIN_EMAIL = 'admin@example.com' jest.clearAllMocks() mockClear() }) @@ -23,7 +23,7 @@ describe('store', () => { let firestoreAdapter: FirestoreAdapter beforeEach(() => { - firestoreAdapter = new FirestoreAdapter() + firestoreAdapter = new FirestoreAdapter(mockConfig()) }) describe('has correct week', () => { @@ -69,7 +69,7 @@ describe('store', () => { expect(addDoc).toHaveBeenCalledWith( FirebaseMock.mockCollection('mail'), { - to: 'admin@example.com', + to: 'ADMIN_EMAIL@example.com', message: { subject: 'TNMC RSVP: test name', // eslint-disable-next-line max-len diff --git a/__tests__/controllers/weekController.spec.ts b/__tests__/controllers/weekController.spec.ts index ff763ec6..78a56bd9 100644 --- a/__tests__/controllers/weekController.spec.ts +++ b/__tests__/controllers/weekController.spec.ts @@ -11,6 +11,7 @@ import { Request } from 'express' import { getMockReq, getMockRes } from '@jest-mock/express' import { FirebaseMock } from '../support/firebaseMock' import FirestoreAdapter from '../../src/data/firestore/firestoreAdapter' +import { mockConfig } from '../support/mockConfig' const { res, mockClear } = getMockRes() @@ -31,7 +32,7 @@ describe('index', () => { let req: Request beforeEach(() => { - firestore = new FirestoreAdapter() + firestore = new FirestoreAdapter(mockConfig()) FirebaseMock.mockWeeks([ { date: new Date('2021-01-01'), @@ -88,7 +89,7 @@ describe('index', () => { let req: Request beforeEach(() => { - firestore = new FirestoreAdapter() + firestore = new FirestoreAdapter(mockConfig()) FirebaseMock.mockWeeks([ { date: new Date('2021-01-01'), diff --git a/__tests__/data/firestore/firestoreAdapter.spec.ts b/__tests__/data/firestore/firestoreAdapter.spec.ts index e7986715..3748dccc 100644 --- a/__tests__/data/firestore/firestoreAdapter.spec.ts +++ b/__tests__/data/firestore/firestoreAdapter.spec.ts @@ -18,6 +18,7 @@ import { import { transaction } from '../../../__mocks__/firebase/firestore' import { FirebaseMock } from '../../support/firebaseMock' import Week from '../../../src/models/week' +import { mockConfig } from '../../support/mockConfig' let firestore: FirestoreAdapter @@ -28,13 +29,13 @@ beforeAll(() => { }) beforeEach(() => { - firestore = new FirestoreAdapter() + firestore = new FirestoreAdapter(mockConfig()) jest.clearAllMocks() }) describe('constructor', () => { it('initializes the firestore', () => { - firestore = new FirestoreAdapter() + firestore = new FirestoreAdapter(mockConfig()) expect (applicationDefault).toHaveBeenCalledTimes(1) expect (initializeApp).toHaveBeenCalledTimes(1) diff --git a/__tests__/data/notion/notionAdapter.spec.ts b/__tests__/data/notion/notionAdapter.spec.ts index 6b43af5c..e3875ab6 100644 --- a/__tests__/data/notion/notionAdapter.spec.ts +++ b/__tests__/data/notion/notionAdapter.spec.ts @@ -9,6 +9,7 @@ import { import NotionAdapter from '../../../src/data/notion/notionAdapter' import { NotionMock } from '../../support/notionMock' import Movie from '../../../src/models/movie' +import { mockConfig } from '../../support/mockConfig' let notionMock: NotionMock @@ -21,48 +22,6 @@ beforeEach(() => { jest.clearAllMocks() }) -describe('constructor', () => { - describe('when NOTION_TOKEN and DATABASE_ID are set', () => { - beforeEach(() => { - process.env = { - NOTION_TOKEN: 'NOTION_TOKEN', - DATABASE_ID: 'DATABASE_ID', - } - }) - - it('should be created successfully', () => { - expect(() => new NotionAdapter()).not.toThrow() - }) - }) - - - describe('when NOTION_TOKEN is not set', () => { - beforeEach(() => { - process.env = { - DATABASE_ID: 'DATABASE_ID', - } - }) - - it('should throw an error', () => { - expect(() => new NotionAdapter()) - .toThrowError('Missing NOTION_TOKEN environment variable') - }) - }) - - describe('when DATABASE_ID is not set', () => { - beforeEach(() => { - process.env = { - NOTION_TOKEN: 'NOTION_TOKEN', - } - }) - - it('should throw an error', () => { - expect(() => new NotionAdapter()) - .toThrowError('Missing DATABASE_ID environment variable') - }) - }) -}) - describe('getMovie', () => { beforeEach(() => { process.env = { @@ -78,7 +37,7 @@ describe('getMovie', () => { }) it('should return the movie', async () => { - const movie = await new NotionAdapter().getMovie('movieId') + const movie = await new NotionAdapter(mockConfig()).getMovie('movieId') expect(movie).toEqual({ notionId: 'movieId', @@ -95,7 +54,7 @@ describe('getMovie', () => { }) it ('calls the retrieve method with page_id', async () => { - await new NotionAdapter().getMovie('movieId') + await new NotionAdapter(mockConfig()).getMovie('movieId') expect(notionMock.retrieve).toHaveBeenCalledWith({ 'page_id': 'movieId' }) }) @@ -107,7 +66,7 @@ describe('getMovie', () => { }) it('should throw an error', async () => { - await expect(new NotionAdapter().getMovie('movieId')) + await expect(new NotionAdapter(mockConfig()).getMovie('movieId')) .rejects.toThrowError('Page was not successfully retrieved') }) }) @@ -130,7 +89,7 @@ describe('getWeek', () => { }) it('should return the week', async () => { - const notion = new NotionAdapter() + const notion = new NotionAdapter(mockConfig()) const week = await notion.getWeek('2021-01-01') expect(week).toEqual({ @@ -149,7 +108,7 @@ describe('getWeek', () => { }) it('should throw an error', async () => { - const notion = new NotionAdapter() + const notion = new NotionAdapter(mockConfig()) expect(notion.getWeek('2021-01-01')) .rejects.toThrowError('Page was not successfully retrieved') @@ -168,7 +127,7 @@ describe('getWeeks', () => { }) it('should return the weeks', async () => { - const notion = new NotionAdapter() + const notion = new NotionAdapter(mockConfig()) const weeks = await notion.getWeeks() expect(weeks).toEqual([ @@ -195,7 +154,7 @@ describe('getWeeks', () => { }) it('should call query with the correct parameters', async () => { - const notion = new NotionAdapter() + const notion = new NotionAdapter(mockConfig()) await notion.getWeeks() expect(notionMock.query).toHaveBeenCalledWith({ @@ -227,7 +186,7 @@ describe('setMovie', () => { 'Theater', 'Showing Url', ) - const notion = new NotionAdapter() + const notion = new NotionAdapter(mockConfig()) await notion.setMovie(movie) expect(notionMock.update).toHaveBeenCalledWith(movie.toNotion()) diff --git a/__tests__/data/tmdb/tmdbAdapter.spec.ts b/__tests__/data/tmdb/tmdbAdapter.spec.ts index 1f404129..e4ec3845 100644 --- a/__tests__/data/tmdb/tmdbAdapter.spec.ts +++ b/__tests__/data/tmdb/tmdbAdapter.spec.ts @@ -3,6 +3,7 @@ import TmdbAdapter from '../../../src/data/tmdb/tmdbAdapter' import Movie from '../../../src/models/movie' import { TmdbMock } from '../../support/tmdbMock' import { mockFetch } from '../../support/fetchMock' +import { mockConfig } from '../../support/mockConfig' let tmdbMock: TmdbMock @@ -14,7 +15,7 @@ describe('getMovie', () => { let tmdbAdapter: TmdbAdapter beforeEach(() => { - tmdbAdapter = new TmdbAdapter() + tmdbAdapter = new TmdbAdapter(mockConfig()) }) it('should return a movie', async () => { diff --git a/__tests__/support/mockConfig.ts b/__tests__/support/mockConfig.ts new file mode 100644 index 00000000..157d0d82 --- /dev/null +++ b/__tests__/support/mockConfig.ts @@ -0,0 +1,15 @@ +import Config from '../../src/config/config.js' + +export function mockConfig (): Config { + process.env = { + NOTION_TOKEN: 'NOTION_TOKEN', + DATABASE_ID: 'DATABASE_ID', + PORT: '3000', + GOOGLE_CLOUD_PROJECT: 'GOOGLE_CLOUD_PROJECT', + ADMIN_EMAIL: 'ADMIN_EMAIL@example.com', + TMDB_READ_KEY: 'TMDB_READ_KEY', + CALENDAR_URL: 'https://CALENDAR_URL', + } + + return new Config() +} diff --git a/src/config/firestore.ts b/src/config/firestore.ts index fc6f30c5..be366879 100644 --- a/src/config/firestore.ts +++ b/src/config/firestore.ts @@ -1,11 +1,12 @@ import { getFirestore, Firestore } from 'firebase/firestore' import { initializeApp } from 'firebase/app' import { applicationDefault } from 'firebase-admin/app' +import Config from './config' -export default function setupFirestore (): Firestore { +export default function setupFirestore (config: Config): Firestore { const firebaseConfig = { credential: applicationDefault(), - projectId: process.env.GOOGLE_CLOUD_PROJECT, + projectId: config.googleCloudProject, } const app = initializeApp(firebaseConfig) diff --git a/src/config/mail.ts b/src/config/mail.ts deleted file mode 100644 index 6825655c..00000000 --- a/src/config/mail.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function adminEmail (): string { - const email = process.env.ADMIN_EMAIL - - if (!email) { - throw new Error('ADMIN_EMAIL is not set') - } - - return email -} diff --git a/src/controllers/rsvpController.ts b/src/controllers/rsvpController.ts index 8781d210..b7a504ab 100644 --- a/src/controllers/rsvpController.ts +++ b/src/controllers/rsvpController.ts @@ -1,7 +1,6 @@ import { type Request, type Response } from 'express' import FirestoreAdapter from '../data/firestore/firestoreAdapter.js' import { z } from 'zod' -import { adminEmail } from '../config/mail.js' class RsvpController { static PATHS = { @@ -30,7 +29,7 @@ class RsvpController { res.status(201).json({ message: 'Successfully RSVP\'d' }) - await this.firestore.sendEmail(adminEmail(), { + await this.firestore.sendEmail(this.firestore.adminEmail, { subject: `TNMC RSVP: ${name}`, // eslint-disable-next-line max-len text: `${name} has RSVPed for ${weekId}\n\nEmail: ${email}\nPlus one: ${plusOne}`, diff --git a/src/data/firestore/firestoreAdapter.ts b/src/data/firestore/firestoreAdapter.ts index 7fe37e7c..7172a99e 100644 --- a/src/data/firestore/firestoreAdapter.ts +++ b/src/data/firestore/firestoreAdapter.ts @@ -17,22 +17,25 @@ import { } from 'firebase/firestore' import Week from '../../models/week.js' import setupFirestore from '../../config/firestore.js' +import Config from '../../config/config.js' export default class FirestoreAdapter { static readonly MAIL_COLLECTION_NAME = 'mail' static readonly RSVPS_COLLECTION_NAME = 'rsvps' static readonly WEEKS_COLLECTION_NAME = 'weeks' - #firestore: FirestoreType + private config: Config + private firestore: FirestoreType - constructor () { - this.#firestore = setupFirestore() + constructor (config: Config) { + this.config = config + this.firestore = setupFirestore(config) } async cacheWeeks (weeks: Week[]): Promise { - await runTransaction(this.#firestore, async (transaction) => { + await runTransaction(this.firestore, async (transaction) => { weeks.forEach((week: Week) => { - const ref = doc(this.#firestore, 'weeks', week.dateString) + const ref = doc(this.firestore, 'weeks', week.dateString) transaction.set(ref, week.toFirebaseDTO()) }) }) @@ -103,16 +106,20 @@ export default class FirestoreAdapter { return Timestamp.fromDate(today) } + get adminEmail (): string { + return this.config.adminEmail + } + private get mailCollection (): Collection { - return collection(this.#firestore, FirestoreAdapter.MAIL_COLLECTION_NAME) + return collection(this.firestore, FirestoreAdapter.MAIL_COLLECTION_NAME) } private get rsvpCollection (): Collection { - return collection(this.#firestore, FirestoreAdapter.RSVPS_COLLECTION_NAME) + return collection(this.firestore, FirestoreAdapter.RSVPS_COLLECTION_NAME) } private get weekCollection (): Collection { - return collection(this.#firestore, FirestoreAdapter.WEEKS_COLLECTION_NAME) + return collection(this.firestore, FirestoreAdapter.WEEKS_COLLECTION_NAME) } } diff --git a/src/data/notion/notionAdapter.ts b/src/data/notion/notionAdapter.ts index 54291f05..023fe5bf 100644 --- a/src/data/notion/notionAdapter.ts +++ b/src/data/notion/notionAdapter.ts @@ -8,17 +8,15 @@ import { DatabaseObjectResponse, } from '@notionhq/client/build/src/api-endpoints' import type WeekProperties from '../../types/weekProperties.js' +import Config from '../../config/config.js' export default class NotionAdapter { #notion: Client #databaseId: string - constructor () { - const { NOTION_TOKEN, DATABASE_ID } = this._envVariables() - this.#databaseId = DATABASE_ID - this.#notion = new Client({ - auth: NOTION_TOKEN, - }) + constructor (config: Config) { + this.#databaseId = config.notionDatabaseId + this.#notion = new Client({ auth: config.notionToken }) } async getMovie (id: string): Promise { @@ -79,20 +77,6 @@ export default class NotionAdapter { return Week.fromNotion(record).setMovies(movies) } - - _envVariables (): { NOTION_TOKEN: string, DATABASE_ID: string } - { - const { NOTION_TOKEN, DATABASE_ID } = process.env - - if (typeof NOTION_TOKEN !== 'string') { - throw new Error('Missing NOTION_TOKEN environment variable') - } - if (typeof DATABASE_ID !== 'string') { - throw new Error('Missing DATABASE_ID environment variable') - } - - return { NOTION_TOKEN, DATABASE_ID } - } } type NotionQueryResponse = diff --git a/src/data/tmdb/tmdbAdapter.ts b/src/data/tmdb/tmdbAdapter.ts index bccd85c2..814a62ed 100644 --- a/src/data/tmdb/tmdbAdapter.ts +++ b/src/data/tmdb/tmdbAdapter.ts @@ -1,9 +1,16 @@ +import Config from '../../config/config.js' import Movie from '../../models/movie.js' import { TMDB_BASE_URL } from './constants.js' import MovieResponse from './dtos/movieResponse.js' import SearchResponse from './dtos/searchResponse.js' export default class TmdbAdapter { + private tmdbApiKey: string + + constructor (config: Config) { + this.tmdbApiKey = config.tmdbApiKey + } + async getMovie (name: string): Promise { const search = await this.searchMovie(name) @@ -37,7 +44,7 @@ export default class TmdbAdapter { method: 'GET', headers: { accept: 'application/json', - Authorization: `Bearer ${process.env.TMDB_READ_KEY}`, + Authorization: `Bearer ${this.tmdbApiKey}`, }, } } diff --git a/src/index.ts b/src/index.ts index 1be8e1bf..50696637 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,14 +4,17 @@ import NotionAdapter from './data/notion/notionAdapter.js' import FirestoreAdapter from './data/firestore/firestoreAdapter.js' import setupExpress from './config/express.js' import TmdbAdapter from './data/tmdb/tmdbAdapter.js' +import Config from './config/config.js' dotenv.config() +const config = new Config() + const app = new Application( setupExpress(), - new FirestoreAdapter(), - new NotionAdapter(), - new TmdbAdapter(), + new FirestoreAdapter(config), + new NotionAdapter(config), + new TmdbAdapter(config), ) app.listen()