also looking at this
feat(fleet): refuse to destroy a server still holding a moved site's rollback
#183
4 files
+284
-2
| @@ -0,0 +1,83 @@ | ||
| 1 | import { describe, expect, it } from 'bun:test' | |
| 2 | import { buildDrainedSiteScanScript, formatDrainedSiteRefusal, parseDrainedSites } from './drained-sites' | |
| 3 | ||
| 4 | describe('buildDrainedSiteScanScript', () => { | |
| 5 | it('scans this project\'s trees only', () => { | |
| 6 | const script = buildDrainedSiteScanScript('hq').join('\n') | |
| 7 | expect(script).toContain('/var/www/hq-*') | |
| 8 | expect(script).toContain('^hq-$TS_CLOUD_SITE[-@.]') | |
| 9 | }) | |
| 10 | ||
| 11 | /** A stray folder under /var/www must not block a teardown. */ | |
| 12 | it('counts only directories that hold a release tree', () => { | |
| 13 | expect(buildDrainedSiteScanScript('hq').join('\n')).toContain('[ -d "$TS_CLOUD_DIR/releases" ] || continue') | |
| 14 | }) | |
| 15 | ||
| 16 | /** | |
| 17 | * A scan that could change the box would be a poor thing to run immediately | |
| 18 | * before deciding whether to keep it. | |
| 19 | */ | |
| 20 | it('only reads', () => { | |
| 21 | const script = buildDrainedSiteScanScript('hq').join('\n') | |
| 22 | for (const mutation of ['rm ', 'systemctl stop', 'systemctl start', 'systemctl disable', 'systemctl enable', 'mv ', 'tar ']) { | |
| 23 | expect(script.includes(mutation)).toBe(false) | |
| 24 | } | |
| 25 | // Every redirect to a FILE goes to /dev/null (`>&1` duplicates a descriptor | |
| 26 | // rather than writing anywhere), so nothing lands on disk. | |
| 27 | for (const redirect of script.match(/>(?!&)\s*\S+/g) ?? []) { | |
| 28 | expect(redirect.replace(/^>\s*/, '')).toBe('/dev/null') | |
| 29 | } | |
| 30 | }) | |
| 31 | ||
| 32 | it('escapes a slug with regex metacharacters', () => { | |
| 33 | expect(buildDrainedSiteScanScript('my.app').join('\n')).toContain('^my\\.app-') | |
| 34 | }) | |
| 35 | ||
| 36 | it('always exits 0, so a scan is never mistaken for a failure', () => { | |
| 37 | expect(buildDrainedSiteScanScript('hq').at(-1)).toBe('exit 0') | |
| 38 | }) | |
| 39 | }) | |
| 40 | ||
| 41 | describe('parseDrainedSites', () => { | |
| 42 | it('reports the trees with nothing running for them', () => { | |
| 43 | const drained = parseDrainedSites('site:bughq:no:1.2G\nsite:loghq:yes:800M') | |
| 44 | expect(drained).toEqual([{ name: 'bughq', size: '1.2G' }]) | |
| 45 | }) | |
| 46 | ||
| 47 | /** | |
| 48 | * A site with something active is a site on a box being torn down — ordinary, | |
| 49 | * and already covered by the teardown's own confirmation. | |
| 50 | */ | |
| 51 | it('ignores a site that is still running', () => { | |
| 52 | expect(parseDrainedSites('site:bughq:yes:1.2G')).toEqual([]) | |
| 53 | }) | |
| 54 | ||
| 55 | it('is empty for a box with no trees, or no output at all', () => { | |
| 56 | expect(parseDrainedSites('')).toEqual([]) | |
| 57 | expect(parseDrainedSites(undefined)).toEqual([]) | |
| 58 | expect(parseDrainedSites('some unrelated chatter')).toEqual([]) | |
| 59 | }) | |
| 60 | ||
| 61 | it('survives a missing size', () => { | |
| 62 | expect(parseDrainedSites('site:bughq:no:')).toEqual([{ name: 'bughq', size: '?' }]) | |
| 63 | }) | |
| 64 | }) | |
| 65 | ||
| 66 | describe('formatDrainedSiteRefusal', () => { | |
| 67 | it('names the sites, what the files are for, and the way forward', () => { | |
| 68 | const message = formatDrainedSiteRefusal([{ name: 'bughq', size: '1.2G' }], 'hq', '--discard-drained-sites') | |
| 69 | expect(message).toContain('hq-bughq (1.2G)') | |
| 70 | expect(message).toContain('they are the rollback') | |
| 71 | expect(message).toContain('--discard-drained-sites') | |
| 72 | }) | |
| 73 | ||
| 74 | it('reads correctly for several', () => { | |
| 75 | const message = formatDrainedSiteRefusal( | |
| 76 | [{ name: 'a', size: '1G' }, { name: 'b', size: '2G' }], | |
| 77 | 'hq', | |
| 78 | '--discard-drained-sites', | |
| 79 | ) | |
| 80 | expect(message).toContain('2 site trees') | |
| 81 | expect(message).toContain('for them') | |
| 82 | }) | |
| 83 | }) | |