diff --git a/src/config.ts b/src/config.ts index f136a9c..f5e4215 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,14 +21,21 @@ const InputConfigSchema = z.object({ outputImages: z.array(OutputImageConfigSchema).min(1), }); +const CredentialsSchema = z.object({ + accessKeyId: z.string(), + secretKey: z.string(), + endpointUrl: z.string(), +}); + const ConfigSchema = z.object({ bucketName: z.string(), inputConfig: InputConfigSchema, + credentials: CredentialsSchema.optional(), }); export type Config = z.infer; -const validateConfig = (config: unknown): Config => { +export const validateConfig = (config: unknown): Config => { const configObj = ConfigSchema.parse(config); const { inputConfig } = configObj; @@ -51,6 +58,28 @@ const validateConfig = (config: unknown): Config => { return configObj; }; +export const getCredentials = (configCredentials?: { + accessKeyId?: string; + secretKey?: string; + endpointUrl?: string; +}) => { + const envCredentials = { + accessKeyId: process.env.ACCESS_KEY_ID, + secretKey: process.env.SECRET_KEY, + endpointUrl: process.env.ENDPOINT_URL, + }; + + const finalCredentials = { + accessKeyId: configCredentials?.accessKeyId || envCredentials.accessKeyId, + secretKey: configCredentials?.secretKey || envCredentials.secretKey, + endpointUrl: configCredentials?.endpointUrl || envCredentials.endpointUrl, + }; + + const parsedCredentials = CredentialsSchema.parse(finalCredentials); + + return parsedCredentials; +}; + export const getConfig = (): Config => { try { const explorerSync = cosmiconfigSync("lilsync"); @@ -60,7 +89,10 @@ export const getConfig = (): Config => { throw new Error("configuration file not found or is empty"); } - return validateConfig(result.config); + const config = validateConfig(result.config); + config.credentials = getCredentials(result.config.credentials); + + return config; } catch (err) { if (err instanceof z.ZodError) { const formattedErrors = err.errors diff --git a/src/test/config.test.ts b/src/test/config.test.ts new file mode 100644 index 0000000..0ac05b7 --- /dev/null +++ b/src/test/config.test.ts @@ -0,0 +1,181 @@ +import { cosmiconfigSync } from "cosmiconfig"; + +import { getConfig, validateConfig, getCredentials } from "../config"; + +jest.mock("cosmiconfig"); +jest.mock("dotenv"); + +describe("Config Tests", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("validateConfig", () => { + const validConfig = { + bucketName: "test-bucket", + inputConfig: { + outputPath: "/output/path", + inputPath: "/input/path", + outputImages: [{ width: 1000, height: 2000, ext: "png", quality: 90 }], + }, + }; + + const invalidConfigs = [ + { + case: "missing bucketName", + config: { + inputConfig: { + outputPath: "/output/path", + inputPath: "/input/path", + outputImages: [ + { width: 1000, height: 2000, ext: "png", quality: 90 }, + ], + }, + }, + }, + { + case: "duplicate dimensions", + config: { + bucketName: "test-bucket", + inputConfig: { + outputPath: "/output/path", + inputPath: "/input/path", + outputImages: [ + { width: 1000, height: 2000, ext: "png", quality: 90 }, + { width: 1000, height: 3000, ext: "jpg", quality: 80 }, + ], + }, + }, + }, + ]; + + test("should validate a valid config", () => { + expect(() => validateConfig(validConfig)).not.toThrow(); + }); + + test.each(invalidConfigs)("should throw error for $case", ({ config }) => { + expect(() => validateConfig(config)).toThrow(Error); + }); + }); + + describe("getCredentials", () => { + const envCredentials = { + ACCESS_KEY_ID: "env-access-key-id", + SECRET_KEY: "env-secret-key", + ENDPOINT_URL: "env-endpoint-url", + }; + + const configCredentials = { + accessKeyId: "config-access-key-id", + secretKey: "config-secret-key", + endpointUrl: "config-endpoint-url", + }; + + const testCases = [ + { + description: + "if both config and env credentials are provided, use config credentials", + env: envCredentials, + config: configCredentials, + expected: configCredentials, + }, + { + description: + "if only env credentials are provided, use env credentials", + env: envCredentials, + config: {}, + expected: { + accessKeyId: "env-access-key-id", + secretKey: "env-secret-key", + endpointUrl: "env-endpoint-url", + }, + }, + { + description: + "if only config credentials are provided, use config credentials", + env: {}, + config: configCredentials, + expected: configCredentials, + }, + ]; + + test.each(testCases)( + "should return correct credentials $description", + ({ env, config, expected }) => { + process.env = { ...env }; + const result = getCredentials(config); + expect(result).toEqual(expected); + } + ); + }); + + describe("getConfig", () => { + const validConfig = { + config: { + bucketName: "test-bucket", + inputConfig: { + outputPath: "/output/path", + inputPath: "/input/path", + outputImages: [ + { width: 1000, height: 2000, ext: "png", quality: 90 }, + ], + }, + credentials: { + accessKeyId: "config-access-key-id", + secretKey: "config-secret-key", + endpointUrl: "config-endpoint-url", + }, + }, + }; + + const cosmiconfigMock = cosmiconfigSync as jest.Mock; + + test("should return parsed config if everything is valid", () => { + cosmiconfigMock.mockReturnValue({ + search: jest.fn().mockReturnValue(validConfig), + }); + + const result = getConfig(); + expect(result).toEqual({ + ...validConfig.config, + }); + }); + + test("should throw error if config file is not found or empty", () => { + cosmiconfigMock.mockReturnValue({ + search: jest.fn().mockReturnValue(null), + }); + + expect(() => getConfig()).toThrow( + "configuration file not found or is empty" + ); + }); + + test("should throw validation error with formatted message", () => { + const invalidConfig = { + config: { + bucketName: 12345, // Invalid type + inputConfig: { + outputPath: "/output/path", + inputPath: "/input/path", + outputImages: [ + { width: 1000, height: 2000, ext: "png", quality: 101 }, // Invalid quality + ], + }, + }, + }; + + cosmiconfigMock.mockReturnValue({ + search: jest.fn().mockReturnValue(invalidConfig), + }); + + try { + getConfig(); + } catch (error) { + if (error instanceof Error) { + expect(error.message).toContain("Configuration validation error:"); + } + } + }); + }); +});