Home Reference Source Test Repository

test/BuilderTest.js

import {expect} from 'chai';
import sinon from 'sinon';
import Builder from '../src/Eloquent/Builder';

/** @test {Builder} */
describe('Builder', () => {
    let builder; // instance of Builder under test

    let connectionStub;
    let dummyResult;

    let Person; // model class
    let person; // model instance

    beforeEach('builderSetup', () => {
        // Reset dependencies
        Person = class {
            constructor(attr) {
                Object.assign(this, attr || {});
                this.constructor.endpoint = 'api';
            }
            hydrate(results) {
                return results.map(result => new Person(result));
            }
        };
        person = new Person({ id: 5 });
        person.exists = true;

        // Stub out the http calls
        dummyResult = [
            { id: 1, name: "first" },
            { id: 2, name: "second" },
            { id: 3, name: "third" },
        ];
        connectionStub = {
            create: sinon.stub().resolves(),
            read: sinon.stub().resolves(dummyResult),
            update: sinon.stub().resolves(),
            delete: sinon.stub().resolves(true)
        };

        // New up the builder
        builder = new Builder(connectionStub);
        builder._setModel(person);
    });

    describe('sugar for finding models by primary key', () => {

        beforeEach(() => person.getKeyName = sinon.stub().returns('KEYNAME'));

        /** @test {Builder#find} */
        it('can fetch a single model from :endpoint/:id', () => {
            let result = new Person();
            connectionStub.read.resolves({ some: 'data' });
            person.newInstance = sinon.stub().returns(result);

            let request = builder.find(2);

            expect(connectionStub.read).to.have.been.calledWith(2);
            return request.then(found => {
                expect(person.newInstance).to.have.been.calledWith({ some: 'data' });
                expect(found).to.equal(result);
            });
        })

        /** @test {Builder#find} */
        it('defers to findMany() if an array is given to find()', function() {
            sinon.stub(builder, 'findMany').resolves('FOUND MANY');

            return expect(builder.find([1, 2])).to.eventually.equal('FOUND MANY');
        });

        /** @test {Builder#findMany} */
        it('can fetch multiple models using whereIn', () => {
            sinon.stub(builder, 'whereIn').returnsThis();

            builder.findMany([1, 2, 3]);

            expect(builder.whereIn).to.have.been.calledWith('KEYNAME', [1, 2, 3]);
        });

        /** @test {Builder#findOrFail} */
        it('throws if no model was found', () => {
            connectionStub.read.resolves();

            return expect(builder.findOrFail(1)).to.eventually.be.rejectedWith('ModelNotFoundException');
        });
    });

    /** @test {Builder#first} */
    it('gets the first result from the current query', () => {
        return expect(builder.first()).to.eventually.be.an.instanceOf(Person);
    });

    /** @test {Builder#firstOrFail} */
    it('gets the first result, or throws if no results', () => {
        connectionStub.read.resolves([]);

        return expect(builder.firstOrFail()).to.eventually.be.rejectedWith('ModelNotFoundException');
    });

    context('when you want a partial result', () => {
        /** @test {Builder#value} */
        it('can get a single column\'s value from the first row', () => {
            return expect(builder.value('name')).to.eventually.equal('first');
        });

        /** @test {Builder#lists} */
        it('can get an array of values from a column', () => {
            return expect(builder.lists('name')).to.eventually.eql(['first', 'second', 'third']);
        });
    });

    /** @test {Builder#_setModel} */
    /** @test {Builder#_getModel} */
    describe('underling model instance', () => {
        it('has a setter and getter', () => {
            let differentModel = Object.assign({}, person, { iAm: 'different' });

            builder._setModel(differentModel);

            expect(builder._getModel()).to.equal(differentModel);
        });

        it('is the source of scope methods copied to the builder', () => {
            sinon.spy(builder, 'scope');
            person.constructor.scopes = ['ofBreed', 'living'];
            builder._setModel(person);

            builder.ofBreed('terrier').living();

            expect(builder.scope).to.have.been.calledTwice;
        });

        it('hydrates data returned from a SELECT query', () => {
            return expect(builder.get()).to.eventually.eql(dummyResult.map((row) => new Person(row)));
        });
    });

    /** @test {Builder#scope} */
    describe('scope()', () => {
        it('tracks calls to dynamic scope methods', () => {
            builder.scope('ofType', ['admin']);

            expect(builder.stack.pop()).to.eql(['scope', ['ofType', ['admin']]]);
        });

        it('is chainable', () => {
            expect(builder.scope('ofType', ['admin'])).to.equal(builder);
        });
    });

    describe('query execution', () => {
        /** @test {Builder#get} */
        it('defers to connection.read for SELECT queries', function () {
            builder.where('archived', 0).get();
            expect(connectionStub.read).to.have.been.calledWith([["where", ["archived", 0]]]);
        });

        /** @test {Builder#insert} */
        it('defers to connection.create for INSERT queries', () => {
            let attributes = { name: 'Frank' };
            builder.insert(attributes);
            expect(connectionStub.create).to.have.been.calledWith(attributes);
        });

        /** @test {Builder#update} */
        it('defers to connection.update for UPDATE queries', () => {
            builder.where('name', 'Francis').update({ active: 0 });
            expect(connectionStub.update).to.have.been.calledWith(builder.stack, { active: 0 });
        });

        /** @test {Builder#delete} */
        it('defers to connection.update for DELETE queries', () => {
            builder.where('name', 'Francis').delete();
            expect(connectionStub.delete).to.have.been.calledWith(builder.stack);
        });
    });

    // These methods have no implementation beyond simply
    // tracking the order of their calls and their arguments.
    context('methods without client-side implementation', () => {
        let methods = [
            'select', 'addSelect',
            'distinct',
            'where', 'orWhere',
            'whereBetween', 'orWhereBetween', 'whereNotBetween', 'orWhereNotBetween',
            'whereNested',
            'whereExists', 'orWhereExists', 'whereNotExists', 'orWhereNotExists',
            'whereIn', 'orWhereIn', 'whereNotIn', 'orWhereNotIn',
            'whereNull', 'orWhereNull', 'whereNotNull', 'orWhereNotNull',
            'whereDate', 'whereDay', 'whereMonth', 'whereYear',
            'groupBy',
            'having', 'orHaving',
            'orderBy', 'latest', 'oldest',
            'offset', 'skip', 'limit', 'take', 'forPage',
            'with',
        ];

        let dummyArgumentsForMethod = (method) => {
            switch (method) {
                case 'distinct':
                case 'oldest':
                case 'latest':
                    return [];

                case 'offset':
                case 'skip':
                case 'limit':
                case 'take':
                    return [5];
            }

            return ['test', 'arguments'];
        };

        methods.forEach(method => {
            it(method + '() adds to the call stack and returns self', () => {
                let testArgs = dummyArgumentsForMethod(method);

                let returnValue = builder[method].apply(builder, testArgs);

                expect(returnValue).to.equal(builder);
                expect(builder.stack.pop()).to.eql([method, testArgs]);
            });
        });
    });
});