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)
})
})
})
})