Skip to content

Commit

Permalink
feat: validation for storage credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
nhoss2 committed Jun 20, 2024
1 parent 189059d commit ab792dd
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 2 deletions.
36 changes: 34 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof ConfigSchema>;

const validateConfig = (config: unknown): Config => {
export const validateConfig = (config: unknown): Config => {
const configObj = ConfigSchema.parse(config);
const { inputConfig } = configObj;

Expand All @@ -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");
Expand All @@ -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
Expand Down
181 changes: 181 additions & 0 deletions src/test/config.test.ts
Original file line number Diff line number Diff line change
@@ -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:");
}
}
});
});
});

0 comments on commit ab792dd

Please sign in to comment.