1const t = require('tap') 2const { basename } = require('path') 3const tmock = require('../../fixtures/tmock') 4const mockNpm = require('../../fixtures/mock-npm') 5 6const CURRENT_VERSION = '123.420.69' 7const CURRENT_MAJOR = '122.420.69' 8const CURRENT_MINOR = '123.419.69' 9const CURRENT_PATCH = '123.420.68' 10const NEXT_VERSION = '123.421.70' 11const NEXT_MINOR = '123.420.70' 12const NEXT_PATCH = '123.421.69' 13const CURRENT_BETA = '124.0.0-beta.99999' 14const HAVE_BETA = '124.0.0-beta.0' 15 16const runUpdateNotifier = async (t, { 17 STAT_ERROR, 18 WRITE_ERROR, 19 PACOTE_ERROR, 20 STAT_MTIME = 0, 21 mocks: _mocks = {}, 22 command = 'help', 23 prefixDir, 24 version = CURRENT_VERSION, 25 argv = [], 26 ...config 27} = {}) => { 28 const mockFs = { 29 ...require('fs/promises'), 30 stat: async (path) => { 31 if (basename(path) !== '_update-notifier-last-checked') { 32 t.fail('no stat allowed for non upate notifier files') 33 } 34 if (STAT_ERROR) { 35 throw STAT_ERROR 36 } 37 return { mtime: new Date(STAT_MTIME) } 38 }, 39 writeFile: async (path, content) => { 40 if (content !== '') { 41 t.fail('no write file content allowed') 42 } 43 if (basename(path) !== '_update-notifier-last-checked') { 44 t.fail('no writefile allowed for non upate notifier files') 45 } 46 if (WRITE_ERROR) { 47 throw WRITE_ERROR 48 } 49 }, 50 } 51 52 const MANIFEST_REQUEST = [] 53 const mockPacote = { 54 manifest: async (spec) => { 55 if (!spec.match(/^npm@/)) { 56 t.fail('no pacote manifest allowed for non npm packages') 57 } 58 MANIFEST_REQUEST.push(spec) 59 if (PACOTE_ERROR) { 60 throw PACOTE_ERROR 61 } 62 const manifestV = spec === 'npm@latest' ? CURRENT_VERSION 63 : /-/.test(spec) ? CURRENT_BETA : NEXT_VERSION 64 return { version: manifestV } 65 }, 66 } 67 68 const mocks = { 69 pacote: mockPacote, 70 'fs/promises': mockFs, 71 '{ROOT}/package.json': { version }, 72 'ci-info': { isCI: false, name: null }, 73 ..._mocks, 74 } 75 76 const mock = await mockNpm(t, { 77 command, 78 mocks, 79 config, 80 exec: true, 81 prefixDir, 82 argv, 83 }) 84 const updateNotifier = tmock(t, '{LIB}/utils/update-notifier.js', mocks) 85 86 const result = await updateNotifier(mock.npm) 87 88 return { 89 result, 90 MANIFEST_REQUEST, 91 } 92} 93 94t.test('does not notify by default', async t => { 95 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t) 96 t.not(result) 97 t.equal(MANIFEST_REQUEST.length, 1) 98}) 99 100t.test('situations in which we do not notify', t => { 101 t.test('nothing to do if notifier disabled', async t => { 102 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { 103 'update-notifier': false, 104 }) 105 t.equal(result, null) 106 t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') 107 }) 108 109 t.test('do not suggest update if already updating', async t => { 110 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { 111 command: 'install', 112 prefixDir: { 'package.json': `{"name":"${t.testName}"}` }, 113 argv: ['npm'], 114 global: true, 115 }) 116 t.equal(result, null) 117 t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') 118 }) 119 120 t.test('do not suggest update if already updating with spec', async t => { 121 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { 122 command: 'install', 123 prefixDir: { 'package.json': `{"name":"${t.testName}"}` }, 124 argv: ['npm@latest'], 125 global: true, 126 }) 127 t.equal(result, null) 128 t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') 129 }) 130 131 t.test('do not update if same as latest', async t => { 132 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t) 133 t.equal(result, null) 134 t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') 135 }) 136 t.test('check if stat errors (here for coverage)', async t => { 137 const STAT_ERROR = new Error('blorg') 138 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { STAT_ERROR }) 139 t.equal(result, null) 140 t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') 141 }) 142 t.test('ok if write errors (here for coverage)', async t => { 143 const WRITE_ERROR = new Error('grolb') 144 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { WRITE_ERROR }) 145 t.equal(result, null) 146 t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') 147 }) 148 t.test('ignore pacote failures (here for coverage)', async t => { 149 const PACOTE_ERROR = new Error('pah-KO-tchay') 150 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { PACOTE_ERROR }) 151 t.equal(result, null) 152 t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') 153 }) 154 t.test('do not update if newer than latest, but same as next', async t => { 155 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { version: NEXT_VERSION }) 156 t.equal(result, null) 157 const reqs = ['npm@latest', `npm@^${NEXT_VERSION}`] 158 t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions') 159 }) 160 t.test('do not update if on the latest beta', async t => { 161 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { version: CURRENT_BETA }) 162 t.equal(result, null) 163 const reqs = [`npm@^${CURRENT_BETA}`] 164 t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions') 165 }) 166 167 t.test('do not update in CI', async t => { 168 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { mocks: { 169 'ci-info': { isCI: true, name: 'something' }, 170 } }) 171 t.equal(result, null) 172 t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') 173 }) 174 175 t.test('only check weekly for GA releases', async t => { 176 // One week (plus five minutes to account for test environment fuzziness) 177 const STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 * 7 + 1000 * 60 * 5 178 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { STAT_MTIME }) 179 t.equal(result, null) 180 t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') 181 }) 182 183 t.test('only check daily for betas', async t => { 184 // One day (plus five minutes to account for test environment fuzziness) 185 const STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 5 186 const res = await runUpdateNotifier(t, { STAT_MTIME, version: HAVE_BETA }) 187 t.equal(res.result, null) 188 t.strictSame(res.MANIFEST_REQUEST, [], 'no requests for manifests') 189 }) 190 191 t.end() 192}) 193 194t.test('notification situations', async t => { 195 const cases = { 196 [HAVE_BETA]: [`^{V}`], 197 [NEXT_PATCH]: [`latest`, `^{V}`], 198 [NEXT_MINOR]: [`latest`, `^{V}`], 199 [CURRENT_PATCH]: ['latest'], 200 [CURRENT_MINOR]: ['latest'], 201 [CURRENT_MAJOR]: ['latest'], 202 } 203 204 for (const [version, reqs] of Object.entries(cases)) { 205 for (const color of [false, 'always']) { 206 await t.test(`${version} - color=${color}`, async t => { 207 const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { version, color }) 208 t.matchSnapshot(result) 209 t.strictSame(MANIFEST_REQUEST, reqs.map(r => `npm@${r.replace('{V}', version)}`)) 210 }) 211 } 212 } 213}) 214