Skip to content

Commit

Permalink
Implement session state switch infrastructure required for DevExpress…
Browse files Browse the repository at this point in the history
  • Loading branch information
inikulin authored and miherlosev committed Mar 3, 2017
1 parent 878e07c commit dd8b717
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 5 deletions.
2 changes: 1 addition & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "testcafe-hammerhead",
"description": "A powerful web-proxy used as a core for the TestCafe testing framework (https://github.com/DevExpress/testcafe).",
"version": "10.5.0",
"version": "10.6.0",
"homepage": "https://github.com/DevExpress/testcafe-hammerhead",
"bugs": {
"url": "https://github.com/DevExpress/testcafe-hammerhead/issues"
Expand Down
4 changes: 4 additions & 0 deletions src/request-pipeline/index.js
Expand Up @@ -12,7 +12,11 @@ import { inject as injectUpload } from '../upload';
// Stages
var stages = {
0: async function fetchProxyRequestBody (ctx, next) {
if (ctx.isPage && !ctx.isIframe)
ctx.session.onPageRequest();

ctx.reqBody = await fetchBody(ctx.req);

next();
},

Expand Down
10 changes: 10 additions & 0 deletions src/session/cookies.js
Expand Up @@ -21,6 +21,16 @@ export default class Cookies {
});
}

serializeJar () {
return JSON.stringify(this.cookieJar.serializeSync());
}

setJar (serializedJar) {
this.cookieJar = serializedJar ?
CookieJar.deserializeSync(JSON.parse(serializedJar)) :
new CookieJar();
}

setByServer (url, cookies) {
this._set(url, cookies, false);
}
Expand Down
27 changes: 26 additions & 1 deletion src/session/index.js
Expand Up @@ -23,7 +23,11 @@ export default class Session extends EventEmitter {
this.proxy = null;
this.externalProxySettings = null;
this.pageLoadCount = 0;
this.injectable = {

this.requireStateSwitch = false;
this.pendingStateSnapshot = null;

this.injectable = {
scripts: ['/hammerhead.js'],
styles: []
};
Expand All @@ -34,6 +38,19 @@ export default class Session extends EventEmitter {
return shortId.generate();
}

// State
getStateSnapshot () {
return this.cookies.serializeJar();
}

useStateSnapshot (snapshot) {
// NOTE: we don't perform state switch immediately, since there might be
// pending requests from current page. Therefore, we perform switch in
// onPageRequest handler when new page is requested.
this.requireStateSwitch = true;
this.pendingStateSnapshot = snapshot;
}

async handleServiceMessage (msg, serverInfo) {
if (this[msg.cmd])
return await this[msg.cmd](msg, serverInfo);
Expand Down Expand Up @@ -118,6 +135,14 @@ export default class Session extends EventEmitter {
};
}

onPageRequest () {
if (this.requireStateSwitch) {
this.cookies.setJar(this.pendingStateSnapshot);
this.requireStateSwitch = false;
this.pendingStateSnapshot = null;
}
}

_getIframePayloadScript (/* iframeWithoutSrc */) {
throw new Error('Not implemented');
}
Expand Down
136 changes: 133 additions & 3 deletions test/server/proxy-test.js
Expand Up @@ -94,8 +94,22 @@ describe('Proxy', function () {
res.end();
});

app.get('/cookie/set1', function (req, res) {
res.set('set-cookie', 'Set1_1=value1');
res.set('set-cookie', 'Set1_2=value2');

res.end();
});

app.get('/cookie/set2', function (req, res) {
res.set('set-cookie', 'Set2_1=value1');
res.set('set-cookie', 'Set2_2=value2');

res.end();
});

app.get('/cookie/echo', function (req, res) {
res.end(req.headers['cookie']);
res.end('%% ' + req.headers['cookie'] + ' %%');
});

app.get('/page', function (req, res) {
Expand Down Expand Up @@ -466,7 +480,7 @@ describe('Proxy', function () {
};

request(options, function (err, res, body) {
expect(body).eql('Test=value; value without key');
expect(body).eql('%% Test=value; value without key %%');
done();
});
});
Expand Down Expand Up @@ -946,7 +960,7 @@ describe('Proxy', function () {
});
});

describe('file protocol', function () {
describe('file:// protocol', function () {
var getFileProtocolUrl = function (filePath) {
return path.resolve(__dirname, filePath).replace(/\\/g, '/');
};
Expand Down Expand Up @@ -1031,6 +1045,122 @@ describe('Proxy', function () {
});
});

describe('State switching', function () {
function makeRequest (url, isResource) {
var options = {
url: proxy.openSession(url, session),
headers: {}
};

if (!isResource)
options.headers['accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*!/!*;q=0.8';

return new Promise(function (resolve, reject) {
request(options, function (err, res, body) {
if (err)
reject(err);
else
resolve(body);
});
});
}

function forEachSequentially (arr, fn) {
return arr.reduce(function (promise, item) {
return promise.then(function () {
return fn(item);
});
}, Promise.resolve());
}

it('Should switch states', function () {
var testCases = [
{
state: null,
urls: ['http://127.0.0.1:2000/cookie/set1'],
expected: 'Set1_1=value1; Set1_2=value2'
},
{
state: null,
urls: ['http://127.0.0.1:2000/cookie/set2'],
expected: 'Set2_1=value1; Set2_2=value2'
},
{
state: null,
urls: ['http://127.0.0.1:2000/cookie/set1', 'http://127.0.0.1:2000/cookie/set2'],
expected: 'Set1_1=value1; Set1_2=value2; Set2_1=value1; Set2_2=value2'
}
];

function initializeState (testCase) {
session.useStateSnapshot(null);

return forEachSequentially(testCase.urls, makeRequest).then(function () {
testCase.state = session.getStateSnapshot();
});
}

function assertState (testCase) {
session.useStateSnapshot(testCase.state);

return makeRequest('http://127.0.0.1:2000/cookie/echo').then(function (body) {
expect(body).contains('%% ' + testCase.expected + ' %%');
});
}

return Promise.resolve()
.then(function () {
return forEachSequentially(testCases, initializeState);
})
.then(function () {
return forEachSequentially(testCases, assertState);
});
});

it('Should switch state only on page requests', function () {
var state = null;

return makeRequest('http://127.0.0.1:2000/cookie/set1')
.then(function () {
state = session.getStateSnapshot();

session.useStateSnapshot(null);
})

// Try request empty state with non-page and page requests
.then(function () {
return makeRequest('http://127.0.0.1:2000/cookie/echo', true);
})
.then(function (body) {
expect(body).contains('%% Set1_1=value1; Set1_2=value2 %%');
})
.then(function () {
return makeRequest('http://127.0.0.1:2000/cookie/echo', false);
})
.then(function (body) {
expect(body).not.contains('%% Set1_1=value1; Set1_2=value2 %%');
})

.then(function () {
session.useStateSnapshot(state);
})

// Try request Set1 state with non-page and page requests
.then(function () {
return makeRequest('http://127.0.0.1:2000/cookie/echo', true);
})
.then(function (body) {
expect(body).not.contains('%% Set1_1=value1; Set1_2=value2 %%');
})
.then(function () {
return makeRequest('http://127.0.0.1:2000/cookie/echo', false);
})
.then(function (body) {
expect(body).contains('%% Set1_1=value1; Set1_2=value2 %%');
});
});
});

describe('Regression', function () {
it('Should force "Origin" header for the same-domain requests (B234325)', function (done) {
var options = {
Expand Down

0 comments on commit dd8b717

Please sign in to comment.