// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assert, assertEquals, assertStrictEquals, assertThrows, } from "@std/assert"; import { load, type LoadOptions, loadSync } from "./mod.ts"; import * as path from "@std/path"; import { IS_DENO_2 } from "../internal/_is_deno_2.ts"; const moduleDir = path.dirname(path.fromFileUrl(import.meta.url)); const testdataDir = path.resolve(moduleDir, "testdata"); const testOptions = Object.freeze({ envPath: path.join(testdataDir, ".env"), }); Deno.test("load() handles non-existent .env files", async () => { // .env doesn't exist in the current directory assertEquals({}, await load()); assertEquals({}, loadSync()); const loadOptions = { envPath: "some.nonexistent.env", }; assertEquals({}, await load(loadOptions)); assertEquals({}, loadSync(loadOptions)); }); Deno.test("load() handles comprised .env and .env.defaults", async () => { const conf = loadSync(testOptions); assertEquals(conf.GREETING, "hello world", "loaded from .env"); const asyncConf = await load(testOptions); assertEquals(asyncConf.GREETING, "hello world", "loaded from .env"); }); 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", ); } finally { Deno.env.delete("GREETING"); } } 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", ); } finally { Deno.env.delete("DEFAULT1"); } } Deno.test("load() loads .env 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"); }); 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"), }; 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"); }, ); 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"), }; assertThrows( () => loadSync(loadOptions), IS_DENO_2 // TODO(iuioiua): Just use `Deno.errors.NotCapable` once Deno 2 is released. // deno-lint-ignore no-explicit-any ? (Deno as any).errors.NotCapable : 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"), }; 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 = { envPath: null, } satisfies LoadOptions; const optsEnvPath = { envPath: path.join(testdataDir, "./.env.multiple"), } satisfies LoadOptions; const optsOnlyEnvPath = { ...optsEnvPath, } 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 t.step("loadSync", () => { assertStrictEquals(Object.keys(loadSync(optsNoPaths)).length, 0); assertEnv(loadSync(optsOnlyEnvPath)); }); }, );