Skip to content
File

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

javascript595 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 { rejects, strictEqual, throws } from 'node:assert';
5 
6// Tests node:fs chmod and chown 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 
11// Our implementation of these APIs is mostly a non-op. The most we do
12// is validate the input arguments and verify that the file exists and
13// is accessible. The actual chown and chmod operations are not performed
14// since we do not have a concept of users, groups, or posix-style permissions.
15 
16import {
17 openSync,
18 closeSync,
19 statSync,
20 fstatSync,
21 lstatSync,
22 symlinkSync,
23 chmod,
24 lchmod,
25 fchmod,
26 chmodSync,
27 lchmodSync,
28 fchmodSync,
29 chown,
30 lchown,
31 fchown,
32 chownSync,
33 lchownSync,
34 fchownSync,
35 promises,
36} from 'node:fs';
37 
38strictEqual(typeof openSync, 'function');
39strictEqual(typeof closeSync, 'function');
40strictEqual(typeof statSync, 'function');
41strictEqual(typeof fstatSync, 'function');
42strictEqual(typeof lstatSync, 'function');
43strictEqual(typeof symlinkSync, 'function');
44strictEqual(typeof chmod, 'function');
45strictEqual(typeof lchmod, 'function');
46strictEqual(typeof fchmod, 'function');
47strictEqual(typeof chmodSync, 'function');
48strictEqual(typeof lchmodSync, 'function');
49strictEqual(typeof fchmodSync, 'function');
50strictEqual(typeof chown, 'function');
51strictEqual(typeof lchown, 'function');
52strictEqual(typeof fchown, 'function');
53strictEqual(typeof chownSync, 'function');
54strictEqual(typeof lchownSync, 'function');
55strictEqual(typeof fchownSync, 'function');
56strictEqual(typeof promises.chown, 'function');
57strictEqual(typeof promises.lchown, 'function');
58 
59const kInvalidArgTypeError = { code: 'ERR_INVALID_ARG_TYPE' };
60const kOutOfRangeError = { code: 'ERR_OUT_OF_RANGE' };
61 
62function checkStat(path) {
63 const { uid, gid } = statSync(path);
64 strictEqual(uid, 0);
65 strictEqual(gid, 0);
66}
67 
68function checkfStat(fd) {
69 const { uid, gid } = fstatSync(fd);
70 strictEqual(uid, 0);
71 strictEqual(gid, 0);
72}
73 
74const path = '/tmp';
75const bufferPath = Buffer.from(path);
76const urlPath = new URL(path, 'file:///');
77 
78export const chownSyncTest = {
79 test() {
80 // Incorrect input types should throw.
81 throws(() => chownSync(123), kInvalidArgTypeError);
82 throws(() => chownSync('/', {}), kInvalidArgTypeError);
83 throws(() => chownSync('/', 0, {}), kInvalidArgTypeError);
84 throws(() => chownSync(path, -1000, 0), kOutOfRangeError);
85 throws(() => chownSync(path, 0, -1000), kOutOfRangeError);
86 
87 // We stat the file before and after to verify the impact
88 // of the chown operation. Specifically, the uid and gid
89 // should not change since our impl is a non-op.
90 checkStat(path);
91 chownSync(path, 1000, 1000);
92 checkStat(path);
93 
94 chownSync(bufferPath, 1000, 1000);
95 checkStat(bufferPath);
96 
97 chownSync(urlPath, 1000, 1000);
98 checkStat(urlPath);
99 
100 // A non-existent path should throw ENOENT
101 throws(() => chownSync('/non-existent-path', 1000, 1000), {
102 code: 'ENOENT',
103 // Access because it is an access check under the covers.
104 syscall: 'chown',
105 });
106 },
107};
108 
109export const chownCallbackTest = {
110 async test() {
111 // Incorrect input types should throw synchronously
112 throws(() => chown(123), kInvalidArgTypeError);
113 throws(() => chown('/', {}), kInvalidArgTypeError);
114 throws(() => chown('/', 0, {}), kInvalidArgTypeError);
115 throws(() => chownSync(path, -1000, 0), kOutOfRangeError);
116 throws(() => chownSync(path, 0, -1000), kOutOfRangeError);
117 
118 async function callChown(path) {
119 const { promise, resolve, reject } = Promise.withResolvers();
120 chown(path, 1000, 1000, (err) => {
121 if (err) return reject(err);
122 resolve();
123 });
124 await promise;
125 }
126 
127 // Should be non-op
128 checkStat(path);
129 await callChown(path);
130 checkStat(path);
131 
132 await callChown(bufferPath);
133 checkStat(bufferPath);
134 
135 await callChown(urlPath);
136 checkStat(urlPath);
137 
138 // A non-existent path should throw ENOENT
139 const { promise, resolve, reject } = Promise.withResolvers();
140 chown('/non-existent-path', 1000, 1000, (err) => {
141 if (err) return reject(err);
142 resolve();
143 });
144 await rejects(promise, {
145 code: 'ENOENT',
146 // Access because it is an access check under the covers.
147 syscall: 'chown',
148 });
149 },
150};
151 
152export const chownPromiseTest = {
153 async test() {
154 // Incorrect input types should reject the promise.
155 await rejects(promises.chown(123), kInvalidArgTypeError);
156 await rejects(promises.chown('/', {}), kInvalidArgTypeError);
157 await rejects(promises.chown('/', 0, {}), kInvalidArgTypeError);
158 await rejects(promises.chown(path, -1000, 0), kOutOfRangeError);
159 await rejects(promises.chown(path, 0, -1000), kOutOfRangeError);
160 
161 // Should be non-op
162 checkStat(path);
163 await promises.chown(path, 1000, 1000);
164 checkStat(path);
165 
166 await promises.chown(bufferPath, 1000, 1000);
167 checkStat(bufferPath);
168 
169 await promises.chown(urlPath, 1000, 1000);
170 checkStat(urlPath);
171 
172 // A non-existent path should throw ENOENT
173 await rejects(promises.chown('/non-existent-path', 1000, 1000), {
174 code: 'ENOENT',
175 // Access because it is an access check under the covers.
176 syscall: 'chown',
177 });
178 },
179};
180 
181export const lchownSyncTest = {
182 test() {
183 // Incorrect input types should throw.
184 throws(() => lchownSync(123), kInvalidArgTypeError);
185 throws(() => lchownSync('/', {}), kInvalidArgTypeError);
186 throws(() => lchownSync('/', 0, {}), kInvalidArgTypeError);
187 throws(() => lchownSync(path, -1000, 0), kOutOfRangeError);
188 throws(() => lchownSync(path, 0, -1000), kOutOfRangeError);
189 
190 // We stat the file before and after to verify the impact
191 // of the chown operation. Specifically, the uid and gid
192 // should not change since our impl is a non-op.
193 checkStat(path);
194 lchownSync(path, 1000, 1000);
195 checkStat(path);
196 
197 lchownSync(bufferPath, 1000, 1000);
198 checkStat(bufferPath);
199 
200 lchownSync(urlPath, 1000, 1000);
201 checkStat(urlPath);
202 
203 // A non-existent path should throw ENOENT
204 throws(() => lchownSync('/non-existent-path', 1000, 1000), {
205 code: 'ENOENT',
206 // Access because it is an access check under the covers.
207 syscall: 'lchown',
208 });
209 },
210};
211 
212export const lchownCallbackTest = {
213 async test() {
214 // Incorrect input types should throw synchronously
215 throws(() => lchown(123), kInvalidArgTypeError);
216 throws(() => lchown('/', {}), kInvalidArgTypeError);
217 throws(() => lchown('/', 0, {}), kInvalidArgTypeError);
218 throws(() => lchownSync(path, -1000, 0), kOutOfRangeError);
219 throws(() => lchownSync(path, 0, -1000), kOutOfRangeError);
220 
221 async function callChown(path) {
222 const { promise, resolve, reject } = Promise.withResolvers();
223 lchown(path, 1000, 1000, (err) => {
224 if (err) return reject(err);
225 resolve();
226 });
227 await promise;
228 }
229 
230 // Should be non-op
231 checkStat(path);
232 await callChown(path);
233 checkStat(path);
234 
235 await callChown(bufferPath);
236 checkStat(bufferPath);
237 
238 await callChown(urlPath);
239 checkStat(urlPath);
240 
241 // A non-existent path should throw ENOENT
242 const { promise, resolve, reject } = Promise.withResolvers();
243 lchown('/non-existent-path', 1000, 1000, (err) => {
244 if (err) return reject(err);
245 resolve();
246 });
247 await rejects(promise, {
248 code: 'ENOENT',
249 // Access because it is an access check under the covers.
250 syscall: 'lchown',
251 });
252 },
253};
254 
255export const lchownPromiseTest = {
256 async test() {
257 // Incorrect input types should reject the promise.
258 await rejects(promises.lchown(123), kInvalidArgTypeError);
259 await rejects(promises.lchown('/', {}), kInvalidArgTypeError);
260 await rejects(promises.lchown('/', 0, {}), kInvalidArgTypeError);
261 await rejects(promises.lchown(path, -1000, 0), kOutOfRangeError);
262 await rejects(promises.lchown(path, 0, -1000), kOutOfRangeError);
263 
264 // Should be non-op
265 checkStat(path);
266 await promises.lchown(path, 1000, 1000);
267 checkStat(path);
268 
269 await promises.lchown(bufferPath, 1000, 1000);
270 checkStat(bufferPath);
271 
272 await promises.lchown(urlPath, 1000, 1000);
273 checkStat(urlPath);
274 
275 // A non-existent path should throw ENOENT
276 await rejects(promises.lchown('/non-existent-path', 1000, 1000), {
277 code: 'ENOENT',
278 syscall: 'lchown',
279 });
280 },
281};
282 
283export const fchownSyncTest = {
284 test() {
285 // Incorrect input types should throw.
286 throws(() => fchownSync({}), kInvalidArgTypeError);
287 throws(() => fchownSync(123), kInvalidArgTypeError);
288 throws(() => fchownSync(123, {}), kInvalidArgTypeError);
289 throws(() => fchownSync(123, 0, {}), kInvalidArgTypeError);
290 throws(() => fchownSync(123, -1000, 0), kOutOfRangeError);
291 throws(() => fchownSync(123, 0, -1000), kOutOfRangeError);
292 
293 const fd = openSync('/tmp');
294 
295 // We stat the file before and after to verify the impact
296 // of the chown operation. Specifically, the uid and gid
297 // should not change since our impl is a non-op.
298 checkfStat(fd);
299 fchownSync(fd, 1000, 1000);
300 checkfStat(fd);
301 
302 throws(() => fchownSync(999, 1000, 1000), {
303 code: 'EBADF',
304 syscall: 'fstat',
305 });
306 
307 closeSync(fd);
308 },
309};
310 
311export const fchownCallbackTest = {
312 async test() {
313 // Incorrect input types should throw synchronously
314 throws(() => fchown({}), kInvalidArgTypeError);
315 throws(() => fchown(123), kInvalidArgTypeError);
316 throws(() => fchown(123, {}), kInvalidArgTypeError);
317 throws(() => fchown(123, 0, {}), kInvalidArgTypeError);
318 throws(() => fchown(123, -1000, 0), kOutOfRangeError);
319 throws(() => fchown(123, 0, -1000), kOutOfRangeError);
320 
321 const fd = openSync('/tmp');
322 
323 async function callChown() {
324 const { promise, resolve, reject } = Promise.withResolvers();
325 fchown(fd, 1000, 1000, (err) => {
326 if (err) return reject(err);
327 resolve();
328 });
329 await promise;
330 }
331 
332 // Should be non-op
333 checkfStat(fd);
334 await callChown();
335 checkfStat(fd);
336 
337 const { promise, resolve, reject } = Promise.withResolvers();
338 fchown(999, 1000, 1000, (err) => {
339 if (err) return reject(err);
340 resolve();
341 });
342 await rejects(promise, {
343 code: 'EBADF',
344 syscall: 'fstat',
345 });
346 
347 closeSync(fd);
348 },
349};
350 
351// ===========================================================================
352 
353export const chmodSyncTest = {
354 test() {
355 // Incorrect input types should throw.
356 throws(() => chmodSync(123), kInvalidArgTypeError);
357 throws(() => chmodSync('/', {}), kInvalidArgTypeError);
358 throws(() => chmodSync('/tmp', -1), kOutOfRangeError);
359 
360 // Should be non-op
361 checkStat(path);
362 chmodSync(path, 0o777);
363 checkStat(path);
364 
365 chmodSync(bufferPath, 0o777);
366 checkStat(bufferPath);
367 
368 chmodSync(urlPath, 0o777);
369 checkStat(urlPath);
370 
371 throws(() => chmodSync('/non-existent-path', 0o777), {
372 code: 'ENOENT',
373 // Access because it is an access check under the covers.
374 syscall: 'chmod',
375 });
376 },
377};
378 
379export const chmodCallbackTest = {
380 async test() {
381 // Incorrect input types should throw.
382 throws(() => chmod(123), kInvalidArgTypeError);
383 throws(() => chmod('/', {}), kInvalidArgTypeError);
384 throws(() => chmod('/tmp', -1), kOutOfRangeError);
385 
386 async function callChmod(path) {
387 const { promise, resolve, reject } = Promise.withResolvers();
388 chmod(path, 0o000, (err) => {
389 if (err) return reject(err);
390 resolve();
391 });
392 await promise;
393 }
394 
395 checkStat(path);
396 await callChmod(path);
397 checkStat(path);
398 
399 await callChmod(bufferPath);
400 checkStat(bufferPath);
401 
402 await callChmod(urlPath);
403 checkStat(bufferPath);
404 
405 const { promise, resolve, reject } = Promise.withResolvers();
406 chmod('/non-existent-path', 0o777, (err) => {
407 if (err) return reject(err);
408 resolve();
409 });
410 await rejects(promise, {
411 code: 'ENOENT',
412 // Access because it is an access check under the covers.
413 syscall: 'chmod',
414 });
415 },
416};
417 
418export const chmodPromiseTest = {
419 async test() {
420 // Incorrect input types should reject the promise.
421 await rejects(promises.chmod(123), kInvalidArgTypeError);
422 await rejects(promises.chmod('/', {}), kInvalidArgTypeError);
423 await rejects(promises.chmod('/tmp', -1), kOutOfRangeError);
424 
425 // Should be non-op
426 checkStat(path);
427 await promises.chmod(path, 0o777);
428 checkStat(path);
429 
430 await promises.chmod(bufferPath, 0o777);
431 checkStat(bufferPath);
432 
433 await promises.chmod(urlPath, 0o777);
434 checkStat(urlPath);
435 
436 await rejects(promises.chmod('/non-existent-path', 0o777), {
437 code: 'ENOENT',
438 syscall: 'chmod',
439 });
440 },
441};
442 
443export const lchmodSyncTest = {
444 test() {
445 // Incorrect input types should throw.
446 throws(() => lchmodSync(123), kInvalidArgTypeError);
447 throws(() => lchmodSync('/', {}), kInvalidArgTypeError);
448 throws(() => lchmodSync('/tmp', -1), kOutOfRangeError);
449 
450 // Should be non-op
451 checkStat(path);
452 lchmodSync(path, 0o777);
453 checkStat(path);
454 
455 lchmodSync(bufferPath, 0o777);
456 checkStat(bufferPath);
457 
458 lchmodSync(urlPath, 0o777);
459 checkStat(urlPath);
460 
461 throws(() => lchmodSync('/non-existent-path', 0o777), {
462 code: 'ENOENT',
463 // Access because it is an access check under the covers.
464 syscall: 'lchmod',
465 });
466 },
467};
468 
469export const lchmodCallbackTest = {
470 async test() {
471 // Incorrect input types should throw.
472 throws(() => lchmod(123), kInvalidArgTypeError);
473 throws(() => lchmod('/', {}), kInvalidArgTypeError);
474 throws(() => lchmod('/tmp', -1), kOutOfRangeError);
475 
476 async function callChmod(path) {
477 const { promise, resolve, reject } = Promise.withResolvers();
478 lchmod(path, 0o000, (err) => {
479 if (err) return reject(err);
480 resolve();
481 });
482 await promise;
483 }
484 
485 checkStat(path);
486 await callChmod(path);
487 checkStat(path);
488 
489 await callChmod(bufferPath);
490 checkStat(bufferPath);
491 
492 await callChmod(urlPath);
493 checkStat(bufferPath);
494 
495 const { promise, resolve, reject } = Promise.withResolvers();
496 lchmod('/non-existent-path', 0o777, (err) => {
497 if (err) return reject(err);
498 resolve();
499 });
500 await rejects(promise, {
501 code: 'ENOENT',
502 // Access because it is an access check under the covers.
503 syscall: 'lchmod',
504 });
505 },
506};
507 
508export const lchmodPromiseTest = {
509 async test() {
510 // Incorrect input types should reject the promise.
511 await rejects(promises.lchmod(123), kInvalidArgTypeError);
512 await rejects(promises.lchmod('/', {}), kInvalidArgTypeError);
513 await rejects(promises.lchmod('/tmp', -1), kOutOfRangeError);
514 
515 // Should be non-op
516 checkStat(path);
517 await promises.lchmod(path, 0o777);
518 checkStat(path);
519 
520 await promises.lchmod(bufferPath, 0o777);
521 checkStat(bufferPath);
522 
523 await promises.lchmod(urlPath, 0o777);
524 checkStat(urlPath);
525 
526 await rejects(promises.lchmod('/non-existent-path', 0o777), {
527 code: 'ENOENT',
528 syscall: 'lchmod',
529 });
530 },
531};
532 
533export const fchmodSyncTest = {
534 test() {
535 // Incorrect input types should throw.
536 throws(() => fchmodSync({}), kInvalidArgTypeError);
537 throws(() => fchmodSync(123), kInvalidArgTypeError);
538 throws(() => fchmodSync(123, {}), kInvalidArgTypeError);
539 throws(() => fchmodSync(123, -1000), kOutOfRangeError);
540 
541 const fd = openSync('/tmp');
542 
543 // We stat the file before and after to verify the impact
544 // of the chown operation. Specifically, the uid and gid
545 // should not change since our impl is a non-op.
546 checkfStat(fd);
547 fchmodSync(fd, 0o777);
548 checkfStat(fd);
549 
550 throws(() => fchmodSync(999, 0o777), {
551 code: 'EBADF',
552 syscall: 'fstat',
553 });
554 
555 closeSync(fd);
556 },
557};
558 
559export const fchmodCallbackTest = {
560 async test() {
561 // Incorrect input types should throw synchronously
562 throws(() => fchmod({}), kInvalidArgTypeError);
563 throws(() => fchmod(123), kInvalidArgTypeError);
564 throws(() => fchmod(123, {}), kInvalidArgTypeError);
565 
566 const fd = openSync('/tmp');
567 
568 async function callChmod() {
569 const { promise, resolve, reject } = Promise.withResolvers();
570 fchmod(fd, 0o777, (err) => {
571 if (err) return reject(err);
572 resolve();
573 });
574 await promise;
575 }
576 
577 // Should be non-op
578 checkfStat(fd);
579 await callChmod();
580 checkfStat(fd);
581 
582 const { promise, resolve, reject } = Promise.withResolvers();
583 fchmod(999, 0o777, (err) => {
584 if (err) return reject(err);
585 resolve();
586 });
587 await rejects(promise, {
588 code: 'EBADF',
589 syscall: 'fstat',
590 });
591 
592 closeSync(fd);
593 },
594};