Source: merlins.test.js

import Promise from 'bluebird'
import * as types from './types'
import * as cj from 'cookiejar'
import {MerlinsConnector} from './merlins'
import {getWindow, yakbakServer, propTypeErrors} from './test-utils'

jest.setTimeout(20000)

const merlinsConf = {
    url: 'https://www.merlinspotions.com',
    port: 7780,
    dirname: `${__dirname}/tapes/merlins`,
    proxyUrl: 'http://localhost:7780',
}

const dondeGeoConf = {
    url: 'https://donde-geo-tools.herokuapp.com',
    port: 7781,
    dirname: `${__dirname}/tapes/merlins`,
    proxyUrl: 'http://localhost:7781',
}

const dondeApiConf = {
    url: 'https://api.donde.io',
    port: 7782,
    dirname: `${__dirname}/tapes/merlins`,
    proxyUrl: 'http://localhost:7782',
}

const testCredentials = [
    'obrook+merlinstest@mobify.com',
    'testtesttesttest',
]

const testUser = {
    id: 'me',
    firstName: 'Test First Name',
    lastName: 'Test Last Name',
    email: 'obrook+merlinstest@mobify.com'
}

/**
 * @param {String} setCookie
 * @returns {String}
 */
const convertSetCookieToCookie = (setCookie) => {
    return new cj.Cookie(setCookie).toValueString()
}

test('Should convert a set-cookie header to a cookie header', () => {
    const setCookieHeaders = [
        'a=a; domain=www.merlinspotions.com; HttpOnly; secure',
        'b=b; domain=something-else.com',
        'c=c',
    ]
    const cookieHeaders = setCookieHeaders.map(convertSetCookieToCookie)
    expect(cookieHeaders).toEqual(['a=a', 'b=b', 'c=c'])
})


