Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,6 @@
"@tsconfig/bun": "^1.0.10",
"@types/bun": "^1.3.11",
"bunup": "^0.16.31",
"typescript": "^5.9.3"
"typescript": "^6.0.2"
}
}
10 changes: 4 additions & 6 deletions tests/snapshots/__snapshots__/cli-output.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ exports[`CLI output snapshots template content generated template matches snapsh
# op run --env-file .env.tpl -- npm start
#
# Pushed: 2025-01-01 00:00:00 UTC
# Generated by env2op v1.1.5
# Generated by env2op v0.2.5
# https://github.com/tolgamorf/env2op-cli
# ===========================================================================

Expand All @@ -40,7 +40,7 @@ exports[`CLI output snapshots template content template with no comments matches
# op run --env-file secrets.tpl -- npm start
#
# Pushed: 2025-01-01 00:00:00 UTC
# Generated by env2op v1.1.5
# Generated by env2op v0.2.5
# https://github.com/tolgamorf/env2op-cli
# ===========================================================================

Expand All @@ -49,15 +49,13 @@ KEY=op://v1/i1/f1
`;

exports[`CLI output snapshots usage instructions usage instructions match snapshot 1`] = `
"
Usage:
"Usage:
op2env .env.tpl
op run --env-file .env.tpl -- npm start"
`;

exports[`CLI output snapshots usage instructions usage instructions with path match snapshot 1`] = `
"
Usage:
"Usage:
op2env /path/to/config.tpl
op run --env-file /path/to/config.tpl -- npm start"
`;
128 changes: 64 additions & 64 deletions tests/unit/env-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ const fixturesDir = join(import.meta.dir, "../fixtures");

describe("parseEnvFile", () => {
describe("basic parsing", () => {
test("parses simple KEY=value pairs", () => {
const result = parseEnvFile(join(fixturesDir, "valid.env"));
test("parses simple KEY=value pairs", async () => {
const result = await parseEnvFile(join(fixturesDir, "valid.env"));

expect(result.variables.length).toBe(7);
expect(result.variables[0]).toEqual({
Expand All @@ -19,191 +19,191 @@ describe("parseEnvFile", () => {
});
});

test("returns correct variable count", () => {
const result = parseEnvFile(join(fixturesDir, "valid.env"));
test("returns correct variable count", async () => {
const result = await parseEnvFile(join(fixturesDir, "valid.env"));
expect(result.variables.length).toBe(7);
});

test("captures line numbers", () => {
const result = parseEnvFile(join(fixturesDir, "valid.env"));
test("captures line numbers", async () => {
const result = await parseEnvFile(join(fixturesDir, "valid.env"));
const debugVar = result.variables.find((v) => v.key === "DEBUG");
expect(debugVar?.line).toBe(11);
});
});

describe("quoted values", () => {
test("handles double-quoted values", () => {
const result = parseEnvFile(join(fixturesDir, "quoted-values.env"));
test("handles double-quoted values", async () => {
const result = await parseEnvFile(join(fixturesDir, "quoted-values.env"));
const doubleQuoted = result.variables.find((v) => v.key === "DOUBLE_QUOTED");
expect(doubleQuoted?.value).toBe("hello world");
});

test("handles single-quoted values", () => {
const result = parseEnvFile(join(fixturesDir, "quoted-values.env"));
test("handles single-quoted values", async () => {
const result = await parseEnvFile(join(fixturesDir, "quoted-values.env"));
const singleQuoted = result.variables.find((v) => v.key === "SINGLE_QUOTED");
expect(singleQuoted?.value).toBe("hello world");
});

test("preserves spaces in quoted values", () => {
const result = parseEnvFile(join(fixturesDir, "quoted-values.env"));
test("preserves spaces in quoted values", async () => {
const result = await parseEnvFile(join(fixturesDir, "quoted-values.env"));
const withSpaces = result.variables.find((v) => v.key === "DOUBLE_WITH_SPACES");
expect(withSpaces?.value).toBe(" spaces around ");
});

test("preserves # in quoted values (not treated as comment)", () => {
const result = parseEnvFile(join(fixturesDir, "quoted-values.env"));
test("preserves # in quoted values (not treated as comment)", async () => {
const result = await parseEnvFile(join(fixturesDir, "quoted-values.env"));
const withHash = result.variables.find((v) => v.key === "DOUBLE_WITH_HASH");
expect(withHash?.value).toBe("value # not a comment");
});

test("handles empty quoted values", () => {
const result = parseEnvFile(join(fixturesDir, "quoted-values.env"));
test("handles empty quoted values", async () => {
const result = await parseEnvFile(join(fixturesDir, "quoted-values.env"));
const emptyDouble = result.variables.find((v) => v.key === "DOUBLE_EMPTY");
const emptySingle = result.variables.find((v) => v.key === "SINGLE_EMPTY");
expect(emptyDouble?.value).toBe("");
expect(emptySingle?.value).toBe("");
});

test("handles unquoted values", () => {
const result = parseEnvFile(join(fixturesDir, "quoted-values.env"));
test("handles unquoted values", async () => {
const result = await parseEnvFile(join(fixturesDir, "quoted-values.env"));
const unquoted = result.variables.find((v) => v.key === "UNQUOTED");
expect(unquoted?.value).toBe("simple_value");
});
});

describe("comments", () => {
test("preserves standalone comments in lines array", () => {
const result = parseEnvFile(join(fixturesDir, "comments.env"));
test("preserves standalone comments in lines array", async () => {
const result = await parseEnvFile(join(fixturesDir, "comments.env"));
const commentLines = result.lines.filter((l) => l.type === "comment");
expect(commentLines.length).toBeGreaterThan(0);
});

test("associates comments with following variables", () => {
const result = parseEnvFile(join(fixturesDir, "comments.env"));
test("associates comments with following variables", async () => {
const result = await parseEnvFile(join(fixturesDir, "comments.env"));
const key1 = result.variables.find((v) => v.key === "KEY1");
// KEY1 follows empty line, so no associated comment
expect(key1?.comment).toBeUndefined();
});

test("strips inline comments from unquoted values", () => {
const result = parseEnvFile(join(fixturesDir, "comments.env"));
test("strips inline comments from unquoted values", async () => {
const result = await parseEnvFile(join(fixturesDir, "comments.env"));
const dbPort = result.variables.find((v) => v.key === "DB_PORT");
expect(dbPort?.value).toBe("5432");
});

test("preserves original comment content including #", () => {
const result = parseEnvFile(join(fixturesDir, "comments.env"));
test("preserves original comment content including #", async () => {
const result = await parseEnvFile(join(fixturesDir, "comments.env"));
const firstComment = result.lines.find((l) => l.type === "comment");
expect(firstComment?.type === "comment" && firstComment.content).toContain("#");
});
});

describe("empty lines", () => {
test("preserves empty lines in lines array", () => {
const result = parseEnvFile(join(fixturesDir, "comments.env"));
test("preserves empty lines in lines array", async () => {
const result = await parseEnvFile(join(fixturesDir, "comments.env"));
const emptyLines = result.lines.filter((l) => l.type === "empty");
expect(emptyLines.length).toBeGreaterThan(0);
});
});

describe("edge cases", () => {
test("handles multiple equals signs in value", () => {
const result = parseEnvFile(join(fixturesDir, "edge-cases.env"));
test("handles multiple equals signs in value", async () => {
const result = await parseEnvFile(join(fixturesDir, "edge-cases.env"));
const equation = result.variables.find((v) => v.key === "EQUATION");
expect(equation?.value).toBe("a=b=c=d");
});

test("handles URL with query parameters", () => {
const result = parseEnvFile(join(fixturesDir, "edge-cases.env"));
test("handles URL with query parameters", async () => {
const result = await parseEnvFile(join(fixturesDir, "edge-cases.env"));
const url = result.variables.find((v) => v.key === "URL");
expect(url?.value).toBe("https://example.com?foo=bar&baz=qux");
});

test("handles unicode characters in values", () => {
const result = parseEnvFile(join(fixturesDir, "edge-cases.env"));
test("handles unicode characters in values", async () => {
const result = await parseEnvFile(join(fixturesDir, "edge-cases.env"));
const emoji = result.variables.find((v) => v.key === "EMOJI");
const unicode = result.variables.find((v) => v.key === "UNICODE");
expect(emoji?.value).toContain("🎉");
expect(unicode?.value).toContain("你好");
});

test("handles long values", () => {
const result = parseEnvFile(join(fixturesDir, "edge-cases.env"));
test("handles long values", async () => {
const result = await parseEnvFile(join(fixturesDir, "edge-cases.env"));
const longValue = result.variables.find((v) => v.key === "LONG_VALUE");
expect(longValue?.value.length).toBeGreaterThan(100);
});

test("handles special characters in values", () => {
const result = parseEnvFile(join(fixturesDir, "edge-cases.env"));
test("handles special characters in values", async () => {
const result = await parseEnvFile(join(fixturesDir, "edge-cases.env"));
const special = result.variables.find((v) => v.key === "SPECIAL_CHARS");
expect(special?.value).toContain("@#$%");
});

test("handles JSON in values", () => {
const result = parseEnvFile(join(fixturesDir, "edge-cases.env"));
test("handles JSON in values", async () => {
const result = await parseEnvFile(join(fixturesDir, "edge-cases.env"));
const json = result.variables.find((v) => v.key === "JSON_VALUE");
expect(json?.value).toContain('"key"');
});
});

describe("variable names", () => {
test("accepts keys starting with underscore", () => {
const result = parseEnvFile(join(fixturesDir, "edge-cases.env"));
test("accepts keys starting with underscore", async () => {
const result = await parseEnvFile(join(fixturesDir, "edge-cases.env"));
const underscoreKey = result.variables.find((v) => v.key === "_STARTS_WITH_UNDERSCORE");
expect(underscoreKey).toBeDefined();
});

test("accepts keys with double underscores", () => {
const result = parseEnvFile(join(fixturesDir, "edge-cases.env"));
test("accepts keys with double underscores", async () => {
const result = await parseEnvFile(join(fixturesDir, "edge-cases.env"));
const doubleUnderscore = result.variables.find((v) => v.key === "__DOUBLE_UNDERSCORE");
expect(doubleUnderscore).toBeDefined();
});

test("accepts keys with numbers after first character", () => {
const result = parseEnvFile(join(fixturesDir, "edge-cases.env"));
test("accepts keys with numbers after first character", async () => {
const result = await parseEnvFile(join(fixturesDir, "edge-cases.env"));
const key123 = result.variables.find((v) => v.key === "KEY123");
expect(key123).toBeDefined();
});

test("rejects keys starting with numbers", () => {
test("rejects keys starting with numbers", async () => {
// Need to create a test with invalid key to test this
const result = parseEnvFile(join(fixturesDir, "edge-cases.env"));
const result = await parseEnvFile(join(fixturesDir, "edge-cases.env"));
const invalidKey = result.variables.find((v) => v.key.match(/^[0-9]/));
expect(invalidKey).toBeUndefined();
});
});

describe("error handling", () => {
test("throws Env2OpError for missing file", () => {
expect(() => parseEnvFile("/nonexistent/path/.env")).toThrow(Env2OpError);
test("throws Env2OpError for missing file", async () => {
expect(parseEnvFile("/nonexistent/path/.env")).rejects.toThrow(Env2OpError);
});

test("throws with correct error code for missing file", () => {
test("throws with correct error code for missing file", async () => {
try {
parseEnvFile("/nonexistent/path/.env");
await parseEnvFile("/nonexistent/path/.env");
} catch (e) {
expect(e).toBeInstanceOf(Env2OpError);
expect((e as Env2OpError).code).toBe("ENV_FILE_NOT_FOUND");
}
});

test("reports invalid variable names in errors array", () => {
test("reports invalid variable names in errors array", async () => {
// Create inline test for invalid names
// For now, we test the existing fixtures don't have errors
const result = parseEnvFile(join(fixturesDir, "valid.env"));
const result = await parseEnvFile(join(fixturesDir, "valid.env"));
expect(result.errors.length).toBe(0);
});
});

describe("lines array structure", () => {
test("preserves original file structure order", () => {
const result = parseEnvFile(join(fixturesDir, "comments.env"));
test("preserves original file structure order", async () => {
const result = await parseEnvFile(join(fixturesDir, "comments.env"));
// First line should be a comment
expect(result.lines[0]?.type).toBe("comment");
});

test("variable lines include key and value", () => {
const result = parseEnvFile(join(fixturesDir, "valid.env"));
test("variable lines include key and value", async () => {
const result = await parseEnvFile(join(fixturesDir, "valid.env"));
const varLine = result.lines.find((l) => l.type === "variable");
expect(varLine?.type === "variable" && varLine.key).toBeDefined();
expect(varLine?.type === "variable" && varLine.value).toBeDefined();
Expand All @@ -212,18 +212,18 @@ describe("parseEnvFile", () => {
});

describe("validateParseResult", () => {
test("does not throw when variables exist", () => {
const result = parseEnvFile(join(fixturesDir, "valid.env"));
test("does not throw when variables exist", async () => {
const result = await parseEnvFile(join(fixturesDir, "valid.env"));
expect(() => validateParseResult(result, "valid.env")).not.toThrow();
});

test("throws Env2OpError when no variables found", () => {
const result = parseEnvFile(join(fixturesDir, "empty.env"));
test("throws Env2OpError when no variables found", async () => {
const result = await parseEnvFile(join(fixturesDir, "empty.env"));
expect(() => validateParseResult(result, "empty.env")).toThrow(Env2OpError);
});

test("throws with correct error code for empty file", () => {
const result = parseEnvFile(join(fixturesDir, "empty.env"));
test("throws with correct error code for empty file", async () => {
const result = await parseEnvFile(join(fixturesDir, "empty.env"));
try {
validateParseResult(result, "empty.env");
} catch (e) {
Expand Down
Loading