diff --git a/src/routes/invalidate.js b/src/routes/invalidate.js index ef2c23f..0c15e4d 100644 --- a/src/routes/invalidate.js +++ b/src/routes/invalidate.js @@ -6,6 +6,7 @@ const queue = require('../queue'); const registry = require('../services/registry'); const { createRateLimiter } = require('../helpers/ratelimit'); const { sendToTelemetry } = require('../telemetry'); +const { isValidTagList } = require('../helpers/utils'); const router = express.Router(); @@ -51,6 +52,13 @@ router.post( tags = tags.match(/\[(.*)\]/); // eslint-disable-next-line prefer-destructuring filter.tags = tags[1]; + // Do a sanity check + if (!isValidTagList(filter.tags)) { + logger.info(`Remove previously invalid cache key ${key} during invalidation`); + await registry.del(key); + await registry.delFromSet(setKey, key); + return; + } } catch (e) { // no-op } @@ -154,6 +162,13 @@ router.post( tags = tags.match(/\[(.*)\]/); // eslint-disable-next-line prefer-destructuring filter.tags = tags[1]; + // Do a sanity check + if (!isValidTagList(filter.tags)) { + logger.info(`Remove previously invalid cache key ${key} during invalidation`); + await registry.del(key); + await registry.delFromSet(setKey, key); + return; + } } catch (e) { // no-op } diff --git a/tests/routes/content.spec.js b/tests/routes/content.spec.js index 645da51..64e05d6 100644 --- a/tests/routes/content.spec.js +++ b/tests/routes/content.spec.js @@ -100,6 +100,7 @@ describe('GET /content', () => { .get('/content/lcode') .set('Accept-version', 'v2') .set('Authorization', `Bearer ${token}:secret`); + await sleep(50); } while (res.status === 202); expect(res.status).to.equal(401); @@ -116,6 +117,7 @@ describe('GET /content', () => { .get('/content/lcode') .set('Accept-version', 'v2') .set('Authorization', `Bearer ${token}:secret`); + await sleep(50); } while (res.status === 202); const expected = { @@ -141,6 +143,7 @@ describe('GET /content', () => { .get('/content/en') .set('Accept-version', 'v2') .set('Authorization', `Bearer ${token}:secret`); + await sleep(50); } while (res.status === 202); const expected = { @@ -175,6 +178,7 @@ describe('GET /content', () => { .get('/content/lcode') .set('Accept-version', 'v2') .set('Authorization', `Bearer ${token}:secret`); + await sleep(50); } while (res.status === 202); expect(res.status).to.equal(200); @@ -194,6 +198,7 @@ describe('GET /content', () => { .get('/content/lcode') .set('Accept-version', 'v2') .set('Authorization', `Bearer ${token}:secret`); + await sleep(50); } while (res.status === 202); expect(res.body).to.eqls({ @@ -214,6 +219,7 @@ describe('GET /content', () => { .set('Accept-version', 'v2') .set('Authorization', `Bearer ${token}:secret`) .set('If-None-Match', 'obsolete_value'); + await sleep(50); } while (firstRes.status === 202); expect(firstRes.status).to.equal(200); diff --git a/tests/routes/invalidate.spec.js b/tests/routes/invalidate.spec.js index 84b5fff..ce7d7fe 100644 --- a/tests/routes/invalidate.spec.js +++ b/tests/routes/invalidate.spec.js @@ -36,7 +36,7 @@ describe('Invalidate as user', () => { }); it('should invalidate all languages', async () => { - const spy = sandbox.spy(queue, 'addJob'); + const spy = sandbox.stub(queue, 'addJob'); const res = await req .post('/invalidate') @@ -55,7 +55,7 @@ describe('Invalidate as user', () => { }); it('should invalidate specific languages', async () => { - const spy = sandbox.spy(queue, 'addJob'); + const spy = sandbox.stub(queue, 'addJob'); const res = await req .post('/invalidate/en') @@ -83,6 +83,126 @@ describe('Invalidate as user', () => { expect(res.status).to.equal(403); }); + + it('should invalidate with tags', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}[tag1,tag2]`, content); + + const res = await req + .post('/invalidate') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}:secret`); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 2, + }, + }); + expect(spy.callCount).to.equal(2); + }); + + it('should invalidate with status', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}{reviewed}`, content); + + const res = await req + .post('/invalidate') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}:secret`); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 2, + }, + }); + expect(spy.callCount).to.equal(2); + }); + + it('should invalidate with valid tags only', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}[md5(foo)]`, content); + + const res = await req + .post('/invalidate') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}:secret`); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 1, + }, + }); + expect(spy.callCount).to.equal(1); + }); + + it('should invalidate specific language with tags', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}[tag1,tag2]`, content); + + const res = await req + .post('/invalidate/en') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}:secret`); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 2, + }, + }); + expect(spy.callCount).to.equal(2); + }); + + it('should invalidate specific language with status', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}{reviewed}`, content); + + const res = await req + .post('/invalidate/en') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}:secret`); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 2, + }, + }); + expect(spy.callCount).to.equal(2); + }); + + it('should invalidate specific language with valid tags only', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}[md5(foo)]`, content); + + const res = await req + .post('/invalidate/en') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}:secret`); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 1, + }, + }); + expect(spy.callCount).to.equal(1); + }); }); describe('Invalidate as Transifex', () => { @@ -104,7 +224,7 @@ describe('Invalidate as Transifex', () => { }); it('should invalidate all languages', async () => { - const spy = sandbox.spy(queue, 'addJob'); + const spy = sandbox.stub(queue, 'addJob'); const res = await req .post('/invalidate') @@ -124,7 +244,7 @@ describe('Invalidate as Transifex', () => { }); it('should invalidate specific languages', async () => { - const spy = sandbox.spy(queue, 'addJob'); + const spy = sandbox.stub(queue, 'addJob'); const res = await req .post('/invalidate/en') @@ -154,4 +274,130 @@ describe('Invalidate as Transifex', () => { expect(res.status).to.equal(403); }); + + it('should invalidate with tags', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}[tag1,tag2]`, content); + + const res = await req + .post('/invalidate') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}`) + .set('X-Transifex-Trust-Secret', 'txsecret'); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 2, + }, + }); + expect(spy.callCount).to.equal(2); + }); + + it('should invalidate with status', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}{reviewed}`, content); + + const res = await req + .post('/invalidate') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}`) + .set('X-Transifex-Trust-Secret', 'txsecret'); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 2, + }, + }); + expect(spy.callCount).to.equal(2); + }); + + it('should invalidate with valid tags only', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}[md5(foo)]`, content); + + const res = await req + .post('/invalidate') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}`) + .set('X-Transifex-Trust-Secret', 'txsecret'); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 1, + }, + }); + expect(spy.callCount).to.equal(1); + }); + + it('should invalidate specific language with tags', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}[tag1,tag2]`, content); + + const res = await req + .post('/invalidate/en') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}`) + .set('X-Transifex-Trust-Secret', 'txsecret'); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 2, + }, + }); + expect(spy.callCount).to.equal(2); + }); + + it('should invalidate specific language with status', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}{reviewed}`, content); + + const res = await req + .post('/invalidate/en') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}`) + .set('X-Transifex-Trust-Secret', 'txsecret'); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 2, + }, + }); + expect(spy.callCount).to.equal(2); + }); + + it('should invalidate specific language with valid tags only', async () => { + const spy = sandbox.stub(queue, 'addJob'); + await populateRegistry(token, `${key}[md5(foo)]`, content); + + const res = await req + .post('/invalidate/en') + .set('Accept-version', 'v2') + .set('Authorization', `Bearer ${token}`) + .set('X-Transifex-Trust-Secret', 'txsecret'); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ + data: { + status: 'success', + token, + count: 1, + }, + }); + expect(spy.callCount).to.equal(1); + }); }); diff --git a/tests/routes/languages.spec.js b/tests/routes/languages.spec.js index bcb2412..0b25fc1 100644 --- a/tests/routes/languages.spec.js +++ b/tests/routes/languages.spec.js @@ -20,6 +20,11 @@ const urls = { languages: '/projects/o:oslug:p:pslug/languages', }; +function sleep(ms) { + // eslint-disable-next-line no-promise-executor-return + return new Promise((resolve) => setTimeout(resolve, ms)); +} + describe('GET /languages', () => { beforeEach(async () => { nock(urls.api) @@ -95,6 +100,7 @@ describe('GET /languages', () => { .get('/languages') .set('Accept-version', 'v2') .set('Authorization', `Bearer ${token}:secret`); + await sleep(50); } while (res.status === 202); expect(res.status).to.equal(200); diff --git a/tests/services/cache/route.spec.js b/tests/services/cache/route.spec.js index 5adc814..a43ad9d 100644 --- a/tests/services/cache/route.spec.js +++ b/tests/services/cache/route.spec.js @@ -16,6 +16,11 @@ const cachedKey = `${cachedToken}:en:content`; const uncachedToken = '1/efgh'; const content = JSON.stringify({ foo: 'bar' }); +function sleep(ms) { + // eslint-disable-next-line no-promise-executor-return + return new Promise((resolve) => setTimeout(resolve, ms)); +} + describe('Content cache', () => { let sandbox; @@ -36,6 +41,7 @@ describe('Content cache', () => { .get('/content/en') .set('Accept-version', 'v2') .set('Authorization', `Bearer ${cachedToken}:secret`); + await sleep(50); } while (res.status === 202); expect(res.status).to.equal(200); @@ -54,6 +60,7 @@ describe('Content cache', () => { .get('/content/en') .set('Accept-version', 'v2') .set('Authorization', `Bearer ${uncachedToken}:secret`); + await sleep(50); } while (res.status === 202); expect(res.status).to.equal(200);