src/system/__tests__/System.spec.js

Summary

Maintainability
D
2 days
Test Coverage
import fixtures from '../../__fixtures__/fixtures'
import MockApi from '../../api/Api'
import System from '../System'
import SystemConfiguration from '../SystemConfiguration'
import SystemSettings from '../SystemSettings'

jest.mock('../../api/Api')

describe('System', () => {
    let system
    let apiMock

    beforeEach(() => {
        apiMock = new MockApi()
        system = new System(new SystemSettings(), new SystemConfiguration())
    })

    afterEach(() => {
        MockApi.mockReset()
    })

    it('should be an instance of System', () => {
        expect(system).toBeInstanceOf(System)
    })

    it('should not be allowed to be called without new', () => {
        expect(() => System()).toThrowErrorMatchingSnapshot()
    })

    it('should contain an instance of SystemConfiguration', () => {
        expect(system.configuration).toBeInstanceOf(SystemConfiguration)
    })

    it('should contain an instance of SystemSettings', () => {
        expect(system.settings).toBeInstanceOf(SystemSettings)
    })

    describe('loadInstalledApps()', () => {
        let appsFromApi

        beforeEach(() => {
            appsFromApi = [
                {
                    version: '0.4.8',
                    name: 'Data Approval',
                    description: 'Approvals app for PEPFAR',
                    icons: {
                        48: 'img/icons/dataapproval.png',
                    },
                    developer: {
                        url: 'http://www.dhis2.org',
                        name: 'Mark Polak',
                        company: 'DHIS2 Core Team',
                        email: 'markpo@ifi.uio.no',
                    },
                    activities: {
                        dhis: {
                            href: 'http://localhost:8080/dhis',
                        },
                    },
                    folderName: 'approvals',
                    launchUrl:
                        'http://localhost:8080/dhis/api/apps/approvals/index.html?v=0.4.8',
                    key: 'approvals',
                    launch_path: 'index.html?v=0.4.8',
                    default_locale: 'en',
                },
                {
                    version: '0.0.1',
                    name: 'Data Export Log',
                    description: 'Data export log viewer',
                    icons: {
                        48: 'icons/export.png',
                    },
                    developer: {
                        url: '',
                        name: 'Mark Polak',
                    },
                    activities: {
                        dhis: {
                            href: 'http://localhost:8080/dhis',
                        },
                    },
                    folderName: 'data-export-log',
                    launchUrl:
                        'http://localhost:8080/dhis/api/apps/data-export-log/index.html?1.0.0-rc1',
                    key: 'data-export-log',
                    launch_path: 'index.html?1.0.0-rc1',
                    default_locale: 'en',
                },
            ]

            apiMock.get = jest
                .fn()
                .mockReturnValue(Promise.resolve(appsFromApi))
        })

        it('should set the list of installed apps onto the Settings object', () =>
            system.loadInstalledApps().then(() => {
                expect(system.installedApps).toEqual(appsFromApi)
            }))

        it('should reject the promise if the request fails', () => {
            apiMock.get = jest
                .fn()
                .mockReturnValue(Promise.reject('Apps can not be loaded'))

            return system
                .loadInstalledApps()
                .catch((error) => error)
                .then((message) => {
                    expect(message).toBe('Apps can not be loaded')
                })
        })

        it('should resolve with the returned list of apps', () =>
            system.loadInstalledApps().then((apps) => {
                expect(apps).toEqual(appsFromApi)
            }))
    })

    describe('uploadApp()', () => {
        let appendSpy
        let formData

        beforeEach(() => {
            jest.spyOn(apiMock, 'post').mockReturnValue(Promise.resolve())

            // Fake FormData object
            appendSpy = jest.fn()
            formData = {
                append: appendSpy,
            }
            // Fake formData global constructor
            global.FormData = function FormData() {
                return formData
            }
        })

        afterEach(() => {
            global.FormData = undefined
        })

        it('should be a function on the system object', () => {
            expect(typeof system.uploadApp).toBe('function')
        })

        it('should call the post with the correct options', () => {
            const xhrOptions = {
                contentType: false,
                processData: false,
                xhr: undefined,
            }

            system.uploadApp('ZipFile')

            expect(apiMock.post).toBeCalledWith('apps', formData, xhrOptions)
        })

        it('should call append on the formData object to add the file to upload', () => {
            system.uploadApp('ZipFile')

            expect(formData.append).toBeCalledWith('file', 'ZipFile')
        })

        describe('xhr', () => {
            let progressCallbackSpy
            let xhrMock

            beforeEach(() => {
                progressCallbackSpy = jest.fn()
                xhrMock = {
                    upload: {},
                }
                global.XMLHttpRequest = function XMLHttpRequest() {
                    return xhrMock
                }
            })

            afterEach(() => {
                global.XMLHttpRequest = undefined
            })

            it('should pass custom XMLHttpRequest Object with an on progress callback as an option', () => {
                system.uploadApp('ZipFile', progressCallbackSpy)

                expect(typeof apiMock.post.mock.calls[0][2].xhr).toBe(
                    'function'
                )
                expect(apiMock.post.mock.calls[0][2].xhr.call()).toBe(xhrMock)
            })

            it('should define the onprogress function onto the upload object of the xhr', () => {
                system.uploadApp('ZipFile', progressCallbackSpy)

                expect(typeof xhrMock.upload.onprogress).toBe('function')
            })

            it('should not call the callback if the progress can not be computed', () => {
                system.uploadApp('ZipFile', progressCallbackSpy)
                xhrMock.upload.onprogress({})

                expect(progressCallbackSpy).not.toBeCalled()
            })

            it('should call the callback spy if the progress can be computed', () => {
                system.uploadApp('ZipFile', progressCallbackSpy)
                xhrMock.upload.onprogress({
                    lengthComputable: true,
                    loaded: 10,
                    total: 50,
                })

                expect(progressCallbackSpy).toBeCalledWith(0.2)
            })
        })
    })

    describe('loadAppStore()', () => {
        // Useful constants!
        const sysVersionMinusTwo = '2.26'
        const sysVersionMinusOne = '2.27'
        const sysVersion = '2.28'
        const sysVersionPlusOne = '2.29'
        const sysVersionPlusTwo = '2.30'

        beforeEach(() => {
            system.setSystemInfo({
                version: sysVersion,
            })

            jest.spyOn(apiMock, 'get').mockReturnValue(
                Promise.resolve(fixtures.get('/appStore'))
            )
        })

        it('should be a function on the system object', () => {
            expect(typeof system.loadAppStore).toBe('function')
        })

        it('should return a promise', () => {
            expect(system.loadAppStore()).toBeInstanceOf(Promise)
        })

        it('should request the api for the appStore', () => {
            system.loadAppStore()

            expect(apiMock.get).toBeCalledWith('appStore')
        })

        it('should return the compatible apps from the api', () => {
            // Apps 3 and 6 are compatible with 2.28
            const expectedApps = fixtures
                .get('/appStore')
                .filter((app, i) => [3, 6].includes(i))

            return system.loadAppStore().then((apps) => {
                expect(apps).toEqual(expectedApps)
            })
        })

        describe('with version 2.24', () => {
            beforeEach(() => {
                system.setSystemInfo({
                    version: '2.24',
                })
            })

            it('should return the compatible apps from the API', () => {
                // Apps 2, 4 and 6 are compatible with 2.24
                const expectedApps = fixtures
                    .get('/appStore')
                    .filter((app, i) => [2, 4, 6].includes(i))

                return system.loadAppStore().then((apps) => {
                    expect(apps).toEqual(expectedApps)
                })
            })
        })

        describe('with version 2.25', () => {
            beforeEach(() => {
                system.setSystemInfo({
                    version: '2.25',
                })
            })

            it('should return the compatible apps from the API', () => {
                // Apps 1, 2, 4, 5 and 6 are compatible with 2.25
                const expectedApps = fixtures
                    .get('/appStore')
                    .filter((app, i) => [1, 2, 4, 5, 6].includes(i))

                return system.loadAppStore().then((apps) => {
                    expect(apps).toEqual(expectedApps)
                })
            })
        })

        it('should return the compatible apps', () => {
            // const returnedApps = fixtures.get('/appStore');

            const returnedApps = [
                {
                    versions: [
                        // One version compatible
                        {
                            min_platform_version: sysVersionMinusTwo,
                            max_platform_version: sysVersionMinusOne,
                        },
                        { min_platform_version: sysVersionMinusOne }, // compatible
                    ],
                },
                {
                    versions: [
                        // Both incompatible
                        {
                            min_platform_version: sysVersionPlusOne,
                            max_platform_version: sysVersionPlusTwo,
                        },
                        { min_platform_version: sysVersionPlusTwo },
                    ],
                },
                {
                    versions: [
                        // Compatible
                        { min_platform_version: sysVersionMinusOne },
                    ],
                },
                {
                    versions: [
                        // Incompatible
                        { max_platform_version: sysVersionMinusOne },
                    ],
                },
                {
                    versions: [
                        // One version compatible
                        {
                            minDhisVersion: sysVersionMinusTwo,
                            maxDhisVersion: sysVersionMinusOne,
                        },
                        { minDhisVersion: sysVersionMinusOne }, // compatible
                    ],
                },
                {
                    versions: [
                        // Both incompatible
                        {
                            minDhisVersion: sysVersionPlusOne,
                            maxDhisVersion: sysVersionPlusTwo,
                        },
                        { minDhisVersion: sysVersionPlusTwo },
                    ],
                },
                {
                    versions: [
                        // Compatible
                        { minDhisVersion: sysVersionMinusOne },
                    ],
                },
                {
                    versions: [
                        // Incompatible
                        { maxDhisVersion: sysVersionMinusOne },
                    ],
                },
            ]

            apiMock.get.mockReturnValue(Promise.resolve(returnedApps))

            return system.loadAppStore().then((apps) => {
                expect(apps.length).toBe(4)
            })
        })

        it('should return all the apps when compatibility flag is set to false', () =>
            system.loadAppStore(false).then((apps) => {
                expect(apps.length).toBe(fixtures.get('/appStore').length)
            }))

        it('should reject the promise when the request fails', () => {
            apiMock.get.mockReturnValue(
                Promise.reject('Request for appStore failed')
            )

            return system
                .loadAppStore()
                .catch((error) => error)
                .then((error) => {
                    expect(error).toBe('Request for appStore failed')
                })
        })

        it('should reject the promise when system.version is not set', () => {
            system.version = undefined

            return system
                .loadAppStore()
                .catch((error) => error)
                .then((error) => {
                    expect(error.message).toBe(
                        "Cannot read property 'major' of undefined"
                    )
                })
        })
    })

    describe('installAppVersion()', () => {
        beforeEach(() => {
            apiMock.post.mockReturnValue(Promise.resolve(''))
        })

        it('should be a function on the system object', () => {
            expect(typeof system.installAppVersion).toBe('function')
        })

        it('should reject the promise when the request fails', () => {
            apiMock.post.mockReturnValue(
                Promise.reject('Request for installation failed')
            )

            return system
                .installAppVersion('PyYnjVl5iGt')
                .catch((error) => error)
                .then((errorMessage) => {
                    expect(errorMessage).toBe('Request for installation failed')
                })
        })

        it('should call the api with the correct url', () =>
            system.installAppVersion('PyYnjVl5iGt').then(() => {
                expect(apiMock.post).toBeCalledWith(
                    'appStore/PyYnjVl5iGt',
                    '',
                    { dataType: 'text' }
                )
            }))

        it('should resolve the promise without a value', () =>
            system.installAppVersion('PyYnjVl5iGt').then((response) => {
                expect(response).toBeUndefined()
            }))
    })

    describe('uninstallApp()', () => {
        beforeEach(() => {
            apiMock.delete = jest.fn().mockReturnValue(Promise.resolve({}))
        })

        it('should be a function on the system object', () => {
            expect(typeof system.uninstallApp).toBe('function')
        })

        it('should call the api.delete method with the correct url', () =>
            system.uninstallApp('PyYnjVl5iGt').then(() => {
                expect(apiMock.delete).toBeCalledWith('apps/PyYnjVl5iGt')
            }))

        it('should resolve the request even when the api request fails', () => {
            apiMock.delete = jest.fn().mockReturnValue(Promise.reject({}))

            return system.uninstallApp('PyYnjVl5iGt')
        })
    })

    describe('reloadApps()', () => {
        beforeEach(() => {
            jest.spyOn(apiMock, 'update').mockReturnValue(Promise.resolve())

            jest.spyOn(system, 'loadInstalledApps').mockReturnValueOnce(
                Promise.resolve()
            )
        })

        it('should be a function on the system object', () => {
            expect(typeof system.reloadApps).toBe('function')
        })

        it('should call the update method on the api', () =>
            system.reloadApps().then(() => {
                expect(apiMock.update.mock.calls[0][0]).toBe('apps')
            }))

        it('should call system.loadInstalledApps on success ', () =>
            system.reloadApps().then(() => {
                expect(system.loadInstalledApps).toBeCalled()
            }))

        it('should chain the promise from loadInstalledApps', () => {
            const loadInstalledAppsPromise = Promise.resolve('Apps loaded')

            system.loadInstalledApps.mockReset()
            system.loadInstalledApps.mockReturnValue(loadInstalledAppsPromise)

            return system
                .reloadApps()
                .then((message) => expect(message).toBe('Apps loaded'))
        })

        it('should not call loadInstalledApps when the update request fails', () => {
            apiMock.update.mockReturnValue(Promise.reject())
            jest.spyOn(system, 'loadInstalledApps')

            return system
                .reloadApps()
                .catch((message) => message)
                .then(() => {
                    expect(system.loadInstalledApps).not.toBeCalled()
                })
        })
    })

    describe('compareVersions()', () => {
        let systemVersion
        let appVersion

        beforeEach(() => {
            systemVersion = {
                major: 2,
                minor: 23,
                snapshot: true,
            }
            appVersion = {
                major: 2,
                minor: 23,
                snapshot: true,
            }
        })

        it('should be a function on the system class', () => {
            expect(typeof System.compareVersions).toBe('function')
        })

        it('should return 0 for equal versions', () => {
            expect(System.compareVersions(systemVersion, appVersion)).toBe(0)
        })

        it('should return 1 for a larger major system version', () => {
            systemVersion.major = 3

            expect(System.compareVersions(systemVersion, appVersion)).toBe(1)
        })

        it('should return 1 for a larger minor version', () => {
            systemVersion.minor = 24

            expect(System.compareVersions(systemVersion, appVersion)).toBe(1)
        })

        it('should return 1 when the app is a snapshot version', () => {
            systemVersion.snapshot = false
            appVersion.snapshot = true

            expect(System.compareVersions(systemVersion, appVersion)).toBe(1)
        })

        it('should return -1 when the app is not a snapshot', () => {
            systemVersion.snapshot = true
            appVersion.snapshot = false

            expect(System.compareVersions(systemVersion, appVersion)).toBe(-1)
        })

        it('should do correct comparison when a string is passed as a version', () => {
            expect(System.compareVersions('2.15', '2.16')).toBe(-1)
            expect(System.compareVersions('2.20-SNAPSHOT', '2.16')).toBe(4)
        })
    })

    describe('isVersionCompatible()', () => {
        let appVersion
        let systemVersion

        beforeEach(() => {
            jest.spyOn(System, 'compareVersions')
            appVersion = {
                min_platform_version: '2.23',
                max_platform_version: '2.23',
            }
            systemVersion = '2.23'
        })

        afterEach(() => {
            System.compareVersions.mockRestore()
        })

        it('should return false when the app is too new', () => {
            expect(
                System.isVersionCompatible(
                    systemVersion,
                    Object.assign(appVersion, { min_platform_version: '2.24' })
                )
            ).toBe(false)
        })

        it('should return false when the app is too old', () => {
            expect(
                System.isVersionCompatible(
                    systemVersion,
                    Object.assign(appVersion, { max_platform_version: '2.22' })
                )
            ).toBe(false)
        })

        it('should return true when the system version is within the app version range', () => {
            appVersion.min_platform_version = '2.20'
            appVersion.max_platform_version = '2.25'

            expect(System.isVersionCompatible(systemVersion, appVersion))
        })

        it('should return true when no version bounds are given', () => {
            appVersion = {}

            expect(System.isVersionCompatible(systemVersion, appVersion)).toBe(
                true
            )
        })

        it('should return false when the version is not compatible', () => {
            expect(
                System.isVersionCompatible('2.22', {
                    min_platform_version: '2.17',
                    max_platform_version: '2.20',
                })
            ).toBe(false)
        })

        describe('with 2.28 app version format', () => {
            beforeEach(() => {
                appVersion = {
                    minDhisVersion: '2.22',
                    maxDhisVersion: '2.23-SNAPSHOT',
                }
                systemVersion = '2.23'
            })

            it('should return false when the app is too new', () => {
                expect(
                    System.isVersionCompatible(
                        systemVersion,
                        Object.assign(appVersion, { minDhisVersion: '2.24' })
                    )
                ).toBe(false)
            })

            it('should return false when the app is too old', () => {
                expect(
                    System.isVersionCompatible(
                        systemVersion,
                        Object.assign(appVersion, { maxDhisVersion: '2.22' })
                    )
                ).toBe(false)
            })

            it('should return true when the system version is within the app version range', () => {
                appVersion.minDhisVersion = '2.20'
                appVersion.maxDhisVersion = '2.25'

                expect(System.isVersionCompatible(systemVersion, appVersion))
            })

            it('should return true when no version bounds are given', () => {
                appVersion = {}

                expect(
                    System.isVersionCompatible(systemVersion, appVersion)
                ).toBe(true)
            })

            it('should return false when the version is not compatible', () => {
                expect(
                    System.isVersionCompatible('2.22', {
                        minDhisVersion: '2.17',
                        maxDhisVersion: '2.20',
                    })
                ).toBe(false)
            })
        })
    })

    describe('getSystem', () => {
        it('should return the same instance on consecutive requests', () => {
            expect(System.getSystem()).toBe(System.getSystem())
        })
    })
})