-
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathplugins.test.ts
More file actions
149 lines (121 loc) · 4.69 KB
/
Copy pathplugins.test.ts
File metadata and controls
149 lines (121 loc) · 4.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// Copyright 2023-present Eser Ozvataf and other contributors. All rights reserved. Apache-2.0 license.
import { assert, assertEquals } from "@std/assert";
import { PLUGIN_PREFIX, resolvePlugin, runPlugin } from "./plugins.ts";
// The rule under test is git's: any `eser-<name>` on PATH is reachable as
// `eser <name>`. Generic, so a new binary needs no registration here.
const withPluginOnPath = async (
name: string,
body: string,
run: (dir: string) => Promise<void>,
): Promise<void> => {
const dir = await Deno.makeTempDir({ prefix: "eser-plugin-" });
const file = `${dir}/${PLUGIN_PREFIX}${name}`;
await Deno.writeTextFile(file, body);
await Deno.chmod(file, 0o755);
const previous = Deno.env.get("PATH") ?? "";
Deno.env.set("PATH", `${dir}:${previous}`);
try {
await run(dir);
} finally {
// Restored, not left set: PATH is process-global, so leaking it would
// change what every other test in this process resolves.
Deno.env.set("PATH", previous);
await Deno.remove(dir, { recursive: true });
}
};
Deno.test("an eser-* executable on PATH is resolvable as a subcommand", async () => {
await withPluginOnPath("demo", "#!/bin/sh\nexit 0\n", async () => {
const resolved = await resolvePlugin("demo");
if (resolved === null) {
throw new Error("eser-demo on PATH was not resolved");
}
assert(
resolved.endsWith("eser-demo"),
`resolved to ${resolved}, not the plugin`,
);
});
});
Deno.test("an absent plugin resolves to null rather than throwing", async () => {
assertEquals(await resolvePlugin("nothing-is-called-this-xyz"), null);
});
// Resolution must never leave a PATH directory. Note what actually guarantees
// that: every candidate is `<dir>/eser-<name>`, so a `..` in the name sits
// inside the component `eser-..` and is never its own path segment. The
// explicit reject in resolvePlugin is defence in depth on top of that — removing
// it does not make this test fail, which is why the assertion below targets the
// invariant rather than the guard.
Deno.test("a name cannot escape a PATH directory", async () => {
const outside = await Deno.makeTempDir({ prefix: "eser-outside-" });
const pathDir = await Deno.makeTempDir({ prefix: "eser-plugin-" });
// A real, executable target sitting outside every PATH entry.
await Deno.writeTextFile(`${outside}/victim`, "#!/bin/sh\nexit 0\n");
await Deno.chmod(`${outside}/victim`, 0o755);
const previous = Deno.env.get("PATH") ?? "";
Deno.env.set("PATH", pathDir);
try {
for (
const name of [
"../evil",
"a/b",
"..",
"a\\b",
`../${outside.split("/").pop()}/victim`,
]
) {
assertEquals(await resolvePlugin(name), null, `name ${name} resolved`);
}
} finally {
Deno.env.set("PATH", previous);
await Deno.remove(outside, { recursive: true });
await Deno.remove(pathDir, { recursive: true });
}
});
Deno.test("plugin exit codes propagate", async () => {
await withPluginOnPath("failing", "#!/bin/sh\nexit 7\n", async (dir) => {
const resolved = await resolvePlugin("failing");
if (resolved === null) {
throw new Error("eser-failing was not resolved");
}
assertEquals(await runPlugin(resolved, [], { cwd: dir }), 7);
});
});
Deno.test("arguments reach the plugin verbatim", async () => {
const script = `#!/bin/sh
[ "$1" = "--backend" ] || exit 1
[ "$2" = "kiro" ] || exit 1
[ "$3" = "a b" ] || exit 1
exit 0
`;
await withPluginOnPath("argecho", script, async (dir) => {
const resolved = await resolvePlugin("argecho");
if (resolved === null) {
throw new Error("eser-argecho was not resolved");
}
// "a b" is one argument with a space: the plugin path must not re-split it,
// which is exactly what a shell round-trip would do.
assertEquals(
await runPlugin(resolved, ["--backend", "kiro", "a b"], { cwd: dir }),
0,
);
});
});
// A non-executable file with the right name must not be "found". Reporting it
// as a plugin moves the failure to spawn time, with an error about permissions
// rather than about the plugin — which is the confusion PATH lookup exists to
// prevent.
Deno.test("a non-executable file with the right name is not a plugin", async () => {
const dir = await Deno.makeTempDir({ prefix: "eser-plugin-" });
await Deno.writeTextFile(
`${dir}/${PLUGIN_PREFIX}inert`,
"#!/bin/sh\nexit 0\n",
);
await Deno.chmod(`${dir}/${PLUGIN_PREFIX}inert`, 0o644);
const previous = Deno.env.get("PATH") ?? "";
Deno.env.set("PATH", `${dir}:${previous}`);
try {
assertEquals(await resolvePlugin("inert"), null);
} finally {
Deno.env.set("PATH", previous);
await Deno.remove(dir, { recursive: true });
}
});