1'use strict' 2 3const fs = require('fs') 4const path = require('path') 5 6const test = require('tap').test 7const Tacks = require('tacks') 8const Dir = Tacks.Dir 9const File = Tacks.File 10const common = require('../common-tap.js') 11const isWindows = require('../../lib/utils/is-windows.js') 12 13const base = common.pkg 14const noFunding = path.join(base, 'no-funding-package') 15const maintainerOwnsAllDeps = path.join(base, 'maintainer-owns-all-deps') 16const nestedNoFundingPackages = path.join(base, 'nested-no-funding-packages') 17const nestedMultipleFundingPackages = path.join(base, 'nested-multiple-funding-packages') 18const fundingStringShorthand = path.join(base, 'funding-string-shorthand') 19 20function getFixturePackage ({ name, version, dependencies, funding }, extras) { 21 const getDeps = () => Object 22 .keys(dependencies) 23 .reduce((res, dep) => (Object.assign({}, res, { 24 [dep]: '*' 25 })), {}) 26 27 return Dir(Object.assign({ 28 'package.json': File({ 29 name, 30 version: version || '1.0.0', 31 funding: (funding === undefined) ? { 32 type: 'individual', 33 url: 'http://example.com/donate' 34 } : funding, 35 dependencies: dependencies && getDeps(dependencies) 36 }) 37 }, extras)) 38} 39 40const fixture = new Tacks(Dir({ 41 'funding-string-shorthand': Dir({ 42 'package.json': File({ 43 name: 'funding-string-shorthand', 44 version: '0.0.0', 45 funding: 'https://example.com/sponsor' 46 }) 47 }), 48 'no-funding-package': Dir({ 49 'package.json': File({ 50 name: 'no-funding-package', 51 version: '0.0.0' 52 }) 53 }), 54 'maintainer-owns-all-deps': getFixturePackage({ 55 name: 'maintainer-owns-all-deps', 56 dependencies: { 57 'dep-foo': '*', 58 'dep-bar': '*' 59 } 60 }, { 61 node_modules: Dir({ 62 'dep-foo': getFixturePackage({ 63 name: 'dep-foo', 64 dependencies: { 65 'dep-sub-foo': '*' 66 } 67 }, { 68 node_modules: Dir({ 69 'dep-sub-foo': getFixturePackage({ 70 name: 'dep-sub-foo' 71 }) 72 }) 73 }), 74 'dep-bar': getFixturePackage({ 75 name: 'dep-bar' 76 }) 77 }) 78 }), 79 'nested-no-funding-packages': getFixturePackage({ 80 name: 'nested-no-funding-packages', 81 funding: null, 82 dependencies: { 83 foo: '*' 84 }, 85 devDependencies: { 86 lorem: '*' 87 } 88 }, { 89 node_modules: Dir({ 90 foo: getFixturePackage({ 91 name: 'foo', 92 dependencies: { 93 bar: '*' 94 }, 95 funding: null 96 }, { 97 node_modules: Dir({ 98 bar: getFixturePackage({ 99 name: 'bar' 100 }, { 101 node_modules: Dir({ 102 'sub-bar': getFixturePackage({ 103 name: 'sub-bar', 104 funding: 'https://example.com/sponsor' 105 }) 106 }) 107 }) 108 }) 109 }), 110 lorem: getFixturePackage({ 111 name: 'lorem', 112 funding: { 113 url: 'https://example.com/lorem' 114 } 115 }) 116 }) 117 }), 118 'nested-multiple-funding-packages': getFixturePackage({ 119 name: 'nested-multiple-funding-packages', 120 funding: [ 121 'https://one.example.com', 122 'https://two.example.com' 123 ], 124 dependencies: { 125 foo: '*' 126 }, 127 devDependencies: { 128 bar: '*' 129 } 130 }, { 131 node_modules: Dir({ 132 foo: getFixturePackage({ 133 name: 'foo', 134 funding: [ 135 'http://example.com', 136 { url: 'http://sponsors.example.com/me' }, 137 'http://collective.example.com' 138 ] 139 }), 140 bar: getFixturePackage({ 141 name: 'bar', 142 funding: [ 143 'http://collective.example.com', 144 { url: 'http://sponsors.example.com/you' } 145 ] 146 }) 147 }) 148 }) 149})) 150 151function checkOutput (t, { code, stdout, stderr }) { 152 t.is(code, 0, `exited code 0`) 153 t.is(stderr, '', 'no warnings') 154} 155 156function jsonTest (t, { assertionMsg, expected, stdout }) { 157 let parsed = JSON.parse(stdout) 158 t.deepEqual(parsed, expected, assertionMsg) 159} 160 161function snapshotTest (t, { stdout, assertionMsg }) { 162 t.matchSnapshot(stdout, assertionMsg) 163} 164 165function testFundCmd ({ title, assertionMsg, args = [], opts = {}, output = checkOutput, assertion = snapshotTest, expected }) { 166 const validate = (t) => (err, code, stdout, stderr) => { 167 if (err) throw err 168 169 output(t, { code, stdout, stderr }) 170 assertion(t, { assertionMsg, expected, stdout }) 171 } 172 173 return test(title, (t) => { 174 t.plan(3) 175 common.npm(['fund', '--unicode=false'].concat(args), opts, validate(t)) 176 }) 177} 178 179test('setup', function (t) { 180 fixture.remove(base) 181 fixture.create(base) 182 t.end() 183}) 184 185testFundCmd({ 186 title: 'fund with no package containing funding', 187 assertionMsg: 'should print empty funding info', 188 opts: { cwd: noFunding } 189}) 190 191testFundCmd({ 192 title: 'fund in which same maintainer owns all its deps', 193 assertionMsg: 'should print stack packages together', 194 opts: { cwd: maintainerOwnsAllDeps } 195}) 196 197testFundCmd({ 198 title: 'fund in which same maintainer owns all its deps, using --json option', 199 assertionMsg: 'should print stack packages together', 200 args: ['--json'], 201 opts: { cwd: maintainerOwnsAllDeps }, 202 assertion: jsonTest, 203 expected: { 204 length: 3, 205 name: 'maintainer-owns-all-deps', 206 version: '1.0.0', 207 funding: { type: 'individual', url: 'http://example.com/donate' }, 208 dependencies: { 209 'dep-bar': { 210 version: '1.0.0', 211 funding: { type: 'individual', url: 'http://example.com/donate' } 212 }, 213 'dep-foo': { 214 version: '1.0.0', 215 funding: { type: 'individual', url: 'http://example.com/donate' }, 216 dependencies: { 217 'dep-sub-foo': { 218 version: '1.0.0', 219 funding: { type: 'individual', url: 'http://example.com/donate' } 220 } 221 } 222 } 223 } 224 } 225}) 226 227testFundCmd({ 228 title: 'fund containing multi-level nested deps with no funding', 229 assertionMsg: 'should omit dependencies with no funding declared', 230 opts: { cwd: nestedNoFundingPackages } 231}) 232 233testFundCmd({ 234 title: 'fund containing multi-level nested deps with no funding, using --json option', 235 assertionMsg: 'should omit dependencies with no funding declared', 236 args: ['--json'], 237 opts: { cwd: nestedNoFundingPackages }, 238 assertion: jsonTest, 239 expected: { 240 length: 3, 241 name: 'nested-no-funding-packages', 242 version: '1.0.0', 243 dependencies: { 244 lorem: { version: '1.0.0', funding: { url: 'https://example.com/lorem' } }, 245 bar: { 246 version: '1.0.0', 247 funding: { type: 'individual', url: 'http://example.com/donate' }, 248 dependencies: { 249 'sub-bar': { 250 version: '1.0.0', 251 funding: { url: 'https://example.com/sponsor' } 252 } 253 } 254 } 255 } 256 } 257}) 258 259testFundCmd({ 260 title: 'fund containing multi-level nested deps with multiple funding sources, using --json option', 261 assertionMsg: 'should omit dependencies with no funding declared', 262 args: ['--json'], 263 opts: { cwd: nestedMultipleFundingPackages }, 264 assertion: jsonTest, 265 expected: { 266 length: 2, 267 name: 'nested-multiple-funding-packages', 268 version: '1.0.0', 269 funding: [ 270 { 271 url: 'https://one.example.com' 272 }, 273 { 274 url: 'https://two.example.com' 275 } 276 ], 277 dependencies: { 278 bar: { 279 version: '1.0.0', 280 funding: [ 281 { 282 url: 'http://collective.example.com' 283 }, 284 { 285 url: 'http://sponsors.example.com/you' 286 } 287 ] 288 }, 289 foo: { 290 version: '1.0.0', 291 funding: [ 292 { 293 url: 'http://example.com' 294 }, 295 { 296 url: 'http://sponsors.example.com/me' 297 }, 298 { 299 url: 'http://collective.example.com' 300 } 301 ] 302 } 303 } 304 } 305}) 306 307testFundCmd({ 308 title: 'fund does not support global', 309 assertionMsg: 'should throw EFUNDGLOBAL error', 310 args: ['--global'], 311 output: (t, { code, stdout, stderr }) => { 312 t.is(code, 1, `exited code 0`) 313 const [ errCode, errCmd ] = stderr.split('\n') 314 t.matchSnapshot(`${errCode}\n${errCmd}`, 'should write error msgs to stderr') 315 } 316}) 317 318testFundCmd({ 319 title: 'fund does not support global, using --json option', 320 assertionMsg: 'should throw EFUNDGLOBAL error', 321 args: ['--global', '--json'], 322 output: (t, { code, stdout, stderr }) => { 323 t.is(code, 1, `exited code 0`) 324 const [ errCode, errCmd ] = stderr.split('\n') 325 t.matchSnapshot(`${errCode}\n${errCmd}`, 'should write error msgs to stderr') 326 }, 327 assertion: jsonTest, 328 expected: { 329 error: { 330 code: 'EFUNDGLOBAL', 331 summary: '`npm fund` does not support global packages', 332 detail: '' 333 } 334 } 335}) 336 337testFundCmd({ 338 title: 'fund using package argument with no browser', 339 assertionMsg: 'should open funding url', 340 args: ['.', '--no-browser'], 341 opts: { cwd: maintainerOwnsAllDeps } 342}) 343 344testFundCmd({ 345 title: 'fund using string shorthand', 346 assertionMsg: 'should open string-only url', 347 args: ['.', '--no-browser'], 348 opts: { cwd: fundingStringShorthand } 349}) 350 351testFundCmd({ 352 title: 'fund using nested packages with multiple sources', 353 assertionMsg: 'should prompt with all available URLs', 354 args: ['.'], 355 opts: { cwd: nestedMultipleFundingPackages } 356}) 357 358testFundCmd({ 359 title: 'fund using nested packages with multiple sources, with a source number', 360 assertionMsg: 'should open the numbered URL', 361 args: ['.', '--which=1', '--no-browser'], 362 opts: { cwd: nestedMultipleFundingPackages } 363}) 364 365testFundCmd({ 366 title: 'fund using package argument with no browser, using --json option', 367 assertionMsg: 'should open funding url', 368 args: ['.', '--json', '--no-browser'], 369 opts: { cwd: maintainerOwnsAllDeps }, 370 assertion: jsonTest, 371 expected: { 372 title: 'individual funding available at the following URL', 373 url: 'http://example.com/donate' 374 } 375}) 376 377if (!isWindows) { 378 test('fund using package argument', function (t) { 379 const fakeBrowser = path.join(common.pkg, '_script.sh') 380 const outFile = path.join(common.pkg, '_output') 381 382 const s = '#!/usr/bin/env bash\n' + 383 'echo "$@" > ' + JSON.stringify(common.pkg) + '/_output\n' 384 fs.writeFileSync(fakeBrowser, s) 385 fs.chmodSync(fakeBrowser, '0755') 386 387 common.npm([ 388 'fund', '.', 389 '--loglevel=silent', 390 '--browser=' + fakeBrowser 391 ], { cwd: maintainerOwnsAllDeps }, function (err, code, stdout, stderr) { 392 t.ifError(err, 'repo command ran without error') 393 t.equal(code, 0, 'exit ok') 394 var res = fs.readFileSync(outFile, 'utf8') 395 t.equal(res, 'http://example.com/donate\n') 396 t.end() 397 }) 398 }) 399} 400 401test('cleanup', function (t) { 402 t.pass(base) 403 fixture.remove(base) 404 t.end() 405}) 406