diff --git a/setup.ts b/setup.ts index 760de987aa7..0d42f47d5ad 100644 --- a/setup.ts +++ b/setup.ts @@ -2,7 +2,7 @@ import * as cryptolib from "crypto"; import dotenv from "dotenv"; import fs from "fs"; import inquirer from "inquirer"; -import path from "path"; +import path, { dirname } from "path"; import type { ExecException } from "child_process"; import { exec, spawn, execSync } from "child_process"; import { MongoClient } from "mongodb"; @@ -31,6 +31,7 @@ import { verifySmtpConnection } from "@setup/verifySmtpConnection"; import { loadDefaultOrganiation } from "@utilities/loadDefaultOrg"; import { isMinioInstalled } from "@setup/isMinioInstalled"; import { installMinio } from "@setup/installMinio"; +import { fileURLToPath } from "url"; dotenv.config(); @@ -139,12 +140,13 @@ export async function accessAndRefreshTokens( } } -function transactionLogPath(logPath: string | null): void { +export function transactionLogPath(logPath: string | null): void { const config = dotenv.parse(fs.readFileSync(".env")); + const currDir = dirname(fileURLToPath(import.meta.url)); config.LOG = "true"; if (!logPath) { // Check if the logs/transaction.log file exists, if not, create it - const defaultLogPath = path.resolve(__dirname, "logs"); + const defaultLogPath = path.resolve(currDir, "logs"); const defaultLogFile = path.join(defaultLogPath, "transaction.log"); if (!fs.existsSync(defaultLogPath)) { console.log("Creating logs/transaction.log file..."); @@ -154,7 +156,7 @@ function transactionLogPath(logPath: string | null): void { config.LOG_PATH = defaultLogFile; } else { // Remove the logs files, if exists - const logsDirPath = path.resolve(__dirname, "logs"); + const logsDirPath = path.resolve(currDir, "logs"); if (fs.existsSync(logsDirPath)) { fs.readdirSync(logsDirPath).forEach((file: string) => { if (file !== "README.md") { @@ -167,12 +169,12 @@ function transactionLogPath(logPath: string | null): void { } } -async function askForTransactionLogPath(): Promise { +export async function askForTransactionLogPath(): Promise { let logPath: string | null = null; let isValidPath = false; while (!isValidPath) { - const response = await inquirer.prompt([ + const { logpath } = await inquirer.prompt([ { type: "input", name: "logPath", @@ -180,7 +182,7 @@ async function askForTransactionLogPath(): Promise { default: null, }, ]); - logPath = response.logPath; + logPath = logpath; if (logPath && fs.existsSync(logPath)) { try { @@ -221,7 +223,7 @@ export async function wipeExistingData(url: string): Promise { console.log("All existing data has been deleted."); } } catch { - console.error("Could not connect to database to check for data"); + throw new Error("Could not connect to database to check for data"); } client.close(); // return shouldImport; @@ -237,6 +239,7 @@ export async function wipeExistingData(url: string): Promise { export async function checkDb(url: string): Promise { let dbEmpty = false; const client = new MongoClient(`${url}`); + console.log(`Connecting to database...`); try { await client.connect(); const db = client.db(); @@ -248,7 +251,7 @@ export async function checkDb(url: string): Promise { dbEmpty = true; } } catch { - console.error("Could not connect to database to check for data"); + throw new Error("Could not connect to database to check for data"); } client.close(); return dbEmpty; @@ -267,7 +270,7 @@ export async function importData(): Promise { console.log("Importing sample data..."); if (process.env.NODE_ENV !== "test") { - await exec( + exec( "npm run import:sample-data", (error: ExecException | null, stdout: string, stderr: string) => { if (error) { @@ -278,7 +281,7 @@ export async function importData(): Promise { console.error(`Error: ${stderr}`); abort(); } - console.log(`Output: ${stdout}`); + console.log(`\nOutput: ${stdout}`); }, ); } @@ -465,7 +468,7 @@ export async function mongoDB(): Promise { For Docker setup */ -function getDockerComposeCommand(): { command: string; args: string[] } { +export function getDockerComposeCommand(): { command: string; args: string[] } { let dockerComposeCmd = "docker-compose"; // Default to v1 let args = ["-f", "docker-compose.dev.yaml", "up", "--build", "-d"]; @@ -484,7 +487,7 @@ function getDockerComposeCommand(): { command: string; args: string[] } { } const DOCKER_COMPOSE_TIMEOUT_MS = 300000; -async function runDockerComposeWithLogs(): Promise { +export async function runDockerComposeWithLogs(): Promise { // Check if Docker daemon is running try { await new Promise((resolve, reject) => { @@ -912,6 +915,159 @@ export async function configureMinio( console.log("[MINIO] MinIO configuration added successfully.\n"); } +export async function dataImportWithoutDocker( + checkDb: (url: string) => Promise, + wipeExistingData: (url: string) => Promise, + importDefaultData: () => Promise, + importData: () => Promise, +): Promise { + if (!process.env.MONGO_DB_URL) { + console.log("Couldn't find mongodb url"); + return; + } + + let isDbEmpty: boolean; + try { + isDbEmpty = await checkDb(process.env.MONGO_DB_URL); + } catch (error) { + throw new Error( + `Failed to check database: ${error instanceof Error ? error.message : String(error)}`, + ); + } + if (!isDbEmpty) { + const { shouldOverwriteData } = await inquirer.prompt({ + type: "confirm", + name: "shouldOverwriteData", + message: "Do you want to overwrite the existing data?", + default: false, + }); + if (shouldOverwriteData) { + const { overwriteDefaultData } = await inquirer.prompt({ + type: "confirm", + name: "overwriteDefaultData", + message: + "Do you want to DESTROY the existing data, replacing it with the default data required for a fresh production system?", + default: false, + }); + if (overwriteDefaultData) { + await wipeExistingData(process.env.MONGO_DB_URL); + await importDefaultData(); + } else { + const { overwriteSampleData } = await inquirer.prompt({ + type: "confirm", + name: "overwriteSampleData", + message: + "Do you want to DESTROY the existing data, replacing it with data suitable for testing and evaluation?", + default: false, + }); + if (overwriteSampleData) { + await wipeExistingData(process.env.MONGO_DB_URL); + await importData(); + } + } + } + } else { + const { shouldImportDefaultData } = await inquirer.prompt({ + type: "confirm", + name: "shouldImportDefaultData", + message: + "Do you want to import default data required for a fresh production system?", + default: false, + }); + if (shouldImportDefaultData) { + await importDefaultData(); + } else { + const { shouldImportSampleData } = await inquirer.prompt({ + type: "confirm", + name: "shouldImportSampleData", + message: + "Do you want to import data suitable for testing and evaluation?", + default: false, + }); + if (shouldImportSampleData) { + await importData(); + } + } + } +} + +async function connectToDatabase(): Promise { + console.log("Waiting for mongoDB to be ready..."); + let isConnected = false; + const maxRetries = 30; // 30 seconds timeout + let retryCount = 0; + while (!isConnected) { + if (retryCount >= maxRetries) { + throw new Error( + "Timed out waiting for MongoDB to be ready after 30 seconds", + ); + } + try { + const client = new MongoClient(process.env.MONGO_DB_URL as string); + await client.connect(); + await client.db().command({ ping: 1 }); + client.close(); + isConnected = true; + console.log("MongoDB is ready!"); + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + console.log( + `Waiting for MongoDB to be ready... Retry ${retryCount + 1}/${maxRetries}. Details: ${error}`, + ); + await new Promise((resolve) => setTimeout(resolve, 1000)); + retryCount++; + } + } + return Promise.resolve(); +} + +export async function dataImportWithDocker( + runDockerComposeWithLogs: () => Promise, + importDefaultData: () => Promise, + importData: () => Promise, + connectToDatabase: () => Promise, +): Promise { + const { shouldStartDockerContainers } = await inquirer.prompt({ + type: "confirm", + name: "shouldStartDockerContainers", + message: "Do you want to start the Docker containers now?", + default: true, + }); + if (shouldStartDockerContainers) { + console.log("Starting docker container..."); + try { + await runDockerComposeWithLogs(); + console.log("Docker containers have been built successfully!"); + // Wait for mongoDB to be ready + await connectToDatabase(); + + const { shouldImportDefaultData } = await inquirer.prompt({ + type: "confirm", + name: "shouldImportDefaultData", + message: + "Do you want to import default data required for a fresh production system?", + default: false, + }); + if (shouldImportDefaultData) { + await importDefaultData(); + } else { + const { shouldImportSampleData } = await inquirer.prompt({ + type: "confirm", + name: "shouldImportSampleData", + message: + "Do you want to import data suitable for testing and evaluation?", + default: false, + }); + if (shouldImportSampleData) { + await importData(); + } + } + } catch (err) { + console.log("Some error occurred: " + err); + } + } +} + /** * The main function sets up the Talawa API by prompting the user to configure various environment * variables and import sample data if desired. @@ -1223,120 +1379,24 @@ async function main(): Promise { await setImageUploadSize(imageSizeLimit * 1000); if (!isDockerInstallation) { - if (!process.env.MONGO_DB_URL) { - console.log("Couldn't find mongodb url"); - return; - } - const isDbEmpty = await checkDb(process.env.MONGO_DB_URL); - if (!isDbEmpty) { - const { shouldOverwriteData } = await inquirer.prompt({ - type: "confirm", - name: "shouldOverwriteData", - message: "Do you want to overwrite the existing data?", - default: false, - }); - if (shouldOverwriteData) { - await wipeExistingData(process.env.MONGO_DB_URL); - const { overwriteDefaultData } = await inquirer.prompt({ - type: "confirm", - name: "overwriteDefaultData", - message: - "Do you want to import the required default data to start using Talawa in a production environment?", - default: false, - }); - if (overwriteDefaultData) { - await importDefaultData(); - } else { - const { overwriteSampleData } = await inquirer.prompt({ - type: "confirm", - name: "overwriteSampleData", - message: - "Do you want to import Talawa sample data for testing and evaluation purposes?", - default: false, - }); - if (overwriteSampleData) { - await importData(); - } - } - } - } else { - const { shouldImportSampleData } = await inquirer.prompt({ - type: "confirm", - name: "shouldImportSampleData", - message: - "Do you want to import Talawa sample data for testing and evaluation purposes?", - default: false, - }); - await wipeExistingData(process.env.MONGO_DB_URL); - if (shouldImportSampleData) { - await importData(); - } else { - await importDefaultData(); - } - } + await dataImportWithoutDocker( + checkDb, + wipeExistingData, + importDefaultData, + importData, + ); + } else { + await dataImportWithDocker( + runDockerComposeWithLogs, + importDefaultData, + importData, + connectToDatabase, + ); } console.log( "\nCongratulations! Talawa API has been successfully setup! 🥂🎉", ); - - const { shouldStartDockerContainers } = await inquirer.prompt({ - type: "confirm", - name: "shouldStartDockerContainers", - message: "Do you want to start the Docker containers now?", - default: true, - }); - - const { shouldImportSampleData } = await inquirer.prompt({ - type: "confirm", - name: "shouldImportSampleData", - message: - "Do you want to import Talawa sample data for testing and evaluation purposes?", - default: true, - }); - - if (isDockerInstallation) { - if (shouldStartDockerContainers) { - console.log("Starting docker container..."); - try { - await runDockerComposeWithLogs(); - console.log("Docker containers have been built successfully!"); - // Wait for mongoDB to be ready - console.log("Waiting for mongoDB to be ready..."); - let isConnected = false; - const maxRetries = 30; // 30 seconds timeout - let retryCount = 0; - while (!isConnected) { - if (retryCount >= maxRetries) { - throw new Error( - "Timed out waiting for MongoDB to be ready after 30 seconds", - ); - } - try { - const client = new MongoClient(process.env.MONGO_DB_URL as string); - await client.connect(); - await client.db().command({ ping: 1 }); - client.close(); - isConnected = true; - console.log("MongoDB is ready!"); - } catch (err) { - const error = err instanceof Error ? err.message : String(err); - console.log( - `Waiting for MongoDB to be ready... Retry ${retryCount + 1}/${maxRetries}. Details: ${error}`, - ); - await new Promise((resolve) => setTimeout(resolve, 1000)); - retryCount++; - } - } - - if (shouldImportSampleData) { - await importData(); - } - } catch (err) { - console.log("Some error occurred: " + err); - } - } - } } main(); diff --git a/tests/setup/dataImportFlow.spec.ts b/tests/setup/dataImportFlow.spec.ts new file mode 100644 index 00000000000..674a6a06f24 --- /dev/null +++ b/tests/setup/dataImportFlow.spec.ts @@ -0,0 +1,579 @@ +import { describe, it, vi, expect, beforeEach, afterEach } from "vitest"; +import { dataImportWithoutDocker, dataImportWithDocker } from "../../setup"; +import inquirer from "inquirer"; + +describe("Data Importation Without Docker", () => { + const mockEnv = { ...process.env }; // Backup the environment variables + + beforeEach(() => { + process.env.MONGO_DB_URL = "mongodb://localhost:27017/sample-table"; + vi.resetAllMocks(); + }); + + afterEach(() => { + process.env = { ...mockEnv }; // Restore environment variables + }); + + it("should import default data if the database is empty and user opts to import default data", async () => { + const checkDbMock = vi + .fn() + .mockImplementation(async (): Promise => { + return true; + }); + const wipeExistingDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + shouldImportDefaultData: true, + }); + + await dataImportWithoutDocker( + checkDbMock, + wipeExistingDataMock, + importDefaultDataMock, + importDataMock, + ); + expect(checkDbMock).toBeCalled(); + expect(wipeExistingDataMock).not.toBeCalled(); + expect(importDefaultDataMock).toBeCalled(); + expect(importDataMock).not.toBeCalled(); + }); + + it("should import sample data if the database is empty and user opts not to import default data but import sample data", async () => { + const checkDbMock = vi + .fn() + .mockImplementation(async (): Promise => { + return true; + }); + const wipeExistingDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt") + .mockResolvedValueOnce({ shouldImportDefaultData: false }) + .mockResolvedValueOnce({ shouldImportSampleData: true }); + + await dataImportWithoutDocker( + checkDbMock, + wipeExistingDataMock, + importDefaultDataMock, + importDataMock, + ); + expect(checkDbMock).toBeCalled(); + expect(wipeExistingDataMock).not.toBeCalled(); + expect(importDefaultDataMock).not.toBeCalled(); + expect(importDataMock).toBeCalled(); + }); + + it("should do no-op if the database is empty and user imports neither default nor sample data", async () => { + const checkDbMock = vi + .fn() + .mockImplementation(async (): Promise => { + return true; + }); + const wipeExistingDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt") + .mockResolvedValueOnce({ shouldImportDefaultData: false }) + .mockResolvedValueOnce({ shouldImportSampleData: false }); + + await dataImportWithoutDocker( + checkDbMock, + wipeExistingDataMock, + importDefaultDataMock, + importDataMock, + ); + expect(checkDbMock).toBeCalled(); + expect(wipeExistingDataMock).not.toBeCalled(); + expect(importDefaultDataMock).not.toBeCalled(); + expect(importDataMock).not.toBeCalled(); + }); + + it("should import sample data if the database is not empty and user opts to overwrite and import sample data", async () => { + const checkDbMock = vi + .fn() + .mockImplementation(async (): Promise => { + return false; + }); + const wipeExistingDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt") + .mockResolvedValueOnce({ shouldOverwriteData: true }) + .mockResolvedValueOnce({ overwriteDefaultData: false }) + .mockResolvedValueOnce({ overwriteSampleData: true }); + + await dataImportWithoutDocker( + checkDbMock, + wipeExistingDataMock, + importDefaultDataMock, + importDataMock, + ); + expect(checkDbMock).toBeCalled(); + expect(wipeExistingDataMock).toBeCalled(); + expect(importDefaultDataMock).not.toBeCalled(); + expect(importDataMock).toBeCalled(); + }); + + it("should import default data if the database is not empty and user opts to overwrite and import default data", async () => { + const checkDbMock = vi + .fn() + .mockImplementation(async (): Promise => { + return false; + }); + const wipeExistingDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt") + .mockResolvedValueOnce({ shouldOverwriteData: true }) + .mockResolvedValueOnce({ overwriteDefaultData: true }); + + await dataImportWithoutDocker( + checkDbMock, + wipeExistingDataMock, + importDefaultDataMock, + importDataMock, + ); + expect(checkDbMock).toBeCalled(); + expect(wipeExistingDataMock).toBeCalled(); + expect(importDefaultDataMock).toBeCalled(); + expect(importDataMock).not.toBeCalled(); + }); + + it("should do no-op if db not empty and user imports neither default nor sample data", async () => { + const checkDbMock = vi + .fn() + .mockImplementation(async (): Promise => { + return false; + }); + const wipeExistingDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt") + .mockResolvedValueOnce({ shouldOverwriteData: true }) + .mockResolvedValueOnce({ overwriteDefaultData: false }) + .mockResolvedValueOnce({ overwriteSampleData: false }); + + await dataImportWithoutDocker( + checkDbMock, + wipeExistingDataMock, + importDefaultDataMock, + importDataMock, + ); + expect(checkDbMock).toBeCalled(); + expect(wipeExistingDataMock).not.toBeCalled(); + expect(importDefaultDataMock).not.toBeCalled(); + expect(importDataMock).not.toBeCalled(); + }); + + it("should do no-op if db not empty and user opts not to overwrite", async () => { + const checkDbMock = vi + .fn() + .mockImplementation(async (): Promise => { + return false; + }); + const wipeExistingDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + shouldOverwriteData: false, + }); + + await dataImportWithoutDocker( + checkDbMock, + wipeExistingDataMock, + importDefaultDataMock, + importDataMock, + ); + expect(checkDbMock).toBeCalled(); + expect(wipeExistingDataMock).not.toBeCalled(); + expect(importDefaultDataMock).not.toBeCalled(); + expect(importDataMock).not.toBeCalled(); + }); + + it("should handle database connection failure gracefully", async () => { + const checkDbMock = vi + .fn() + .mockImplementation(async (): Promise => { + return false; + }); + const wipeExistingDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const errorMessage = "Database connection failed"; + checkDbMock.mockRejectedValueOnce(new Error(errorMessage)); + + await expect( + dataImportWithoutDocker( + checkDbMock, + wipeExistingDataMock, + importDefaultDataMock, + importDataMock, + ), + ).rejects.toThrow(errorMessage); + + expect(wipeExistingDataMock).not.toBeCalled(); + expect(importDefaultDataMock).not.toBeCalled(); + expect(importDataMock).not.toBeCalled(); + }); +}); + +describe("Data Importation With Docker", () => { + const mockEnv = { ...process.env }; // Backup the environment variables + + beforeEach(() => { + process.env.MONGO_DB_URL = "mongodb://localhost:27017/sample-table"; + vi.resetAllMocks(); + }); + + afterEach(() => { + process.env = { ...mockEnv }; // Restore environment variables + }); + + it("should do no-op if user opts not to start containers", async () => { + const runDockerComposeMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const connectDatabaseMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + shouldStartDockerContainers: false, + }); + + await dataImportWithDocker( + runDockerComposeMock, + importDefaultDataMock, + importDataMock, + connectDatabaseMock, + ); + expect(runDockerComposeMock).not.toBeCalled(); + expect(connectDatabaseMock).not.toBeCalled(); + expect(importDefaultDataMock).not.toBeCalled(); + expect(importDataMock).not.toBeCalled(); + }); + + it("should terminate execution if error is encountered while starting containers", async () => { + const runDockerComposeMock = vi + .fn() + .mockImplementation(async (): Promise => { + throw new Error("Error starting containers"); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const connectDatabaseMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + shouldStartDockerContainers: true, + }); + + await dataImportWithDocker( + runDockerComposeMock, + importDefaultDataMock, + importDataMock, + connectDatabaseMock, + ); + expect(runDockerComposeMock).toBeCalled(); + expect(connectDatabaseMock).not.toBeCalled(); + expect(importDefaultDataMock).not.toBeCalled(); + expect(importDataMock).not.toBeCalled(); + }); + + it("should terminate execution if error is encountered while connecting to database", async () => { + const runDockerComposeMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const connectDatabaseMock = vi + .fn() + .mockImplementation(async (): Promise => { + throw new Error("Error starting containers"); + }); + vi.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + shouldStartDockerContainers: true, + }); + + await dataImportWithDocker( + runDockerComposeMock, + importDefaultDataMock, + importDataMock, + connectDatabaseMock, + ); + expect(runDockerComposeMock).toBeCalled(); + expect(connectDatabaseMock).toBeCalled(); + expect(importDefaultDataMock).not.toBeCalled(); + expect(importDataMock).not.toBeCalled(); + }); + + it("should import default data if user opts to import default data", async () => { + const runDockerComposeMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const connectDatabaseMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt") + .mockResolvedValueOnce({ shouldStartDockerContainers: true }) + .mockResolvedValueOnce({ shouldImportDefaultData: true }); + + await dataImportWithDocker( + runDockerComposeMock, + importDefaultDataMock, + importDataMock, + connectDatabaseMock, + ); + expect(runDockerComposeMock).toBeCalled(); + expect(connectDatabaseMock).toBeCalled(); + expect(importDefaultDataMock).toBeCalled(); + expect(importDataMock).not.toBeCalled(); + }); + + it("should import sample data if user opts to import sample data", async () => { + const runDockerComposeMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const connectDatabaseMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt") + .mockResolvedValueOnce({ shouldStartDockerContainers: true }) + .mockResolvedValueOnce({ shouldImportDefaultData: false }) + .mockResolvedValueOnce({ shouldImportSampleData: true }); + + await dataImportWithDocker( + runDockerComposeMock, + importDefaultDataMock, + importDataMock, + connectDatabaseMock, + ); + expect(runDockerComposeMock).toBeCalled(); + expect(connectDatabaseMock).toBeCalled(); + expect(importDefaultDataMock).not.toBeCalled(); + expect(importDataMock).toBeCalled(); + }); + + it("should do no-op if user opts to import neither sample data nor default data", async () => { + const runDockerComposeMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const importDefaultDataMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + const connectDatabaseMock = vi + .fn() + .mockImplementation(async (): Promise => { + return Promise.resolve(); + }); + vi.spyOn(inquirer, "prompt") + .mockResolvedValueOnce({ shouldStartDockerContainers: true }) + .mockResolvedValueOnce({ shouldImportDefaultData: false }) + .mockResolvedValueOnce({ shouldImportSampleData: false }); + + await dataImportWithDocker( + runDockerComposeMock, + importDefaultDataMock, + importDataMock, + connectDatabaseMock, + ); + expect(runDockerComposeMock).toBeCalled(); + expect(connectDatabaseMock).toBeCalled(); + expect(importDefaultDataMock).not.toBeCalled(); + expect(importDataMock).not.toBeCalled(); + }); + + it("should handle Docker container startup timeout", async () => { + const runDockerComposeMock = vi.fn().mockImplementation(async () => { + return new Promise((_, reject) => { + setTimeout(() => { + reject(new Error("Docker compose operation timed out")); + }, 3000); + }); + }); + const importDataMock = vi.fn(); + const importDefaultDataMock = vi.fn(); + const connectDatabaseMock = vi.fn(); + + vi.spyOn(inquirer, "prompt").mockResolvedValueOnce({ + shouldStartDockerContainers: true, + }); + + await dataImportWithDocker( + runDockerComposeMock, + importDefaultDataMock, + importDataMock, + connectDatabaseMock, + ); + + expect(runDockerComposeMock).toBeCalled(); + expect(connectDatabaseMock).not.toBeCalled(); + }); +}); diff --git a/tests/setup/transactionLog.spec.ts b/tests/setup/transactionLog.spec.ts new file mode 100644 index 00000000000..c1d785db752 --- /dev/null +++ b/tests/setup/transactionLog.spec.ts @@ -0,0 +1,46 @@ +import { it, expect, vi, describe, beforeEach } from "vitest"; +import inquirer from "inquirer"; +import fs from "fs"; +import { askForTransactionLogPath } from "../../setup"; + +/* + Test Case 1: + Description: function askForSuperAdminEmail should return email as entered. + Expected Behavior: When the askForSuperAdminEmail function is called, it should prompt the user to enter an email address, and upon entering, it should return the entered email address. + + Test Case 2: + Description: superAdmin prompts user, updates .env_test, and does not throw errors. + Expected Behavior: When the superAdmin function is called, it should prompt the user to enter an email address for the last resort super admin, update the .env_test file with the entered email address, and it should not throw any errors during execution. + + Note: Each test case involves mocking user input using inquirer, executing the relevant function, and asserting the expected behavior by checking the returned email or the updated .env_test file. +*/ +describe("Setup -> transactionLogPath", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should return the transaction log path as entered by the user", async () => { + const currDir = process.cwd(); + const testPath = `${currDir}/tests/setup/test-transaction.log`; + vi.spyOn(inquirer, "prompt").mockResolvedValueOnce({ logpath: testPath }); + vi.spyOn(fs, "existsSync").mockReturnValueOnce(true); + vi.spyOn(fs, "accessSync").mockReturnValueOnce(undefined); + + const result = await askForTransactionLogPath(); + expect(result).toEqual(testPath); + }); + + it("should handle invalid path and prompt user again", async () => { + const currDir = process.cwd(); + const testPath = `${currDir}/tests/setup/test-transaction.log`; + vi.spyOn(inquirer, "prompt") + .mockResolvedValueOnce({ logpath: "invalidpath" }) + .mockResolvedValueOnce({ logpath: testPath }); + vi.spyOn(fs, "existsSync") + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + vi.spyOn(fs, "accessSync").mockReturnValueOnce(undefined); + const result = await askForTransactionLogPath(); + expect(result).toEqual(testPath); + }); +});