File
Blob: src/workerd/api/node/tests/fs-link-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 { deepStrictEqual, ok, rejects, strictEqual, throws } from 'node:assert'; |
| 5 | |
| 6 | import { |
| 7 | existsSync, |
| 8 | statSync, |
| 9 | linkSync, |
| 10 | lstatSync, |
| 11 | symlinkSync, |
| 12 | readlinkSync, |
| 13 | realpathSync, |
| 14 | unlinkSync, |
| 15 | link, |
| 16 | symlink, |
| 17 | readlink, |
| 18 | realpath, |
| 19 | unlink, |
| 20 | promises, |
| 21 | } from 'node:fs'; |
| 22 | |
| 23 | strictEqual(typeof existsSync, 'function'); |
| 24 | strictEqual(typeof statSync, 'function'); |
| 25 | strictEqual(typeof linkSync, 'function'); |
| 26 | strictEqual(typeof lstatSync, 'function'); |
| 27 | strictEqual(typeof symlinkSync, 'function'); |
| 28 | strictEqual(typeof readlinkSync, 'function'); |
| 29 | strictEqual(typeof realpathSync, 'function'); |
| 30 | strictEqual(typeof unlinkSync, 'function'); |
| 31 | strictEqual(typeof link, 'function'); |
| 32 | strictEqual(typeof symlink, 'function'); |
| 33 | strictEqual(typeof readlink, 'function'); |
| 34 | strictEqual(typeof realpath, 'function'); |
| 35 | strictEqual(typeof unlink, 'function'); |
| 36 | strictEqual(typeof promises.link, 'function'); |
| 37 | strictEqual(typeof promises.symlink, 'function'); |
| 38 | strictEqual(typeof promises.readlink, 'function'); |
| 39 | strictEqual(typeof promises.realpath, 'function'); |
| 40 | strictEqual(typeof promises.unlink, 'function'); |
| 41 | |
| 42 | const src = '/dev/null'; |
| 43 | const dst = '/tmp/test-link'; |
| 44 | |
| 45 | const kInvalidArgTypeError = { code: 'ERR_INVALID_ARG_TYPE' }; |
| 46 | const kEInvalError = { code: 'EINVAL' }; |
| 47 | const kENoentError = { code: 'ENOENT' }; |
| 48 | const kEExistError = { code: 'EEXIST' }; |
| 49 | const kEPermError = { code: 'EPERM' }; |
| 50 | const kEIsDir = { code: 'EISDIR' }; |
| 51 | |
| 52 | export const linkAndUnlinkSyncTest = { |
| 53 | test() { |
| 54 | throws(() => linkSync(), kInvalidArgTypeError); |
| 55 | throws(() => linkSync(''), kInvalidArgTypeError); |
| 56 | throws(() => realpathSync(), kInvalidArgTypeError); |
| 57 | throws(() => readlinkSync(), kInvalidArgTypeError); |
| 58 | throws(() => unlinkSync(), kInvalidArgTypeError); |
| 59 | |
| 60 | ok(existsSync(src)); |
| 61 | ok(!existsSync(dst)); |
| 62 | linkSync(src, dst); |
| 63 | ok(existsSync(dst)); |
| 64 | |
| 65 | // These are the same file. |
| 66 | deepStrictEqual(statSync(dst), statSync(src)); |
| 67 | deepStrictEqual(lstatSync(dst), statSync(src)); |
| 68 | |
| 69 | // Because this is a hard link, the realpath is /tmp/a |
| 70 | strictEqual(realpathSync(dst), dst); |
| 71 | |
| 72 | // And readlinkSync throws |
| 73 | throws(() => readlinkSync(dst), kEInvalError); |
| 74 | |
| 75 | // Linking from a non-existent file throws. |
| 76 | throws(() => linkSync('/does/not/exist', '/tmp/abc'), kENoentError); |
| 77 | |
| 78 | // Make sure the destination does not exist after the failed link. |
| 79 | ok(!existsSync('/tmp/abc')); |
| 80 | throws(() => linkSync('/dev/foo', '/tmp/abc'), kENoentError); |
| 81 | ok(!existsSync('/tmp/abc')); |
| 82 | |
| 83 | // Creating a link in a non-existent directory throws. |
| 84 | throws(() => linkSync('/dev/null', '/tmp/abc/xyz'), kENoentError); |
| 85 | |
| 86 | // Linking when the path already exists throws. |
| 87 | throws(() => linkSync(src, dst), kEExistError); |
| 88 | |
| 89 | // Linking with an empty file name throws. |
| 90 | throws(() => linkSync(src, '/foo/'), kEInvalError); |
| 91 | |
| 92 | // Invalid paths throw |
| 93 | throws(() => linkSync(src, '/dev/null/test'), kEInvalError); |
| 94 | throws(() => linkSync('/dev/null/test', '/tmp/null/test'), kENoentError); |
| 95 | |
| 96 | // Attempting to link directories throws |
| 97 | throws(() => linkSync('/tmp', '/tmp/abc'), kEPermError); |
| 98 | |
| 99 | // The destination can be unlinked |
| 100 | unlinkSync(dst); |
| 101 | ok(!existsSync(dst)); |
| 102 | |
| 103 | // But unlinking does not remove the source. |
| 104 | ok(existsSync(src)); |
| 105 | |
| 106 | // Cannot unlink read-only files |
| 107 | throws(() => unlinkSync('/bundle/worker'), kEPermError); |
| 108 | throws(() => unlinkSync('/bundle'), kEIsDir); |
| 109 | |
| 110 | // Make sure that the failed unlink had no effect. |
| 111 | ok(existsSync('/bundle/worker')); |
| 112 | ok(existsSync('/bundle')); |
| 113 | }, |
| 114 | }; |
| 115 | |
| 116 | export const linkAndUnlinkAsyncCallbackTest = { |
| 117 | async test() { |
| 118 | const doLinkTest = async (src, dst) => { |
| 119 | const { promise, resolve, reject } = Promise.withResolvers(); |
| 120 | link(src, dst, (err) => { |
| 121 | try { |
| 122 | if (err) return reject(err); |
| 123 | ok(existsSync(dst)); |
| 124 | deepStrictEqual(statSync(dst), statSync(src)); |
| 125 | deepStrictEqual(lstatSync(dst), statSync(src)); |
| 126 | strictEqual(realpathSync(dst), dst); |
| 127 | throws(() => readlinkSync(dst), kEInvalError); |
| 128 | } catch (err) { |
| 129 | reject(err); |
| 130 | } |
| 131 | resolve(); |
| 132 | }); |
| 133 | await promise; |
| 134 | }; |
| 135 | |
| 136 | const doFailedLinkTest = async (src, dst, expectedError) => { |
| 137 | const { promise, resolve, reject } = Promise.withResolvers(); |
| 138 | link(src, dst, (err) => { |
| 139 | try { |
| 140 | strictEqual(err.code, expectedError.code); |
| 141 | ok(!existsSync(dst)); |
| 142 | } catch (err) { |
| 143 | reject(err); |
| 144 | } |
| 145 | resolve(); |
| 146 | }); |
| 147 | await promise; |
| 148 | }; |
| 149 | |
| 150 | const doUnlinkTest = async (dst) => { |
| 151 | const { promise, resolve, reject } = Promise.withResolvers(); |
| 152 | unlink(dst, (err) => { |
| 153 | try { |
| 154 | if (err) return reject(err); |
| 155 | ok(!existsSync(dst)); |
| 156 | ok(existsSync(src)); |
| 157 | } catch (err) { |
| 158 | reject(err); |
| 159 | } |
| 160 | resolve(); |
| 161 | }); |
| 162 | await promise; |
| 163 | }; |
| 164 | |
| 165 | const doFailedUnlinkTest = async (dst, expectedError) => { |
| 166 | const { promise, resolve, reject } = Promise.withResolvers(); |
| 167 | unlink(dst, (err) => { |
| 168 | try { |
| 169 | strictEqual(err.code, expectedError.code); |
| 170 | ok(existsSync(dst)); |
| 171 | } catch (err) { |
| 172 | reject(err); |
| 173 | } |
| 174 | resolve(); |
| 175 | }); |
| 176 | await promise; |
| 177 | }; |
| 178 | |
| 179 | throws(() => link(), kInvalidArgTypeError); |
| 180 | throws(() => link(''), kInvalidArgTypeError); |
| 181 | throws(() => link('', ''), kInvalidArgTypeError); |
| 182 | throws(() => realpath(), kInvalidArgTypeError); |
| 183 | throws(() => realpath(''), kInvalidArgTypeError); |
| 184 | throws(() => readlink(), kInvalidArgTypeError); |
| 185 | throws(() => readlink(''), kInvalidArgTypeError); |
| 186 | throws(() => unlink(), kInvalidArgTypeError); |
| 187 | throws(() => unlink(''), kInvalidArgTypeError); |
| 188 | |
| 189 | ok(existsSync(src)); |
| 190 | ok(!existsSync(dst)); |
| 191 | await doLinkTest(src, dst); |
| 192 | |
| 193 | await doFailedLinkTest('/does/not/exist', '/tmp/abc', kENoentError); |
| 194 | ok(!existsSync('/tmp/abc')); |
| 195 | |
| 196 | await doFailedLinkTest('/dev/foo', '/tmp/abc', kENoentError); |
| 197 | ok(!existsSync('/tmp/abc')); |
| 198 | |
| 199 | // Creating a link in a non-existent directory throws. |
| 200 | await doFailedLinkTest('/dev/null', '/tmp/abc/xyz', kENoentError); |
| 201 | |
| 202 | // Invalid paths throw |
| 203 | await doFailedLinkTest(src, '/dev/null/test', kEInvalError); |
| 204 | await doFailedLinkTest('/dev/null/test', '/tmp/null/test', kENoentError); |
| 205 | |
| 206 | // Attempting to link directories throws |
| 207 | await doFailedLinkTest('/tmp', '/tmp/abc', kEPermError); |
| 208 | |
| 209 | await doUnlinkTest(dst); |
| 210 | |
| 211 | await doFailedUnlinkTest('/bundle/worker', kEPermError); |
| 212 | await doFailedUnlinkTest('/bundle', kEIsDir); |
| 213 | }, |
| 214 | }; |
| 215 | |
| 216 | export const linkAndLinkPromisesTest = { |
| 217 | async test() { |
| 218 | await rejects(promises.link(), kInvalidArgTypeError); |
| 219 | await rejects(promises.link(''), kInvalidArgTypeError); |
| 220 | await rejects(promises.realpath(), kInvalidArgTypeError); |
| 221 | await rejects(promises.readlink(), kInvalidArgTypeError); |
| 222 | await rejects(promises.unlink(), kInvalidArgTypeError); |
| 223 | |
| 224 | ok(existsSync(src)); |
| 225 | ok(!existsSync(dst)); |
| 226 | await promises.link(src, dst); |
| 227 | |
| 228 | // These are the same file. |
| 229 | deepStrictEqual(statSync(dst), statSync(src)); |
| 230 | deepStrictEqual(lstatSync(dst), statSync(src)); |
| 231 | |
| 232 | // Because this is a hard link, the realpath is /tmp/a |
| 233 | strictEqual(await promises.realpath(dst), dst); |
| 234 | |
| 235 | // And readlinkSync throws |
| 236 | await rejects(promises.readlink(dst), kEInvalError); |
| 237 | |
| 238 | await rejects(promises.link('/does/not/exist', '/tmp/abc'), kENoentError); |
| 239 | ok(!existsSync('/tmp/abc')); |
| 240 | |
| 241 | await rejects(promises.link('/dev/null', '/tmp/abc/xyz'), kENoentError); |
| 242 | |
| 243 | await rejects(promises.link(src, dst), kEExistError); |
| 244 | |
| 245 | await rejects(promises.link(src, '/foo/'), kEInvalError); |
| 246 | |
| 247 | await rejects(promises.link(src, '/dev/null/test'), kEInvalError); |
| 248 | |
| 249 | await rejects( |
| 250 | promises.link('/dev/null/test', '/tmp/null/test'), |
| 251 | kENoentError |
| 252 | ); |
| 253 | |
| 254 | await rejects(promises.link('/tmp', '/tmp/abc'), kEPermError); |
| 255 | |
| 256 | await promises.unlink(dst); |
| 257 | ok(!existsSync(dst)); |
| 258 | // But unlinking does not remove the source. |
| 259 | ok(existsSync(src)); |
| 260 | |
| 261 | await rejects(promises.unlink('/bundle/worker'), kEPermError); |
| 262 | await rejects(promises.unlink('/bundle'), kEIsDir); |
| 263 | }, |
| 264 | }; |
| 265 | |
| 266 | export const symlinkAndUnlinkSyncTest = { |
| 267 | test() { |
| 268 | throws(() => symlinkSync(), kInvalidArgTypeError); |
| 269 | throws(() => symlinkSync(''), kInvalidArgTypeError); |
| 270 | |
| 271 | ok(existsSync(src)); |
| 272 | ok(!existsSync(dst)); |
| 273 | symlinkSync(src, dst); |
| 274 | ok(existsSync(dst)); |
| 275 | |
| 276 | deepStrictEqual(statSync(dst), statSync(src)); |
| 277 | |
| 278 | // The symlink points to the source. |
| 279 | strictEqual(readlinkSync(dst), src); |
| 280 | |
| 281 | // The realpath is the destination |
| 282 | strictEqual(realpathSync(dst), src); |
| 283 | |
| 284 | // And readlinkSync does not throw |
| 285 | strictEqual(readlinkSync(dst), src); |
| 286 | |
| 287 | // Linking from a non-existent file works |
| 288 | symlinkSync('/does/not/exist', '/tmp/abc'); |
| 289 | |
| 290 | // Exists sync follows the symlink, and show return false. |
| 291 | ok(!existsSync('/tmp/abc')); |
| 292 | |
| 293 | // But the symlink does exist. |
| 294 | const linkStat = lstatSync('/tmp/abc'); |
| 295 | ok(linkStat.isSymbolicLink()); |
| 296 | |
| 297 | // Creating a link in a non-existent directory throws. |
| 298 | throws(() => symlinkSync('/dev/null', '/tmp/abc/xyz'), kENoentError); |
| 299 | |
| 300 | // Linking when the path already exists throws. |
| 301 | throws(() => symlinkSync(src, dst), kEExistError); |
| 302 | |
| 303 | // Linking with an empty file name throws. |
| 304 | throws(() => symlinkSync(src, '/foo/'), kEInvalError); |
| 305 | |
| 306 | // Invalid paths throw |
| 307 | throws(() => symlinkSync(src, '/dev/null/test'), kEInvalError); |
| 308 | throws(() => symlinkSync('/dev/null/test', '/tmp/null/test'), kENoentError); |
| 309 | |
| 310 | // Symlinking directories works. |
| 311 | symlinkSync('/tmp', '/tmp/linked-dir'); |
| 312 | ok(existsSync('/tmp/linked-dir')); |
| 313 | |
| 314 | // The destination can be unlinked |
| 315 | unlinkSync(dst); |
| 316 | ok(!existsSync(dst)); |
| 317 | |
| 318 | // But unlinking does not remove the source. |
| 319 | ok(existsSync(src)); |
| 320 | }, |
| 321 | }; |
| 322 | |
| 323 | export const symlinkAndUnlinkAsyncCallbackTest = { |
| 324 | async test() { |
| 325 | throws(() => symlink(), kInvalidArgTypeError); |
| 326 | throws(() => symlink(''), kInvalidArgTypeError); |
| 327 | throws(() => symlink('', ''), kInvalidArgTypeError); |
| 328 | throws(() => realpath(), kInvalidArgTypeError); |
| 329 | throws(() => realpath(''), kInvalidArgTypeError); |
| 330 | throws(() => readlink(), kInvalidArgTypeError); |
| 331 | throws(() => readlink(''), kInvalidArgTypeError); |
| 332 | throws(() => unlink(), kInvalidArgTypeError); |
| 333 | throws(() => unlink(''), kInvalidArgTypeError); |
| 334 | |
| 335 | const doSymlinkTest = async (src, dst) => { |
| 336 | const { promise, resolve, reject } = Promise.withResolvers(); |
| 337 | symlink(src, dst, (err) => { |
| 338 | if (err) return reject(err); |
| 339 | try { |
| 340 | const srcStat = statSync(src, { throwIfNoEntry: false }); |
| 341 | if (srcStat != null) { |
| 342 | const dstStat = statSync(dst, { throwIfNoEntry: false }); |
| 343 | deepStrictEqual(dstStat, srcStat); |
| 344 | } else { |
| 345 | // The source does not exist but the symlink should still. |
| 346 | const dstStat = lstatSync(dst); |
| 347 | ok(dstStat.isSymbolicLink()); |
| 348 | } |
| 349 | strictEqual(readlinkSync(dst), src); |
| 350 | strictEqual(realpathSync(dst), src); |
| 351 | } catch (err) { |
| 352 | reject(err); |
| 353 | } |
| 354 | resolve(); |
| 355 | }); |
| 356 | await promise; |
| 357 | }; |
| 358 | |
| 359 | const doFailedSymlinkTest = async (src, dst, expectedError) => { |
| 360 | const { promise, resolve, reject } = Promise.withResolvers(); |
| 361 | symlink(src, dst, (err) => { |
| 362 | try { |
| 363 | strictEqual(err.code, expectedError.code); |
| 364 | } catch (err) { |
| 365 | reject(err); |
| 366 | } |
| 367 | resolve(); |
| 368 | }); |
| 369 | await promise; |
| 370 | }; |
| 371 | |
| 372 | const doUnlinkTest = async (dst) => { |
| 373 | const { promise, resolve, reject } = Promise.withResolvers(); |
| 374 | unlink(dst, (err) => { |
| 375 | try { |
| 376 | if (err) return reject(err); |
| 377 | ok(!existsSync(dst)); |
| 378 | ok(existsSync(src)); |
| 379 | } catch (err) { |
| 380 | reject(err); |
| 381 | } |
| 382 | resolve(); |
| 383 | }); |
| 384 | await promise; |
| 385 | }; |
| 386 | |
| 387 | ok(existsSync(src)); |
| 388 | ok(!existsSync(dst)); |
| 389 | await doSymlinkTest(src, dst); |
| 390 | await doSymlinkTest('/does/not/exist', '/tmp/abc'); |
| 391 | |
| 392 | // Exists sync follows the symlink, and show return false. |
| 393 | ok(!existsSync('/tmp/abc')); |
| 394 | |
| 395 | await doFailedSymlinkTest(src, dst, kEExistError); |
| 396 | await doFailedSymlinkTest(src, '/foo/', kEInvalError); |
| 397 | await doFailedSymlinkTest(src, '/dev/null/test', kEInvalError); |
| 398 | await doFailedSymlinkTest('/dev/null/test', '/tmp/null/test', kENoentError); |
| 399 | |
| 400 | // Symlinking directories works. |
| 401 | await doSymlinkTest('/tmp', '/tmp/linked-dir'); |
| 402 | |
| 403 | // The destination can be unlinked |
| 404 | await doUnlinkTest(dst); |
| 405 | }, |
| 406 | }; |
| 407 | |
| 408 | export const symlinkAndUnlinkPromisesTest = { |
| 409 | async test() { |
| 410 | await rejects(promises.symlink(), kInvalidArgTypeError); |
| 411 | await rejects(promises.symlink(''), kInvalidArgTypeError); |
| 412 | await rejects(promises.realpath(), kInvalidArgTypeError); |
| 413 | await rejects(promises.readlink(), kInvalidArgTypeError); |
| 414 | await rejects(promises.unlink(), kInvalidArgTypeError); |
| 415 | |
| 416 | ok(existsSync(src)); |
| 417 | ok(!existsSync(dst)); |
| 418 | await promises.symlink(src, dst); |
| 419 | |
| 420 | deepStrictEqual(statSync(dst), statSync(src)); |
| 421 | |
| 422 | // The symlink points to the source. |
| 423 | strictEqual(await promises.readlink(dst), src); |
| 424 | |
| 425 | // The realpath is the destination |
| 426 | strictEqual(await promises.realpath(dst), src); |
| 427 | |
| 428 | // And readlink does not throw |
| 429 | strictEqual(await promises.readlink(dst), src); |
| 430 | |
| 431 | // Linking from a non-existent file works |
| 432 | await promises.symlink('/does/not/exist', '/tmp/abc'); |
| 433 | |
| 434 | // Exists sync follows the symlink, and show return false. |
| 435 | ok(!existsSync('/tmp/abc')); |
| 436 | |
| 437 | // But the symlink does exist. |
| 438 | const linkStat = lstatSync('/tmp/abc'); |
| 439 | ok(linkStat.isSymbolicLink()); |
| 440 | |
| 441 | // Creating a link in a non-existent directory throws. |
| 442 | await rejects(promises.symlink('/dev/null', '/tmp/abc/xyz'), kENoentError); |
| 443 | |
| 444 | // Linking when the path already exists throws. |
| 445 | await rejects(promises.symlink(src, dst), kEExistError); |
| 446 | |
| 447 | // Linking with an empty file name throws. |
| 448 | await rejects(promises.symlink(src, '/foo/'), kEInvalError); |
| 449 | |
| 450 | // Invalid paths throw |
| 451 | await rejects(promises.symlink(src, '/dev/null/test'), kEInvalError); |
| 452 | await rejects( |
| 453 | promises.symlink('/dev/null/test', '/tmp/null/test'), |
| 454 | kENoentError |
| 455 | ); |
| 456 | |
| 457 | // Symlinking directories works. |
| 458 | await promises.symlink('/tmp', '/tmp/linked-dir'); |
| 459 | ok(existsSync('/tmp/linked-dir')); |
| 460 | |
| 461 | // The destination can be unlinked |
| 462 | await promises.unlink(dst); |
| 463 | ok(!existsSync(dst)); |
| 464 | |
| 465 | // But unlinking does not remove the source. |
| 466 | ok(existsSync(src)); |
| 467 | }, |
| 468 | }; |