fxa-profile-server/test/profileCache.js

370 строки
11 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/. */
/*global describe,it, beforeEach, afterEach, after*/
'use strict';
process.env.CACHE_EXPIRES_IN = 2000;
process.env.USE_REDIS = false;
let Server;
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const assert = require('insist');
const P = require('../lib/promise');
function randomHex(bytes) {
return crypto.randomBytes(bytes).toString('hex');
}
function uid() {
return randomHex(16);
}
function token() {
return randomHex(32);
}
function clearRequireCache() {
// Delete require cache so that correct configuration values get injected when
// recreating server
Object.keys(require.cache).forEach(function (key) {
delete require.cache[key];
});
}
const mock = require('./lib/mock')({userid: uid()});
const imagePath = path.join(__dirname, 'lib', 'firefox.png');
const imageData = fs.readFileSync(imagePath);
const tok = token();
const NAME = 'Fennec';
const MOZILLA_EMAIL = 'user@mozilla.com';
const PROFILE_CHANGED_AT = Date.now();
const PROFILE_CHANGED_AT_LATER_TIME = PROFILE_CHANGED_AT + 1000;
function mockTokens(uid, scope, profileChangedAt) {
mock.token({
user: uid,
scope: scope || ['profile'],
profileChangedAt
});
}
function makeProfileReq(uid, scope, profileChangedAt) {
mockTokens(uid, scope, profileChangedAt);
return Server.api.get({
url: '/profile',
headers: {
authorization: 'Bearer ' + tok
}
});
}
describe('profile cache', function() {
beforeEach(() => {
clearRequireCache();
Server = require('./lib/server');
});
afterEach(() => {
mock.done();
require('../lib/db')._teardown();
});
it('should cache profile info initially, and invalidate cache after 2 seconds', function(done) {
const userid = uid();
this.timeout(5000);
Server.server.initialize(() => {
let lastModified;
// first req, store last modified header
mock.email(MOZILLA_EMAIL);
makeProfileReq(userid)
.then(res => {
assert.ok(res.headers['last-modified']);
lastModified = res.headers['last-modified'];
return P.delay(1000);
})
.then(() => {
// second request verify cached result was returned
return makeProfileReq(userid);
})
.then(res => {
assert.ok(res.headers['last-modified']);
assert.equal(res.headers['last-modified'], lastModified);
return P.delay(1000);
})
.then(() => {
// verify cache was invalidated due to expiration
mock.email(MOZILLA_EMAIL);
return makeProfileReq(userid);
})
.then(res => {
assert.ok(res.headers['last-modified']);
assert.notEqual(res.headers['last-modified'], lastModified);
done();
});
});
});
it('should invalidate cache when display name is updated', function(done) {
this.timeout(5000);
const userid = uid();
Server.server.initialize(() => {
let lastModified;
// first req, store last modified header
mock.email(MOZILLA_EMAIL);
makeProfileReq(userid)
.then(res => {
assert.ok(res.headers['last-modified']);
lastModified = res.headers['last-modified'];
return P.delay(1000);
})
.then(() => {
// second request verify cached result was returned
return makeProfileReq(userid);
})
.then(res => {
assert.ok(res.headers['last-modified']);
assert.equal(res.headers['last-modified'], lastModified);
mock.token({
user: userid,
scope: ['profile:display_name:write']
});
// change display name (should invaldate cache)
return Server.api.post({
url: '/display_name',
payload: {
displayName: NAME
},
headers: {
authorization: 'Bearer ' + tok
}
});
})
.then((res) => {
assert.equal(res.statusCode, 200);
// third req, verify cache invalidated
mock.email(MOZILLA_EMAIL);
return makeProfileReq(userid);
})
.then((res) => {
assert.ok(res.headers['last-modified']);
assert.notEqual(res.headers['last-modified'], lastModified);
done();
});
});
});
it('should invalidate cache when avatar is updated', function(done) {
const userid = uid();
this.timeout(5000);
Server.server.initialize(() => {
let lastModified;
// first req, store last modified header
mock.email(MOZILLA_EMAIL);
makeProfileReq(userid)
.then(res => {
assert.ok(res.headers['last-modified']);
lastModified = res.headers['last-modified'];
return P.delay(1000);
})
.then(() => {
// second request verify cached result was returned
return makeProfileReq(userid);
})
.then(res => {
assert.ok(res.headers['last-modified']);
assert.equal(res.headers['last-modified'], lastModified);
mock.token({
user: userid,
scope: ['profile:avatar:write']
});
// upload avatar (should invaldate cache)
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((res) => {
assert.equal(res.statusCode, 201);
// third req verify cache invalidated
mock.email(MOZILLA_EMAIL);
return makeProfileReq(userid);
})
.then((res) => {
assert.ok(res.headers['last-modified']);
assert.notEqual(res.headers['last-modified'], lastModified);
done();
});
});
});
it('should invalidate cache when auth-server profileChangedAt is greater than cached version', function (done) {
const userid = uid();
this.timeout(5000);
Server.server.initialize(() => {
let lastModified;
// first req, store last modified header
mock.profileChangedAt(MOZILLA_EMAIL, PROFILE_CHANGED_AT);
makeProfileReq(userid)
.then(res => {
assert.ok(res.headers['last-modified']);
lastModified = res.headers['last-modified'];
return P.delay(500);
})
.then(() => {
// second request verify cached result was returned
return makeProfileReq(userid);
})
.then(res => {
assert.ok(res.headers['last-modified']);
assert.equal(res.headers['last-modified'], lastModified);
return P.delay(500);
})
.then(() => {
// verify cache was invalidated due to profileChangedAt update
mock.profileChangedAt(MOZILLA_EMAIL, PROFILE_CHANGED_AT);
return makeProfileReq(userid, undefined, PROFILE_CHANGED_AT_LATER_TIME);
})
.then(res => {
assert.ok(res.headers['last-modified']);
assert.equal(res.headers['last-modified'] > lastModified, true, 'last-modified updated');
done();
});
});
});
it('should not cache reads with unusual sets of scopes', function(done) {
this.timeout(5000);
const userid = uid();
const PARTIAL_SCOPES = ['profile:display_name', 'profile:uid'];
Server.server.initialize(() => {
let lastModified;
return makeProfileReq(userid, PARTIAL_SCOPES)
.then(res => {
assert.ok(res.headers['last-modified']);
lastModified = res.headers['last-modified'];
return P.delay(1000);
})
.then(() => {
return makeProfileReq(userid, PARTIAL_SCOPES);
})
.then(res => {
assert.ok(res.headers['last-modified']);
assert.ok(lastModified < res.headers['last-modified']);
done();
});
});
});
it('should separately cache full and partial profile reads', function(done) {
this.timeout(5000);
const userid = uid();
const PARTIAL_SCOPES = ['profile:email', 'profile:display_name', 'profile:uid'];
Server.server.initialize(() => {
let avatarUrl, lastModifiedPartial, lastModifiedFull;
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((res) => {
const body = JSON.parse(res.payload);
avatarUrl = body.url;
mock.email(MOZILLA_EMAIL);
return makeProfileReq(userid, PARTIAL_SCOPES);
})
.then(res => {
const body = JSON.parse(res.payload);
assert.equal(body.email, MOZILLA_EMAIL);
assert.equal(body.avatar, undefined);
assert.ok(res.headers['last-modified']);
lastModifiedPartial = res.headers['last-modified'];
return P.delay(1000);
})
.then(() => {
mock.email(MOZILLA_EMAIL);
return makeProfileReq(userid);
})
.then(res => {
const body = JSON.parse(res.payload);
assert.equal(body.email, MOZILLA_EMAIL);
assert.equal(body.avatar, avatarUrl);
assert.ok(res.headers['last-modified']);
lastModifiedFull = res.headers['last-modified'];
assert.notEqual(lastModifiedFull, lastModifiedPartial);
})
.then(() => {
return makeProfileReq(userid, PARTIAL_SCOPES);
})
.then(res => {
const body = JSON.parse(res.payload);
assert.equal(body.email, MOZILLA_EMAIL);
assert.equal(body.avatar, undefined);
assert.equal(lastModifiedPartial, res.headers['last-modified']);
return P.delay(1000);
})
.then(() => {
return makeProfileReq(userid);
})
.then(res => {
const body = JSON.parse(res.payload);
assert.equal(body.email, MOZILLA_EMAIL);
assert.equal(body.avatar, avatarUrl);
assert.ok(res.headers['last-modified']);
assert.equal(lastModifiedFull, res.headers['last-modified']);
done();
});
});
});
it('should not leak unauthorized data from cached profile', function(done) {
this.timeout(5000);
const userid = uid();
Server.server.initialize(() => {
mock.coreProfile({
email: MOZILLA_EMAIL,
authenticationMethods: ['pwd', 'otp'],
authenticatorAssuranceLevel: 2
});
return makeProfileReq(userid)
.then(res => {
const body = JSON.parse(res.payload);
assert.equal(body.email, MOZILLA_EMAIL);
assert.deepEqual(body.amrValues, ['pwd', 'otp']);
return P.delay(1000);
})
.then(() => {
mock.email(MOZILLA_EMAIL);
return makeProfileReq(userid, ['profile:email']);
})
.then(res => {
const body = JSON.parse(res.payload);
assert.equal(body.email, MOZILLA_EMAIL);
assert.equal(body.amrValues, undefined);
done();
});
});
});
});