/** * Integration tests for variable and resource CLI commands. * Tests list and push operations via CLI and direct API. */ import { expect, test, describe } from "bun:test"; import { writeFile, mkdir, readFile } from "node:fs/promises"; import { join } from "node:path"; import { withTestBackend } from "./test_backend.ts"; import { addWorkspace } from "../workspace.ts"; async function setupWorkspaceProfile(backend: any): Promise { await addWorkspace( { remote: backend.baseUrl, workspaceId: backend.workspace, name: "localhost_test", token: backend.token, }, { force: true, configDir: backend.testConfigDir } ); } // ============================================================================= // Variable Tests // ============================================================================= describe("variable", () => { test("list returns seeded variables", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); const result = await backend.runCLICommand(["variable"], tempDir); expect(result.code).toEqual(0); // seedTestData creates f/test/my_variable expect(result.stdout).toContain("f/test/my_variable"); }); }); test("push creates a new variable via sync push", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); const uniqueId = Date.now(); // Create wmill.yaml await writeFile( join(tempDir, "wmill.yaml"), `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, "utf-8" ); // Create variable file await mkdir(join(tempDir, "f", "test"), { recursive: true }); const varPath = `f/test/test_var_${uniqueId}.variable.yaml`; await writeFile( join(tempDir, varPath), `value: "hello_from_test_${uniqueId}"\nis_secret: false\ndescription: "Test variable created by integration test"\n`, "utf-8" ); // Push with sync push targeting just our variable const pushResult = await backend.runCLICommand( ["sync", "push", "--yes", "--includes", `f/test/test_var_${uniqueId}**`], tempDir ); expect(pushResult.code).toEqual(0); // Verify via API that the variable was created const apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/get/f/test/test_var_${uniqueId}` ); expect(apiResp.status).toEqual(200); const varData = await apiResp.json(); expect(varData.path).toBe(`f/test/test_var_${uniqueId}`); expect(varData.is_secret).toBe(false); }); }); test("push updates an existing variable", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); const uniqueId = Date.now(); // Create variable via API first const createResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: `f/test/update_var_${uniqueId}`, value: "original_value", is_secret: false, description: "Original description", }), } ); expect(createResp.status).toBeLessThan(300); await createResp.text(); // Create wmill.yaml and updated variable file await writeFile( join(tempDir, "wmill.yaml"), `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, "utf-8" ); await mkdir(join(tempDir, "f", "test"), { recursive: true }); await writeFile( join(tempDir, `f/test/update_var_${uniqueId}.variable.yaml`), `value: "updated_value"\nis_secret: false\ndescription: "Updated description"\n`, "utf-8" ); // Push the update const pushResult = await backend.runCLICommand( ["sync", "push", "--yes", "--includes", `f/test/update_var_${uniqueId}**`], tempDir ); expect(pushResult.code).toEqual(0); // Verify the update via API const apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/get/f/test/update_var_${uniqueId}` ); expect(apiResp.status).toEqual(200); const varData = await apiResp.json(); expect(varData.description).toBe("Updated description"); }); }); test("add creates secret by default and preserves fields on update", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); const uniqueId = Date.now(); const varPath = `f/test/add_var_${uniqueId}`; // Create: secret by default, --description sets the description const createResult = await backend.runCLICommand( ["variable", "add", "v1", varPath, "--description", "first desc"], tempDir ); expect(createResult.code).toEqual(0); let apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/get/${varPath}` ); expect(apiResp.status).toEqual(200); let varData = await apiResp.json(); expect(varData.is_secret).toBe(true); expect(varData.description).toBe("first desc"); // Update with --yes only: no prompt, is_secret and description preserved const updateResult = await backend.runCLICommand( ["variable", "add", "v2", varPath, "--yes"], tempDir ); expect(updateResult.code).toEqual(0); apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/get/${varPath}` ); expect(apiResp.status).toEqual(200); varData = await apiResp.json(); expect(varData.is_secret).toBe(true); expect(varData.description).toBe("first desc"); expect(varData.value).toBe("v2"); // Update with --no-secret: flips to non-secret const noSecretResult = await backend.runCLICommand( ["variable", "add", "v3", varPath, "--yes", "--no-secret"], tempDir ); expect(noSecretResult.code).toEqual(0); apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/get/${varPath}` ); expect(apiResp.status).toEqual(200); varData = await apiResp.json(); expect(varData.is_secret).toBe(false); expect(varData.value).toBe("v3"); // Update the non-secret variable with no secret flags: is_secret must // stay false (preserved, not re-defaulted to secret) const preserveResult = await backend.runCLICommand( ["variable", "add", "v4", varPath, "--yes"], tempDir ); expect(preserveResult.code).toEqual(0); apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/get/${varPath}` ); expect(apiResp.status).toEqual(200); varData = await apiResp.json(); expect(varData.is_secret).toBe(false); expect(varData.value).toBe("v4"); // Explicit --secret flips it back const secretResult = await backend.runCLICommand( ["variable", "add", "v5", varPath, "--yes", "--secret"], tempDir ); expect(secretResult.code).toEqual(0); apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/get/${varPath}` ); expect(apiResp.status).toEqual(200); varData = await apiResp.json(); expect(varData.is_secret).toBe(true); expect(varData.value).toBe("v5"); }); }); test("push encrypts a plaintext secret value (no --plain-secrets) and round-trips", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); const uniqueId = Date.now(); const varPath = `f/test/sec_push_${uniqueId}`; // Existing secret variable (server-encrypted). const createResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: varPath, value: "original-secret", is_secret: true, description: "", }), } ); expect(createResp.status).toBeLessThan(300); await createResp.text(); // A hand-authored spec file: plaintext value, is_secret: true. Pushing it // without --plain-secrets must encrypt the value server-side, not store the // plaintext verbatim as ciphertext (which would make every read fail). const specPath = join(tempDir, "v.yaml"); await writeFile( specPath, `value: |\n some: plaintext\nis_secret: true\ndescription: ""\n`, "utf-8" ); const pushResult = await backend.runCLICommand( ["variable", "push", specPath, varPath], tempDir ); expect(pushResult.code).toEqual(0); // The value must decrypt cleanly to the pushed plaintext. const apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/get/${varPath}?decrypt_secret=true` ); expect(apiResp.status).toEqual(200); const varData = await apiResp.json(); expect(varData.is_secret).toBe(true); expect(varData.value).toBe("some: plaintext\n"); }); }); test("push flips is_secret from true to false", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); const uniqueId = Date.now(); const varPath = `f/test/sec_down_${uniqueId}`; const createResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: varPath, value: "original-secret", is_secret: true, description: "", }), } ); expect(createResp.status).toBeLessThan(300); await createResp.text(); const specPath = join(tempDir, "v_down.yaml"); await writeFile( specPath, `value: "now-public"\nis_secret: false\ndescription: ""\n`, "utf-8" ); const pushResult = await backend.runCLICommand( ["variable", "push", specPath, varPath], tempDir ); expect(pushResult.code).toEqual(0); const apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/get/${varPath}?decrypt_secret=true` ); expect(apiResp.status).toEqual(200); const varData = await apiResp.json(); expect(varData.is_secret).toBe(false); expect(varData.value).toBe("now-public"); }); }); test("pull retrieves variables into local files", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); const uniqueId = Date.now(); // Create a variable via API const createResp = await backend.apiRequest!( `/api/w/${backend.workspace}/variables/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: `f/test/pull_var_${uniqueId}`, value: "pull_test_value", is_secret: false, description: "Variable for pull test", }), } ); expect(createResp.status).toBeLessThan(300); await createResp.text(); // Create wmill.yaml await writeFile( join(tempDir, "wmill.yaml"), `defaultTs: bun\nincludes:\n - "f/test/pull_var_${uniqueId}**"\nexcludes: []\n`, "utf-8" ); // Pull const pullResult = await backend.runCLICommand( ["sync", "pull", "--yes"], tempDir ); expect(pullResult.code).toEqual(0); // Check the file was created const content = await readFile( join(tempDir, `f/test/pull_var_${uniqueId}.variable.yaml`), "utf-8" ); expect(content).toContain("pull_test_value"); expect(content).toContain("is_secret: false"); }); }); }); // ============================================================================= // Resource Tests // ============================================================================= describe("resource", () => { test("list returns seeded resources", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); const result = await backend.runCLICommand(["resource"], tempDir); expect(result.code).toEqual(0); // seedTestData creates f/test/my_resource expect(result.stdout).toContain("f/test/my_resource"); }); }); test("push creates a new resource via sync push", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); const uniqueId = Date.now(); // Create wmill.yaml await writeFile( join(tempDir, "wmill.yaml"), `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, "utf-8" ); // Create resource file await mkdir(join(tempDir, "f", "test"), { recursive: true }); const resPath = `f/test/test_res_${uniqueId}.resource.yaml`; await writeFile( join(tempDir, resPath), `resource_type: "any"\nvalue:\n host: "localhost"\n port: 3000\ndescription: "Test resource"\n`, "utf-8" ); // Push const pushResult = await backend.runCLICommand( ["sync", "push", "--yes", "--includes", `f/test/test_res_${uniqueId}**`], tempDir ); expect(pushResult.code).toEqual(0); // Verify via API const apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/resources/get/f/test/test_res_${uniqueId}` ); expect(apiResp.status).toEqual(200); const resData = await apiResp.json(); expect(resData.path).toBe(`f/test/test_res_${uniqueId}`); expect(resData.resource_type).toBe("any"); expect(resData.value.host).toBe("localhost"); }); }); test("push updates an existing resource", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); const uniqueId = Date.now(); // Create resource via API const createResp = await backend.apiRequest!( `/api/w/${backend.workspace}/resources/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: `f/test/update_res_${uniqueId}`, resource_type: "any", value: { host: "old_host" }, description: "Original", }), } ); expect(createResp.status).toBeLessThan(300); await createResp.text(); // Create wmill.yaml and updated resource file await writeFile( join(tempDir, "wmill.yaml"), `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, "utf-8" ); await mkdir(join(tempDir, "f", "test"), { recursive: true }); await writeFile( join(tempDir, `f/test/update_res_${uniqueId}.resource.yaml`), `resource_type: "any"\nvalue:\n host: "new_host"\n port: 9999\ndescription: "Updated"\n`, "utf-8" ); // Push the update const pushResult = await backend.runCLICommand( ["sync", "push", "--yes", "--includes", `f/test/update_res_${uniqueId}**`], tempDir ); expect(pushResult.code).toEqual(0); // Verify update const apiResp = await backend.apiRequest!( `/api/w/${backend.workspace}/resources/get/f/test/update_res_${uniqueId}` ); expect(apiResp.status).toEqual(200); const resData = await apiResp.json(); expect(resData.value.host).toBe("new_host"); expect(resData.value.port).toBe(9999); }); }); test("pull retrieves resources into local files", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); const uniqueId = Date.now(); // Create resource via API const createResp = await backend.apiRequest!( `/api/w/${backend.workspace}/resources/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: `f/test/pull_res_${uniqueId}`, resource_type: "any", value: { key: "pull_test" }, }), } ); expect(createResp.status).toBeLessThan(300); await createResp.text(); // Create wmill.yaml await writeFile( join(tempDir, "wmill.yaml"), `defaultTs: bun\nincludes:\n - "f/test/pull_res_${uniqueId}**"\nexcludes: []\nskipVariables: true\n`, "utf-8" ); // Pull const pullResult = await backend.runCLICommand( ["sync", "pull", "--yes"], tempDir ); expect(pullResult.code).toEqual(0); // Check the resource file was created const content = await readFile( join(tempDir, `f/test/pull_res_${uniqueId}.resource.yaml`), "utf-8" ); expect(content).toContain("pull_test"); }); }); });