Skip to content

Commit

Permalink
feat(#1): Store session request for concurrent fetches (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
dianabarsan authored Feb 13, 2024
1 parent c0d1297 commit d52bfcc
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 24 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pouchdb-session-authentication",
"version": "1.0.0",
"version": "1.1.0",
"description": "Plugin that forces session authentication for PouchDb http adapter",
"main": "src/index.js",
"scripts": {
Expand Down
50 changes: 29 additions & 21 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ const parseCookie = (response) => {
};
};

const getExistingSession = (db) => {
const urlString = getSessionKey(db);
if (sessions[urlString] && !isExpired(sessions[urlString])) {
return sessions[urlString];
}
};

const getSessionKey = (db) => {
const sessionUrl = getSessionUrl(db);
return `${db.credentials.username}:${db.credentials.password}:${sessionUrl}`;
Expand All @@ -60,24 +53,21 @@ const authenticate = async (db) => {

const body = JSON.stringify({ name: db.credentials.username, password: db.credentials.password});
const response = await db.originalFetch(url.toString(), { method: 'POST', headers, body });
updateSession(db, response);
return updateSession(db, response);
};

const updateSession = (db, response) => {
const session = parseCookie(response);
if (session) {
const url = getSessionKey(db);
sessions[url] = session;
const sessionKey = getSessionKey(db);
sessions[sessionKey] = session;
return session;
}
};

const invalidateSession = db => {
const url = getSessionKey(db);
delete sessions[url];
};

const isExpired = (session) => {
return Date.now() > session.expires;
const sessionKey = getSessionKey(db);
delete sessions[sessionKey];
};

const extractAuth = (opts) => {
Expand All @@ -96,6 +86,28 @@ const extractAuth = (opts) => {
};
};

const isValid = (session) => {
if (!session || !session.expires) {
return false;
}
const isExpired = Date.now() > session.expires;
return !isExpired;
};

const getValidSession = async (db) => {
const sessionKey = getSessionKey(db);

if (sessions[sessionKey]) {
const session = await sessions[sessionKey];
if (isValid(session)) {
return session;
}
}

sessions[sessionKey] = authenticate(db);
return sessions[sessionKey];
};

// eslint-disable-next-line func-style
function wrapAdapter (PouchDB, HttpPouch) {
// eslint-disable-next-line func-style
Expand All @@ -108,11 +120,7 @@ function wrapAdapter (PouchDB, HttpPouch) {

db.originalFetch = db.fetch || PouchDB.fetch;
db.fetch = async (url, opts = {}) => {
let session = getExistingSession(db);
if (!session) {
await authenticate(db);
session = getExistingSession(db);
}
const session = await getValidSession(db);

if (session) {
opts.headers = opts.headers || new Headers();
Expand Down
19 changes: 19 additions & 0 deletions test/integration/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,5 +143,24 @@ describe(`integration with ${authType}`, function () {
const logs = await collectLogs(1000);
expect(utils.getSessionRequests(logs).length).to.equal(1);
});

it('should only request session once for concurrent requests', async () => {
const auth = { username: tempAdminName, password: 'new_password' };
await utils.createAdmin(auth.username, auth.password);
await utils.createDb(tempDbName);

tempDb = getDb(tempDbName, auth, authType);
const collectLogs = await utils.getDockerContainerLogs();
await Promise.all([
tempDb.allDocs(),
tempDb.allDocs(),
tempDb.allDocs(),
tempDb.allDocs(),
tempDb.allDocs(),
]);

const logs = await collectLogs();
expect(utils.getSessionRequests(logs).length).to.equal(1);
});
});

66 changes: 66 additions & 0 deletions test/unit/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,55 @@ describe('Pouchdb Session authentication plugin', () => {
{ headers: new Headers({ 'Cookie': 'AuthSession=user2session' }) }
]);
});

it('should only request session once for concurrent requests', async () => {
db = { name: 'http://admin:pass@localhost:5984/mydb' };
plugin(PoudhDb);
PoudhDb.adapters.http(db);

fetch.resolves({ ok: true, status: 200 });
fetch.withArgs('http://admin:pass@localhost:5984/_session').resolves({
ok: true,
status: 200,
headers: new Headers({ 'set-cookie': getSession('theonetruesession') })
});

await Promise.all([
db.fetch('randomUrl1'),
db.fetch('randomUrl1'),
db.fetch('randomUrl1'),
db.fetch('randomUrl1'),
]);

expect(fetch.callCount).to.equal(5);
expect(fetch.args[0]).to.deep.equal([
'http://admin:pass@localhost:5984/_session',
{
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json',
}),
body: JSON.stringify({ name: 'admin', password: 'pass' }),
}
]);
expect(fetch.args[1]).to.deep.equal([
'randomUrl1',
{ headers: new Headers({ 'Cookie': 'AuthSession=theonetruesession' }) }
]);
expect(fetch.args[2]).to.deep.equal([
'randomUrl1',
{ headers: new Headers({ 'Cookie': 'AuthSession=theonetruesession' }) }
]);
expect(fetch.args[3]).to.deep.equal([
'randomUrl1',
{ headers: new Headers({ 'Cookie': 'AuthSession=theonetruesession' }) }
]);
expect(fetch.args[4]).to.deep.equal([
'randomUrl1',
{ headers: new Headers({ 'Cookie': 'AuthSession=theonetruesession' }) }
]);
});

it('should update the session if server responds with new cookie', async () => {
db = { name: 'http://admin:pass@localhost:5984/mydb' };
Expand Down Expand Up @@ -477,6 +526,23 @@ describe('Pouchdb Session authentication plugin', () => {
}
]);
expect(fetch.args[1]).to.deep.equal([ 'randomUrl', {} ]);

const response2 = await db.fetch('randomUrl');

expect(response2).to.deep.equal({ ok: false, status: 401, body: 'omg' });
expect(fetch.callCount).to.equal(4);
expect(fetch.args[2]).to.deep.equal([
'http://localhost:5984/_session',
{
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json',
}),
body: JSON.stringify({ name: 'admin', password: 'pass' }),
}
]);
expect(fetch.args[3]).to.deep.equal([ 'randomUrl', {} ]);
});

it('should continue when session cookie is not returned', async () => {
Expand Down

0 comments on commit d52bfcc

Please sign in to comment.