Skip to content

Commit 393bce9

Browse files
committed
Add tests for ensureUserWritable and file permission handling
Add unit tests (file-permissions.test.ts) covering: - ensureUserWritable fixes read-only files - ensureUserWritable is no-op for already-writable files - Simulated Nix/deb scenario (copyFile from read-only source) Extend create smoke test to verify all output files are user-writable. Extract withTempDir() helper to tests/utils.ts for reuse. Permission tests are skipped on Windows where chmod is not supported.
1 parent e3a24a6 commit 393bce9

3 files changed

Lines changed: 115 additions & 0 deletions

File tree

tests/smoke/create/create.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { execProcess } from "../../../src/core/process.ts";
99
import { join } from "../../../src/deno_ral/path.ts";
10+
import { walkSync } from "../../../src/deno_ral/fs.ts";
1011
import { CreateResult } from "../../../src/command/create/cmd-types.ts";
1112
import { assert } from "testing/asserts";
1213
import { quartoDevCmd } from "../../utils.ts";
@@ -61,6 +62,27 @@ for (const type of Object.keys(kCreateTypes)) {
6162
assert(process.success, process.stderr);
6263
});
6364

65+
// Verify all created files are user-writable.
66+
// NOTE: In dev environments, resource files are already writable (0o644),
67+
// so this test passes even without ensureUserWritable. It guards against
68+
// regressions; the unit test in file-permissions.test.ts covers the
69+
// read-only → writable transition directly.
70+
await t.step({
71+
name: `> check writable ${type} ${template}`,
72+
ignore: Deno.build.os === "windows",
73+
fn: () => {
74+
for (const entry of walkSync(artifactPath)) {
75+
if (entry.isFile) {
76+
const stat = Deno.statSync(entry.path);
77+
assert(
78+
stat.mode !== null && (stat.mode! & 0o200) !== 0,
79+
`File ${entry.path} is not user-writable (mode: ${stat.mode?.toString(8)})`,
80+
);
81+
}
82+
}
83+
},
84+
});
85+
6486
// Render the artifact
6587
await t.step(`> render ${type} ${template}`, async () => {
6688
const path = result!.path;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* file-permissions.test.ts
3+
*
4+
* Copyright (C) 2020-2026 Posit Software, PBC
5+
*/
6+
7+
import { unitTest } from "../test.ts";
8+
import { withTempDir } from "../utils.ts";
9+
import { assert, assertEquals } from "testing/asserts";
10+
import { join } from "../../src/deno_ral/path.ts";
11+
import { isWindows } from "../../src/deno_ral/platform.ts";
12+
import {
13+
ensureUserWritable,
14+
safeModeFromFile,
15+
} from "../../src/deno_ral/fs.ts";
16+
17+
function writeFile(dir: string, name: string, content: string, mode: number): string {
18+
const path = join(dir, name);
19+
Deno.writeTextFileSync(path, content);
20+
Deno.chmodSync(path, mode);
21+
return path;
22+
}
23+
24+
unitTest(
25+
"file-permissions - ensureUserWritable fixes read-only files",
26+
async () => withTempDir((dir) => {
27+
const file = writeFile(dir, "readonly.txt", "test content", 0o444);
28+
29+
const modeBefore = safeModeFromFile(file);
30+
assert(modeBefore !== undefined);
31+
assert((modeBefore! & 0o200) === 0, "File should be read-only before fix");
32+
33+
ensureUserWritable(file);
34+
35+
assertEquals(safeModeFromFile(file), 0o644,
36+
"Mode should be exactly 0o644 (0o444 | 0o200) — only user write bit added");
37+
}),
38+
{ ignore: isWindows },
39+
);
40+
41+
unitTest(
42+
"file-permissions - ensureUserWritable leaves writable files unchanged",
43+
async () => withTempDir((dir) => {
44+
const file = writeFile(dir, "writable.txt", "test content", 0o644);
45+
const modeBefore = safeModeFromFile(file);
46+
assert(modeBefore !== undefined, "Mode should be readable");
47+
48+
ensureUserWritable(file);
49+
50+
assertEquals(safeModeFromFile(file), modeBefore,
51+
"Mode should be unchanged for already-writable file");
52+
}),
53+
{ ignore: isWindows },
54+
);
55+
56+
// Simulates the Nix/deb scenario: Deno.copyFileSync from a read-only source
57+
// preserves the read-only mode on the copy. ensureUserWritable must fix it.
58+
unitTest(
59+
"file-permissions - copyFileSync from read-only source then ensureUserWritable",
60+
async () => withTempDir((dir) => {
61+
const src = writeFile(dir, "source.lua", "-- filter code", 0o444);
62+
63+
// Copy it (this is what quarto create does internally)
64+
const dest = join(dir, "dest.lua");
65+
Deno.copyFileSync(src, dest);
66+
67+
// Without the fix, dest inherits 0o444 from src
68+
const modeBefore = safeModeFromFile(dest);
69+
assert(modeBefore !== undefined);
70+
assert((modeBefore! & 0o200) === 0, "Copied file should inherit read-only mode from source");
71+
72+
// Make source writable so cleanup succeeds
73+
Deno.chmodSync(src, 0o644);
74+
75+
ensureUserWritable(dest);
76+
77+
assertEquals(safeModeFromFile(dest), 0o644,
78+
"Copied file should be user-writable after ensureUserWritable");
79+
}),
80+
{ ignore: isWindows },
81+
);

tests/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ export function inTempDirectory(fn: (dir: string) => unknown): unknown {
2020
return fn(dir);
2121
}
2222

23+
export async function withTempDir<T>(
24+
fn: (dir: string) => T | Promise<T>,
25+
prefix = "quarto-test",
26+
): Promise<T> {
27+
const dir = Deno.makeTempDirSync({ prefix });
28+
try {
29+
return await fn(dir);
30+
} finally {
31+
Deno.removeSync(dir, { recursive: true });
32+
}
33+
}
34+
2335
// Find a _quarto.yaml file in the directory hierarchy of the input file
2436
export function findProjectDir(input: string, until?: RegExp | undefined): string | undefined {
2537
let dir = dirname(input);

0 commit comments

Comments
 (0)