Asymmetrik/node-rest-starter

View on GitHub
src/app/core/access-checker/access-checker.service.spec.ts

Summary

Maintainability
D
1 day
Test Coverage
import config from 'config';
import _ from 'lodash';
import { DateTime } from 'luxon';
import should from 'should';
import { createSandbox } from 'sinon';

import accessChecker from './access-checker.service';
import { CacheEntry, ICacheEntry } from './cache/cache-entry.model';
import cacheEntryService from './cache/cache-entry.service';

/**
 * Helpers
 */
function clearDatabase() {
    return CacheEntry.deleteMany({}).exec();
}

function cacheSpec(key): ICacheEntry {
    return {
        key: key.toLowerCase(),
        value: {
            name: `${key} Name`,
            organization: `${key} Organization`,
            email: `${key}@mail.com`,
            username: `${key}_username`,
            roles: ['role1', 'role2'],
            groups: ['group1', 'group2']
        }
    } as unknown as ICacheEntry;
}

function providerSpec(key) {
    return {
        name: `${key} Name`,
        organization: `${key} Organization`,
        email: `${key}@mail.com`,
        username: `${key}_username`,
        roles: ['role1', 'role2'],
        groups: ['group1', 'group2']
    };
}

function validateCacheEntry(actual, expected) {
    should.exist(actual);
    should(actual.name).equal(expected.name);
    should(actual.organization).equal(expected.organization);
    should(actual.email).equal(expected.email);
    should(actual.username).equal(expected.username);

    should(actual.roles).be.an.Array();
    should(actual.roles).have.length(expected.roles.length);
    should(actual.roles).containDeep(expected.roles);

    should(actual.groups).be.an.Array();
    should(actual.groups).have.length(expected.groups.length);
    should(actual.groups).containDeep(expected.groups);
}

/**
 * Unit tests
 */
