fxa-auth-server/test/local/server.js

540 строки
18 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict'
const ROOT_DIR = '../..'
const assert = require('insist')
const EndpointError = require('poolee/lib/error')(require('util').inherits)
const error = require(`${ROOT_DIR}/lib/error`)
const hawk = require('hawk')
const mocks = require('../mocks')
const server = require(`${ROOT_DIR}/lib/server`)
const sinon = require('sinon')
describe('lib/server', () => {
describe('trimLocale', () => {
it('trims given locale', () => {
assert.equal(server._trimLocale(' fr-CH, fr;q=0.9 '), 'fr-CH, fr;q=0.9')
})
})
describe('logEndpointErrors', () => {
const msg = 'Timeout'
const reason = 'Socket fail'
const response = {
__proto__: {
name: 'EndpointError'
},
message: msg,
reason: reason
}
it('logs an endpoint error', (done) => {
const mockLog = {
error: (err) => {
assert.equal(err.op, 'server.EndpointError')
assert.equal(err.message, msg)
assert.equal(err.reason, reason)
done()
}
}
assert.equal(server._logEndpointErrors(response, mockLog))
})
it('logs an endpoint error with a method', (done) => {
response.attempt = {
method: 'PUT'
}
const mockLog = {
error: (err) => {
assert.equal(err.op, 'server.EndpointError')
assert.equal(err.message, msg)
assert.equal(err.reason, reason)
assert.equal(err.method, 'PUT')
done()
}
}
assert.equal(server._logEndpointErrors(response, mockLog))
})
})
describe('set up mocks:', () => {
let config, log, locale, routes, Token, translator, response
beforeEach(() => {
config = getConfig()
locale = 'en'
log = mocks.mockLog()
routes = getRoutes()
Token = require(`${ROOT_DIR}/lib/tokens`)(log, config)
translator = {
getTranslator: sinon.spy(() => ({ en: { format: () => {}, language: 'en' } })),
getLocale: sinon.spy(() => locale)
}
})
describe('create:', () => {
let db, instance
beforeEach(() => {
db = mocks.mockDB({
devices: [ { id: 'fake device id' } ]
})
return server.create(log, error, config, routes, db, translator, Token).then((s) => {
instance = s
})
})
describe('server.start:', () => {
beforeEach(() => instance.start())
afterEach(() => instance.stop())
it('did not call log.begin', () => {
assert.equal(log.begin.callCount, 0)
})
it('did not call log.summary', () => {
assert.equal(log.summary.callCount, 0)
})
describe('successful request, authenticated, acceptable locale, signinCodes feature enabled:', () => {
let request
beforeEach(() => {
response = 'ok'
return instance.inject({
credentials: {
uid: 'fake uid'
},
headers: {
'accept-language': 'fr-CH, fr;q=0.9, en-GB, en;q=0.5',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:57.0) Gecko/20100101 Firefox/57.0',
'x-forwarded-for': '63.245.221.32 , moo , 1.2.3.4'
},
method: 'POST',
url: '/account/create',
payload: {
features: [ 'signinCodes' ]
},
remoteAddress: '63.245.221.32'
}).then(response => request = response.request)
})
it('called log.begin correctly', () => {
assert.equal(log.begin.callCount, 1)
const args = log.begin.args[0]
assert.equal(args.length, 2)
assert.equal(args[0], 'server.onRequest')
assert.ok(args[1])
assert.equal(args[1].path, '/account/create')
assert.equal(args[1].app.locale, 'en')
})
it('called log.summary correctly', () => {
assert.equal(log.summary.callCount, 1)
const args = log.summary.args[0]
assert.equal(args.length, 2)
assert.equal(args[0], log.begin.args[0][1])
assert.ok(args[1])
assert.equal(args[1].isBoom, undefined)
assert.equal(args[1].errno, undefined)
assert.equal(args[1].statusCode, 200)
assert.equal(args[1].source, 'ok')
})
it('did not call log.error', () => {
assert.equal(log.error.callCount, 0)
})
it('parsed features correctly', () => {
assert.ok(request.app.features)
assert.equal(typeof request.app.features.has, 'function')
assert.equal(request.app.features.has('signinCodes'), true)
})
it('parsed remote address chain correctly', () => {
assert.ok(Array.isArray(request.app.remoteAddressChain))
assert.equal(request.app.remoteAddressChain.length, 3)
assert.equal(request.app.remoteAddressChain[0], '63.245.221.32')
assert.equal(request.app.remoteAddressChain[1], '1.2.3.4')
assert.equal(request.app.remoteAddressChain[2], request.app.remoteAddressChain[0])
})
it('parsed client address correctly', () => {
assert.equal(request.app.clientAddress, '63.245.221.32')
})
it('parsed accept-language correctly', () => {
assert.equal(request.app.acceptLanguage, 'fr-CH, fr;q=0.9, en-GB, en;q=0.5')
})
it('parsed locale correctly', () => {
assert.equal(translator.getLocale.callCount, 0)
assert.equal(request.app.locale, 'en')
assert.equal(translator.getLocale.callCount, 1)
})
it('parsed user agent correctly', () => {
assert.ok(request.app.ua)
assert.equal(request.app.ua.browser, 'Firefox')
assert.equal(request.app.ua.browserVersion, '57.0')
assert.equal(request.app.ua.os, 'Mac OS X')
assert.equal(request.app.ua.osVersion, '10.11')
assert.equal(request.app.ua.deviceType, null)
assert.equal(request.app.ua.formFactor, null)
})
it('parsed location correctly', () => {
const geo = request.app.geo
assert.ok(geo)
assert.equal(geo.location.city, 'Mountain View')
assert.equal(geo.location.country, 'United States')
assert.equal(geo.location.countryCode, 'US')
assert.equal(geo.location.state, 'California')
assert.equal(geo.location.stateCode, 'CA')
assert.equal(geo.timeZone, 'America/Los_Angeles')
})
it('fetched devices correctly', () => {
assert.ok(request.app.devices)
assert.equal(typeof request.app.devices.then, 'function')
assert.equal(db.devices.callCount, 1)
assert.equal(db.devices.args[0].length, 1)
assert.equal(db.devices.args[0][0], 'fake uid')
return request.app.devices.then(devices => {
assert.deepEqual(devices, [ { id: 'fake device id' } ])
})
})
describe('successful request, unauthenticated, uid in payload:', () => {
let secondRequest
beforeEach(() => {
response = 'ok'
locale = 'fr'
return instance.inject({
headers: {
'accept-language': 'fr-CH, fr;q=0.9, en-GB, en;q=0.5',
'user-agent': 'Firefox-Android-FxAccounts/34.0a1 (Nightly)',
'x-forwarded-for': ' 194.12.187.0 , 194.12.187.1 '
},
method: 'POST',
url: '/account/create',
payload: {
features: [ 'signinCodes' ],
uid: 'another fake uid'
},
remoteAddress: '63.245.221.32'
}).then(response => secondRequest = response.request)
})
it('second request has its own remote address chain', () => {
assert.notEqual(request, secondRequest)
assert.notEqual(request.app.remoteAddressChain, secondRequest.app.remoteAddressChain)
assert.equal(secondRequest.app.remoteAddressChain.length, 3)
assert.equal(secondRequest.app.remoteAddressChain[0], '194.12.187.0')
assert.equal(secondRequest.app.remoteAddressChain[1], '194.12.187.1')
assert.equal(secondRequest.app.remoteAddressChain[2], '63.245.221.32')
})
it('second request has its own client address', () => {
assert.equal(secondRequest.app.clientAddress, '63.245.221.32')
})
it('second request has its own accept-language', () => {
assert.equal(secondRequest.app.acceptLanguage, 'fr-CH, fr;q=0.9, en-GB, en;q=0.5')
})
it('second request has its own locale', () => {
assert.equal(translator.getLocale.callCount, 0)
assert.equal(secondRequest.app.locale, 'fr')
assert.equal(translator.getLocale.callCount, 1)
})
it('second request has its own user agent info', () => {
assert.notEqual(request.app.ua, secondRequest.app.ua)
assert.equal(secondRequest.app.ua.browser, 'Nightly')
assert.equal(secondRequest.app.ua.browserVersion, '34.0a1')
assert.equal(secondRequest.app.ua.os, 'Android')
assert.equal(secondRequest.app.ua.osVersion, null)
assert.equal(secondRequest.app.ua.deviceType, 'mobile')
assert.equal(secondRequest.app.ua.formFactor, null)
})
it('second request has its own location info', () => {
const geo = secondRequest.app.geo
assert.notEqual(request.app.geo, secondRequest.app.geo)
assert.equal(geo.location.city, 'Mountain View')
assert.equal(geo.location.country, 'United States')
assert.equal(geo.location.countryCode, 'US')
assert.equal(geo.location.state, 'California')
assert.equal(geo.location.stateCode, 'CA')
assert.equal(geo.timeZone, 'America/Los_Angeles')
})
it('second request fetched devices correctly', () => {
assert.notEqual(request.app.devices, secondRequest.app.devices)
assert.equal(db.devices.callCount, 2)
assert.equal(db.devices.args[1].length, 1)
assert.equal(db.devices.args[1][0], 'another fake uid')
return request.app.devices.then(devices => {
assert.deepEqual(devices, [ { id: 'fake device id' } ])
})
})
})
})
describe('successful request, unacceptable locale, no features enabled:', () => {
let request
beforeEach(() => {
response = 'ok'
return instance.inject({
headers: {
'accept-language': 'fr-CH, fr;q=0.9',
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1'
},
method: 'POST',
url: '/account/create',
payload: {},
remoteAddress: 'this is not an ip address'
}).then(response => request = response.request)
})
it('called log.begin correctly', () => {
assert.equal(log.begin.callCount, 1)
const args = log.begin.args[0]
assert.equal(args[1].app.locale, 'en')
assert.equal(args[1].app.ua.browser, 'Chrome Mobile iOS')
assert.equal(args[1].app.ua.browserVersion, '56.0.2924')
assert.equal(args[1].app.ua.os, 'iOS')
assert.equal(args[1].app.ua.osVersion, '10.3')
assert.equal(args[1].app.ua.deviceType, 'mobile')
assert.equal(args[1].app.ua.formFactor, 'iPhone')
})
it('called log.summary once', () => {
assert.equal(log.summary.callCount, 1)
})
it('did not call log.error', () => {
assert.equal(log.error.callCount, 0)
})
it('parsed features correctly', () => {
assert.ok(request.app.features)
assert.equal(request.app.features.has('signinCodes'), false)
})
it('ignored invalid remoteAddress', () => {
assert.equal(request.app.clientAddress, undefined)
})
})
describe('unsuccessful request:', () => {
beforeEach(() => {
response = error.requestBlocked()
return instance.inject({
method: 'POST',
url: '/account/create',
payload: {}
}).catch(() => {})
})
it('called log.begin', () => {
assert.equal(log.begin.callCount, 1)
})
it('called log.summary correctly', () => {
assert.equal(log.summary.callCount, 1)
const args = log.summary.args[0]
assert.equal(args.length, 2)
assert.equal(args[0], log.begin.args[0][1])
assert.ok(args[1])
assert.equal(args[1].statusCode, undefined)
assert.equal(args[1].source, undefined)
assert.equal(args[1].isBoom, true)
assert.equal(args[1].message, 'The request was blocked for security reasons')
assert.equal(args[1].errno, 125)
})
it('did not call log.error', () => {
assert.equal(log.error.callCount, 0)
})
})
describe('unsuccessful request, db error:', () => {
beforeEach(() => {
response = new EndpointError('request failed', { reason: 'because i said so' })
return instance.inject({
method: 'POST',
url: '/account/create',
payload: {}
}).catch(() => {})
})
it('called log.begin', () => {
assert.equal(log.begin.callCount, 1)
})
it('called log.summary', () => {
assert.equal(log.summary.callCount, 1)
})
it('called log.error correctly', () => {
assert.equal(log.error.callCount, 1)
const args = log.error.args[0]
assert.equal(args.length, 1)
assert.deepEqual(args[0], {
op: 'server.EndpointError',
message: 'request failed',
reason: 'because i said so'
})
})
})
describe('authenticated request, session token not expired:', () => {
beforeEach(() => {
response = 'ok'
const auth = hawk.client.header(`${config.publicUrl}account/status`, 'GET', {
credentials: {
id: 'deadbeef',
key: 'baadf00d',
algorithm: 'sha256'
}
})
return instance.inject({
headers: {
authorization: auth.header
},
method: 'GET',
url: '/account/status'
})
})
it('called db.sessionToken correctly', () => {
assert.equal(db.sessionToken.callCount, 1)
const args = db.sessionToken.args[0]
assert.equal(args.length, 1)
assert.equal(args[0], 'deadbeef')
})
it('did not call db.pruneSessionTokens', () => {
assert.equal(db.pruneSessionTokens.callCount, 0)
})
})
})
})
describe('authenticated request, session token expired:', () => {
let db, instance
beforeEach(() => {
response = 'ok'
db = mocks.mockDB({
sessionTokenId: 'wibble',
uid: 'blee',
expired: true
})
return server.create(log, error, config, routes, db, translator, Token).then((s) => {
instance = s
return instance.start()
.then(() => {
const auth = hawk.client.header(`${config.publicUrl}account/status`, 'GET', {
credentials: {
id: 'deadbeef',
key: 'baadf00d',
algorithm: 'sha256'
}
})
return instance.inject({
headers: {
authorization: auth.header
},
method: 'GET',
url: '/account/status'
})
})
})
})
afterEach(() => instance.stop())
it('called db.sessionToken', () => {
assert.equal(db.sessionToken.callCount, 1)
})
it('called db.pruneSessionTokens correctly', () => {
assert.equal(db.pruneSessionTokens.callCount, 1)
const args = db.pruneSessionTokens.args[0]
assert.equal(args.length, 2)
assert.equal(args[0], 'blee')
assert.ok(Array.isArray(args[1]))
assert.equal(args[1].length, 1)
assert.equal(args[1][0].id, 'wibble')
})
})
function getRoutes () {
return [
{
path: '/account/create',
method: 'POST',
handler (request) {
return response
}
},
{
path: '/account/status',
method: 'GET',
config: {
auth: {
mode: 'required',
strategy: 'sessionToken'
}
},
handler (request) {
return response
}
}
]
}
})
})
function getConfig () {
return {
publicUrl: 'http://example.org/',
corsOrigin: [ '*' ],
maxEventLoopDelay: 0,
listen: {
host: '127.0.0.1',
port: 9000
},
useHttps: false,
hpkpConfig: {
enabled: false
},
oauth: {
clientIds: {},
url: 'http://localhost:9010',
keepAlive: false
},
env: 'prod',
memcached: {
lifetime: 0,
address: 'none'
},
metrics: {
flow_id_expiry: 7200000,
flow_id_key: 'wibble'
}
}
}