Skip to content
Draft
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
18 changes: 18 additions & 0 deletions src/github-action/dotenv-parse-alignment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { parseDotenv as parseApp } from "@/lib/dotenv-parse";
import { parseDotenv as parseAction } from "./dotenv-parse";

/** Keeps GitHub Action parsing aligned with vault UI / server (shared contract). */
describe("dotenv-parse parity (action vs app)", () => {
const fixtures = [
"",
"\n# c\n\nA=1\n",
"B=\"x y\"\nC='z'\r\n",
"EMPTY=\nD=tail",
"no=good\n=bad\nKEY_ONLY",
];

it.each(fixtures)("matches for fixture %#", (content) => {
expect(parseAction(content)).toEqual(parseApp(content));
});
});
103 changes: 103 additions & 0 deletions src/lib/dotenv-parse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, expect, it } from "vitest";
import { appendDotenvKey, parseDotenv, removeDotenvKey } from "./dotenv-parse";

describe("parseDotenv", () => {
it("ignores blanks and # comments", () => {
expect(parseDotenv("\n \n# x=1\nFOO=bar\n")).toEqual([
{ key: "FOO", value: "bar" },
]);
});

it("handles CRLF line endings", () => {
expect(parseDotenv("A=1\r\nB=2\r\n")).toEqual([
{ key: "A", value: "1" },
{ key: "B", value: "2" },
]);
});

it("strips optional single or double quotes around values", () => {
expect(parseDotenv("X=\"a b\"\nY='c'")).toEqual([
{ key: "X", value: "a b" },
{ key: "Y", value: "c" },
]);
});

it("skips lines without = or with empty key", () => {
expect(parseDotenv("=nope\nnoequals\nOK=1")).toEqual([
{ key: "OK", value: "1" },
]);
});

it("parses unquoted values that contain spaces", () => {
expect(parseDotenv("MSG=hello world")).toEqual([
{ key: "MSG", value: "hello world" },
]);
});
});

describe("removeDotenvKey", () => {
it("removes assignment lines for the key and preserves comments and blanks", () => {
const src = "# keep\n\nAPI=old\n\nOTHER=1\n";
expect(removeDotenvKey(src, "API")).toBe("# keep\n\n\nOTHER=1\n");
});

it("removes every matching assignment line", () => {
expect(removeDotenvKey("A=1\nA=2\nB=3", "A")).toBe("B=3");
});
});

describe("appendDotenvKey", () => {
it("appends a new line and rejects duplicate keys", () => {
const base = "EXISTING=1\n";
expect(appendDotenvKey(base, "NEW", "v")).toEqual({
ok: true,
content: "EXISTING=1\nNEW=v",
});
expect(appendDotenvKey("EXISTING=1\n", "EXISTING", "x")).toEqual({
ok: false,
error: '"EXISTING" is already defined',
});
});

it("trims trailing whitespace on the file before appending", () => {
expect(appendDotenvKey("A=1\n \n", "B", "2")).toEqual({
ok: true,
content: "A=1\nB=2",
});
});

it("quotes values that contain spaces or # and round-trips when no inner quotes", () => {
const r = appendDotenvKey("", "K", "a b # c");
expect(r.ok).toBe(true);
if (r.ok) {
expect(parseDotenv(r.content)).toEqual([{ key: "K", value: "a b # c" }]);
}
});

it("escapes embedded double quotes in the serialized line", () => {
const r = appendDotenvKey("", "K", 'say "hi"');
expect(r.ok).toBe(true);
if (r.ok) {
expect(r.content).toBe('K="say \\"hi\\""');
}
});

it("rejects invalid keys and multiline values with clear errors", () => {
expect(appendDotenvKey("", "", "v")).toEqual({
ok: false,
error: "Enter a variable name",
});
expect(appendDotenvKey("", "a=b", "v")).toEqual({
ok: false,
error: 'Key cannot contain "="',
});
expect(appendDotenvKey("", "#x", "v")).toEqual({
ok: false,
error: "Key cannot start with #",
});
expect(appendDotenvKey("", "K", "a\nb")).toEqual({
ok: false,
error: "Value cannot contain line breaks",
});
});
});
21 changes: 21 additions & 0 deletions src/lib/key-token.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { decodeObjectKeyToken, encodeObjectKeyToken } from "./key-token";

describe("encodeObjectKeyToken / decodeObjectKeyToken", () => {
it("round-trips arbitrary UTF-8 keys", () => {
const key = "team/prod/.env";
const token = encodeObjectKeyToken(key);
expect(token).not.toMatch(/[+/=]/);
expect(decodeObjectKeyToken(token)).toBe(key);
});

it("round-trips keys with slashes, plus signs, and unicode", () => {
const key = "vault/a+b/café/密钥";
expect(decodeObjectKeyToken(encodeObjectKeyToken(key))).toBe(key);
});

it("produces stable output for the same input", () => {
const k = "x/y";
expect(encodeObjectKeyToken(k)).toBe(encodeObjectKeyToken(k));
});
});
Loading