describe('Access Checker Service:', () => {
    let sandbox;

    // Specs for tests
    const spec: { cache: { [key: string]: ICacheEntry } } = {
        cache: {}
    };
    const provider: { [key: string]: Record<string, unknown> } = {};

    // Cache and provider agree, entry is current
    spec.cache.good = cacheSpec('good');
    provider.good = providerSpec('good');

    // Cache and provider disagree, entry is expired
    spec.cache.expired = cacheSpec('expired');
    spec.cache.expired.ts = DateTime.now()
        .minus({
            milliseconds: 1000 * 60 * 60 * 24 * 10
        })
        .toJSDate();
    provider.expired = providerSpec('expirednew');

    // Cache and provider disagree, entry key not expired
    spec.cache.outdated = cacheSpec('outdated');
    spec.cache.outdated.ts = DateTime.now()
        .minus({
            milliseconds: 1000 * 60 * 60 * 24 * 10
        })
        .toJSDate();
    provider.outdated = providerSpec('outdatednew');

    // Cache has entry that is now missing from provider
    spec.cache.cacheonly = cacheSpec('cacheonly');

    // Entry is only in the provider
    provider.provideronly = providerSpec('provideronly');

    const cache: { [key: string]: ICacheEntry } = {};

    beforeEach(async () => {
        sandbox = createSandbox();

        await clearDatabase();
        await Promise.all(
            _.keys(spec.cache).map((k) => {
                return new CacheEntry(spec.cache[k]).save().then((value) => {
                    cache[k] = value;
                });
            })
        );
    });

    afterEach(async () => {
        sandbox.restore();

        await clearDatabase();
    });

    /**
     * Test functionality with the access checker provider fails
     */
    describe('Broken Access Checker', () => {
        beforeEach(() => {
            const configGetStub = sandbox.stub(config, 'get');
            configGetStub
                .withArgs('auth.accessChecker.provider.file')
                .returns(
                    'src/app/core/access-checker/providers/failure-provider.service'
                );
            configGetStub.withArgs('auth.accessChecker.provider.config').returns({});
            configGetStub.callThrough();

            // Need to clear cached provider from service to ensure proper test run.
            accessChecker.provider = null;
        });

        // Provider fails on get
        it('should not update the cache when the access checker provider fails', async () => {
            await accessChecker.get('provideronly').should.be.rejected();

            const result = await CacheEntry.findOne({ key: 'provideronly' }).exec();
            should.not.exist(result);
        });

        // Provider fails on refresh attempt
        it('should not update the cache on refresh when the access checker provider fails', async () => {
            await accessChecker
                .refreshEntry(spec.cache.outdated.key)
                .should.be.rejected();

            // Query for the cache object and verify it hasn't been updated
            const result = await CacheEntry.findOne({
                _id: cache.outdated._id
            }).exec();
            validateCacheEntry(result.value, spec.cache.outdated.value);
        });

        // Provider fails on refresh attempt
        it('should fail when no key is specified', async () => {
            await accessChecker.refreshEntry(null).should.be.rejected();

            // Query for the cache object and verify it hasn't been updated
            const result = await CacheEntry.findOne({
                _id: cache.outdated._id
            }).exec();
            validateCacheEntry(result.value, spec.cache.outdated.value);
        });
    });

    /**
     * Test basic functionality of a working provider
     */
    describe('Working Access Checker', () => {
        beforeEach(() => {
            const configGetStub = sandbox.stub(config, 'get');
            configGetStub
                .withArgs('auth.accessChecker.provider.file')
                .returns('src/app/core/access-checker/providers/example.provider');
            configGetStub
                .withArgs('auth.accessChecker.provider.config')
                .returns(provider);
            configGetStub.callThrough();

            // Need to clear cached provider from service to ensure proper test run.
            accessChecker.provider = null;
        });

        // Pull from cache
        it('should fail when no key is specified', async () => {
            await accessChecker.get(null).should.be.rejected();
        });

        // Pull from cache
        it('should pull from cache when the entry is current and present', async () => {
            const info = await accessChecker.get(spec.cache.good.key);
            validateCacheEntry(info, spec.cache.good.value);
        });

        // Pull from provider
        it('should pull from provider and update cache when entry is expired', async () => {
            const info = await accessChecker.get(spec.cache.expired.key);
            validateCacheEntry(info, provider.expired);

            const result = await CacheEntry.findOne({
                key: cache.expired.key
            }).exec();
            validateCacheEntry(result.value, provider.expired);
        });

        // Cache only
        it('should return the cache entry if the entry is missing from the provider', async () => {
            const info = await accessChecker.get(spec.cache.cacheonly.key);
            validateCacheEntry(info, spec.cache.cacheonly.value);

            const result = await CacheEntry.findOne({
                key: cache.cacheonly.key
            }).exec();
            validateCacheEntry(result.value, spec.cache.cacheonly.value);
        });

        // Provider only
        it('should update the cache when pulling from the provider', async () => {
            const info = await accessChecker.get('provideronly');
            validateCacheEntry(info, provider.provideronly);

            const result = await CacheEntry.findOne({ key: 'provideronly' }).exec();
            validateCacheEntry(result.value, provider.provideronly);
        });

        // Pull from provider
        it('should pull from provider and return result even if cache update fails', async () => {
            sandbox.stub(cacheEntryService, 'upsert').rejects(new Error('error'));
            const info = await accessChecker.get('provideronly');
            validateCacheEntry(info, provider.provideronly);
        });

        // Refresh cache entry
        it('should refresh the cache when forced', async () => {
            // should return the info from the cache
            await accessChecker.refreshEntry('provideronly');

            const result = await CacheEntry.findOne({
                key: 'provideronly'
            }).exec();

            validateCacheEntry(result.value, provider.provideronly);
        });
    });

    /**
     * Test functionality with missing access checker config
     */
    describe('Missing Access Checker Config', () => {
        beforeEach(() => {
            // Need to clear cached provider from service to ensure proper test run.
            accessChecker.provider = null;
        });

        // Provider fails on get
        it('should throw error when no provider is configured', async () => {
            await accessChecker
                .get('notincache')
                .should.be.rejectedWith(
                    new Error(
                        'Error retrieving entry from the access checker provider: Configuration property "auth.accessChecker.provider.file" is not defined'
                    )
                );
        });
    });

    /**
     * Test functionality with missing access checker provider file
     */
    describe('Invalid Access Checker Config', () => {
        beforeEach(() => {
            const configGetStub = sandbox.stub(config, 'get');
            configGetStub
                .withArgs('auth.accessChecker.provider.file')
                .returns('invalid/path/to/provider');
            configGetStub.withArgs('auth.accessChecker.provider.config').returns({});
            configGetStub.callThrough();

            // Need to clear cached provider from service to ensure proper test run.
            accessChecker.provider = null;
        });

        // Provider fails on get
        it('should throw error when provider is configured with invalid file path', async () => {
            await accessChecker
                .get('notincache')
                .should.be.rejectedWith(
                    'Error retrieving entry from the access checker provider: Failed to load access checker provider.'
                );
        });
    });
});