var expect = require('expect.js'); var Q = require('q'); var path = require('path'); var mout = require('mout'); var fs = require('../../lib/util/fs'); var rimraf = require('../../lib/util/rimraf'); var RegistryClient = require('bower-registry-client'); var Logger = require('bower-logger'); var proxyquire = require('proxyquire'); var defaultConfig = require('../../lib/config'); var ResolveCache = require('../../lib/core/ResolveCache'); var resolvers = require('../../lib/core/resolvers'); var copy = require('../../lib/util/copy'); var helpers = require('../helpers'); describe('PackageRepository', function () { var packageRepository; var resolver; var resolverFactoryHook; var resolverFactoryClearHook; var testPackage = path.resolve(__dirname, '../assets/package-a'); var tempPackage = path.resolve(__dirname, '../tmp/temp-package'); var packagesCacheDir = path.join(__dirname, '../tmp/temp-resolve-cache'); var registryCacheDir = path.join(__dirname, '../tmp/temp-registry-cache'); var mockSource = helpers.localSource(testPackage); var forceCaching = true; after(function () { rimraf.sync(registryCacheDir); rimraf.sync(packagesCacheDir); }); beforeEach(function (next) { var PackageRepository; var config; var logger = new Logger(); // Config config = defaultConfig({ storage: { packages: packagesCacheDir, registry: registryCacheDir } }); // Mock the resolver factory to always return a resolver for the test package function resolverFactory(decEndpoint, options, _registryClient) { var _config = options.config; var _logger = options.logger; expect(_config).to.eql(config); expect(_logger).to.be.an(Logger); expect(_registryClient).to.be.an(RegistryClient); decEndpoint = mout.object.deepMixIn({}, decEndpoint); decEndpoint.source = mockSource; resolver = new resolvers.GitRemote(decEndpoint, _config, _logger); if (forceCaching) { // Force to use cache even for local resources resolver.isCacheable = function () { return true; }; } resolverFactoryHook(resolver); return Q.resolve(resolver); } resolverFactory.getConstructor = function () { return Q.resolve([resolvers.GitRemote, { source: helpers.localSource(testPackage) }]); }; resolverFactory.clearRuntimeCache = function () { resolverFactoryClearHook(); }; PackageRepository = proxyquire('../../lib/core/PackageRepository', { './resolverFactory': resolverFactory }); packageRepository = new PackageRepository(config, logger); // Reset hooks resolverFactoryHook = resolverFactoryClearHook = function () {}; // Remove temp package rimraf.sync(tempPackage); // Clear the repository packageRepository.clear() .then(next.bind(next, null), next); }); describe('.constructor', function () { it('should pass the config correctly to the registry client, including its cache folder', function () { expect(packageRepository._registryClient._config.cache).to.equal(registryCacheDir); }); }); describe('.fetch', function () { it('should call the resolver factory to get the appropriate resolver', function (next) { var called; resolverFactoryHook = function () { called = true; }; packageRepository.fetch({ name: '', source: 'foo', target: '~0.1.0' }) .spread(function (canonicalDir, pkgMeta) { expect(called).to.be(true); expect(fs.existsSync(canonicalDir)).to.be(true); expect(pkgMeta).to.be.an('object'); expect(pkgMeta.name).to.be('package-a'); expect(pkgMeta.version).to.be('0.1.1'); next(); }) .done(); }); it('should just call the resolver resolve method if force was specified', function (next) { var called = []; resolverFactoryHook = function (resolver) { var originalResolve = resolver.resolve; resolver.resolve = function () { called.push('resolve'); return originalResolve.apply(this, arguments); }; resolver.hasNew = function () { called.push('hasNew'); return Q.resolve(false); }; }; packageRepository._resolveCache.retrieve = function () { called.push('retrieve'); return Q.resolve([]); }; packageRepository._config.force = true; packageRepository.fetch({ name: '', source: 'foo', target: ' ~0.1.0' }) .spread(function (canonicalDir, pkgMeta) { expect(called).to.eql(['resolve']); expect(fs.existsSync(canonicalDir)).to.be(true); expect(pkgMeta).to.be.an('object'); expect(pkgMeta.name).to.be('package-a'); expect(pkgMeta.version).to.be('0.1.1'); next(); }) .done(); }); it('should attempt to retrieve a resolved package from the resolve package', function (next) { var called = false; var originalRetrieve = packageRepository._resolveCache.retrieve; packageRepository._resolveCache.retrieve = function (source) { called = true; expect(source).to.be(mockSource); return originalRetrieve.apply(this, arguments); }; packageRepository.fetch({ name: '', source: 'foo', target: '~0.1.0' }) .spread(function (canonicalDir, pkgMeta) { expect(called).to.be(true); expect(fs.existsSync(canonicalDir)).to.be(true); expect(pkgMeta).to.be.an('object'); expect(pkgMeta.name).to.be('package-a'); expect(pkgMeta.version).to.be('0.1.1'); next(); }) .done(); }); it('should avoid using cache for local resources', function (next) { forceCaching = false; var called = false; var originalRetrieve = packageRepository._resolveCache.retrieve; packageRepository._resolveCache.retrieve = function (source) { called = true; expect(source).to.be(mockSource); return originalRetrieve.apply(this, arguments); }; packageRepository.fetch({ name: '', source: helpers.localSource(testPackage), target: '~0.1.0' }) .spread(function (canonicalDir, pkgMeta) { expect(called).to.be(false); expect(fs.existsSync(canonicalDir)).to.be(true); expect(pkgMeta).to.be.an('object'); expect(pkgMeta.name).to.be('package-a'); expect(pkgMeta.version).to.be('0.1.1'); forceCaching = true; next(); }) .done(); }); it('should just call the resolver resolve method if no appropriate package was found in the resolve cache', function (next) { var called = []; resolverFactoryHook = function (resolver) { var originalResolve = resolver.resolve; resolver.resolve = function () { called.push('resolve'); return originalResolve.apply(this, arguments); }; resolver.hasNew = function () { called.push('hasNew'); }; }; packageRepository._resolveCache.retrieve = function () { return Q.resolve([]); }; packageRepository.fetch({ name: '', source: 'foo', target: ' ~0.1.0' }) .spread(function (canonicalDir, pkgMeta) { expect(called).to.eql(['resolve']); expect(fs.existsSync(canonicalDir)).to.be(true); expect(pkgMeta).to.be.an('object'); expect(pkgMeta.name).to.be('package-a'); expect(pkgMeta.version).to.be('0.1.1'); next(); }) .done(); }); it('should call the resolver hasNew method if an appropriate package was found in the resolve cache', function (next) { var json = { name: 'a', version: '0.2.1' }; var called; resolverFactoryHook = function (resolver) { var originalHasNew = resolver.hasNew; resolver.hasNew = function (pkgMeta) { expect(pkgMeta).to.eql(json); called = true; return originalHasNew.apply(this, arguments); }; }; packageRepository._resolveCache.retrieve = function () { return Q.resolve([tempPackage, json]); }; copy.copyDir(testPackage, tempPackage, { ignore: ['.git'] }) .then(function () { fs.writeFileSync(path.join(tempPackage, '.bower.json'), JSON.stringify(json)); return packageRepository.fetch({ name: '', source: 'foo', target: '~0.1.0' }) .spread(function (canonicalDir, pkgMeta) { expect(called).to.be(true); expect(fs.existsSync(canonicalDir)).to.be(true); expect(pkgMeta).to.be.an('object'); expect(pkgMeta.name).to.be('package-a'); expect(pkgMeta.version).to.be('0.1.1'); next(); }); }) .done(); }); it('should call the resolver resolve method if hasNew resolved to true', function (next) { var json = { name: 'a', version: '0.2.0' }; var called = []; resolverFactoryHook = function (resolver) { var originalResolve = resolver.resolve; resolver.resolve = function () { called.push('resolve'); return originalResolve.apply(this, arguments); }; resolver.hasNew = function (pkgMeta) { expect(pkgMeta).to.eql(json); called.push('hasNew'); return Q.resolve(true); }; }; packageRepository._resolveCache.retrieve = function () { return Q.resolve([tempPackage, json]); }; copy.copyDir(testPackage, tempPackage, { ignore: ['.git'] }) .then(function () { fs.writeFileSync(path.join(tempPackage, '.bower.json'), JSON.stringify(json)); return packageRepository.fetch({ name: '', source: 'foo', target: '~0.2.0' }) .spread(function (canonicalDir, pkgMeta) { expect(called).to.eql(['hasNew', 'resolve']); expect(fs.existsSync(canonicalDir)).to.be(true); expect(pkgMeta).to.be.an('object'); expect(pkgMeta.name).to.be('a'); expect(pkgMeta.version).to.be('0.2.2'); next(); }); }) .done(); }); it('should resolve to the cached package if hasNew resolve to false', function (next) { var json = { name: 'a', version: '0.2.0' }; var called = []; resolverFactoryHook = function (resolver) { var originalResolve = resolver.resolve; resolver.resolve = function () { called.push('resolve'); return originalResolve.apply(this, arguments); }; resolver.hasNew = function (pkgMeta) { expect(pkgMeta).to.eql(json); called.push('hasNew'); return Q.resolve(false); }; }; packageRepository._resolveCache.retrieve = function () { return Q.resolve([tempPackage, json]); }; copy.copyDir(testPackage, tempPackage, { ignore: ['.git'] }) .then(function () { fs.writeFileSync(path.join(tempPackage, '.bower.json'), JSON.stringify(json)); return packageRepository.fetch({ name: '', source: 'foo', target: '~0.2.0' }) .spread(function (canonicalDir, pkgMeta) { expect(called).to.eql(['hasNew']); expect(canonicalDir).to.equal(tempPackage); expect(pkgMeta).to.eql(json); next(); }); }) .done(); }); it('should just use the cached package if offline was specified', function (next) { var json = { name: 'a', version: '0.2.0' }; var called = []; resolverFactoryHook = function (resolver) { var originalResolve = resolver.resolve; resolver.hasNew = function (pkgMeta) { expect(pkgMeta).to.eql(json); called.push('resolve'); return originalResolve.apply(this, arguments); }; resolver.hasNew = function () { called.push('hasNew'); return Q.resolve(false); }; }; packageRepository._resolveCache.retrieve = function () { return Q.resolve([tempPackage, json]); }; copy.copyDir(testPackage, tempPackage, { ignore: ['.git'] }) .then(function () { fs.writeFileSync(path.join(tempPackage, '.bower.json'), JSON.stringify(json)); packageRepository._config.offline = true; return packageRepository.fetch({ name: '', source: 'foo', target: '~0.2.0' }) .spread(function (canonicalDir, pkgMeta) { expect(called.length).to.be(0); expect(canonicalDir).to.equal(tempPackage); expect(pkgMeta).to.eql(json); next(); }); }) .done(); }); it('should error out if there is no appropriate package in the resolve cache and offline was specified', function (next) { packageRepository._config.offline = true; packageRepository.fetch({ name: '', source: 'foo', target: '~0.2.0' }) .then(function () { throw new Error('Should have failed'); }, function (err) { expect(err).to.be.an(Error); expect(err.code).to.equal('ENOCACHE'); next(); }) .done(); }); }); describe('.versions', function () { it('should call the versions method on the concrete resolver', function (next) { var called = []; var originalVersions = resolvers.GitRemote.versions; resolvers.GitRemote.versions = function (source) { expect(source).to.equal(mockSource); called.push('resolver'); return Q.resolve([]); }; packageRepository._resolveCache.versions = function () { called.push('resolve-cache'); return Q.resolve([]); }; packageRepository.versions('foo') .then(function (versions) { expect(called).to.eql(['resolver']); expect(versions).to.be.an('array'); expect(versions.length).to.be(0); next(); }) .fin(function () { resolvers.GitRemote.versions = originalVersions; }) .done(); }); it('should call the versions method on the resolve cache if offline was specified', function (next) { var called = []; var originalVersions = resolvers.GitRemote.versions; resolvers.GitRemote.versions = function () { called.push('resolver'); return Q.resolve([]); }; packageRepository._resolveCache.versions = function (source) { expect(source).to.equal(mockSource); called.push('resolve-cache'); return Q.resolve([]); }; packageRepository._config.offline = true; packageRepository.versions('foo') .then(function (versions) { expect(called).to.eql(['resolve-cache']); expect(versions).to.be.an('array'); expect(versions.length).to.be(0); next(); }) .fin(function () { resolvers.GitRemote.versions = originalVersions; }) .done(); }); }); describe('.eliminate', function () { it('should call the eliminate method from the resolve cache', function (next) { var called; var json = { name: 'a', version: '0.2.0', _source: 'foo' }; packageRepository._resolveCache.eliminate = function (pkgMeta) { expect(pkgMeta).to.eql(json); called = true; return Q.resolve(); }; packageRepository.eliminate(json) .then(function () { expect(called).to.be(true); next(); }) .done(); }); it('should call the clearCache method with the name from the registry client', function (next) { var called; var json = { name: 'a', version: '0.2.0', _source: 'foo' }; packageRepository._registryClient.clearCache = function (name, callback) { expect(name).to.eql(json.name); called = true; callback(); }; packageRepository.eliminate(json) .then(function () { expect(called).to.be(true); next(); }) .done(); }); }); describe('.list', function () { it('should proxy to the resolve cache list method', function (next) { var called; var originalList = packageRepository._resolveCache.list; packageRepository._resolveCache.list = function () { called = true; return originalList.apply(this, arguments); }; packageRepository.list() .then(function (entries) { expect(called).to.be(true); expect(entries).to.be.an('array'); next(); }) .done(); }); }); describe('.clear', function () { it('should call the clear method from the resolve cache', function (next) { var called; packageRepository._resolveCache.clear = function () { called = true; return Q.resolve(); }; packageRepository.clear() .then(function () { expect(called).to.be(true); next(); }) .done(); }); it('should call the clearCache method without name from the registry client', function (next) { var called; packageRepository._registryClient.clearCache = function (callback) { called = true; callback(); }; packageRepository.clear() .then(function () { expect(called).to.be(true); next(); }) .done(); }); }); describe('.reset', function () { it('should call the reset method from the resolve cache', function () { var called; packageRepository._resolveCache.reset = function () { called = true; return packageRepository._resolveCache; }; packageRepository.reset(); expect(called).to.be(true); }); it('should call the resetCache method without name from the registry client', function () { var called; packageRepository._registryClient.resetCache = function () { called = true; return packageRepository._registryClient; }; packageRepository.reset(); expect(called).to.be(true); }); }); describe('.getRegistryClient', function () { it('should return the underlying registry client', function () { expect(packageRepository.getRegistryClient()).to.be.an(RegistryClient); }); }); describe('.getResolveCache', function () { it('should return the underlying resolve cache', function () { expect(packageRepository.getResolveCache()).to.be.an(ResolveCache); }); }); describe('#clearRuntimeCache', function () { it('should clear the resolve cache runtime cache', function () { var called; var originalClearRuntimeCache = ResolveCache.clearRuntimeCache; // No need to restore the original method since the constructor // gets re-assigned every time in beforeEach ResolveCache.clearRuntimeCache = function () { called = true; return originalClearRuntimeCache.apply(ResolveCache, arguments); }; packageRepository.constructor.clearRuntimeCache(); expect(called).to.be(true); }); it('should clear the resolver factory runtime cache', function () { var called; resolverFactoryClearHook = function () { called = true; }; packageRepository.constructor.clearRuntimeCache(); expect(called).to.be(true); }); it('should clear the registry runtime cache', function () { var called; var originalClearRuntimeCache = RegistryClient.clearRuntimeCache; // No need to restore the original method since the constructor // gets re-assigned every time in beforeEach RegistryClient.clearRuntimeCache = function () { called = true; return originalClearRuntimeCache.apply(RegistryClient, arguments); }; packageRepository.constructor.clearRuntimeCache(); expect(called).to.be(true); }); }); });