diff --git a/packages/helix-shared-tokencache/src/CachePlugin.d.ts b/packages/helix-shared-tokencache/src/CachePlugin.d.ts new file mode 100644 index 00000000..fc889e80 --- /dev/null +++ b/packages/helix-shared-tokencache/src/CachePlugin.d.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { ICachePlugin, TokenCacheContext } from '@azure/msal-node'; + +export declare interface CachePlugin extends ICachePlugin { + + afterCacheAccess(tokenCacheContext: TokenCacheContext): Promise; + + beforeCacheAccess(tokenCacheContext: TokenCacheContext): Promise; + + deleteCache(): Promise; + + location: string; + + /** + * gets plugin metadata. + * @returns the plugin metadata + */ + getPluginMetadata(): Promise; + + /** + * sets the plugin metadata. + * @param meta + */ + setPluginMetadata(meta): Promise; +} diff --git a/packages/helix-shared-tokencache/src/FSCachePlugin.d.ts b/packages/helix-shared-tokencache/src/FSCachePlugin.d.ts index fd90a7c0..be6326b6 100644 --- a/packages/helix-shared-tokencache/src/FSCachePlugin.d.ts +++ b/packages/helix-shared-tokencache/src/FSCachePlugin.d.ts @@ -9,23 +9,14 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { ICachePlugin, TokenCacheContext } from '@azure/msal-node'; - -import { Logger } from "../OneDrive"; +import { CachePlugin } from './CachePlugin'; export declare interface FSCachePluginOptions { log: Console; filePath: string; } -export declare class FSCachePlugin implements ICachePlugin { +export declare class FSCachePlugin implements CachePlugin { constructor(opts: FSCachePluginOptions); - deleteCache(): Promise; - - afterCacheAccess(tokenCacheContext: TokenCacheContext): Promise; - - beforeCacheAccess(tokenCacheContext: TokenCacheContext): Promise; - - location: string; } diff --git a/packages/helix-shared-tokencache/src/FSCachePlugin.js b/packages/helix-shared-tokencache/src/FSCachePlugin.js index df2903e9..4b67214e 100644 --- a/packages/helix-shared-tokencache/src/FSCachePlugin.js +++ b/packages/helix-shared-tokencache/src/FSCachePlugin.js @@ -27,9 +27,13 @@ export class FSCachePlugin { constructor(opts) { this.filePath = opts.filePath; this.log = opts.log || console; + this.meta = null; + this.data = null; } async deleteCache() { + this.data = null; + this.meta = null; try { await fs.rm(this.filePath); } catch (e) { @@ -38,34 +42,77 @@ export class FSCachePlugin { } } - /** - * @param {TokenCacheContext} cacheContext - * @returns {Promise} if cache was updated - */ - async beforeCacheAccess(cacheContext) { + async #loadData() { const { log, filePath } = this; try { - cacheContext.tokenCache.deserialize(await fs.readFile(filePath, 'utf-8')); - return true; + let raw = await fs.readFile(filePath, 'utf-8'); + const data = JSON.parse(raw); + if (data.cachePluginMetadata) { + this.meta = data.cachePluginMetadata; + delete data.cachePluginMetadata; + raw = JSON.stringify(data); + } else { + this.meta = {}; + } + this.data = data; + return raw; } catch (e) { if (e.code !== 'ENOENT') { // only log warnings if file exists, otherwise ignore log.warn('FSCachePlugin: unable to deserialize', e); } } + this.data = null; + return null; + } + + async #saveData() { + const { filePath } = this; + const data = this.data || {}; + if (Object.keys(this.meta || {}).length) { + data.cachePluginMetadata = this.meta; + } + const raw = JSON.stringify(data, null, 2); + delete data.cachePluginMetadata; + await fs.writeFile(filePath, raw, 'utf-8'); + } + + /** + * @param {TokenCacheContext} cacheContext + * @returns {Promise} if cache was updated + */ + async beforeCacheAccess(cacheContext) { + const raw = await this.#loadData(); + if (raw) { + cacheContext.tokenCache.deserialize(raw); + return true; + } return false; } + async getPluginMetadata() { + if (!this.meta) { + await this.#loadData(); + } + return this.meta; + } + + async setPluginMetadata(meta) { + if (!this.data) { + await this.#loadData(); + } + this.meta = meta || {}; + await this.#saveData(); + } + /** * @param {TokenCacheContext} cacheContext * @returns {Promise} if cache was updated */ async afterCacheAccess(cacheContext) { - const { filePath } = this; if (cacheContext.cacheHasChanged) { - // reparse and create a nice formatted JSON - const tokens = JSON.parse(cacheContext.tokenCache.serialize()); - await fs.writeFile(filePath, JSON.stringify(tokens, null, 2), 'utf-8'); + this.data = JSON.parse(cacheContext.tokenCache.serialize()); + await this.#saveData(); return true; } return false; diff --git a/packages/helix-shared-tokencache/src/MemCachePlugin.d.ts b/packages/helix-shared-tokencache/src/MemCachePlugin.d.ts index 3863fc45..8cf433c9 100644 --- a/packages/helix-shared-tokencache/src/MemCachePlugin.d.ts +++ b/packages/helix-shared-tokencache/src/MemCachePlugin.d.ts @@ -9,9 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { ICachePlugin, TokenCacheContext } from '@azure/msal-node'; - -import { Logger } from "../OneDrive"; +import {CachePlugin} from "./CachePlugin"; export declare interface MemCachePluginOptions { log: Console; @@ -19,18 +17,11 @@ export declare interface MemCachePluginOptions { * memory cache key */ key: string; - base: ICachePlugin; + base: CachePlugin; caches?: Map; } -export declare class MemCachePlugin implements ICachePlugin { +export declare class MemCachePlugin implements CachePlugin { constructor(opts: MemCachePluginOptions); - deleteCache(): Promise; - - afterCacheAccess(tokenCacheContext: TokenCacheContext): Promise; - - beforeCacheAccess(tokenCacheContext: TokenCacheContext): Promise; - - location: string; } diff --git a/packages/helix-shared-tokencache/src/MemCachePlugin.js b/packages/helix-shared-tokencache/src/MemCachePlugin.js index 59e2a3b6..86b59a5c 100644 --- a/packages/helix-shared-tokencache/src/MemCachePlugin.js +++ b/packages/helix-shared-tokencache/src/MemCachePlugin.js @@ -45,22 +45,31 @@ export class MemCachePlugin { } } + #getOrCreateCache() { + let cache = this.caches.get(this.key); + if (!cache) { + cache = {}; + this.caches.set(this.key, cache); + } + return cache; + } + /** * @param {TokenCacheContext} cacheContext */ async beforeCacheAccess(cacheContext) { try { this.log.debug('mem: read token cache', this.key); - const cache = this.caches.get(this.key); - if (cache) { - cacheContext.tokenCache.deserialize(cache); + const cache = this.#getOrCreateCache(); + if (cache.data) { + cacheContext.tokenCache.deserialize(cache.data); return true; } else if (this.base) { this.log.debug('mem: read token cache failed. asking base'); const ret = await this.base.beforeCacheAccess(cacheContext); if (ret) { this.log.debug('mem: base updated. remember.'); - this.caches.set(this.key, cacheContext.tokenCache.serialize()); + cache.data = cacheContext.tokenCache.serialize(); } return ret; } @@ -76,7 +85,8 @@ export class MemCachePlugin { async afterCacheAccess(cacheContext) { if (cacheContext.cacheHasChanged) { this.log.debug('mem: write token cache', this.key); - this.caches.set(this.key, cacheContext.tokenCache.serialize()); + const cache = this.#getOrCreateCache(); + cache.data = cacheContext.tokenCache.serialize(); if (this.base) { this.log.debug('mem: write token cache done. telling base', this.key); return this.base.afterCacheAccess(cacheContext); @@ -89,4 +99,18 @@ export class MemCachePlugin { get location() { return this.base ? this.base.location : this.key; } + + async getPluginMetadata() { + const cache = this.#getOrCreateCache(); + if (!cache.metadata && this.base) { + cache.metadata = await this.base.getPluginMetadata(); + } + return cache.metadata; + } + + async setPluginMetadata(meta) { + const cache = this.#getOrCreateCache(); + cache.metadata = meta; + await this.base?.setPluginMetadata(meta); + } } diff --git a/packages/helix-shared-tokencache/src/S3CachePlugin.d.ts b/packages/helix-shared-tokencache/src/S3CachePlugin.d.ts index 87d8465f..a97cb4e1 100644 --- a/packages/helix-shared-tokencache/src/S3CachePlugin.d.ts +++ b/packages/helix-shared-tokencache/src/S3CachePlugin.d.ts @@ -9,9 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { ICachePlugin, TokenCacheContext } from '@azure/msal-node'; - -import { Logger } from "../OneDrive"; +import {CachePlugin} from "./CachePlugin"; export declare interface S3CachePluginOptions { log: Console; @@ -20,7 +18,7 @@ export declare interface S3CachePluginOptions { secret: string; } -export declare class S3CachePlugin implements ICachePlugin { +export declare class S3CachePlugin implements CachePlugin { /** * Decrypts a AES-GCM encrypted digest. * @param {string} key encryption key / password @@ -40,12 +38,4 @@ export declare class S3CachePlugin implements ICachePlugin { static decrypt(key: string, data: Buffer):Buffer; constructor(opts: S3CachePluginOptions); - - deleteCache(): Promise; - - location: string; - - afterCacheAccess(tokenCacheContext: TokenCacheContext): Promise; - - beforeCacheAccess(tokenCacheContext: TokenCacheContext): Promise; } diff --git a/packages/helix-shared-tokencache/src/S3CachePlugin.js b/packages/helix-shared-tokencache/src/S3CachePlugin.js index 0c417f13..c9ff2f53 100644 --- a/packages/helix-shared-tokencache/src/S3CachePlugin.js +++ b/packages/helix-shared-tokencache/src/S3CachePlugin.js @@ -39,6 +39,8 @@ export class S3CachePlugin { this.key = opts.key; this.secret = opts.secret; this.s3 = new S3Client(); + this.meta = null; + this.data = null; } async deleteCache() { @@ -58,7 +60,7 @@ export class S3CachePlugin { } } - async beforeCacheAccess(cacheContext) { + async #loadData() { const { log, secret, key, bucket, } = this; @@ -68,14 +70,22 @@ export class S3CachePlugin { Bucket: bucket, Key: key, })); - let data = await new Response(res.Body, {}).buffer(); + let raw = await new Response(res.Body, {}).buffer(); if (secret) { - data = decrypt(secret, data).toString('utf-8'); + raw = decrypt(secret, raw).toString('utf-8'); } else { - data = data.toString('utf-8'); + raw = raw.toString('utf-8'); } - cacheContext.tokenCache.deserialize(data); - return true; + const data = JSON.parse(raw); + if (data.cachePluginMetadata) { + this.meta = data.cachePluginMetadata; + delete data.cachePluginMetadata; + raw = JSON.stringify(data); + } else { + this.meta = {}; + } + this.data = data; + return raw; } catch (e) { if (e.$metadata?.httpStatusCode === 404) { log.info('s3: unable to deserialize token cache: not found'); @@ -83,34 +93,69 @@ export class S3CachePlugin { log.warn('s3: unable to deserialize token cache', e); } } + return null; + } + + async beforeCacheAccess(cacheContext) { + const raw = await this.#loadData(); + if (raw) { + cacheContext.tokenCache.deserialize(raw); + return true; + } + return false; + } + + async #saveData() { + const { + log, secret, key, bucket, + } = this; + try { + log.debug('s3: write token cache', key); + const data = this.data || {}; + if (Object.keys(this.meta || {}).length) { + data.cachePluginMetadata = this.meta; + } + let raw = JSON.stringify(data); + delete data.cachePluginMetadata; + if (secret) { + raw = encrypt(secret, Buffer.from(raw, 'utf-8')); + } + await this.s3.send(new PutObjectCommand({ + Bucket: bucket, + Key: key, + Body: raw, + ContentType: secret ? 'application/octet-stream' : 'text/plain', + })); + return true; + } catch (e) { + log.warn('s3: unable to serialize token cache', e); + } return false; } async afterCacheAccess(cacheContext) { if (cacheContext.cacheHasChanged) { - const { - log, secret, key, bucket, - } = this; - try { - log.debug('s3: write token cache', key); - let data = cacheContext.tokenCache.serialize(); - if (secret) { - data = encrypt(secret, Buffer.from(data, 'utf-8')); - } - await this.s3.send(new PutObjectCommand({ - Bucket: bucket, - Key: key, - Body: data, - ContentType: secret ? 'application/octet-stream' : 'text/plain', - })); - return true; - } catch (e) { - log.warn('s3: unable to serialize token cache', e); - } + this.data = JSON.parse(cacheContext.tokenCache.serialize()); + return this.#saveData(); } return false; } + async getPluginMetadata() { + if (!this.meta) { + await this.#loadData(); + } + return this.meta; + } + + async setPluginMetadata(meta) { + if (!this.data) { + await this.#loadData(); + } + this.meta = meta || {}; + await this.#saveData(); + } + get location() { return `${this.bucket}/${this.key}`; } diff --git a/packages/helix-shared-tokencache/test/fs-cache-plugin.test.js b/packages/helix-shared-tokencache/test/fs-cache-plugin.test.js index 4585d344..ff16565a 100644 --- a/packages/helix-shared-tokencache/test/fs-cache-plugin.test.js +++ b/packages/helix-shared-tokencache/test/fs-cache-plugin.test.js @@ -49,6 +49,61 @@ describe('FSCachePlugin Test', () => { assert.strictEqual(p.location, testFilePath); }); + it('writes the plugin metadata cache data to the filesystem', async () => { + const p = new FSCachePlugin({ + filePath: testFilePath, + }); + + const ctx = new MockTokenCacheContext({ + cacheHasChanged: true, + tokens: '{ "access_token": "1234" }', + }); + const ret = await p.afterCacheAccess(ctx); + assert.strictEqual(ret, true); + assert.deepStrictEqual(JSON.parse(await fs.readFile(testFilePath, 'utf-8')), { + access_token: '1234', + }); + + await p.setPluginMetadata({ foo: 'bar' }); + assert.deepStrictEqual(JSON.parse(await fs.readFile(testFilePath, 'utf-8')), { + access_token: '1234', + cachePluginMetadata: { + foo: 'bar', + }, + }); + assert.strictEqual(p.location, testFilePath); + }); + + it('writes the plugin metadata cache data pristine plugin', async () => { + const p = new FSCachePlugin({ + filePath: testFilePath, + }); + await p.setPluginMetadata({ foo: 'bar' }); + assert.deepStrictEqual(JSON.parse(await fs.readFile(testFilePath, 'utf-8')), { + cachePluginMetadata: { + foo: 'bar', + }, + }); + assert.strictEqual(p.location, testFilePath); + }); + + it('can clear plugin metadata', async () => { + await fs.writeFile(testFilePath, JSON.stringify({ + access_token: '1234', + cachePluginMetadata: { + foo: 'bar', + }, + }), 'utf-8'); + + const p = new FSCachePlugin({ + filePath: testFilePath, + }); + await p.setPluginMetadata(); + assert.deepStrictEqual(JSON.parse(await fs.readFile(testFilePath, 'utf-8')), { + access_token: '1234', + }); + }); + it('does not the cache data to the filesystem if context not changed', async () => { const p = new FSCachePlugin({ filePath: testFilePath, @@ -84,7 +139,45 @@ describe('FSCachePlugin Test', () => { assert.strictEqual(ctx.tokens, '{ "access_token": "1234" }'); }); - it('read cache data ignores inexistant file', async () => { + it('read cache plugin metadata from the filesystem', async () => { + await fs.writeFile(testFilePath, JSON.stringify({ + access_token: '1234', + cachePluginMetadata: { + foo: 'bar', + }, + }), 'utf-8'); + + const p = new FSCachePlugin({ + filePath: testFilePath, + }); + + const ctx = new MockTokenCacheContext({ + }); + const ret = await p.beforeCacheAccess(ctx); + assert.strictEqual(ret, true); + assert.strictEqual(ctx.tokens, '{"access_token":"1234"}'); + assert.deepStrictEqual(await p.getPluginMetadata(), { + foo: 'bar', + }); + }); + + it('read cache plugin metadata from pristine plugin', async () => { + await fs.writeFile(testFilePath, JSON.stringify({ + access_token: '1234', + cachePluginMetadata: { + foo: 'bar', + }, + }), 'utf-8'); + + const p = new FSCachePlugin({ + filePath: testFilePath, + }); + assert.deepStrictEqual(await p.getPluginMetadata(), { + foo: 'bar', + }); + }); + + it('read cache data ignores nonexistent file', async () => { const p = new FSCachePlugin({ filePath: testRoot, }); diff --git a/packages/helix-shared-tokencache/test/mem-cache-plugin.test.js b/packages/helix-shared-tokencache/test/mem-cache-plugin.test.js index 5ff126ee..47d3836d 100644 --- a/packages/helix-shared-tokencache/test/mem-cache-plugin.test.js +++ b/packages/helix-shared-tokencache/test/mem-cache-plugin.test.js @@ -12,16 +12,26 @@ /* eslint-env mocha */ import assert from 'assert'; -import { MemCachePlugin } from '../src/index.js'; +import path from 'path'; +import crypto from 'crypto'; +import { promises as fs } from 'fs'; +import { FSCachePlugin, MemCachePlugin } from '../src/index.js'; import { MockTokenCacheContext } from './MockTokenCacheContext.js'; describe('MemCachePlugin Test', () => { - beforeEach(() => { + let testRoot; + let testFilePath; + + beforeEach(async () => { new MemCachePlugin().clear(); + testRoot = path.resolve(__testdir, 'tmp', crypto.randomUUID()); + await fs.mkdir(testRoot, { recursive: true }); + testFilePath = path.resolve(testRoot, 'auth.json'); }); - afterEach(() => { + afterEach(async () => { new MemCachePlugin().clear(); + await fs.rm(testRoot, { recursive: true }); }); it('writes the cache w/o base with local cache', async () => { @@ -38,7 +48,7 @@ describe('MemCachePlugin Test', () => { }); const ret = await p.afterCacheAccess(ctx); assert.strictEqual(ret, true); - assert.strictEqual(caches.get('foobar-key'), 'foobar'); + assert.deepStrictEqual(caches.get('foobar-key'), { data: 'foobar' }); assert.strictEqual(p.location, 'foobar-key'); }); @@ -108,7 +118,7 @@ describe('MemCachePlugin Test', () => { tokens: 'foobar', }); await p.afterCacheAccess(ctx); - assert.strictEqual(caches.get('foobar-key'), 'foobar'); + assert.deepStrictEqual(caches.get('foobar-key'), { data: 'foobar' }); p.deleteCache(); assert.strictEqual(caches.get('foobar-key'), undefined); }); @@ -132,7 +142,7 @@ describe('MemCachePlugin Test', () => { }); const ret = await p.afterCacheAccess(ctx); assert.strictEqual(ret, true); - assert.strictEqual(caches.get('foobar-key'), 'foobar'); + assert.deepStrictEqual(caches.get('foobar-key'), { data: 'foobar' }); }); it('read the cache from base', async () => { @@ -141,7 +151,7 @@ describe('MemCachePlugin Test', () => { key: 'foobar-key', caches: baseCaches, }); - baseCaches.set('foobar-key', 'foobar'); + baseCaches.set('foobar-key', { data: 'foobar' }); const caches = new Map(); const p = new MemCachePlugin({ @@ -156,7 +166,7 @@ describe('MemCachePlugin Test', () => { const ret = await p.beforeCacheAccess(ctx); assert.strictEqual(ret, true); assert.strictEqual(ctx.tokens, 'foobar'); - assert.strictEqual(caches.get('foobar-key'), 'foobar'); + assert.deepStrictEqual(caches.get('foobar-key'), { data: 'foobar' }); }); it('read the cache from base is missing', async () => { @@ -198,4 +208,45 @@ describe('MemCachePlugin Test', () => { await p.deleteCache(); assert.strictEqual(baseCaches.get('foobar-key'), undefined); }); + + it('writes the metadata to base', async () => { + const base = new FSCachePlugin({ + filePath: testFilePath, + }); + const p = new MemCachePlugin({ + log: console, + key: 'foobar-key', + base, + }); + + await p.setPluginMetadata({ foo: 'bar' }); + assert.deepStrictEqual(JSON.parse(await fs.readFile(testFilePath, 'utf-8')), { + cachePluginMetadata: { + foo: 'bar', + }, + }); + }); + + it('reads the metadata from base', async () => { + await fs.writeFile(testFilePath, JSON.stringify({ + access_token: '1234', + cachePluginMetadata: { + foo: 'bar', + }, + }), 'utf-8'); + + const base = new FSCachePlugin({ + filePath: testFilePath, + }); + const p = new MemCachePlugin({ + log: console, + key: 'foobar-key', + base, + }); + + const meta = await p.getPluginMetadata(); + assert.deepStrictEqual(meta, { + foo: 'bar', + }); + }); }); diff --git a/packages/helix-shared-tokencache/test/s3-cache-plugin.test.js b/packages/helix-shared-tokencache/test/s3-cache-plugin.test.js index 799b4e70..5c86662f 100644 --- a/packages/helix-shared-tokencache/test/s3-cache-plugin.test.js +++ b/packages/helix-shared-tokencache/test/s3-cache-plugin.test.js @@ -35,19 +35,20 @@ describe('S3CachePlugin Test', () => { process.env = savedProcessEnv; }); - it('writes the cache data to s3 w/o encryption', async () => { + it('writes the cache data to s3 w/o encryption and metadata', async () => { const p = new S3CachePlugin({ bucket: 'test-bucket', key: 'myproject/auth-default/json', secret: '', }); - let objectData = ''; + const objectData = []; nock('https://test-bucket.s3.us-east-1.amazonaws.com') .put('/myproject/auth-default/json?x-id=PutObject') + .twice() .reply((_, body) => { - objectData = body; + objectData.push(body); return [204]; }); @@ -57,7 +58,59 @@ describe('S3CachePlugin Test', () => { }); const ret = await p.afterCacheAccess(ctx); assert.strictEqual(ret, true); - assert.strictEqual(objectData, '{ "access_token": "1234" }'); + assert.strictEqual(objectData.shift(), '{"access_token":"1234"}'); + + await p.setPluginMetadata({ foo: 'bar' }); + assert.strictEqual(objectData.shift(), '{"access_token":"1234","cachePluginMetadata":{"foo":"bar"}}'); + }); + + it('can clear plugin metadata', async () => { + const p = new S3CachePlugin({ + bucket: 'test-bucket', + key: 'myproject/auth-default/json', + secret: '', + }); + + const objectData = []; + + nock('https://test-bucket.s3.us-east-1.amazonaws.com') + .get('/myproject/auth-default/json?x-id=GetObject') + .reply(200, JSON.stringify({ + access_token: '1234', + cachePluginMetadata: { + foo: 'bar', + }, + })) + .put('/myproject/auth-default/json?x-id=PutObject') + .reply((_, body) => { + objectData.push(body); + return [204]; + }); + + await p.setPluginMetadata(); + assert.strictEqual(objectData.shift(), '{"access_token":"1234"}'); + }); + + it('writes metadata to pristine plugin', async () => { + const p = new S3CachePlugin({ + bucket: 'test-bucket', + key: 'myproject/auth-default/json', + secret: '', + }); + + const objectData = []; + + nock('https://test-bucket.s3.us-east-1.amazonaws.com') + .get('/myproject/auth-default/json?x-id=GetObject') + .reply(404) + .put('/myproject/auth-default/json?x-id=PutObject') + .reply((_, body) => { + objectData.push(body); + return [204]; + }); + + await p.setPluginMetadata({ foo: 'bar' }); + assert.strictEqual(objectData.shift(), '{"cachePluginMetadata":{"foo":"bar"}}'); }); it('writes the cache data to s3 with encryption', async () => { @@ -82,7 +135,7 @@ describe('S3CachePlugin Test', () => { }); const ret = await p.afterCacheAccess(ctx); assert.strictEqual(ret, true); - assert.strictEqual(decrypt('foobar', objectData).toString('utf-8'), '{ "access_token": "1234" }'); + assert.strictEqual(decrypt('foobar', objectData).toString('utf-8'), '{"access_token":"1234"}'); }); it('does not the write data to s3 if context not changed', async () => { @@ -128,16 +181,21 @@ describe('S3CachePlugin Test', () => { nock('https://test-bucket.s3.us-east-1.amazonaws.com') .get('/myproject/auth-default/json?x-id=GetObject') - .reply(200, '{ "access_token": "1234" }'); - - const ctx = new MockTokenCacheContext({ - }); + .reply(200, JSON.stringify({ + access_token: '1234', + cachePluginMetadata: { + foo: 'bar', + }, + })); + + const ctx = new MockTokenCacheContext({}); const ret = await p.beforeCacheAccess(ctx); assert.strictEqual(ret, true); - assert.strictEqual(ctx.tokens, '{ "access_token": "1234" }'); + assert.strictEqual(ctx.tokens, '{"access_token":"1234"}'); + assert.deepStrictEqual(await p.getPluginMetadata(), { foo: 'bar' }); }); - it('read cache data from s3 with/o encryption', async () => { + it('read cache data from s3 with encryption', async () => { const p = new S3CachePlugin({ bucket: 'test-bucket', key: 'myproject/auth-default/json', @@ -173,6 +231,25 @@ describe('S3CachePlugin Test', () => { assert.strictEqual(ctx.tokens, ''); }); + it('read plugin metadata from pristine plugin', async () => { + const p = new S3CachePlugin({ + bucket: 'test-bucket', + key: 'myproject/auth-default/json', + secret: '', + }); + + nock('https://test-bucket.s3.us-east-1.amazonaws.com') + .get('/myproject/auth-default/json?x-id=GetObject') + .reply(200, JSON.stringify({ + access_token: '1234', + cachePluginMetadata: { + foo: 'bar', + }, + })); + + assert.deepStrictEqual(await p.getPluginMetadata(), { foo: 'bar' }); + }); + it('read cache data handles generic error', async () => { const p = new S3CachePlugin({ bucket: 'test-bucket',