1193 строки
32 KiB
JavaScript
1193 строки
32 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';
|
|
|
|
/*global describe,it,before,after,afterEach,beforeEach*/
|
|
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const checksum = require('checksum');
|
|
|
|
const assert = require('insist');
|
|
const P = require('../lib/promise');
|
|
const config = require('../lib/config');
|
|
const avatarShared = require('../lib/routes/avatar/_shared');
|
|
const assertSecurityHeaders = require('./lib/util').assertSecurityHeaders;
|
|
|
|
const DEFAULT_AVATAR_ID = config.get('img.defaultAvatarId');
|
|
|
|
function randomHex(bytes) {
|
|
return crypto.randomBytes(bytes).toString('hex');
|
|
}
|
|
|
|
function uid() {
|
|
return randomHex(16);
|
|
}
|
|
|
|
function avatarId() {
|
|
return randomHex(16);
|
|
}
|
|
|
|
function token() {
|
|
return randomHex(32);
|
|
}
|
|
|
|
const USERID = uid();
|
|
const mock = require('./lib/mock')({ userid: USERID });
|
|
|
|
const db = require('../lib/db');
|
|
const Server = require('./lib/server');
|
|
const Static = require('./lib/static');
|
|
|
|
const SIZES = require('../lib/img').SIZES;
|
|
|
|
var imagePath = path.join(__dirname, 'lib', 'firefox.png');
|
|
var imageData = fs.readFileSync(imagePath);
|
|
|
|
const SIZE_SUFFIXES = Object.keys(SIZES).map(function(val) {
|
|
if (val === 'default') {
|
|
return '';
|
|
}
|
|
return '_' + val;
|
|
});
|
|
|
|
const GRAVATAR =
|
|
'https://secure.gravatar.com/avatar/00000000000000000000000000000000';
|
|
|
|
afterEach(function() {
|
|
mock.done();
|
|
});
|
|
|
|
describe('/profile', function() {
|
|
var tok = token();
|
|
var user = uid();
|
|
|
|
it('should return all of a profile', function() {
|
|
mock.tokenGood();
|
|
mock.email('user@example.domain');
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(Object.keys(res.result).length, 4);
|
|
assert.equal(res.result.uid, USERID);
|
|
assert.equal(res.result.email, 'user@example.domain');
|
|
assert.equal(res.result.avatar, avatarShared.fxaUrl(DEFAULT_AVATAR_ID), 'return default avatar');
|
|
assert.equal(res.result.avatarDefault, true, 'has the default avatar flag');
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should handle auth server errors', () => {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:write']
|
|
});
|
|
mock.emailFailure();
|
|
|
|
mock.log('server', rec => {
|
|
return rec.levelname === 'ERROR'
|
|
&& rec.args[0] === 'summary'
|
|
&& rec.args[1].path === '/v1/_core_profile';
|
|
});
|
|
|
|
mock.log('batch', rec => {
|
|
return rec.levelname === 'ERROR'
|
|
&& rec.args[0] === '/v1/_core_profile:503';
|
|
});
|
|
|
|
mock.log('server', rec => {
|
|
return rec.levelname === 'ERROR'
|
|
&& rec.args[0] === 'summary'
|
|
&& rec.args[1].path === '/v1/profile';
|
|
});
|
|
|
|
mock.log('routes._core_profile', rec => {
|
|
return rec.levelname === 'ERROR'
|
|
&& rec.args[0] === 'request.auth_server.fail';
|
|
});
|
|
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(res => {
|
|
assert.equal(res.statusCode, 503);
|
|
assert.equal(res.result.errno, 105);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should handle accounts deleted on auth server', () => {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:write']
|
|
});
|
|
mock.emailFailure({ code: 400, errno: 102 });
|
|
|
|
mock.log('batch', rec => {
|
|
return rec.levelname === 'ERROR'
|
|
&& rec.args[0] === '/v1/_core_profile:401';
|
|
});
|
|
|
|
mock.log('routes._core_profile', rec => {
|
|
return rec.levelname === 'INFO'
|
|
&& rec.args[0] === 'request.auth_server.fail'
|
|
&& rec.args[1].errno === 102;
|
|
});
|
|
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(res => {
|
|
assert.equal(res.statusCode, 401);
|
|
assert.equal(res.result.errno, 100);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should handle unexpected 401 errors from auth server', () => {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:write']
|
|
});
|
|
mock.emailFailure({ code: 401 });
|
|
|
|
mock.log('batch', rec => {
|
|
return rec.levelname === 'ERROR'
|
|
&& rec.args[0] === '/v1/_core_profile:401';
|
|
});
|
|
|
|
mock.log('routes._core_profile', rec => {
|
|
return rec.levelname === 'INFO'
|
|
&& rec.args[0] === 'request.auth_server.fail'
|
|
&& rec.args[1].code === 401;
|
|
});
|
|
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(res => {
|
|
assert.equal(res.statusCode, 401);
|
|
assert.equal(res.result.errno, 100);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should error out on unexpected 400s from auth server', () => {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:write']
|
|
});
|
|
mock.emailFailure({ code: 400, errno: 107 });
|
|
|
|
mock.log('batch', rec => {
|
|
return rec.levelname === 'ERROR'
|
|
&& rec.args[0] === '/v1/_core_profile:500';
|
|
});
|
|
|
|
mock.log('routes._core_profile', rec => {
|
|
return rec.levelname === 'ERROR'
|
|
&& rec.args[0] === 'request.auth_server.fail'
|
|
&& rec.args[1].code === 400;
|
|
});
|
|
|
|
mock.log('server', function(rec) {
|
|
return rec.levelname === 'ERROR'
|
|
&& rec.args[0] === 'summary'
|
|
&& rec.args[1].path === '/v1/_core_profile';
|
|
});
|
|
|
|
mock.log('server', function(rec) {
|
|
return rec.levelname === 'ERROR'
|
|
&& rec.args[0] === 'summary'
|
|
&& rec.args[1].path === '/v1/profile';
|
|
});
|
|
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(res => {
|
|
assert.equal(res.statusCode, 500);
|
|
assert.equal(res.result.errno, 999);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should handle oauth server failure', function() {
|
|
mock.tokenFailure();
|
|
|
|
mock.log('server', function(rec) {
|
|
return rec.levelname === 'ERROR'
|
|
&& rec.args[0] === 'summary'
|
|
&& rec.args[1].path === '/v1/profile';
|
|
});
|
|
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 503);
|
|
assert.equal(res.result.errno, 104);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should return an avatar if selected', function() {
|
|
mock.token({
|
|
user: user,
|
|
scope: ['profile']
|
|
});
|
|
mock.email('user@example.domain');
|
|
var aid = avatarId();
|
|
var PROVIDER = 'gravatar';
|
|
return db.addAvatar(aid, user, GRAVATAR, PROVIDER)
|
|
.then(function() {
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(res.result.avatar, GRAVATAR);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should return a display name if set', function() {
|
|
mock.token({
|
|
user: user,
|
|
scope: ['profile']
|
|
});
|
|
mock.email('user@example.domain');
|
|
return db.setDisplayName(user, 'Spock')
|
|
.then(function() {
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(res.result.displayName, 'Spock');
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should return filtered profile if smaller scope', function() {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:email']
|
|
});
|
|
mock.email('user@example.domain');
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(res.result.email, 'user@example.domain');
|
|
assert.equal(Object.keys(res.result).length, 1);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should require a profile:* scope', function() {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['some:other:scope']
|
|
});
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 403);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should include an etag in the http response', function() {
|
|
mock.token({
|
|
user: user,
|
|
scope: ['profile']
|
|
});
|
|
mock.email('user@example.domain');
|
|
return db.setDisplayName(user, 'Spock')
|
|
.then(function() {
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
var etag = res.headers.etag.substr(1, 40);
|
|
var expectedEtag = checksum(JSON.stringify(res.result));
|
|
assert.equal(etag, expectedEtag);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should support openid-connect "email" scope', function() {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['openid', 'email']
|
|
});
|
|
mock.email('user@example.domain');
|
|
return Server.api.get({
|
|
url: '/profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(res.result.email, 'user@example.domain');
|
|
assert.equal(res.result.sub, USERID);
|
|
assert.equal(Object.keys(res.result).length, 2);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
describe('/email', function() {
|
|
var tok = token();
|
|
|
|
it('should return an email', function() {
|
|
mock.tokenGood();
|
|
mock.email('user@example.domain');
|
|
return Server.api.get({
|
|
url: '/email',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(JSON.parse(res.payload).email, 'user@example.domain');
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should NOT return email if wrong scope', function() {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:uid']
|
|
});
|
|
return Server.api.get({
|
|
url: '/email',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 403);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('/_core_profile', () => {
|
|
const tok = token();
|
|
|
|
it('should be hidden from external callers by default', () => {
|
|
return Server.api.get({
|
|
url: '/_core_profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(res => {
|
|
assert.equal(res.statusCode, 404);
|
|
});
|
|
});
|
|
|
|
it('should return all fields returned by auth-server', () => {
|
|
mock.tokenGood();
|
|
mock.coreProfile({
|
|
email: 'user@example.domain',
|
|
locale: 'en-US',
|
|
authenticationMethods: ['pwd'],
|
|
authenticatorAssuranceLevel: 1
|
|
});
|
|
return Server.api.get({
|
|
allowInternals: true,
|
|
url: '/_core_profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(res => {
|
|
assert.equal(res.statusCode, 200);
|
|
const body = JSON.parse(res.payload);
|
|
assert.equal(Object.keys(body).length, 4);
|
|
assert.equal(body.email, 'user@example.domain');
|
|
assert.equal(body.locale, 'en-US');
|
|
assert.deepEqual(body.amrValues, ['pwd']);
|
|
assert.equal(body.twoFactorAuthentication, false);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should omit `email` if not returned by auth-server', () => {
|
|
mock.tokenGood();
|
|
mock.coreProfile({
|
|
locale: 'en-US',
|
|
authenticationMethods: ['pwd'],
|
|
authenticatorAssuranceLevel: 1
|
|
});
|
|
return Server.api.get({
|
|
allowInternals: true,
|
|
url: '/_core_profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(res => {
|
|
assert.equal(res.statusCode, 200);
|
|
const body = JSON.parse(res.payload);
|
|
assert.equal(Object.keys(body).length, 3);
|
|
assert.equal(body.locale, 'en-US');
|
|
assert.deepEqual(body.amrValues, ['pwd']);
|
|
assert.equal(body.twoFactorAuthentication, false);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should omit `locale` if not returned by auth-server', () => {
|
|
mock.tokenGood();
|
|
mock.coreProfile({
|
|
email: 'user@example.domain',
|
|
authenticationMethods: ['pwd'],
|
|
authenticatorAssuranceLevel: 1
|
|
});
|
|
return Server.api.get({
|
|
allowInternals: true,
|
|
url: '/_core_profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(res => {
|
|
assert.equal(res.statusCode, 200);
|
|
const body = JSON.parse(res.payload);
|
|
assert.equal(Object.keys(body).length, 3);
|
|
assert.equal(body.email, 'user@example.domain');
|
|
assert.deepEqual(body.amrValues, ['pwd']);
|
|
assert.equal(body.twoFactorAuthentication, false);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should omit auth info if not returned by auth-server', () => {
|
|
mock.tokenGood();
|
|
mock.coreProfile({
|
|
email: 'user@example.domain',
|
|
locale: 'en-AU'
|
|
});
|
|
return Server.api.get({
|
|
allowInternals: true,
|
|
url: '/_core_profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(res => {
|
|
assert.equal(res.statusCode, 200);
|
|
const body = JSON.parse(res.payload);
|
|
assert.equal(Object.keys(body).length, 2);
|
|
assert.equal(body.email, 'user@example.domain');
|
|
assert.equal(body.locale, 'en-AU');
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should correctly reflect 2FA status of the account', () => {
|
|
mock.tokenGood();
|
|
mock.coreProfile({
|
|
email: 'user@example.domain',
|
|
locale: 'en-AU',
|
|
authenticationMethods: ['pwd', 'otp'],
|
|
authenticatorAssuranceLevel: 2
|
|
});
|
|
return Server.api.get({
|
|
allowInternals: true,
|
|
url: '/_core_profile',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(res => {
|
|
assert.equal(res.statusCode, 200);
|
|
const body = JSON.parse(res.payload);
|
|
assert.equal(Object.keys(body).length, 4);
|
|
assert.equal(body.email, 'user@example.domain');
|
|
assert.equal(body.locale, 'en-AU');
|
|
assert.deepEqual(body.amrValues, ['pwd', 'otp']);
|
|
assert.equal(body.twoFactorAuthentication, true);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('/uid', function() {
|
|
var tok = token();
|
|
|
|
it('should return an uid', function() {
|
|
mock.tokenGood();
|
|
return Server.api.get({
|
|
url: '/uid',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(JSON.parse(res.payload).uid, USERID);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should NOT return a uid if wrong scope', function() {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:email']
|
|
});
|
|
return Server.api.get({
|
|
url: '/uid',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 403);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('/avatar', function() {
|
|
var tok = token();
|
|
var PROVIDER = 'gravatar';
|
|
var user = uid();
|
|
var id1 = avatarId();
|
|
var id2 = avatarId();
|
|
|
|
describe('GET', function() {
|
|
before(function() {
|
|
var grav1 = GRAVATAR.slice(0, -1) + '1';
|
|
return db.addAvatar(id1, user, grav1, PROVIDER)
|
|
.then(function() {
|
|
// replace grav1 as selected
|
|
return db.addAvatar(id2, user, GRAVATAR, PROVIDER);
|
|
});
|
|
});
|
|
it('should return selected avatar', function() {
|
|
mock.token({
|
|
user: user,
|
|
scope: ['profile:avatar']
|
|
});
|
|
return Server.api.get({
|
|
url: '/avatar',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(Object.keys(res.result).length, 3);
|
|
assert.equal(res.result.avatar, GRAVATAR);
|
|
assert.equal(res.result.id, id2);
|
|
assert.equal(res.result.avatarDefault, false);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
it('should include an etag in the http response', function() {
|
|
mock.token({
|
|
user: user,
|
|
scope: ['profile:avatar']
|
|
});
|
|
return Server.api.get({
|
|
url: '/avatar',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(res.result.avatar, GRAVATAR);
|
|
assert.equal(res.result.id, id2);
|
|
|
|
var etag = res.headers.etag.substr(1, 32);
|
|
assert.equal(etag, id2);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should log an avatar.get activity event', function(done) {
|
|
mock.token({
|
|
user: user,
|
|
scope: ['profile:avatar']
|
|
});
|
|
|
|
mock.log('routes.avatar.get', function(rec) {
|
|
if (rec.levelname === 'INFO') {
|
|
assert.equal(rec.args[0], 'activityEvent');
|
|
assert.equal(rec.args[1].event, 'avatar.get');
|
|
assert.equal(rec.args[1].uid, user);
|
|
done();
|
|
return true;
|
|
}
|
|
});
|
|
|
|
Server.api.get({
|
|
url: '/avatar',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('upload', function() {
|
|
|
|
it('should upload a new avatar', function() {
|
|
this.slow(2000);
|
|
this.timeout(3000);
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:avatar:write']
|
|
});
|
|
mock.image(imageData.length);
|
|
return Server.api.post({
|
|
url: '/avatar/upload',
|
|
payload: imageData,
|
|
headers: { authorization: 'Bearer ' + tok,
|
|
'content-type': 'image/png',
|
|
'content-length': imageData.length
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 201);
|
|
assert(res.result.url);
|
|
assert(res.result.id);
|
|
assertSecurityHeaders(res);
|
|
return res.result.url;
|
|
}).then(function(s3url) {
|
|
return P.all(SIZE_SUFFIXES).map(function(suffix) {
|
|
return Static.get(s3url + suffix);
|
|
});
|
|
}).then(function(responses) {
|
|
assert.equal(responses.length, SIZE_SUFFIXES.length);
|
|
responses.forEach(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should fail with an error if image cannot be identified', function() {
|
|
this.slow(2000);
|
|
this.timeout(3000);
|
|
|
|
var dataLength = 2;
|
|
mock.token({
|
|
user: USERID,
|
|
email: 'user@example.domain',
|
|
scope: ['profile:avatar:write']
|
|
});
|
|
mock.log('img_workers', function(rec) {
|
|
if (rec.levelname === 'ERROR') {
|
|
return true;
|
|
}
|
|
});
|
|
mock.log('server', function(rec) {
|
|
if (rec.levelname === 'ERROR') {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
return Server.api.post({
|
|
url: '/avatar/upload',
|
|
payload: Buffer.from('{}'),
|
|
headers: { authorization: 'Bearer ' + tok,
|
|
'content-type': 'image/png',
|
|
'content-length': dataLength
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 500);
|
|
assert.equal(res.result.message, 'Image processing error');
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should gracefully handle and report upload failures', function() {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:avatar:write']
|
|
});
|
|
mock.workerFailure('post', imageData.length);
|
|
mock.log('img_workers', function(rec) {
|
|
if (rec.levelname === 'ERROR') {
|
|
assert.equal(
|
|
rec.message,
|
|
'upload.worker.error unexpected server error'
|
|
);
|
|
return true;
|
|
}
|
|
});
|
|
mock.log('server', function(rec) {
|
|
if (rec.levelname === 'ERROR') {
|
|
assert.equal(rec.args[0], 'summary');
|
|
return true;
|
|
}
|
|
});
|
|
return Server.api.post({
|
|
url: '/avatar/upload',
|
|
payload: imageData,
|
|
headers: {
|
|
authorization: 'Bearer ' + tok,
|
|
'content-type': 'image/png',
|
|
'content-length': imageData.length
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 500);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should require :write scope', function() {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile', 'profile:avatar']
|
|
});
|
|
return Server.api.post({
|
|
url: '/avatar/upload',
|
|
payload: imageData,
|
|
headers: {
|
|
authorization: 'Bearer ' + tok,
|
|
'content-type': 'image/png',
|
|
'content-length': imageData.length
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 403);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('DELETE', function() {
|
|
var user = uid();
|
|
|
|
|
|
it('should require :write scope', function() {
|
|
mock.token({
|
|
user: user,
|
|
scope: ['profile', 'profile:avatar']
|
|
});
|
|
var id = avatarId();
|
|
return Server.api.delete({
|
|
url: '/avatar/' + id,
|
|
headers: {
|
|
authorization: 'Bearer ' + tok,
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 403);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
describe('gravatar', function() {
|
|
var id = avatarId();
|
|
before(function() {
|
|
return db.addAvatar(id, user, GRAVATAR, PROVIDER);
|
|
});
|
|
|
|
it('should fail if not owned by user', function() {
|
|
mock.token({
|
|
user: uid(),
|
|
scope: ['profile:avatar:write']
|
|
});
|
|
return Server.api.delete({
|
|
url: '/avatar/' + id,
|
|
headers: {
|
|
authorization: 'Bearer ' + tok,
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 401);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should remove avatar from user', function() {
|
|
mock.token({
|
|
user: user,
|
|
scope: ['profile:avatar:write']
|
|
});
|
|
return Server.api.delete({
|
|
url: '/avatar/' + id,
|
|
headers: {
|
|
authorization: 'Bearer ' + tok,
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assertSecurityHeaders(res);
|
|
return db.getAvatar(id);
|
|
}).then(function(avatar) {
|
|
assert.equal(avatar, undefined);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('uploaded', function() {
|
|
var s3url;
|
|
var id;
|
|
beforeEach(function() {
|
|
mock.token({
|
|
user: user,
|
|
scope: ['profile:avatar:write']
|
|
});
|
|
mock.image(imageData.length);
|
|
return Server.api.post({
|
|
url: '/avatar/upload',
|
|
payload: imageData,
|
|
headers: { authorization: 'Bearer ' + tok,
|
|
'content-type': 'image/png',
|
|
'content-length': imageData.length
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 201);
|
|
assertSecurityHeaders(res);
|
|
s3url = res.result.url;
|
|
id = res.result.id;
|
|
});
|
|
});
|
|
|
|
it('should remove avatar from db and s3', function() {
|
|
mock.token({
|
|
user: user,
|
|
scope: ['profile:avatar:write']
|
|
});
|
|
mock.deleteImage();
|
|
return Server.api.delete({
|
|
url: '/avatar/' + id,
|
|
headers: {
|
|
authorization: 'Bearer ' + tok,
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assertSecurityHeaders(res);
|
|
return db.getAvatar(id);
|
|
}).then(function(avatar) {
|
|
assert.equal(avatar, undefined);
|
|
return P.all(SIZE_SUFFIXES).map(function(suffix) {
|
|
return Static.get(s3url + suffix);
|
|
}).map(function(res) {
|
|
assert.equal(res.statusCode, 404, res.raw.req.url);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should be able to delete without id parameter', function() {
|
|
mock.token({
|
|
user: user,
|
|
scope: ['profile:avatar:write']
|
|
});
|
|
mock.deleteImage();
|
|
return Server.api.delete({
|
|
url: '/avatar',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok,
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assertSecurityHeaders(res);
|
|
return db.getAvatar(id);
|
|
}).then(function(avatar) {
|
|
assert.equal(avatar, undefined);
|
|
return P.all(SIZE_SUFFIXES).map(function(suffix) {
|
|
return Static.get(s3url + suffix);
|
|
}).map(function(res) {
|
|
assert.equal(res.statusCode, 404, res.raw.req.url);
|
|
});
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
});
|
|
|
|
describe('/display_name', function() {
|
|
var tok = token();
|
|
|
|
describe('GET', function() {
|
|
it('should return a displayName', function() {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:display_name']
|
|
});
|
|
|
|
return db.setDisplayName(USERID, 'Spock')
|
|
.then(function() {
|
|
return Server.api.get({
|
|
url: '/display_name',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(JSON.parse(res.payload).displayName, 'Spock');
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should return 204 if not set', function() {
|
|
var userId = uid();
|
|
mock.token({
|
|
user: userId,
|
|
scope: ['profile:display_name']
|
|
});
|
|
|
|
return Server.api.get({
|
|
url: '/display_name',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 204);
|
|
assert(! res.payload);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should NOT return a display_name if wrong scope', function() {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:email']
|
|
});
|
|
return Server.api.get({
|
|
url: '/display_name',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 403);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('POST', function() {
|
|
it('should post a new display name', function() {
|
|
var NAME = 'Spock';
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:display_name:write']
|
|
});
|
|
return Server.api.post({
|
|
url: '/display_name',
|
|
payload: {
|
|
displayName: NAME
|
|
},
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assertSecurityHeaders(res);
|
|
return db.getDisplayName(USERID);
|
|
}).then(function(res) {
|
|
assert.equal(res.displayName, NAME);
|
|
});
|
|
});
|
|
|
|
it('should fail post if display name longer than 256 chars', function() {
|
|
var NAME = Array.from('x'.repeat('257')).join('');
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:display_name:write']
|
|
});
|
|
return Server.api.post({
|
|
url: '/display_name',
|
|
payload: {
|
|
displayName: NAME
|
|
},
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 400);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should require :write scope', function() {
|
|
var NAME = 'Spock';
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile']
|
|
});
|
|
return Server.api.post({
|
|
url: '/display_name',
|
|
payload: {
|
|
displayName: NAME
|
|
},
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 403);
|
|
assertSecurityHeaders(res);
|
|
return db.getDisplayName(USERID);
|
|
}).then(function(res) {
|
|
assert.equal(res.displayName, NAME);
|
|
});
|
|
});
|
|
|
|
it('should unset the display name if given an empty string', function() {
|
|
var NAME = 'Spock';
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:display_name:write']
|
|
});
|
|
return Server.api.post({
|
|
url: '/display_name',
|
|
payload: {
|
|
displayName: NAME
|
|
},
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assertSecurityHeaders(res);
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:display_name:write']
|
|
});
|
|
return Server.api.post({
|
|
url: '/display_name',
|
|
payload: {
|
|
displayName: ''
|
|
},
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
});
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assertSecurityHeaders(res);
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:display_name']
|
|
});
|
|
return Server.api.get({
|
|
url: '/display_name',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
});
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 204);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
|
|
it('should accept a variety of unicode characters', function() {
|
|
var NAMES = [
|
|
'André Citroën',
|
|
'the unblinking ಠ_ಠ of ckarlof',
|
|
'abominable ☃',
|
|
// emoji
|
|
'👍',
|
|
'👍🏼',
|
|
'蚋',
|
|
'鱑',
|
|
'☃ 👍 André Citroën ಠ_ಠ',
|
|
'astral symbol 𝌆 🙀'
|
|
];
|
|
return P.resolve(NAMES).each(function(NAME) {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:display_name:write']
|
|
});
|
|
return Server.api.post({
|
|
url: '/display_name',
|
|
payload: {
|
|
displayName: NAME
|
|
},
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assertSecurityHeaders(res);
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:display_name']
|
|
});
|
|
return Server.api.get({
|
|
url: '/display_name',
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
});
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(JSON.parse(res.payload).displayName, NAME);
|
|
assert.equal(res.result.displayName, NAME);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should reject unicode control characters', function() {
|
|
var NAMES = [
|
|
'null\0terminator',
|
|
'ring\u0007my\u0007bell',
|
|
'new\nline',
|
|
'line\rfeed',
|
|
'C1 next \u0085 line',
|
|
'paragraph \u2028 separator',
|
|
'private \uE005 use \uF8FF block',
|
|
'specials \uFFFB annotation terminator'
|
|
];
|
|
return P.resolve(NAMES).each(function(NAME) {
|
|
mock.token({
|
|
user: USERID,
|
|
scope: ['profile:display_name:write']
|
|
});
|
|
return Server.api.post({
|
|
url: '/display_name',
|
|
payload: {
|
|
displayName: NAME
|
|
},
|
|
headers: {
|
|
authorization: 'Bearer ' + tok
|
|
}
|
|
}).then(function(res) {
|
|
assert.equal(res.statusCode, 400);
|
|
assert.equal(res.result.errno, 101);
|
|
assertSecurityHeaders(res);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
after(() => {
|
|
return db._teardown();
|
|
});
|