From d52bfccf17fa7437fa55d4261e90cf99fa7fb822 Mon Sep 17 00:00:00 2001 From: Diana Barsan <35681649+dianabarsan@users.noreply.github.com> Date: Tue, 13 Feb 2024 18:37:04 +0700 Subject: [PATCH] feat(#1): Store session request for concurrent fetches (#2) --- package-lock.json | 4 +-- package.json | 2 +- src/index.js | 50 +++++++++++++++----------- test/integration/index.spec.js | 19 ++++++++++ test/unit/index.spec.js | 66 ++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d195b6..3ac9ce1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pouchdb-session-authentication", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pouchdb-session-authentication", - "version": "1.0.0", + "version": "1.1.0", "license": "AGPL-3.0-only", "dependencies": { "pouchdb-fetch": "^8.0.1" diff --git a/package.json b/package.json index e0d8c87..b011a9f 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/index.js b/src/index.js index 407a7b3..7854056 100644 --- a/src/index.js +++ b/src/index.js @@ -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}`; @@ -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) => { @@ -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 @@ -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(); diff --git a/test/integration/index.spec.js b/test/integration/index.spec.js index 4a6ae69..5d16201 100644 --- a/test/integration/index.spec.js +++ b/test/integration/index.spec.js @@ -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); + }); }); diff --git a/test/unit/index.spec.js b/test/unit/index.spec.js index ef2edf6..20a6be4 100644 --- a/test/unit/index.spec.js +++ b/test/unit/index.spec.js @@ -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' }; @@ -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 () => {