|
30 | 30 | // 2. A path through a regular file: refused with `ENOTDIR`, naming the file. |
31 | 31 | // 3. CONTROL: a creatable unified-default path three levels deep is made. |
32 | 32 | // 4. CONTROL: any other source is left to the serve child — nothing is made. |
| 33 | +// 5. A dangling symlink AS the directory (`OS_HOME/data` pointing nowhere): |
| 34 | +// refused at once with `EEXIST`, naming the file and the directory (#24090). |
| 35 | +// The walk used to accept any `EEXIST` on a created level, so the link went |
| 36 | +// through to the serve child, whose boot failed seconds later with a |
| 37 | +// plugin-start error naming neither (measured at `os dev` on `origin/main` |
| 38 | +// b9ac35cbf: exit 1 after 8.3 s, "Plugin com.objectstack.auth failed to |
| 39 | +// start"). |
| 40 | +// 6. The answers the fold onto `makeDirectorySync` keeps: a directory that |
| 41 | +// already exists, and a live symlink to one, are accepted as they stand; a |
| 42 | +// regular file holding the directory's own name is refused with `EEXIST`. |
33 | 43 |
|
34 | 44 | import { describe, it, expect, beforeAll, afterAll } from 'vitest'; |
35 | 45 | import { spawnSync } from 'node:child_process'; |
36 | | -import { existsSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs'; |
| 46 | +import { existsSync, lstatSync, mkdirSync, mkdtempSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs'; |
37 | 47 | import { tmpdir } from 'node:os'; |
38 | 48 | import { dirname, join, resolve } from 'node:path'; |
39 | 49 | import { fileURLToPath, pathToFileURL } from 'node:url'; |
@@ -62,11 +72,22 @@ let outcomes: Map<string, Outcome>; |
62 | 72 | beforeAll(() => { |
63 | 73 | root = mkdtempSync(join(tmpdir(), 'os-23737-dev-')); |
64 | 74 | writeFileSync(join(root, 'plain'), ''); |
| 75 | + mkdirSync(join(root, 'dangling')); |
| 76 | + symlinkSync(join(root, 'nowhere'), join(root, 'dangling', 'data')); |
| 77 | + mkdirSync(join(root, 'existing', 'data'), { recursive: true }); |
| 78 | + mkdirSync(join(root, 'live', 'target'), { recursive: true }); |
| 79 | + symlinkSync(join(root, 'live', 'target'), join(root, 'live', 'data')); |
| 80 | + mkdirSync(join(root, 'held')); |
| 81 | + writeFileSync(join(root, 'held', 'data'), ''); |
65 | 82 | const cases: Array<{ label: string; url: string; source: string }> = [ |
66 | 83 | ...(RUNS_PROC ? [{ label: 'proc', url: `file:${PROC_FILE}`, source: 'unified-default' }] : []), |
67 | 84 | { label: 'through-file', url: `file:${join(root, 'plain', 'data', 'objectstack.db')}`, source: 'unified-default' }, |
68 | 85 | { label: 'creatable', url: `file:${join(root, 'home', 'a', 'data', 'objectstack.db')}`, source: 'unified-default' }, |
69 | 86 | { label: 'other-source', url: `file:${join(root, 'env', 'a', 'data', 'objectstack.db')}`, source: 'env' }, |
| 87 | + { label: 'dangling', url: `file:${join(root, 'dangling', 'data', 'objectstack.db')}`, source: 'unified-default' }, |
| 88 | + { label: 'existing', url: `file:${join(root, 'existing', 'data', 'objectstack.db')}`, source: 'unified-default' }, |
| 89 | + { label: 'live-link', url: `file:${join(root, 'live', 'data', 'objectstack.db')}`, source: 'unified-default' }, |
| 90 | + { label: 'held', url: `file:${join(root, 'held', 'data', 'objectstack.db')}`, source: 'unified-default' }, |
70 | 91 | ]; |
71 | 92 | const script = [ |
72 | 93 | 'const [subject, json] = process.argv.slice(1);', |
@@ -144,3 +165,29 @@ describe('prepareDevDatabaseDirectory — os dev\'s unified-default directory (# |
144 | 165 | expect(existsSync(join(root, 'env'))).toBe(false); |
145 | 166 | }); |
146 | 167 | }); |
| 168 | + |
| 169 | +describe('prepareDevDatabaseDirectory — a dangling symlink in the path is refused at once (#24090)', () => { |
| 170 | + it('refuses a dangling symlink as the directory with EEXIST, naming the file and the directory', () => { |
| 171 | + const o = outcomeOf('dangling'); |
| 172 | + expect(o.outcome).toBe('rejected'); |
| 173 | + expect(o.code).toBe('EEXIST'); |
| 174 | + expect(o.message).toContain(`'${join(root, 'dangling', 'data', 'objectstack.db')}'`); |
| 175 | + expect(o.message).toContain(`'${join(root, 'dangling', 'data')}'`); |
| 176 | + expect(lstatSync(join(root, 'dangling', 'data')).isSymbolicLink()).toBe(true); |
| 177 | + expect(existsSync(join(root, 'nowhere'))).toBe(false); |
| 178 | + }); |
| 179 | + |
| 180 | + it('CONTROL: a directory that already exists, and a live symlink to one, are accepted as they stand', () => { |
| 181 | + expect(outcomeOf('existing').outcome).toBe('returned'); |
| 182 | + expect(statSync(join(root, 'existing', 'data')).isDirectory()).toBe(true); |
| 183 | + expect(outcomeOf('live-link').outcome).toBe('returned'); |
| 184 | + expect(lstatSync(join(root, 'live', 'data')).isSymbolicLink()).toBe(true); |
| 185 | + }); |
| 186 | + |
| 187 | + it('refuses a regular file holding the directory\'s own name with EEXIST, naming the file', () => { |
| 188 | + const o = outcomeOf('held'); |
| 189 | + expect(o.outcome).toBe('rejected'); |
| 190 | + expect(o.code).toBe('EEXIST'); |
| 191 | + expect(o.message).toContain(`'${join(root, 'held', 'data', 'objectstack.db')}'`); |
| 192 | + }); |
| 193 | +}); |
0 commit comments