create-cloud-db 1.0.127 → 1.0.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "CLI to create a Turso database and write TURSO_* env vars to .env file",
4
4
  "type": "module",
5
5
  "author": "vtempest",
6
- "version": "1.0.127",
6
+ "version": "1.0.129",
7
7
  "main": "create-cloud-db.js",
8
8
  "bin": {
9
9
  "create-cloud-db": "create-cloud-db.js"
@@ -0,0 +1,306 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const h = vi.hoisted(() => ({
4
+ execSync: vi.fn(),
5
+ answers: [],
6
+ existsSync: vi.fn(),
7
+ readFileSync: vi.fn(),
8
+ writeFileSync: vi.fn(),
9
+ }));
10
+
11
+ vi.mock("child_process", () => ({ execSync: h.execSync, default: { execSync: h.execSync } }));
12
+ vi.mock("readline", () => {
13
+ const createInterface = () => ({
14
+ question: (_q, cb) => cb(h.answers.shift() ?? ""),
15
+ close: () => {},
16
+ });
17
+ return { createInterface, default: { createInterface } };
18
+ });
19
+ vi.mock("fs", () => {
20
+ const api = {
21
+ existsSync: h.existsSync,
22
+ readFileSync: h.readFileSync,
23
+ writeFileSync: h.writeFileSync,
24
+ };
25
+ return { ...api, default: api };
26
+ });
27
+
28
+ class ExitSignal extends Error {}
29
+
30
+ let exitCalls;
31
+ let logs;
32
+ let throwOnExit;
33
+
34
+ /** Run the CLI (it executes `main()` on import) and wait for it to settle. */
35
+ async function runCli(argv = []) {
36
+ vi.resetModules();
37
+ process.argv = ["node", "create-cloud-db.js", ...argv];
38
+ await import("../create-cloud-db.js");
39
+ await new Promise((r) => setTimeout(r, 20));
40
+ }
41
+
42
+ /** Script `turso` responses by command prefix. */
43
+ function turso(map) {
44
+ h.execSync.mockImplementation((cmd) => {
45
+ for (const [prefix, out] of Object.entries(map)) {
46
+ if (cmd.startsWith(prefix)) {
47
+ if (out instanceof Error) throw out;
48
+ return out;
49
+ }
50
+ }
51
+ return "";
52
+ });
53
+ }
54
+
55
+ beforeEach(() => {
56
+ exitCalls = [];
57
+ logs = [];
58
+ throwOnExit = true;
59
+ h.answers.length = 0;
60
+ h.execSync.mockReset();
61
+ h.existsSync.mockReset().mockReturnValue(false);
62
+ h.readFileSync.mockReset();
63
+ h.writeFileSync.mockReset();
64
+ delete process.env.TURSO_DATABASE_URL;
65
+ delete process.env.TURSO_AUTH_TOKEN;
66
+ vi.spyOn(process, "exit").mockImplementation((code) => {
67
+ exitCalls.push(code);
68
+ // First exit halts the flow; main()'s catch-handler exit is a no-op.
69
+ if (throwOnExit && exitCalls.length === 1) throw new ExitSignal(`exit ${code}`);
70
+ });
71
+ for (const m of ["log", "warn", "error"]) {
72
+ vi.spyOn(console, m).mockImplementation((...a) => logs.push(a.join(" ")));
73
+ }
74
+ });
75
+
76
+ afterEach(() => {
77
+ vi.restoreAllMocks();
78
+ });
79
+
80
+ describe("create-cloud-db", () => {
81
+ it("creates the database and writes TURSO_* values to a new .env", async () => {
82
+ turso({
83
+ "turso auth token": "tok_logged_in",
84
+ "turso db create": "created",
85
+ "turso db show": "libsql://mydb.turso.io\n",
86
+ "turso db tokens create": "secret-token\n",
87
+ });
88
+
89
+ await runCli(["mydb"]);
90
+
91
+ expect(exitCalls).toEqual([]);
92
+ expect(h.execSync).toHaveBeenCalledWith(
93
+ "turso db create mydb",
94
+ expect.any(Object),
95
+ );
96
+ expect(h.writeFileSync).toHaveBeenCalledTimes(1);
97
+ const [file, body] = h.writeFileSync.mock.calls[0];
98
+ expect(file.endsWith(".env")).toBe(true);
99
+ expect(body).toBe(
100
+ "TURSO_DATABASE_URL=libsql://mydb.turso.io\nTURSO_AUTH_TOKEN=secret-token\n",
101
+ );
102
+ expect(process.env.TURSO_DATABASE_URL).toBe("libsql://mydb.turso.io");
103
+ expect(process.env.TURSO_AUTH_TOKEN).toBe("secret-token");
104
+ });
105
+
106
+ it("prompts for the db name when argv has none", async () => {
107
+ h.answers.push("promptdb");
108
+ turso({
109
+ "turso auth token": "tok",
110
+ "turso db show": "libsql://promptdb",
111
+ "turso db tokens create": "t",
112
+ });
113
+
114
+ await runCli();
115
+
116
+ expect(h.execSync).toHaveBeenCalledWith(
117
+ "turso db create promptdb",
118
+ expect.any(Object),
119
+ );
120
+ });
121
+
122
+ it("exits 1 when no database name is given", async () => {
123
+ h.answers.push("");
124
+ turso({ "turso auth token": "tok" });
125
+
126
+ await runCli();
127
+
128
+ expect(exitCalls[0]).toBe(1);
129
+ expect(logs.join("\n")).toContain("Database name is required");
130
+ expect(h.writeFileSync).not.toHaveBeenCalled();
131
+ });
132
+
133
+ it("exits 1 when not logged in and the user declines to log in", async () => {
134
+ h.answers.push("n");
135
+ turso({
136
+ "turso auth token": "You are not logged in, please login with turso auth login",
137
+ });
138
+
139
+ await runCli(["db"]);
140
+
141
+ expect(exitCalls[0]).toBe(1);
142
+ expect(logs.join("\n")).toContain("You are not logged in");
143
+ });
144
+
145
+ it("treats an empty token response as logged out", async () => {
146
+ h.answers.push("no");
147
+ turso({ "turso auth token": "" });
148
+
149
+ await runCli(["db"]);
150
+
151
+ expect(exitCalls[0]).toBe(1);
152
+ });
153
+
154
+ it("logs in on request and then continues", async () => {
155
+ h.answers.push("y");
156
+ let tokenCalls = 0;
157
+ h.execSync.mockImplementation((cmd) => {
158
+ if (cmd === "turso auth token") {
159
+ tokenCalls += 1;
160
+ return tokenCalls === 1 ? "" : "tok";
161
+ }
162
+ if (cmd.startsWith("turso db show")) return "libsql://x";
163
+ if (cmd.startsWith("turso db tokens create")) return "tk";
164
+ return "";
165
+ });
166
+
167
+ await runCli(["db"]);
168
+
169
+ expect(h.execSync).toHaveBeenCalledWith("turso auth login", expect.any(Object));
170
+ expect(exitCalls).toEqual([]);
171
+ expect(h.writeFileSync).toHaveBeenCalled();
172
+ });
173
+
174
+ it("exits 1 when `turso auth login` throws", async () => {
175
+ h.answers.push("yes");
176
+ h.execSync.mockImplementation((cmd) => {
177
+ if (cmd === "turso auth token") return "";
178
+ if (cmd === "turso auth login") throw new Error("boom");
179
+ return "";
180
+ });
181
+
182
+ await runCli(["db"]);
183
+
184
+ expect(exitCalls[0]).toBe(1);
185
+ expect(logs.join("\n")).toContain("Login failed");
186
+ });
187
+
188
+ it("exits 1 when still logged out after login", async () => {
189
+ h.answers.push("y");
190
+ h.execSync.mockImplementation(() => "");
191
+
192
+ await runCli(["db"]);
193
+
194
+ expect(exitCalls[0]).toBe(1);
195
+ expect(logs.join("\n")).toContain("Still not logged in");
196
+ });
197
+
198
+ it("keeps existing values and exits 0 when overwrite is declined", async () => {
199
+ h.answers.push("n");
200
+ h.existsSync.mockReturnValue(true);
201
+ h.readFileSync.mockReturnValue(
202
+ "TURSO_DATABASE_URL=libsql://old\nTURSO_AUTH_TOKEN=oldtoken\n",
203
+ );
204
+ turso({ "turso auth token": "tok" });
205
+
206
+ await runCli(["db"]);
207
+
208
+ expect(exitCalls[0]).toBe(0);
209
+ expect(h.writeFileSync).not.toHaveBeenCalled();
210
+ expect(logs.join("\n")).toContain("libsql://old");
211
+ });
212
+
213
+ it("notices existing values from process.env too", async () => {
214
+ process.env.TURSO_AUTH_TOKEN = "envtoken";
215
+ h.answers.push("n");
216
+ turso({ "turso auth token": "tok" });
217
+
218
+ await runCli(["db"]);
219
+
220
+ expect(exitCalls[0]).toBe(0);
221
+ expect(logs.join("\n")).toContain("TURSO_AUTH_TOKEN: envtoken");
222
+ });
223
+
224
+ it("preserves comments and unrelated keys while overwriting on confirm", async () => {
225
+ h.answers.push("yes");
226
+ h.existsSync.mockReturnValue(true);
227
+ h.readFileSync.mockReturnValue(
228
+ [
229
+ "# my config",
230
+ "",
231
+ 'TURSO_DATABASE_URL="libsql://old"',
232
+ "KEEP_ME='yes'",
233
+ "not a pair",
234
+ "TURSO_AUTH_TOKEN=old",
235
+ ].join("\r\n"),
236
+ );
237
+ turso({
238
+ "turso auth token": "tok",
239
+ "turso db show": "libsql://new",
240
+ "turso db tokens create": "newtok",
241
+ });
242
+
243
+ await runCli(["db"]);
244
+
245
+ const body = h.writeFileSync.mock.calls[0][1];
246
+ expect(body).toBe(
247
+ [
248
+ "# my config",
249
+ "",
250
+ "TURSO_DATABASE_URL=libsql://new",
251
+ // Every parsed key is rewritten, so surrounding quotes are dropped.
252
+ "KEEP_ME=yes",
253
+ "not a pair",
254
+ "TURSO_AUTH_TOKEN=newtok",
255
+ "",
256
+ ].join("\n"),
257
+ );
258
+ });
259
+
260
+ it("warns and continues when `turso db create` fails (db exists)", async () => {
261
+ h.execSync.mockImplementation((cmd) => {
262
+ if (cmd === "turso auth token") return "tok";
263
+ if (cmd.startsWith("turso db create")) throw new Error("already exists");
264
+ if (cmd.startsWith("turso db show")) return "libsql://exists";
265
+ if (cmd.startsWith("turso db tokens create")) return "t";
266
+ return "";
267
+ });
268
+
269
+ await runCli(["db"]);
270
+
271
+ expect(logs.join("\n")).toContain("may already exist");
272
+ expect(h.writeFileSync).toHaveBeenCalled();
273
+ });
274
+
275
+ it("returns stderr output from a failing ignorable command", async () => {
276
+ h.execSync.mockImplementation((cmd) => {
277
+ if (cmd === "turso auth token") {
278
+ const e = new Error("fail");
279
+ e.stdout = "";
280
+ e.stderr = Buffer.from("You are not logged in, please login with turso auth login");
281
+ throw e;
282
+ }
283
+ return "";
284
+ });
285
+ h.answers.push("n");
286
+
287
+ await runCli(["db"]);
288
+
289
+ expect(exitCalls[0]).toBe(1);
290
+ });
291
+
292
+ it("reports unexpected errors and exits 1", async () => {
293
+ // The exit comes from main()'s own catch handler, so it must not throw.
294
+ throwOnExit = false;
295
+ h.execSync.mockImplementation((cmd) => {
296
+ if (cmd === "turso auth token") return "tok";
297
+ if (cmd.startsWith("turso db show")) throw new Error("show failed");
298
+ return "";
299
+ });
300
+
301
+ await runCli(["db"]);
302
+
303
+ expect(exitCalls[0]).toBe(1);
304
+ expect(logs.join("\n")).toContain("Unexpected error");
305
+ });
306
+ });