From 2f68501826ec297bc9b9c7999d339296181d02ed Mon Sep 17 00:00:00 2001 From: Vanshika Sabharwal <143436704+VanshikaSabharwal@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:57:21 +0530 Subject: [PATCH] Docker setup in Setup Script (#3187) * first commit * migrated jest to vitest in validateRecaptcha --- setup.ts | 203 +++++------------- .../askAndSetDockerOption.spec.ts | 61 ++++++ .../askAndSetDockerOption.ts | 35 +++ .../askAndUpdatePort/askAndUpdatePort.ts | 25 +++ .../askAndUpdatePort/askForUpdatePort.spec.ts | 55 +++++ src/setup/askForDocker/askForDocker.spec.ts | 69 ++++++ src/setup/askForDocker/askForDocker.ts | 99 +++++++++ src/setup/updateEnvFile/updateEnvFile.spec.ts | 88 ++++++++ src/setup/updateEnvFile/updateEnvFile.ts | 21 ++ ...tcha.test.ts => validateRecaptcha.spec.ts} | 1 + 10 files changed, 507 insertions(+), 150 deletions(-) create mode 100644 src/setup/askAndSetDockerOption/askAndSetDockerOption.spec.ts create mode 100644 src/setup/askAndSetDockerOption/askAndSetDockerOption.ts create mode 100644 src/setup/askAndUpdatePort/askAndUpdatePort.ts create mode 100644 src/setup/askAndUpdatePort/askForUpdatePort.spec.ts create mode 100644 src/setup/askForDocker/askForDocker.spec.ts create mode 100644 src/setup/askForDocker/askForDocker.ts create mode 100644 src/setup/updateEnvFile/updateEnvFile.spec.ts create mode 100644 src/setup/updateEnvFile/updateEnvFile.ts rename src/setup/validateRecaptcha/{validateRecaptcha.test.ts => validateRecaptcha.spec.ts} (95%) diff --git a/setup.ts b/setup.ts index 2a6c437fa3..2c39924be8 100644 --- a/setup.ts +++ b/setup.ts @@ -1,165 +1,48 @@ import dotenv from 'dotenv'; import fs from 'fs'; import inquirer from 'inquirer'; -import { checkConnection } from './src/setup/checkConnection/checkConnection'; -import { askForTalawaApiUrl } from './src/setup/askForTalawaApiUrl/askForTalawaApiUrl'; import { checkEnvFile } from './src/setup/checkEnvFile/checkEnvFile'; import { validateRecaptcha } from './src/setup/validateRecaptcha/validateRecaptcha'; -import { askForCustomPort } from './src/setup/askForCustomPort/askForCustomPort'; - -export async function main(): Promise { - console.log('Welcome to the Talawa Admin setup! šŸš€'); - - if (!fs.existsSync('.env')) { - fs.openSync('.env', 'w'); - const config = dotenv.parse(fs.readFileSync('.env.example')); - for (const key in config) { - fs.appendFileSync('.env', `${key}=${config[key]}\n`); - } - } else { - checkEnvFile(); - } - - let shouldSetCustomPort: boolean; - - if (process.env.PORT) { - console.log( - `\nCustom port for development server already exists with the value:\n${process.env.PORT}`, - ); - shouldSetCustomPort = true; - } else { - const { shouldSetCustomPortResponse } = await inquirer.prompt({ +import askAndSetDockerOption from './src/setup/askAndSetDockerOption/askAndSetDockerOption'; +import updateEnvFile from './src/setup/updateEnvFile/updateEnvFile'; +import askAndUpdatePort from './src/setup/askAndUpdatePort/askAndUpdatePort'; +import { askAndUpdateTalawaApiUrl } from './src/setup/askForDocker/askForDocker'; + +// Ask and set up reCAPTCHA +const askAndSetRecaptcha = async (): Promise => { + try { + const { shouldUseRecaptcha } = await inquirer.prompt({ type: 'confirm', - name: 'shouldSetCustomPortResponse', - message: 'Would you like to set up a custom port?', + name: 'shouldUseRecaptcha', + message: 'Would you like to set up reCAPTCHA?', default: true, }); - shouldSetCustomPort = shouldSetCustomPortResponse; - } - - if (shouldSetCustomPort) { - const customPort = await askForCustomPort(); - - const port = dotenv.parse(fs.readFileSync('.env')).PORT; - - fs.readFile('.env', 'utf8', (err, data) => { - const result = data.replace(`PORT=${port}`, `PORT=${customPort}`); - fs.writeFileSync('.env', result, 'utf8'); - }); - } - - let shouldSetTalawaApiUrl: boolean; - - if (process.env.REACT_APP_TALAWA_URL) { - console.log( - `\nEndpoint for accessing talawa-api graphql service already exists with the value:\n${process.env.REACT_APP_TALAWA_URL}`, - ); - shouldSetTalawaApiUrl = true; - } else { - const { shouldSetTalawaApiUrlResponse } = await inquirer.prompt({ - type: 'confirm', - name: 'shouldSetTalawaApiUrlResponse', - message: 'Would you like to set up talawa-api endpoint?', - default: true, - }); - shouldSetTalawaApiUrl = shouldSetTalawaApiUrlResponse; - } - - if (shouldSetTalawaApiUrl) { - let isConnected = false, - endpoint = ''; - - while (!isConnected) { - endpoint = await askForTalawaApiUrl(); - const url = new URL(endpoint); - isConnected = await checkConnection(url.origin); - } - const envPath = '.env'; - const currentEnvContent = fs.readFileSync(envPath, 'utf8'); - const talawaApiUrl = dotenv.parse(currentEnvContent).REACT_APP_TALAWA_URL; - - const updatedEnvContent = currentEnvContent.replace( - `REACT_APP_TALAWA_URL=${talawaApiUrl}`, - `REACT_APP_TALAWA_URL=${endpoint}`, - ); - - fs.writeFileSync(envPath, updatedEnvContent, 'utf8'); - const websocketUrl = endpoint.replace(/^http(s)?:\/\//, 'ws$1://'); - const currentWebSocketUrl = - dotenv.parse(updatedEnvContent).REACT_APP_BACKEND_WEBSOCKET_URL; - - const finalEnvContent = updatedEnvContent.replace( - `REACT_APP_BACKEND_WEBSOCKET_URL=${currentWebSocketUrl}`, - `REACT_APP_BACKEND_WEBSOCKET_URL=${websocketUrl}`, - ); - - fs.writeFileSync(envPath, finalEnvContent, 'utf8'); - } - - const { shouldUseRecaptcha } = await inquirer.prompt({ - type: 'confirm', - name: 'shouldUseRecaptcha', - message: 'Would you like to set up ReCAPTCHA?', - default: true, - }); - - if (shouldUseRecaptcha) { - const useRecaptcha = dotenv.parse( - fs.readFileSync('.env'), - ).REACT_APP_USE_RECAPTCHA; - - fs.readFile('.env', 'utf8', (err, data) => { - const result = data.replace( - `REACT_APP_USE_RECAPTCHA=${useRecaptcha}`, - `REACT_APP_USE_RECAPTCHA=yes`, - ); - fs.writeFileSync('.env', result, 'utf8'); - }); - let shouldSetRecaptchaSiteKey: boolean; - if (process.env.REACT_APP_RECAPTCHA_SITE_KEY) { - console.log( - `\nreCAPTCHA site key already exists with the value ${process.env.REACT_APP_RECAPTCHA_SITE_KEY}`, - ); - shouldSetRecaptchaSiteKey = true; - } else { - const { shouldSetRecaptchaSiteKeyResponse } = await inquirer.prompt({ - type: 'confirm', - name: 'shouldSetRecaptchaSiteKeyResponse', - message: 'Would you like to set up a reCAPTCHA site key?', - default: true, - }); - shouldSetRecaptchaSiteKey = shouldSetRecaptchaSiteKeyResponse; - } - if (shouldSetRecaptchaSiteKey) { + if (shouldUseRecaptcha) { const { recaptchaSiteKeyInput } = await inquirer.prompt([ { type: 'input', name: 'recaptchaSiteKeyInput', message: 'Enter your reCAPTCHA site key:', - validate: async (input: string): Promise => { - if (validateRecaptcha(input)) { - return true; - } - return 'Invalid reCAPTCHA site key. Please try again.'; + validate: (input: string): boolean | string => { + return ( + validateRecaptcha(input) || + 'Invalid reCAPTCHA site key. Please try again.' + ); }, }, ]); - const recaptchaSiteKey = dotenv.parse( - fs.readFileSync('.env'), - ).REACT_APP_RECAPTCHA_SITE_KEY; - - fs.readFile('.env', 'utf8', (err, data) => { - const result = data.replace( - `REACT_APP_RECAPTCHA_SITE_KEY=${recaptchaSiteKey}`, - `REACT_APP_RECAPTCHA_SITE_KEY=${recaptchaSiteKeyInput}`, - ); - fs.writeFileSync('.env', result, 'utf8'); - }); + updateEnvFile('REACT_APP_RECAPTCHA_SITE_KEY', recaptchaSiteKeyInput); } + } catch (error) { + console.error('Error setting up reCAPTCHA:', error); + throw new Error(`Failed to set up reCAPTCHA: ${(error as Error).message}`); } +}; +// Ask and set up logging errors in the console +const askAndSetLogErrors = async (): Promise => { const { shouldLogErrors } = await inquirer.prompt({ type: 'confirm', name: 'shouldLogErrors', @@ -169,17 +52,37 @@ export async function main(): Promise { }); if (shouldLogErrors) { - const logErrors = dotenv.parse(fs.readFileSync('.env')).ALLOW_LOGS; - - fs.readFile('.env', 'utf8', (err, data) => { - const result = data.replace(`ALLOW_LOGS=${logErrors}`, 'ALLOW_LOGS=YES'); - fs.writeFileSync('.env', result, 'utf8'); - }); + updateEnvFile('ALLOW_LOGS', 'YES'); } +}; - console.log( - '\nCongratulations! Talawa Admin has been successfully setup! šŸ„‚šŸŽ‰', - ); +// Main function to run the setup process +export async function main(): Promise { + try { + console.log('Welcome to the Talawa Admin setup! šŸš€'); + + checkEnvFile(); + await askAndSetDockerOption(); + const envConfig = dotenv.parse(fs.readFileSync('.env', 'utf8')); + const useDocker = envConfig.USE_DOCKER === 'YES'; + + // Only run these commands if Docker is NOT used + if (!useDocker) { + await askAndUpdatePort(); + await askAndUpdateTalawaApiUrl(); + } + + await askAndSetRecaptcha(); + await askAndSetLogErrors(); + + console.log( + '\nCongratulations! Talawa Admin has been successfully set up! šŸ„‚šŸŽ‰', + ); + } catch (error) { + console.error('\nāŒ Setup failed:', error); + console.log('\nPlease try again or contact support if the issue persists.'); + process.exit(1); + } } main(); diff --git a/src/setup/askAndSetDockerOption/askAndSetDockerOption.spec.ts b/src/setup/askAndSetDockerOption/askAndSetDockerOption.spec.ts new file mode 100644 index 0000000000..6efc8a7a1d --- /dev/null +++ b/src/setup/askAndSetDockerOption/askAndSetDockerOption.spec.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock modules +vi.mock('inquirer', () => ({ + default: { + prompt: vi.fn(), + }, +})); + +vi.mock('setup/updateEnvFile/updateEnvFile', () => ({ + default: vi.fn(), +})); + +vi.mock('setup/askForDocker/askForDocker', () => ({ + askForDocker: vi.fn(), +})); + +// Import after mocking +import askAndSetDockerOption from './askAndSetDockerOption'; +import inquirer from 'inquirer'; +import updateEnvFile from 'setup/updateEnvFile/updateEnvFile'; +import { askForDocker } from 'setup/askForDocker/askForDocker'; + +describe('askAndSetDockerOption', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should set up Docker when user selects yes', async () => { + (inquirer.prompt as unknown as jest.Mock).mockResolvedValueOnce({ + useDocker: true, + }); + (askForDocker as jest.Mock).mockResolvedValueOnce(8080); + + await askAndSetDockerOption(); + + expect(updateEnvFile).toHaveBeenCalledWith('USE_DOCKER', 'YES'); + expect(updateEnvFile).toHaveBeenCalledWith('DOCKER_PORT', 8080); + }); + + it('should set up without Docker when user selects no', async () => { + (inquirer.prompt as unknown as jest.Mock).mockResolvedValueOnce({ + useDocker: false, + }); + + await askAndSetDockerOption(); + + expect(updateEnvFile).toHaveBeenCalledWith('USE_DOCKER', 'NO'); + }); + + it('should handle errors when askForDocker fails', async () => { + (inquirer.prompt as unknown as jest.Mock).mockResolvedValueOnce({ + useDocker: true, + }); + (askForDocker as jest.Mock).mockRejectedValueOnce( + new Error('Docker error'), + ); + + await expect(askAndSetDockerOption()).rejects.toThrow('Docker error'); + }); +}); diff --git a/src/setup/askAndSetDockerOption/askAndSetDockerOption.ts b/src/setup/askAndSetDockerOption/askAndSetDockerOption.ts new file mode 100644 index 0000000000..877ef92faf --- /dev/null +++ b/src/setup/askAndSetDockerOption/askAndSetDockerOption.ts @@ -0,0 +1,35 @@ +import inquirer from 'inquirer'; +import updateEnvFile from 'setup/updateEnvFile/updateEnvFile'; +import { askForDocker } from 'setup/askForDocker/askForDocker'; + +// Function to manage Docker setup +const askAndSetDockerOption = async (): Promise => { + const { useDocker } = await inquirer.prompt({ + type: 'confirm', + name: 'useDocker', + message: 'Would you like to set up with Docker?', + default: false, + }); + + if (useDocker) { + console.log('Setting up with Docker...'); + updateEnvFile('USE_DOCKER', 'YES'); + const answers = await askForDocker(); + const DOCKER_PORT_NUMBER = answers; + updateEnvFile('DOCKER_PORT', DOCKER_PORT_NUMBER); + + const DOCKER_NAME = 'talawa-admin'; + console.log(` + + Run the commands below after setup:- + 1. docker build -t ${DOCKER_NAME} . + 2. docker run -d -p ${DOCKER_PORT_NUMBER}:${DOCKER_PORT_NUMBER} ${DOCKER_NAME} + + `); + } else { + console.log('Setting up without Docker...'); + updateEnvFile('USE_DOCKER', 'NO'); + } +}; + +export default askAndSetDockerOption; diff --git a/src/setup/askAndUpdatePort/askAndUpdatePort.ts b/src/setup/askAndUpdatePort/askAndUpdatePort.ts new file mode 100644 index 0000000000..5dfe997288 --- /dev/null +++ b/src/setup/askAndUpdatePort/askAndUpdatePort.ts @@ -0,0 +1,25 @@ +import updateEnvFile from 'setup/updateEnvFile/updateEnvFile'; +import { askForCustomPort } from 'setup/askForCustomPort/askForCustomPort'; +import inquirer from 'inquirer'; + +// Ask and update the custom port +const askAndUpdatePort = async (): Promise => { + const { shouldSetCustomPortResponse } = await inquirer.prompt({ + type: 'confirm', + name: 'shouldSetCustomPortResponse', + message: + 'Would you like to set up a custom port for running Talawa Admin without Docker?', + default: true, + }); + + if (shouldSetCustomPortResponse) { + const customPort = await askForCustomPort(); + if (customPort < 1024 || customPort > 65535) { + throw new Error('Port must be between 1024 and 65535'); + } + + updateEnvFile('PORT', String(customPort)); + } +}; + +export default askAndUpdatePort; diff --git a/src/setup/askAndUpdatePort/askForUpdatePort.spec.ts b/src/setup/askAndUpdatePort/askForUpdatePort.spec.ts new file mode 100644 index 0000000000..3f01605a55 --- /dev/null +++ b/src/setup/askAndUpdatePort/askForUpdatePort.spec.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, vi } from 'vitest'; +import askAndUpdatePort from './askAndUpdatePort'; +import { askForCustomPort } from 'setup/askForCustomPort/askForCustomPort'; +import updateEnvFile from 'setup/updateEnvFile/updateEnvFile'; +import inquirer from 'inquirer'; + +vi.mock('setup/askForCustomPort/askForCustomPort'); +vi.mock('setup/updateEnvFile/updateEnvFile'); +vi.mock('inquirer'); + +describe('askAndUpdatePort', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should update the port when user confirms and provides a valid port', async () => { + // Mock user confirmation and valid port + vi.mocked(inquirer.prompt).mockResolvedValueOnce({ + shouldSetCustomPortResponse: true, + }); + vi.mocked(askForCustomPort).mockResolvedValueOnce(3000); + + // Act + await askAndUpdatePort(); + + // Assert + expect(updateEnvFile).toHaveBeenCalledWith('PORT', '3000'); + }); + + it('should not update the port when user declines', async () => { + // Mock user declining by returning false + vi.mocked(inquirer.prompt).mockResolvedValueOnce({ + shouldSetCustomPortResponse: false, + }); + + // Act + await askAndUpdatePort(); + + // Assert + expect(updateEnvFile).not.toHaveBeenCalled(); + }); + + it('should throw an error for an invalid port', async () => { + // Mock user confirmation and invalid port + vi.mocked(inquirer.prompt).mockResolvedValueOnce({ + shouldSetCustomPortResponse: true, + }); + vi.mocked(askForCustomPort).mockResolvedValueOnce(800); + + // Act & Assert + await expect(askAndUpdatePort()).rejects.toThrowError( + 'Port must be between 1024 and 65535', + ); + }); +}); diff --git a/src/setup/askForDocker/askForDocker.spec.ts b/src/setup/askForDocker/askForDocker.spec.ts new file mode 100644 index 0000000000..a791b67da9 --- /dev/null +++ b/src/setup/askForDocker/askForDocker.spec.ts @@ -0,0 +1,69 @@ +import inquirer from 'inquirer'; +import { askForDocker } from './askForDocker'; +import { describe, test, expect, vi } from 'vitest'; + +vi.mock('inquirer'); + +describe('askForDocker', () => { + test('should return default Docker port if user provides no input', async () => { + vi.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ + dockerAppPort: '4321', + }); + + const result = await askForDocker(); + expect(result).toBe('4321'); + }); + + test('should return user-provided valid port', async () => { + vi.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ + dockerAppPort: '8080', + }); + + const result = await askForDocker(); + expect(result).toBe('8080'); + }); + + test('should reject non-numeric input with validation error', async () => { + // Mock the validation function to simulate an error for non-numeric input + vi.spyOn(inquirer, 'prompt').mockImplementationOnce(() => { + throw new Error( + 'Please enter a valid port number between 1024 and 65535', + ); + }); + + await expect(askForDocker()).rejects.toThrow( + 'Please enter a valid port number between 1024 and 65535', + ); + }); + + test('should reject port outside valid range with validation error', async () => { + // Mock the validation function to simulate an error for an out-of-range port + vi.spyOn(inquirer, 'prompt').mockImplementationOnce(() => { + throw new Error( + 'Please enter a valid port number between 1024 and 65535', + ); + }); + + await expect(askForDocker()).rejects.toThrow( + 'Please enter a valid port number between 1024 and 65535', + ); + }); + + test('should handle edge case: maximum valid port', async () => { + vi.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ + dockerAppPort: '65535', + }); + + const result = await askForDocker(); + expect(result).toBe('65535'); + }); + + test('should handle edge case: minimum valid port', async () => { + vi.spyOn(inquirer, 'prompt').mockResolvedValueOnce({ + dockerAppPort: '1024', + }); + + const result = await askForDocker(); + expect(result).toBe('1024'); + }); +}); diff --git a/src/setup/askForDocker/askForDocker.ts b/src/setup/askForDocker/askForDocker.ts new file mode 100644 index 0000000000..fa926839d4 --- /dev/null +++ b/src/setup/askForDocker/askForDocker.ts @@ -0,0 +1,99 @@ +import inquirer from 'inquirer'; +import { askForTalawaApiUrl } from '../askForTalawaApiUrl/askForTalawaApiUrl'; +import updateEnvFile from '../updateEnvFile/updateEnvFile'; + +// Mock implementation of checkConnection +const checkConnection = async (): Promise => { + // Simulate checking connection + return true; // Replace with actual connection check logic +}; + +// Function to ask for Docker port +export const askForDocker = async (): Promise => { + const answers = await inquirer.prompt<{ dockerAppPort: string }>([ + { + type: 'input', + name: 'dockerAppPort', + message: 'Enter the port to expose Docker (default: 4321):', + default: '4321', + validate: (input: string) => { + const port = Number(input); + if (Number.isNaN(port) || port < 1024 || port > 65535) { + return 'Please enter a valid port number between 1024 and 65535'; + } + return true; + }, + }, + ]); + + return answers.dockerAppPort; +}; + +// Function to ask and update Talawa API URL +export const askAndUpdateTalawaApiUrl = async (): Promise => { + try { + const { shouldSetTalawaApiUrlResponse } = await inquirer.prompt({ + type: 'confirm', + name: 'shouldSetTalawaApiUrlResponse', + message: 'Would you like to set up Talawa API endpoint?', + default: true, + }); + + if (shouldSetTalawaApiUrlResponse) { + let endpoint = ''; + let isConnected = false; + let retryCount = 0; + const MAX_RETRIES = 3; + while (!isConnected && retryCount < MAX_RETRIES) { + try { + endpoint = await askForTalawaApiUrl(); + const url = new URL(endpoint); + if (!['http:', 'https:'].includes(url.protocol)) { + throw new Error('Invalid URL protocol. Must be http or https'); + } + isConnected = await checkConnection(); + if (!isConnected) { + console.log( + `Connection attempt ${retryCount + 1}/${MAX_RETRIES} failed`, + ); + } + } catch (error) { + console.error('Error checking connection:', error); + isConnected = false; + } + retryCount++; + } + if (!isConnected) { + throw new Error( + 'Failed to establish connection after maximum retry attempts', + ); + } + updateEnvFile('REACT_APP_TALAWA_URL', endpoint); + const websocketUrl = endpoint.replace(/^http(s)?:\/\//, 'ws$1://'); + try { + const wsUrl = new URL(websocketUrl); + if (!['ws:', 'wss:'].includes(wsUrl.protocol)) { + throw new Error('Invalid WebSocket protocol'); + } + updateEnvFile('REACT_APP_BACKEND_WEBSOCKET_URL', websocketUrl); + } catch { + throw new Error('Invalid WebSocket URL generated: '); + } + + if (endpoint.includes('localhost')) { + const dockerUrl = endpoint.replace('localhost', 'host.docker.internal'); + try { + const url = new URL(dockerUrl); + if (!['http:', 'https:'].includes(url.protocol)) { + throw new Error('Invalid Docker URL protocol'); + } + } catch { + throw new Error('Invalid Docker URL generated'); + } + updateEnvFile('REACT_APP_DOCKER_TALAWA_URL', dockerUrl); + } + } + } catch (error) { + console.error('Error setting up Talawa API URL:', error); + } +}; diff --git a/src/setup/updateEnvFile/updateEnvFile.spec.ts b/src/setup/updateEnvFile/updateEnvFile.spec.ts new file mode 100644 index 0000000000..c3ff4a5242 --- /dev/null +++ b/src/setup/updateEnvFile/updateEnvFile.spec.ts @@ -0,0 +1,88 @@ +import fs from 'fs'; +import updateEnvFile from './updateEnvFile'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; + +/** + * Unit tests for the `updateEnvFile` function. + * + * These tests verify: + * - Updating an existing key in the `.env` file. + * - Appending a new key if it does not exist in the `.env` file. + * - Handling an empty `.env` file. + */ + +vi.mock('fs'); + +describe('updateEnvFile', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('should update an existing key in the .env file', () => { + const envContent = 'EXISTING_KEY=old_value\nANOTHER_KEY=another_value\n'; + const updatedEnvContent = + 'EXISTING_KEY=new_value\nANOTHER_KEY=another_value\n'; + + // Mock file system read and write operations + vi.spyOn(fs, 'readFileSync').mockReturnValueOnce(envContent); + const writeMock = vi.spyOn(fs, 'writeFileSync'); + + updateEnvFile('EXISTING_KEY', 'new_value'); + + // Verify that the updated content is written to the file + expect(writeMock).toHaveBeenCalledWith('.env', updatedEnvContent, 'utf8'); + }); + + it('should append a new key if it does not exist in the .env file', () => { + const envContent = 'EXISTING_KEY=existing_value\n'; + const newKey = 'NEW_KEY=new_value'; + + // Mock file system read and append operations + vi.spyOn(fs, 'readFileSync').mockReturnValueOnce(envContent); + const appendMock = vi.spyOn(fs, 'appendFileSync'); + + updateEnvFile('NEW_KEY', 'new_value'); + + // Verify that the new key is appended to the file + expect(appendMock).toHaveBeenCalledWith('.env', `\n${newKey}`, 'utf8'); + }); + + it('should handle an empty .env file and append the new key', () => { + const envContent = ''; + const newKey = 'NEW_KEY=new_value'; + + // Mock file system read and append operations + vi.spyOn(fs, 'readFileSync').mockReturnValueOnce(envContent); + const appendMock = vi.spyOn(fs, 'appendFileSync'); + + updateEnvFile('NEW_KEY', 'new_value'); + + // Verify that the new key is appended to the file + expect(appendMock).toHaveBeenCalledWith('.env', `\n${newKey}`, 'utf8'); + }); + + it('should not throw errors when .env file does not exist and create the file with the key', () => { + const newKey = 'NEW_KEY=new_value'; + + const appendMock = vi.spyOn(fs, 'appendFileSync'); + + updateEnvFile('NEW_KEY', 'new_value'); + + // Verify that the new key is appended to the file + expect(appendMock).toHaveBeenCalledWith('.env', `\n${newKey}`, 'utf8'); + }); + + it('should correctly handle keys with special characters', () => { + const envContent = 'EXISTING_KEY=old_value\n'; + const updatedEnvContent = 'EXISTING_KEY=value_with=special_characters\n'; + + // Mock file system read and write operations + vi.spyOn(fs, 'readFileSync').mockReturnValueOnce(envContent); + const writeMock = vi.spyOn(fs, 'writeFileSync'); + + updateEnvFile('EXISTING_KEY', 'value_with=special_characters'); + + // Verify that the updated content is written to the file + expect(writeMock).toHaveBeenCalledWith('.env', updatedEnvContent, 'utf8'); + }); +}); diff --git a/src/setup/updateEnvFile/updateEnvFile.ts b/src/setup/updateEnvFile/updateEnvFile.ts new file mode 100644 index 0000000000..93ba918749 --- /dev/null +++ b/src/setup/updateEnvFile/updateEnvFile.ts @@ -0,0 +1,21 @@ +import fs from 'fs'; + +const updateEnvFile = (key: string, value: string): void => { + try { + const currentEnvContent = fs.readFileSync('.env', 'utf8'); + const keyRegex = new RegExp(`^${key}=.*$`, 'm'); + if (keyRegex.test(currentEnvContent)) { + const updatedEnvContent = currentEnvContent.replace( + keyRegex, + `${key}=${value}`, + ); + fs.writeFileSync('.env', updatedEnvContent, 'utf8'); + } else { + fs.appendFileSync('.env', `\n${key}=${value}`, 'utf8'); + } + } catch (error) { + console.error('Error updating the .env file:', error); + } +}; + +export default updateEnvFile; diff --git a/src/setup/validateRecaptcha/validateRecaptcha.test.ts b/src/setup/validateRecaptcha/validateRecaptcha.spec.ts similarity index 95% rename from src/setup/validateRecaptcha/validateRecaptcha.test.ts rename to src/setup/validateRecaptcha/validateRecaptcha.spec.ts index c77c9ed62b..cd1ff7125a 100644 --- a/src/setup/validateRecaptcha/validateRecaptcha.test.ts +++ b/src/setup/validateRecaptcha/validateRecaptcha.spec.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { validateRecaptcha } from './validateRecaptcha'; describe('validateRecaptcha', () => {