// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assert, assertEquals, assertRejects, assertStrictEquals, assertThrows, } from "@std/assert"; import { load, type LoadOptions, loadSync, MissingEnvVarsError, } from "./mod.ts"; import * as path from "@std/path"; const moduleDir = path.dirname(path.fromFileUrl(import.meta.url)); const testdataDir = path.resolve(moduleDir, "testdata"); const testOptions = Object.freeze({ envPath: path.join(testdataDir, ".env"), defaultsPath: path.join(testdataDir, ".env.defaults"), }); Deno.test("load() handles non-existent .env files", async () => { //n.b. neither .env nor .env.default exist in the current directory assertEquals({}, await load()); assertEquals({}, loadSync()); const loadOptions = { envPath: "some.nonexistent.env", examplePath: "some.nonexistent.example", defaultsPath: "some.nonexistent.defaults", }; assertEquals({}, await load(loadOptions)); assertEquals({}, loadSync(loadOptions)); }); Deno.test("load() handles build from .env.default only", async () => { const conf = loadSync({ defaultsPath: path.join(testdataDir, ".env.defaults"), }); assertEquals(conf.DEFAULT1, "Some Default", "loaded from .env.default"); const asyncConf = await load({ defaultsPath: path.join(testdataDir, ".env.defaults"), }); assertEquals(asyncConf.DEFAULT1, "Some Default", "loaded from .env.default"); }); Deno.test("load() handles comprised .env and .env.defaults", async () => { const conf = loadSync(testOptions); assertEquals(conf.GREETING, "hello world", "loaded from .env"); assertEquals(conf.DEFAULT1, "Some Default", "loaded from .env.default"); const asyncConf = await load(testOptions); assertEquals(asyncConf.GREETING, "hello world", "loaded from .env"); assertEquals(asyncConf.DEFAULT1, "Some Default", "loaded from .env.default"); }); Deno.test("load() handles exported entries accessibility in Deno.env", async () => { assert(Deno.env.get("GREETING") === undefined, "GREETING is not set"); assert(Deno.env.get("DEFAULT1") === undefined, "DEFAULT1 is not set"); loadSync({ ...testOptions, export: true }); validateExport(); await load({ ...testOptions, export: true }); validateExport(); }); function validateExport(): void { try { assertEquals( Deno.env.get("GREETING"), "hello world", "exported from .env -> Deno.env", ); assertEquals( Deno.env.get("DEFAULT1"), "Some Default", "exported from .env.default -> Deno.env", ); } finally { Deno.env.delete("GREETING"); Deno.env.delete("DEFAULT1"); } } Deno.test("load() process does not overridde env vars by .env values", async () => { Deno.env.set("GREETING", "Do not override!"); assert(Deno.env.get("DEFAULT1") === undefined, "DEFAULT1 is not set"); validateNotOverridden(loadSync({ ...testOptions, export: true })); validateNotOverridden(await load({ ...testOptions, export: true })); }); function validateNotOverridden(conf: Record): void { try { assertEquals(conf.GREETING, "hello world", "value from .env"); assertEquals( Deno.env.get("GREETING"), "Do not override!", "not exported from .env -> Deno.env", ); assertEquals( Deno.env.get("DEFAULT1"), "Some Default", "exported from .env.default -> Deno.env", ); } finally { Deno.env.delete("DEFAULT1"); } } Deno.test("load() handles example file key in .env, no issues loading", async () => { //Both .env.example.test and .env contain "GREETING" const loadOptions = { ...testOptions, examplePath: path.join(testdataDir, "./.env.example.test"), }; loadSync(loadOptions); await load(loadOptions); }); Deno.test("load() handles example file key in .env.default, no issues loading", async () => { //Both .env.example3.test and .env.default contain "DEFAULT1" const loadOptions = { ...testOptions, examplePath: path.join(testdataDir, "./.env.example3.test"), }; loadSync(loadOptions); await load(loadOptions); }); Deno.test("load() handles example file key not in .env or .env.defaults, error thrown", async () => { // Example file key of "ANOTHER" is not present in .env or .env.defaults const error: MissingEnvVarsError = assertThrows(() => { loadSync({ ...testOptions, examplePath: path.join(testdataDir, "./.env.example2.test"), }); }, MissingEnvVarsError); assertEquals(error.missing, ["ANOTHER"]); const asyncError: MissingEnvVarsError = await assertRejects(async () => { await load({ ...testOptions, examplePath: path.join(testdataDir, "./.env.example2.test"), }); }, MissingEnvVarsError); assertEquals(asyncError.missing, ["ANOTHER"]); }); Deno.test("load() handles omitted allowEmptyValues, empty required keys throw error", async () => { // Example file key of "ANOTHER" is present but empty in .env const error: MissingEnvVarsError = assertThrows(() => { loadSync({ envPath: path.join(testdataDir, "./.env.required.empty.test"), examplePath: path.join(testdataDir, "./.env.example2.test"), }); }, MissingEnvVarsError); assertEquals(error.missing, ["ANOTHER"]); const asyncError: MissingEnvVarsError = await assertRejects(async () => { await load({ envPath: path.join(testdataDir, "./.env.required.empty.test"), examplePath: path.join(testdataDir, "./.env.example2.test"), }); }, MissingEnvVarsError); assertEquals(asyncError.missing, ["ANOTHER"]); }); Deno.test("load() handles allowEmptyValues, empty required keys do not throw error", async () => { // Example file key of "ANOTHER" is present but empty in .env const loadOptions = { envPath: path.join(testdataDir, "./.env.required.empty.test"), examplePath: path.join(testdataDir, "./.env.example2.test"), allowEmptyValues: true, }; loadSync(loadOptions); await load(loadOptions); }); Deno.test("load() checks that required keys can be sourced from process environment", async () => { try { Deno.env.set("ANOTHER", "VAR"); // Example file key of "ANOTHER" is not present in .env or .env.defaults const loadOptions = { envPath: path.join(testdataDir, "./.env"), examplePath: path.join(testdataDir, "./.env.example2.test"), }; loadSync(loadOptions); await load(loadOptions); } finally { Deno.env.delete("ANOTHER"); } }); Deno.test("load() checks that required keys sourced from process environment cannot be empty", async () => { try { Deno.env.set("ANOTHER", ""); // Example file key of "ANOTHER" is not present in .env or .env.defaults const loadOptions = { envPath: path.join(testdataDir, "./.env"), examplePath: path.join(testdataDir, "./.env.example2.test"), }; const error: MissingEnvVarsError = assertThrows(() => { loadSync(loadOptions); }, MissingEnvVarsError); assertEquals(error.missing, ["ANOTHER"]); const asyncError: MissingEnvVarsError = await assertRejects(async () => { await load(loadOptions); }, MissingEnvVarsError); assertEquals(asyncError.missing, ["ANOTHER"]); } finally { Deno.env.delete("ANOTHER"); } }); Deno.test("load() checks that required keys sourced from process environment can be empty with allowEmptyValues", async () => { try { Deno.env.set("ANOTHER", ""); // Example file key of "ANOTHER" is not present in .env or .env.defaults const loadOptions = { envPath: path.join(testdataDir, "./.env"), examplePath: path.join(testdataDir, "./.env.example2.test"), allowEmptyValues: true, }; loadSync(loadOptions); await load(loadOptions); } finally { Deno.env.delete("ANOTHER"); } }); Deno.test("load() loads .env and .env.defaults successfully from default file names/paths", async () => { const command = new Deno.Command(Deno.execPath(), { args: [ "run", "--no-lock", "--allow-read", "--allow-env", path.join(testdataDir, "./app_defaults.ts"), ], cwd: testdataDir, }); const { stdout } = await command.output(); const decoder = new TextDecoder(); const conf = JSON.parse(decoder.decode(stdout).trim()); assertEquals(conf.GREETING, "hello world", "fetches .env by default"); assertEquals(conf.DEFAULT1, "Some Default", "default value loaded"); }); Deno.test("load() expands empty values from process env expand as empty value", async () => { try { Deno.env.set("EMPTY", ""); // .env.single.expand contains one key which expands to the "EMPTY" process env var const loadOptions = { envPath: path.join(testdataDir, "./.env.single.expand"), allowEmptyValues: true, }; const conf = loadSync(loadOptions); assertEquals( conf.EXPECT_EMPTY, "", "empty value expanded from process env", ); const asyncConf = await load(loadOptions); assertEquals( asyncConf.EXPECT_EMPTY, "", "empty value expanded from process env", ); } finally { Deno.env.delete("EMPTY"); } }); Deno.test( "loadSync() checks that --allow-env is not required if no process env vars are expanded upon", { permissions: { read: true, }, }, () => { // note lack of --allow-env permission const conf = loadSync(testOptions); assertEquals(conf.GREETING, "hello world"); assertEquals(conf.DEFAULT1, "Some Default"); }, ); Deno.test( "loadSync() checks that --allow-env is required when process env vars are expanded upon", { permissions: { read: true, }, }, () => { // ./app_permission_test.ts loads a .env with one key which expands a process env var // note lack of --allow-env permission const loadOptions = { envPath: path.join(testdataDir, "./.env.single.expand"), defaultsPath: null, examplePath: null, }; assertThrows( () => loadSync(loadOptions), Deno.errors.PermissionDenied, `Requires env access to "EMPTY", run again with the --allow-env flag`, ); }, ); Deno.test( "loadSync() checks that --allow-env restricted access works when process env vars are expanded upon", { permissions: { read: true, env: ["EMPTY"], }, }, () => { try { Deno.env.set("EMPTY", ""); const loadOptions = { envPath: path.join(testdataDir, "./.env.single.expand"), defaultsPath: null, examplePath: null, }; const conf = loadSync(loadOptions); assertEquals( conf.EXPECT_EMPTY, "", "empty value expanded from process env", ); } finally { Deno.env.delete("EMPTY"); } }, ); //TODO test permissions Deno.test( "loadSync() prevents file system reads of default path parameter values by using explicit null", { permissions: { env: ["GREETING", "DO_NOT_OVERRIDE"], read: [path.join(testdataDir, "./.env.multiple")], }, }, async (t) => { const optsNoPaths = { defaultsPath: null, envPath: null, examplePath: null, } satisfies LoadOptions; const optsEnvPath = { envPath: path.join(testdataDir, "./.env.multiple"), } satisfies LoadOptions; const optsOnlyEnvPath = { ...optsEnvPath, defaultsPath: null, examplePath: null, } satisfies LoadOptions; const assertEnv = (env: Record): void => { assertStrictEquals(Object.keys(env).length, 2); assertStrictEquals(env["GREETING"], "hello world"); assertStrictEquals(env["DO_NOT_OVERRIDE"], "overridden"); }; await t.step("load", async () => { assertStrictEquals(Object.keys(await load(optsNoPaths)).length, 0); assertEnv(await load(optsOnlyEnvPath)); await assertRejects( () => load(optsEnvPath), Deno.errors.PermissionDenied, `Requires read access to ".env.defaults"`, ); await assertRejects( () => load({ ...optsEnvPath, defaultsPath: null }), Deno.errors.PermissionDenied, `Requires read access to ".env.example"`, ); await assertRejects( () => load({ ...optsEnvPath, examplePath: null }), Deno.errors.PermissionDenied, `Requires read access to ".env.defaults"`, ); }); await t.step("loadSync", () => { assertStrictEquals(Object.keys(loadSync(optsNoPaths)).length, 0); assertEnv(loadSync(optsOnlyEnvPath)); assertThrows( () => loadSync(optsEnvPath), Deno.errors.PermissionDenied, `Requires read access to ".env.defaults"`, ); assertThrows( () => loadSync({ ...optsEnvPath, defaultsPath: null }), Deno.errors.PermissionDenied, `Requires read access to ".env.example"`, ); assertThrows( () => loadSync({ ...optsEnvPath, examplePath: null }), Deno.errors.PermissionDenied, `Requires read access to ".env.defaults"`, ); }); }, );