Skip to content
File

Blob: src/workerd/api/node/tests/fs-access-test.js

javascript355 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4import { ok, rejects, strictEqual, throws } from 'node:assert';
5 
6// Tests node:fs access and exists APIs
7// These tests are not a straight up port of the Node.js tests but have
8// been adapted and verified to match the behavior of the Node.js APIs
9// as closely as possible.
10 
11import {
12 accessSync,
13 existsSync,
14 access,
15 exists,
16 promises,
17 constants,
18 fstatSync,
19 closeSync,
20 openSync,
21 writeSync,
22 writeFileSync,
23 readSync,
24} from 'node:fs';
25 
26const { F_OK, R_OK, W_OK, X_OK } = constants;
27 
28strictEqual(typeof accessSync, 'function');
29strictEqual(typeof access, 'function');
30strictEqual(typeof existsSync, 'function');
31strictEqual(typeof exists, 'function');
32strictEqual(typeof promises, 'object');
33strictEqual(typeof promises.access, 'function');
34 
35strictEqual(typeof F_OK, 'number');
36strictEqual(typeof R_OK, 'number');
37strictEqual(typeof W_OK, 'number');
38strictEqual(typeof X_OK, 'number');
39strictEqual(F_OK, 0);
40strictEqual(X_OK, 1);
41strictEqual(W_OK, 2);
42strictEqual(R_OK, 4);
43 
44const kKnownPaths = [
45 { path: '/', writable: false },
46 { path: '/bundle', writable: false },
47 { path: '/bundle/worker', writable: false },
48 { path: '/dev', writable: false },
49 { path: '/dev/null', writable: true },
50 { path: '/dev/zero', writable: true },
51 { path: '/dev/full', writable: true },
52 { path: '/dev/random', writable: true },
53 { path: '/tmp', writable: true },
54];
55 
56const UV_ENOENT = -2;
57 
58const kNoEntError = {
59 code: 'ENOENT',
60 errno: UV_ENOENT,
61 syscall: 'access',
62};
63 
64const kInvalidArgTypeError = { code: 'ERR_INVALID_ARG_TYPE' };
65const kOutOfRangeError = { code: 'ERR_OUT_OF_RANGE' };
66 
67function mustNotCall() {
68 throw new Error('This function should not be called');
69}
70 
71export const accessInputValidationTest = {
72 async test() {
73 // For both sync and async callback versions, input validation errors
74 // are thrown synchronously.
75 [
76 123n,
77 {
78 [Symbol.toPrimitive]() {
79 return R_OK;
80 },
81 },
82 '/',
83 ].forEach((i) => {
84 throws(() => accessSync('/', i), kInvalidArgTypeError);
85 throws(() => access('/', i, mustNotCall), kInvalidArgTypeError);
86 throws(() => access('/', F_OK, i), kInvalidArgTypeError);
87 });
88 throws(() => accessSync(123), kInvalidArgTypeError);
89 throws(() => access(123, '', mustNotCall), kInvalidArgTypeError);
90 
91 // Mode is out of range
92 throws(() => accessSync('/', -1), kOutOfRangeError);
93 throws(
94 () => accessSync('/', (F_OK | R_OK | W_OK | X_OK) + 1),
95 kOutOfRangeError
96 );
97 
98 // For the promise version, input validation errors cause rejected promises.
99 await rejects(promises.access('/', 123n), kInvalidArgTypeError);
100 await rejects(promises.access(123), kInvalidArgTypeError);
101 await rejects(promises.access('/', -1), kOutOfRangeError);
102 await rejects(
103 promises.access('/', (F_OK | R_OK | W_OK | X_OK) + 1),
104 kOutOfRangeError
105 );
106 },
107};
108 
109export const accessSyncTest = {
110 test() {
111 kKnownPaths.forEach(({ path, writable }) => {
112 const bufferPath = Buffer.from(path);
113 const urlPath = new URL(path, 'file:///');
114 accessSync(path);
115 accessSync(bufferPath);
116 accessSync(urlPath);
117 accessSync(path, F_OK);
118 accessSync(bufferPath, F_OK);
119 accessSync(urlPath, F_OK);
120 accessSync(path, R_OK);
121 accessSync(bufferPath, R_OK);
122 accessSync(urlPath, R_OK);
123 
124 // Some paths are writable (W_OK)
125 if (writable) {
126 accessSync(path, W_OK);
127 accessSync(bufferPath, W_OK);
128 accessSync(urlPath, W_OK);
129 } else {
130 throws(() => accessSync(path, W_OK), kNoEntError);
131 throws(() => accessSync(bufferPath, W_OK), kNoEntError);
132 throws(() => accessSync(urlPath, W_OK), kNoEntError);
133 }
134 
135 // No path is executable (X_OK)
136 throws(() => accessSync(path, X_OK), kNoEntError);
137 throws(() => accessSync(bufferPath, X_OK), kNoEntError);
138 throws(() => accessSync(urlPath, X_OK), kNoEntError);
139 });
140 
141 // Paths that don't exist...
142 throws(() => accessSync('/does/not/exist'), kNoEntError);
143 throws(() => accessSync('/does/not/exist', F_OK), kNoEntError);
144 throws(() => accessSync('/does/not/exist', R_OK), kNoEntError);
145 throws(() => accessSync('/does/not/exist', W_OK), kNoEntError);
146 throws(() => accessSync('/does/not/exist', X_OK), kNoEntError);
147 },
148};
149 
150export const accessCallbackTest = {
151 async test() {
152 const doTest = (path, mode) => {
153 const { promise, resolve, reject } = Promise.withResolvers();
154 access(path, mode, (err) => {
155 if (err) reject(err);
156 else resolve();
157 });
158 return promise;
159 };
160 
161 const cases = [];
162 kKnownPaths.forEach(({ path, writable }) => {
163 const bufferPath = Buffer.from(path);
164 const urlPath = new URL(path, 'file:///');
165 cases.push(
166 doTest(path, F_OK),
167 doTest(bufferPath, F_OK),
168 doTest(urlPath, F_OK),
169 doTest(path, R_OK),
170 doTest(bufferPath, R_OK),
171 doTest(urlPath, R_OK)
172 );
173 if (writable) {
174 cases.push(
175 doTest(path, W_OK),
176 doTest(bufferPath, W_OK),
177 doTest(urlPath, W_OK)
178 );
179 } else {
180 cases.push(
181 rejects(doTest(path, W_OK), kNoEntError),
182 rejects(doTest(bufferPath, W_OK), kNoEntError),
183 rejects(doTest(urlPath, W_OK), kNoEntError)
184 );
185 }
186 // No paths are executable (X_OK)
187 cases.push(
188 rejects(doTest(path, X_OK), kNoEntError),
189 rejects(doTest(bufferPath, X_OK), kNoEntError),
190 rejects(doTest(urlPath, X_OK), kNoEntError)
191 );
192 });
193 // Paths that don't exist have no permissions.
194 cases.push(
195 rejects(doTest('/does/not/exist', F_OK), kNoEntError),
196 rejects(doTest('/does/not/exist', R_OK), kNoEntError),
197 rejects(doTest('/does/not/exist', W_OK), kNoEntError),
198 rejects(doTest('/does/not/exist', X_OK), kNoEntError)
199 );
200 strictEqual(cases.length, kKnownPaths.length * 12 + 4);
201 await Promise.all(cases);
202 },
203};
204 
205export const accessPromiseTest = {
206 async test() {
207 const cases = [];
208 kKnownPaths.forEach(({ path, writable }) => {
209 const bufferPath = Buffer.from(path);
210 const urlPath = new URL(path, 'file:///');
211 cases.push(
212 promises.access(path, F_OK),
213 promises.access(bufferPath, F_OK),
214 promises.access(urlPath, F_OK),
215 promises.access(path, R_OK),
216 promises.access(bufferPath, R_OK),
217 promises.access(urlPath, R_OK)
218 );
219 if (writable) {
220 cases.push(
221 promises.access(path, W_OK),
222 promises.access(bufferPath, W_OK),
223 promises.access(urlPath, W_OK)
224 );
225 } else {
226 cases.push(
227 rejects(promises.access(path, W_OK), kNoEntError),
228 rejects(promises.access(bufferPath, W_OK), kNoEntError),
229 rejects(promises.access(urlPath, W_OK), kNoEntError)
230 );
231 }
232 // No paths are executable (X_OK)
233 cases.push(
234 rejects(promises.access(path, X_OK), kNoEntError),
235 rejects(promises.access(bufferPath, X_OK), kNoEntError),
236 rejects(promises.access(urlPath, X_OK), kNoEntError)
237 );
238 });
239 // Paths that don't exist have no permissions.
240 cases.push(
241 rejects(promises.access('/does/not/exist', F_OK), kNoEntError),
242 rejects(promises.access('/does/not/exist', R_OK), kNoEntError),
243 rejects(promises.access('/does/not/exist', W_OK), kNoEntError),
244 rejects(promises.access('/does/not/exist', X_OK), kNoEntError)
245 );
246 strictEqual(cases.length, kKnownPaths.length * 12 + 4);
247 await Promise.all(cases);
248 },
249};
250 
251export const existsSyncTest = {
252 test() {
253 kKnownPaths.forEach(({ path }) => {
254 const bufferPath = Buffer.from(path);
255 const urlPath = new URL(path, 'file:///');
256 ok(existsSync(path));
257 ok(existsSync(bufferPath));
258 ok(existsSync(urlPath));
259 });
260 
261 // Paths that don't exist
262 ok(!existsSync('/does/not/exist'));
263 // Incorrect inputs types results in false returned
264 ok(!existsSync(123));
265 },
266};
267 
268export const existsCallbackTest = {
269 async test() {
270 const doTest = (path, expected) => {
271 const { promise, resolve, reject } = Promise.withResolvers();
272 exists(path, (exists) => {
273 if (exists !== expected)
274 reject(new Error(`Expected ${path} to be ${expected}`));
275 else resolve();
276 });
277 return promise;
278 };
279 
280 const cases = [];
281 kKnownPaths.forEach(({ path }) => {
282 const bufferPath = Buffer.from(path);
283 const urlPath = new URL(path, 'file:///');
284 cases.push(
285 doTest(path, true),
286 doTest(bufferPath, true),
287 doTest(urlPath, true)
288 );
289 });
290 // Paths that don't exist
291 cases.push(doTest('/does/not/exist', false));
292 // Incorrect inputs types results in false returned
293 cases.push(doTest(123, false));
294 
295 strictEqual(cases.length, kKnownPaths.length * 3 + 2);
296 await Promise.all(cases);
297 },
298};
299 
300export const stdioFds = {
301 test() {
302 let stat1 = fstatSync(0);
303 let stat2 = fstatSync(1);
304 let stat3 = fstatSync(2);
305 ok(stat1.isFile());
306 ok(stat2.isFile());
307 ok(stat3.isFile());
308 closeSync(0);
309 closeSync(1);
310 closeSync(2);
311 // After closing the stdio file descriptors, they can still be used.
312 // That is, we don't actually close them.
313 stat1 = fstatSync(0);
314 stat2 = fstatSync(1);
315 stat3 = fstatSync(2);
316 ok(stat1.isFile());
317 ok(stat2.isFile());
318 ok(stat3.isFile());
319 
320 // Writing to the stdio file descriptors is permitted, always in append mode.
321 // Position is ignored.
322 writeSync(0, 'Hello, stdin!\n', 1000);
323 writeSync(0, 'test', 500);
324 writeSync(1, 'Hello, stdin!\n', 1000);
325 writeSync(1, 'test', 500);
326 writeSync(2, 'Hello, stdin!\n', 1000);
327 writeSync(2, 'test', 500);
328 
329 stat1 = fstatSync(0);
330 strictEqual(stat1.size, 0);
331 
332 const buf = Buffer.alloc(20);
333 const _bytesRead = readSync(0, buf, { position: 1000 });
334 
335 const fd = openSync('/tmp/test.txt', 'w');
336 strictEqual(fd, 3);
337 },
338};
339// There is no promise version of exists in Node.js, so no test for it here.
340 
341export const fsPercentEncodingTest = {
342 test() {
343 // The ? and # in the string path will be percent-encoded as part of the
344 // normalization, and the empty // in the path will be replaced as /.
345 // the %FF is left as is.
346 const path = '/tmp//?#a%FF';
347 ok(!existsSync('/tmp/%3F%23a%FF'));
348 writeFileSync(path, 'test');
349 
350 // The casing of the percent-encoded characters is not significant.
351 ok(existsSync('/tmp/%3f%23a%FF'));
352 ok(existsSync('/tmp/%3F%23a%ff'));
353 },
354};