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