describe(`The Merlins Connector`, () => {
    let window
    let servers

    beforeAll(() => {
        const configs = [merlinsConf, dondeGeoConf, dondeApiConf]
        return Promise.all(configs.map((config) => yakbakServer(config)))
            .then((_servers) => { servers = _servers })
            .then(() => getWindow(merlinsConf.proxyUrl))
            .then((_window) => { window = _window })
    })

    afterAll(() => {
        const promises = servers.map((server) => new Promise((resolve) => server.kill(resolve)))
        return Promise.all(promises)
    })

    const makeConnector = () => new MerlinsConnector({
        window,
        basePath: merlinsConf.url,
        dondeGeoBasePath: dondeGeoConf.url,
        dondeApiBasePath: dondeApiConf.url,
        cookieDomainRewrites: {'www.merlinspotions.com': 'www.merlins-proxy.com'}
    })

    describe('Searching for products', () => {
        const sorts = [
            undefined,
            {query: 'name', key: 'name', compare: (a, b) => a.localeCompare(b)},
            {query: 'price', key: 'price', compare: (a, b) => a - b},
        ]
        sorts.forEach((sortOpts) => {
            test(`with a sort order [sort: ${sortOpts && sortOpts.query}]`, () => {
                const connector = makeConnector()
                const query = {
                    filters: {categoryId: 'potions'},
                    ...(sortOpts && {sort: sortOpts.query})
                }
                return connector.searchProducts(query).then((data) => {
                    expect(propTypeErrors(types.ProductSearch, data)).toBeFalsy()
                    if (sortOpts) {
                        const {key, compare} = sortOpts
                        const values = data.results.map((item) => item[key]) // eslint-disable-line max-nested-callbacks
                        const expected = values.slice().sort(compare)
                        expect(values).toEqual(expected)
                    }
                })
            })
        })

        const counts = [5]
        counts.forEach((count) => {
            test(`with a max count (count: ${count})`, () => {
                const connector = makeConnector()
                const query = {
                    filters: {categoryId: 'potions'},
                    count
                }
                return connector.searchProducts(query).then((data) => {
                    expect(propTypeErrors(types.ProductSearch, data)).toBeFalsy()
                    expect(data.results.length).toEqual(count)
                })
            })
        })
    })

    describe('Getting a product', () => {
        test('with a valid id should resolve with a Product instance', () => {
            const connector = makeConnector()
            return connector.getProduct('eye-of-newt')
                .then((data) => {
                    expect(propTypeErrors(types.Product, data)).toBeFalsy()
                })
        })

        test('with an invalid id should reject', () => {
            const connector = makeConnector()
            expect(connector.getProduct('horse-pants')).rejects.toThrow()
        })
    })

    describe('Getting products', () => {
        test('with valid ids should resolve with a ProductList instance with populated data', () => {
            const connector = makeConnector()
            const validIds = ['eye-of-newt', 'unicorn-blood']
            return connector.getProducts(validIds)
                .then((result) => {
                    expect(propTypeErrors(types.ProductList, result)).toBeFalsy()
                    expect(result.data.length).toEqual(validIds.length)
                })
        })

        test('with invalid ids should resolve with a ProductList instance with empty data', () => {
            const connector = makeConnector()
            const invalidIds = ['horse-pants', 'wrinkly-ears']
            return connector.getProducts(invalidIds)
                .then((result) => {
                    expect(propTypeErrors(types.ProductList, result)).toBeFalsy()
                    expect(result.data.length).toEqual(0)
                })
        })

        test('with a mix of valid and invalid ids should resolve with a ProductList instance with only valid data', () => {
            const connector = makeConnector()
            const validIds = ['eye-of-newt', 'unicorn-blood']
            const invalidIds = ['horse-pants', 'wrinkly-ears']

            return connector.getProducts([...validIds, ...invalidIds])
                .then((result) => {
                    expect(propTypeErrors(types.ProductList, result)).toBeFalsy()
                    expect(result.data.length).toEqual(validIds.length)
                    expect(result.total).toEqual(validIds.length)
                    expect(result.count).toEqual(validIds.length)
                })
        })
    })

    test('Getting a store', () => {
        const connector = makeConnector()
        const id = 'bc/vancouver/948-homer-street'
        return connector.getStore(id).then((data) => {
            expect(propTypeErrors(types.Store, data)).toBeFalsy()
        })
    })

    describe('Searching for stores', () => {
        // searchStoreRequest is using default values for start and count if not provided
        test('Only longitude provided, missing latitude', () => {
            const connector = makeConnector()
            const searchStoreRequest = {
                latlon: {
                    longitude: -123.08359
                }
            }
            expect(() => {
                connector.searchStores(searchStoreRequest)
            }).toThrowError('Provide Latitude and Longitude coordinates')
        })

        test('Only latitude provided, missing longitude', () => {
            const connector = makeConnector()
            const searchStoreRequest = {
                latlon: {
                    latitude: 49.24667,
                }
            }
            expect(() => {
                connector.searchStores(searchStoreRequest)
            }).toThrowError('Provide Latitude and Longitude coordinates')
        })

        test('Valid latitude and longitude provided', () => {
            const connector = makeConnector()
            const searchStoreRequest = {
                latlon: {
                    latitude: 49.24667,
                    longitude: -123.08359
                }
            }
            return connector.searchStores(searchStoreRequest)
                .then((data) => {
                    expect(data.stores).not.toHaveLength(0)
                    expect(data.stores).toHaveLength(data.count)
                    expect(propTypeErrors(types.StoreSearchResult, data)).toBeFalsy()
                })
        })

        test('0.0, 0.0 geo-coordinates allowed', () => {
            const connector = makeConnector()
            const searchStoreRequest = {
                latlon: {
                    latitude: 0.0,
                    longitude: 0.0
                }
            }
            return connector.searchStores(searchStoreRequest)
                .then((data) => {
                    expect(data.stores).not.toHaveLength(0)
                    expect(data.stores).toHaveLength(data.count)
                    expect(propTypeErrors(types.StoreSearchResult, data)).toBeFalsy()
                })
        })
    })

    describe('Get Product Categories', () => {
        test(`getCategory should resolve to a Category instance`, () => {
            const connector = makeConnector()
            const category = 'root'
            return connector.getCategory(category).then((data) => {
                expect(propTypeErrors(types.Category, data)).toBeFalsy()
                expect(data.id).toEqual(category)
            })
        })

        test(`getCategories should resolve to a CategoryList instance`, () => {
            const connector = makeConnector()
            return connector.getCategories(['books', 'potions']).then((result) => {
                expect(propTypeErrors(types.CategoryList, result)).toBeFalsy()
                expect(result.count).toEqual(2)
            })
        })
    })

    describe('Authentication', () => {
        test(`should allow a user to login and logout`, () => {
            const connector = makeConnector()
            return connector.login(...testCredentials)
                .then((data) => expect(data).toEqual(testUser))
                .then(() => connector.logout())
                .then(() => expect(connector.getCustomer('me')).rejects.toEqual(new Error('logged out')))
        })

        test(`should allow a session to be restored on a new Connector instance`, () => {
            const connector = makeConnector()
            return connector.login(...testCredentials)
                .then(() => {
                    const connector2 = makeConnector()
                    const cookieString = (
                        connector.getCookies()
                        .map(convertSetCookieToCookie)
                        .join('; ')
                    )
                    connector2.setCookies(cookieString)
                    return connector2.getCustomer('me')
                })
                .then((data) => expect(data).toEqual(testUser))
        })

        test(`should only return cookies that are new or changed`, () => {
            // Get all cookies and return them indexed by name
            const indexedCookies = (connector) => {
                const cookies = (
                    connector.getCookies()
                    .map((c) => new cj.Cookie(c))
                )
                return Object.assign({},
                    ...cookies.map((c) => ({[c.name]: c}))
                )
            }

            const connector = makeConnector()
            return connector.login(...testCredentials)
                .then(() => {
                    const indexed = indexedCookies(connector)
                    const expected = [
                        'PHPSESSID',
                        'mage-messages',
                        'private_content_version',
                        'persistent_shopping_cart',
                        'X-Magento-Vary',
                    ]
                    expect(Object.keys(indexed).sort()).toEqual(expected.sort())
                    // Expect some cookies to have rewritten domains
                    expect(indexed.PHPSESSID.domain).toEqual('www.merlins-proxy.com')
                })
                .then(() => connector.setCookies(null))
                .then(() => connector.getCustomer('me'))
                .then((data) => {
                    expect(data).toEqual(testUser)
                    // Expiry is updated on every request for these two cookies.
                    // Only changed cookies should be returned.
                    const expected = [
                        'mage-messages',
                        'persistent_shopping_cart',
                    ]
                    const indexed = indexedCookies(connector)
                    expect(Object.keys(indexed)).toEqual(expected)
                })
        })
    })
})