cloudflare/vinext

Public

mirrored from https://github.com/cloudflare/vinextAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.0.9

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

tests/dotenv.test.ts

264lines · modecode

1import { afterEach, beforeEach, describe, expect, it } from "vitest";
2import fs from "node:fs";
3import os from "node:os";
4import path from "node:path";
5import {
6 loadDotenv,
7 getDotenvFiles,
8} from "../packages/vinext/src/config/dotenv.js";
9
10let tmpDir: string;
11
12function writeFile(relativePath: string, content: string): void {
13 const fullPath = path.join(tmpDir, relativePath);
14 fs.mkdirSync(path.dirname(fullPath), { recursive: true });
15 fs.writeFileSync(fullPath, content, "utf-8");
16}
17
18beforeEach(() => {
19 tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-dotenv-test-"));
20});
21
22afterEach(() => {
23 fs.rmSync(tmpDir, { recursive: true, force: true });
24});
25
26describe("getDotenvFiles", () => {
27 it("returns correct file list for development", () => {
28 expect(getDotenvFiles("development")).toEqual([
29 ".env.development.local",
30 ".env.local",
31 ".env.development",
32 ".env",
33 ]);
34 });
35
36 it("returns correct file list for production", () => {
37 expect(getDotenvFiles("production")).toEqual([
38 ".env.production.local",
39 ".env.local",
40 ".env.production",
41 ".env",
42 ]);
43 });
44
45 it("skips .env.local for test mode", () => {
46 const files = getDotenvFiles("test");
47 expect(files).toEqual([
48 ".env.test.local",
49 ".env.test",
50 ".env",
51 ]);
52 expect(files).not.toContain(".env.local");
53 });
54});
55
56describe("loadDotenv", () => {
57 it("applies Next.js precedence for development mode", () => {
58 writeFile(".env", "ORDER=env\nSECOND=env\n");
59 writeFile(".env.development", "ORDER=mode\nSECOND=mode\n");
60 writeFile(".env.local", "ORDER=local\nSECOND=local\n");
61 writeFile(".env.development.local", "ORDER=mode-local\n");
62
63 const env: NodeJS.ProcessEnv = {
64 ORDER: "process",
65 FROM_PROCESS: "yes",
66 };
67
68 const result = loadDotenv({
69 root: tmpDir,
70 mode: "development",
71 processEnv: env,
72 });
73
74 // process.env values always win
75 expect(env.ORDER).toBe("process");
76 // .env.local beats .env.development and .env
77 expect(env.SECOND).toBe("local");
78 // pre-existing values are untouched
79 expect(env.FROM_PROCESS).toBe("yes");
80
81 // loadedEnv only contains keys actually written (not pre-existing)
82 expect(result.loadedEnv).not.toHaveProperty("ORDER");
83 expect(result.loadedEnv.SECOND).toBe("local");
84
85 // all existing files are reported as loaded
86 expect(result.loadedFiles).toEqual([
87 ".env.development.local",
88 ".env.local",
89 ".env.development",
90 ".env",
91 ]);
92 });
93
94 it("applies Next.js precedence for production mode", () => {
95 writeFile(".env", "DB_HOST=from-env\nAPI_KEY=from-env\n");
96 writeFile(".env.production", "DB_HOST=from-prod\n");
97 writeFile(".env.local", "DB_HOST=from-local\nAPI_KEY=from-local\n");
98 writeFile(".env.production.local", "DB_HOST=from-prod-local\n");
99
100 const env: NodeJS.ProcessEnv = {};
101
102 const result = loadDotenv({
103 root: tmpDir,
104 mode: "production",
105 processEnv: env,
106 });
107
108 // .env.production.local has highest file priority
109 expect(env.DB_HOST).toBe("from-prod-local");
110 // .env.local beats .env.production and .env
111 expect(env.API_KEY).toBe("from-local");
112
113 expect(result.loadedEnv.DB_HOST).toBe("from-prod-local");
114 expect(result.loadedEnv.API_KEY).toBe("from-local");
115 expect(result.loadedFiles).toEqual([
116 ".env.production.local",
117 ".env.local",
118 ".env.production",
119 ".env",
120 ]);
121 });
122
123 it("skips .env.local in test mode", () => {
124 writeFile(".env.local", "TEST_VALUE=from-local\n");
125 writeFile(".env.test", "TEST_VALUE=from-test\n");
126 writeFile(".env", "TEST_VALUE=from-env\n");
127
128 const env: NodeJS.ProcessEnv = {};
129 const result = loadDotenv({
130 root: tmpDir,
131 mode: "test",
132 processEnv: env,
133 });
134
135 expect(env.TEST_VALUE).toBe("from-test");
136 expect(result.loadedFiles).not.toContain(".env.local");
137 expect(result.loadedFiles).toContain(".env.test");
138 expect(result.loadedEnv.TEST_VALUE).toBe("from-test");
139 });
140
141 it("expands variables using $VAR syntax", () => {
142 writeFile(
143 ".env",
144 "BASE_URL=https://example.com\nAPI_URL=$BASE_URL/v1\n",
145 );
146
147 const env: NodeJS.ProcessEnv = {};
148 const result = loadDotenv({
149 root: tmpDir,
150 mode: "development",
151 processEnv: env,
152 });
153
154 expect(env.API_URL).toBe("https://example.com/v1");
155 expect(result.loadedEnv.API_URL).toBe("https://example.com/v1");
156 });
157
158 it("expands variables using ${VAR} syntax", () => {
159 writeFile(
160 ".env",
161 "HOST=localhost\nPORT=3000\nURL=http://${HOST}:${PORT}\n",
162 );
163
164 const env: NodeJS.ProcessEnv = {};
165 loadDotenv({ root: tmpDir, mode: "development", processEnv: env });
166
167 expect(env.URL).toBe("http://localhost:3000");
168 });
169
170 it("expansion prefers process.env over file values", () => {
171 writeFile(
172 ".env.development.local",
173 "BASE_URL=https://from-file.example.com\nAPI_URL=$BASE_URL/v1\n",
174 );
175
176 const env: NodeJS.ProcessEnv = {
177 BASE_URL: "https://from-process.example.com",
178 };
179
180 loadDotenv({
181 root: tmpDir,
182 mode: "development",
183 processEnv: env,
184 });
185
186 // BASE_URL from process.env wins, and expansion uses it
187 expect(env.BASE_URL).toBe("https://from-process.example.com");
188 expect(env.API_URL).toBe("https://from-process.example.com/v1");
189 });
190
191 it("handles escaped dollar signs", () => {
192 writeFile(".env", "PRICE=\\$100\n");
193
194 const env: NodeJS.ProcessEnv = {};
195 loadDotenv({ root: tmpDir, mode: "development", processEnv: env });
196
197 expect(env.PRICE).toBe("$100");
198 });
199
200 it("handles circular variable references without infinite loop", () => {
201 writeFile(".env", "A=$B\nB=$A\n");
202
203 const env: NodeJS.ProcessEnv = {};
204 // Should not hang — cycle detection breaks the loop
205 loadDotenv({ root: tmpDir, mode: "development", processEnv: env });
206
207 // Both resolve to empty string (cycle with no base value)
208 expect(env.A).toBeDefined();
209 expect(env.B).toBeDefined();
210 });
211
212 it("skips missing files without error", () => {
213 // Only .env exists, others are missing
214 writeFile(".env", "ONLY=here\n");
215
216 const env: NodeJS.ProcessEnv = {};
217 const result = loadDotenv({
218 root: tmpDir,
219 mode: "development",
220 processEnv: env,
221 });
222
223 expect(env.ONLY).toBe("here");
224 expect(result.loadedFiles).toEqual([".env"]);
225 });
226
227 it("expands variables across files (lower-priority file references higher-priority)", () => {
228 // DB_HOST is defined in .env.development.local (highest file priority)
229 // DB_URL in .env references it — should resolve via processEnv accumulation
230 writeFile(".env.development.local", "DB_HOST=localhost\n");
231 writeFile(".env", "DB_URL=postgres://$DB_HOST/mydb\n");
232
233 const env: NodeJS.ProcessEnv = {};
234 loadDotenv({ root: tmpDir, mode: "development", processEnv: env });
235
236 expect(env.DB_HOST).toBe("localhost");
237 expect(env.DB_URL).toBe("postgres://localhost/mydb");
238 });
239
240 it("handles multi-line quoted values", () => {
241 writeFile(
242 ".env",
243 'MULTI="line1\nline2\nline3"\nSINGLE=plain\n',
244 );
245
246 const env: NodeJS.ProcessEnv = {};
247 loadDotenv({ root: tmpDir, mode: "development", processEnv: env });
248
249 expect(env.MULTI).toBe("line1\nline2\nline3");
250 expect(env.SINGLE).toBe("plain");
251 });
252
253 it("returns empty result when no .env files exist", () => {
254 const env: NodeJS.ProcessEnv = {};
255 const result = loadDotenv({
256 root: tmpDir,
257 mode: "development",
258 processEnv: env,
259 });
260
261 expect(result.loadedFiles).toEqual([]);
262 expect(result.loadedEnv).toEqual({});
263 });
264});
265