@tshifhiwa/ohrm-ui-automation-framework
Version:
Playwright and TypeScript–based test automation framework for validating core UI features and workflows of the OrangeHRM demo application.
419 lines (375 loc) • 15.1 kB
text/typescript
import { AsyncFileManager } from "../../../fileManager/asyncFileManager.js";
import SecretFilePathResolver from "../resolvers/secretFilePathResolver.js";
import { FileEncoding } from "../../../fileManager/internal/file-encoding.enum.js";
import ErrorHandler from "../../../errorHandling/errorHandler.js";
import logger from "../../../logger/loggerManager.js";
export default class SecretFileManager {
// File lock registry to track ongoing operations
private static readonly fileLocks = new Map<string, Promise<void>>();
/**
* Stores or updates an environment key-value pair in the base environment file
* @param keyName - The name of the environment key
* @param value - The value to store for the key
* @param options - Optional parameters to control behavior
* @param options.skipIfExists - If true, skips storing if key already exists (default: false)
* @returns Promise resolving to boolean indicating if the key was actually stored/updated
*/
public static async storeEnvironmentKey(
keyName: string,
value: string,
options: { skipIfExists?: boolean } = {},
): Promise<boolean> {
return this.storeKeyInFile(SecretFilePathResolver.getSecretFilePath(), keyName, value, options);
}
/**
* Stores or updates an environment key-value pair in any environment file
* @param filePath - Path to the environment file
* @param keyName - The name of the environment key
* @param value - The value to store for the key
* @param options - Optional parameters to control behavior
* @param options.skipIfExists - If true, skips storing if key already exists (default: false)
* @returns Promise resolving to boolean indicating if the key was actually stored/updated
*/
public static async storeKeyInFile(
filePath: string,
keyName: string,
value: string,
options: { skipIfExists?: boolean } = {},
): Promise<boolean> {
return this.executeWithFileLock(filePath, async () => {
try {
const fileContent = await this.readEnvFileContent(filePath);
if (this.shouldSkipExistingKey(fileContent, keyName, options.skipIfExists)) {
return false;
}
const updatedContent = this.updateEnvironmentKey(fileContent, keyName, value);
await this.writeUpdatedEnvironmentFile(filePath, updatedContent, keyName);
return true;
} catch (error) {
ErrorHandler.captureError(
error,
"storeKeyInFile",
`Failed to store key "${keyName}" value in "${filePath}"`,
);
throw error;
}
});
}
/**
* Retrieves the value for a specific environment key from any file
* @param filePath - Path to the environment file
* @param keyName - The name of the environment key to retrieve
* @returns Promise resolving to the value of the key, or undefined if not found
*/
public static async getKeyValue(filePath: string, keyName: string): Promise<string | undefined> {
// Wait for any ongoing write operations to complete before reading
await this.waitForFileLock(filePath);
try {
const fileContent = await this.readEnvFileContent(filePath);
return this.extractKeyValue(fileContent, keyName);
} catch (error) {
ErrorHandler.captureError(
error,
"getKeyValue",
`Failed to retrieve value for key "${keyName}" from "${filePath}".`,
);
throw error;
}
}
/**
* Verifies if a secret key exists in the environment file
* @param secretKeyName - The name of the secret key to verify (e.g., "SECRET_KEY_DEV")
* @param filePath - Optional path to environment file, defaults to base environment file
* @returns Promise resolving to boolean indicating if the secret key exists and has a value
*/
public static async verifySecretKeyExists(
secretKeyName: string,
filePath?: string,
): Promise<boolean> {
try {
const targetFilePath = this.resolveFilePath(filePath);
const secretKeyValue = await this.getKeyValue(targetFilePath, secretKeyName);
// Check if key exists and has a non-empty value
const keyExists = secretKeyValue !== undefined && secretKeyValue.trim() !== "";
if (keyExists) {
logger.info(`Secret key "${secretKeyName}" verified successfully`);
} else {
logger.warn(`Secret key "${secretKeyName}" not found or empty in environment file`);
}
return keyExists;
} catch (error) {
ErrorHandler.captureError(
error,
"verifySecretKeyExists",
`Failed to verify secret key "${secretKeyName}"`,
);
return false;
}
}
public static async ensureEnvFileExists(filePath: string): Promise<void> {
const fileExists = await AsyncFileManager.doesFileExist(filePath);
if (!fileExists) {
logger.warn(`Environment file not found at "${filePath}". Creating new empty file.`);
await AsyncFileManager.writeFile(filePath, "", "Created empty environment file");
}
}
public static async readEnvFileContent(filePath: string): Promise<string> {
try {
await this.ensureEnvFileExists(filePath);
return await AsyncFileManager.readFile(filePath, FileEncoding.UTF8);
} catch (error) {
ErrorHandler.captureError(
error,
"readEnvFileContent",
`Failed to read environment file at "${filePath}"`,
);
throw error;
}
}
public static handleMissingEnvFile(filePath: string): void {
const isEncryptionEnabled = (process.env.PLAYWRIGHT_GREP || "").includes("@full-encryption");
if (isEncryptionEnabled) {
return;
}
this.logMissingFileWarning(filePath);
}
/**
* Ensures secret key exists before proceeding with encryption operations
* @param secretKeyName - The name of the secret key to verify
* @param filePath - Optional path to environment file
* @throws Error if secret key doesn't exist
*/
public static async ensureSecretKeyExists(
secretKeyName: string,
filePath?: string,
): Promise<void> {
const targetFilePath = this.resolveFilePath(filePath);
// Wait for any ongoing file operations to complete
await this.waitForFileLock(targetFilePath);
const keyExists = await this.verifySecretKeyExists(secretKeyName, filePath);
if (!keyExists) {
ErrorHandler.logAndThrow(
"ensureSecretKeyExists",
`Secret key variable '${secretKeyName}' not found in environment file. ` +
`Please generate the secret key first before attempting encryption.`,
);
}
}
// Private methods
/**
* Resolves the file path to use, defaulting to secret environment file if not provided
* @param filePath - Optional file path
* @returns Resolved file path
*/
private static resolveFilePath(filePath?: string): string {
return filePath || SecretFilePathResolver.getSecretFilePath();
}
/**
* Executes a function with file lock protection to prevent race conditions
* @param filePath - Path to the file being operated on
* @param operation - The async operation to execute
* @returns Promise resolving to the operation result
*/
private static async executeWithFileLock<T>(
filePath: string,
operation: () => Promise<T>,
): Promise<T> {
// Wait for any existing lock on this file
await this.waitForFileLock(filePath);
// Create a new lock for this operation
const lockPromise = this.createFileLock(filePath, operation);
try {
return await lockPromise;
} finally {
this.fileLocks.delete(filePath);
}
}
/**
* Creates a file lock for the given file path
* @param filePath - Path to the file being locked
* @param operation - The operation to execute under lock
* @returns Promise for the locked operation
*/
private static createFileLock<T>(filePath: string, operation: () => Promise<T>): Promise<T> {
const lockPromise = operation();
this.fileLocks.set(
filePath,
lockPromise.then(() => undefined).catch(() => undefined),
);
return lockPromise;
}
/**
* Waits for any existing file lock to complete
* @param filePath - Path to the file to wait for
*/
private static async waitForFileLock(filePath: string): Promise<void> {
const existingLock = this.fileLocks.get(filePath);
if (existingLock) {
logger.debug(`Waiting for file lock on "${filePath}" to complete...`);
await existingLock;
logger.debug(`File lock on "${filePath}" released, proceeding...`);
}
}
/**
* Checks if key should be skipped based on existence and options
* @param fileContent - The current file content
* @param keyName - The key name to check
* @param skipIfExists - Whether to skip if key exists
* @returns True if key should be skipped, false otherwise
*/
private static shouldSkipExistingKey(
fileContent: string,
keyName: string,
skipIfExists?: boolean,
): boolean {
if (!skipIfExists) {
return false;
}
const existingValue = this.extractKeyValue(fileContent, keyName);
if (existingValue !== undefined) {
logger.info(
`Secret Key for "${keyName}" already exists in target file — skipping to avoid overwrite`,
);
return true;
}
return false;
}
/**
* Writes updated content to environment file and logs success
* @param filePath - Path to the environment file
* @param updatedContent - The content to write
* @param keyName - The key name being updated (for logging)
*/
private static async writeUpdatedEnvironmentFile(
filePath: string,
updatedContent: string,
keyName: string,
): Promise<void> {
await AsyncFileManager.writeFile(filePath, updatedContent, keyName);
logger.info(`Key "${keyName}" stored successfully`);
}
/**
* Logs warning message for missing environment file
* @param filePath - Path to the missing file
*/
private static logMissingFileWarning(filePath: string): void {
const warningMessage = [
`Environment file not found at: ${filePath}.`,
`Expected location based on configuration: ${filePath}.`,
`You can create environment file by running encryption`,
].join("\n");
logger.warn(warningMessage);
}
/**
* Extracts the value for a specific key from the file content
* @param fileContent - The content to search in
* @param keyName - The key name to extract value for
* @returns The value of the key, or undefined if not found
*/
private static extractKeyValue(fileContent: string, keyName: string): string | undefined {
const regex = this.createKeyRegex(keyName, { captureValue: true });
const match = fileContent.match(regex);
return match?.[1];
}
/**
* Creates a regex pattern for matching environment keys
* @param keyName - The key name to create regex for
* @param options - Configuration options
* @param options.captureValue - If true, captures the value in a group (default: false)
* @param options.flags - Regex flags to use (default: "m")
* @returns Regular expression for matching the key
*/
private static createKeyRegex(
keyName: string,
options: {
captureValue?: boolean;
flags?: string;
} = {},
): RegExp {
const { captureValue = false, flags = "m" } = options;
const escapedKeyName = this.escapeKeyNameForRegex(keyName);
const pattern = captureValue ? `^${escapedKeyName}=(.*)$` : `^${escapedKeyName}=.*$`;
return new RegExp(pattern, flags);
}
/**
* Escapes special regex characters in key name for safe pattern matching
* @param keyName - The key name to escape
* @returns Escaped key name safe for regex use
*/
private static escapeKeyNameForRegex(keyName: string): string {
return keyName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Updates an existing environment key or adds a new one to the file content
* @param fileContent - The current content of the environment file
* @param keyName - The name of the environment key
* @param value - The value to set for the key
* @returns The updated file content
*/
private static updateEnvironmentKey(fileContent: string, keyName: string, value: string): string {
if (this.environmentKeyExists(fileContent, keyName)) {
return this.updateExistingEnvironmentKey(fileContent, keyName, value);
} else {
return this.addEnvironmentKey(fileContent, keyName, value);
}
}
/**
* Checks if an environment key exists in the file content
* @param fileContent - The content to search in
* @param keyName - The key name to look for
* @returns True if the key exists, false otherwise
*/
private static environmentKeyExists(fileContent: string, keyName: string): boolean {
const regex = this.createKeyRegex(keyName);
return regex.test(fileContent);
}
/**
* Updates an existing environment key in the file content
* @param fileContent - The current file content
* @param keyName - The key name to update
* @param value - The new value for the key
* @returns Updated file content
*/
private static updateExistingEnvironmentKey(
fileContent: string,
keyName: string,
value: string,
): string {
const regex = this.createKeyRegex(keyName);
const formattedKeyValue = this.formatEnvironmentKeyValue(keyName, value);
return fileContent.replace(regex, formattedKeyValue);
}
/**
* Formats a key-value pair for environment file
* @param keyName - The key name
* @param value - The value
* @returns Formatted string "KEY=VALUE"
*/
private static formatEnvironmentKeyValue(keyName: string, value: string): string {
return `${keyName}=${value}`;
}
/**
* Adds a new environment key to the file content
* @param fileContent - The current file content
* @param keyName - The key name to add
* @param value - The value for the key
* @returns Updated file content with new key appended
*/
private static addEnvironmentKey(fileContent: string, keyName: string, value: string): string {
const contentWithNewline = this.ensureFileEndsWithNewline(fileContent);
const formattedKeyValue = this.formatEnvironmentKeyValue(keyName, value);
logger.warn(`Key "${keyName}" not found, appending to end of file`);
return contentWithNewline + formattedKeyValue;
}
/**
* Ensures file content ends with a newline before adding new content
* @param content - The file content to check
* @returns Content with newline if needed
*/
private static ensureFileEndsWithNewline(content: string): string {
if (content && !content.endsWith("\n")) {
return content + "\n";
}
return content;
}
}