File
Blob: src/workerd/api/node/tests/fs-access-test.js
| 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 |
| 4 | import { 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 | |
| 11 | import { |
| 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 | |
| 26 | const { F_OK, R_OK, W_OK, X_OK } = constants; |
| 27 | |
| 28 | strictEqual(typeof accessSync, 'function'); |
| 29 | strictEqual(typeof access, 'function'); |
| 30 | strictEqual(typeof existsSync, 'function'); |
| 31 | strictEqual(typeof exists, 'function'); |
| 32 | strictEqual(typeof promises, 'object'); |
| 33 | strictEqual(typeof promises.access, 'function'); |
| 34 | |
| 35 | strictEqual(typeof F_OK, 'number'); |
| 36 | strictEqual(typeof R_OK, 'number'); |
| 37 | strictEqual(typeof W_OK, 'number'); |
| 38 | strictEqual(typeof X_OK, 'number'); |
| 39 | strictEqual(F_OK, 0); |
| 40 | strictEqual(X_OK, 1); |
| 41 | strictEqual(W_OK, 2); |
| 42 | strictEqual(R_OK, 4); |
| 43 | |
| 44 | const 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 | |
| 56 | const UV_ENOENT = -2; |
| 57 | |
| 58 | const kNoEntError = { |
| 59 | code: 'ENOENT', |
| 60 | errno: UV_ENOENT, |
| 61 | syscall: 'access', |
| 62 | }; |
| 63 | |
| 64 | const kInvalidArgTypeError = { code: 'ERR_INVALID_ARG_TYPE' }; |
| 65 | const kOutOfRangeError = { code: 'ERR_OUT_OF_RANGE' }; |
| 66 | |
| 67 | function mustNotCall() { |
| 68 | throw new Error('This function should not be called'); |
| 69 | } |
| 70 | |
| 71 | export 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 | |
| 109 | export 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 | |
| 150 | export 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 | |
| 205 | export 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 | |
| 251 | export 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 | |
| 268 | export 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 | |
| 300 | export 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 | |
| 341 | export 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 | }; |