From 6818f4df178aa1184c86e092624dc40f3c35798a Mon Sep 17 00:00:00 2001 From: Nicolas Dupont Date: Tue, 12 Dec 2023 09:57:12 +0100 Subject: [PATCH 1/2] Add `latest` route to Version API --- src/api/routes/versions.js | 63 ++++++++++++- src/api/routes/versions.test.js | 153 +++++++++++++++++++++++--------- 2 files changed, 168 insertions(+), 48 deletions(-) diff --git a/src/api/routes/versions.js b/src/api/routes/versions.js index aea32ae66..6ae3ebacd 100644 --- a/src/api/routes/versions.js +++ b/src/api/routes/versions.js @@ -30,6 +30,57 @@ const router = express.Router(); const versionsRepository = await RepositoryFactory.create(config.get('recorder.versions.storage')).initialize(); +/** + * @swagger + * /version/{serviceId}/{termsType}/latest: + * get: + * summary: Get the latest version of some terms. + * tags: [Versions] + * produces: + * - application/json + * parameters: + * - in: path + * name: serviceId + * description: The ID of the service whose version will be returned. + * schema: + * type: string + * required: true + * - in: path + * name: termsType + * description: The type of terms whose version will be returned. + * schema: + * type: string + * required: true + * responses: + * 200: + * description: A JSON object containing the version content and metadata. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Version' + * 404: + * description: No version found for the specified combination of service ID and terms type. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message indicating that no version is found. + */ +router.get('/version/:serviceId/:termsType/latest', async (req, res) => { + const { serviceId, termsType } = req.params; + + const version = await versionsRepository.findLatest(serviceId, termsType); + + if (!version) { + return res.status(404).json({ error: `No version found for the specified combination of service ID "${serviceId}" and terms type "${termsType}"` }); + } + + return res.status(200).json(toJSON(version)); +}); + /** * @swagger * /version/{serviceId}/{termsType}/{date}: @@ -97,14 +148,18 @@ router.get('/version/:serviceId/:termsType/:date', async (req, res) => { const version = await versionsRepository.findByDate(serviceId, termsType, requestedDate); if (!version) { - return res.status(404).json({ error: `No version found for date ${date}` }); + return res.status(404).json({ error: `No version found the specified combination of service ID "${serviceId}", terms type "${termsType}" and date ${date}` }); } - return res.status(200).json({ + return res.status(200).json(toJSON(version)); +}); + +function toJSON(version) { + return { id: version.id, fetchDate: toISODateWithoutMilliseconds(version.fetchDate), content: version.content, - }); -}); + }; +} export default router; diff --git a/src/api/routes/versions.test.js b/src/api/routes/versions.test.js index 6c342e9b3..7595e923b 100644 --- a/src/api/routes/versions.test.js +++ b/src/api/routes/versions.test.js @@ -13,56 +13,121 @@ const { expect } = chai; const request = supertest(app); describe('Versions API', () => { - describe('GET /version/:serviceId/:termsType/:date', () => { - let expectedResult; - let versionsRepository; - const FETCH_DATE = new Date('2023-01-01T12:00:00Z'); - const VERSION_COMMON_ATTRIBUTES = { - serviceId: 'service-1', - termsType: 'Terms of Service', - snapshotId: ['snapshot_id'], - }; - - before(async () => { - versionsRepository = RepositoryFactory.create(config.get('recorder.versions.storage')); - - await versionsRepository.initialize(); - - const ONE_HOUR = 60 * 60 * 1000; - - await versionsRepository.save(new Version({ - ...VERSION_COMMON_ATTRIBUTES, - content: 'initial content', - fetchDate: new Date(new Date(FETCH_DATE).getTime() - ONE_HOUR), - })); - - const version = new Version({ - ...VERSION_COMMON_ATTRIBUTES, - content: 'updated content', - fetchDate: FETCH_DATE, - }); - - await versionsRepository.save(version); - - await versionsRepository.save(new Version({ - ...VERSION_COMMON_ATTRIBUTES, - content: 'latest content', - fetchDate: new Date(new Date(FETCH_DATE).getTime() + ONE_HOUR), - })); - - expectedResult = { - id: version.id, - fetchDate: toISODateWithoutMilliseconds(version.fetchDate), - content: version.content, - }; + let expectedResult; + let versionsRepository; + let latestVersion; + let version; + + const FETCH_DATE = new Date('2023-01-01T12:00:00Z'); + const VERSION_COMMON_ATTRIBUTES = { + serviceId: 'service-1', + termsType: 'Terms of Service', + snapshotId: ['snapshot_id'], + }; + + let response; + + before(async () => { + versionsRepository = RepositoryFactory.create(config.get('recorder.versions.storage')); + + await versionsRepository.initialize(); + + const ONE_HOUR = 60 * 60 * 1000; + + await versionsRepository.save(new Version({ + ...VERSION_COMMON_ATTRIBUTES, + content: 'initial content', + fetchDate: new Date(new Date(FETCH_DATE).getTime() - ONE_HOUR), + })); + + version = new Version({ + ...VERSION_COMMON_ATTRIBUTES, + content: 'updated content', + fetchDate: FETCH_DATE, }); - after(async () => versionsRepository.removeAll()); + await versionsRepository.save(version); - let response; + latestVersion = new Version({ + ...VERSION_COMMON_ATTRIBUTES, + content: 'latest content', + fetchDate: new Date(new Date(FETCH_DATE).getTime() + ONE_HOUR), + }); + + await versionsRepository.save(latestVersion); + }); + + after(async () => versionsRepository.removeAll()); + + describe('GET /version/:serviceId/:termsType/latest', () => { + context('when a version is found', () => { + before(async () => { + expectedResult = { + id: latestVersion.id, + fetchDate: toISODateWithoutMilliseconds(latestVersion.fetchDate), + content: latestVersion.content, + }; + response = await request.get(`${basePath}/v1/version/service-1/Terms%20of%20Service/latest`); + }); + it('responds with 200 status code', () => { + expect(response.status).to.equal(200); + }); + + it('responds with Content-Type application/json', () => { + expect(response.type).to.equal('application/json'); + }); + + it('returns the expected version', () => { + expect(response.body).to.deep.equal(expectedResult); + }); + }); + + context('when the requested service does not exist', () => { + before(async () => { + response = await request.get(`${basePath}/v1/version/unknown-service/Terms%20of%20Service/latest`); + }); + + it('responds with 404 status code', () => { + expect(response.status).to.equal(404); + }); + + it('responds with Content-Type application/json', () => { + expect(response.type).to.equal('application/json'); + }); + + it('returns an error message', () => { + expect(response.body.error).to.contain('No version found').and.to.contain('unknown-service'); + }); + }); + + context('when the requested terms type does not exist for the given service', () => { + before(async () => { + response = await request.get(`${basePath}/v1/version/service-1/Unknown%20Type/latest`); + }); + + it('responds with 404 status code', () => { + expect(response.status).to.equal(404); + }); + + it('responds with Content-Type application/json', () => { + expect(response.type).to.equal('application/json'); + }); + + it('returns an error message', () => { + expect(response.body.error).to.contain('No version found').and.to.contain('Unknown Type'); + }); + }); + }); + + describe('GET /version/:serviceId/:termsType/:date', () => { context('when a version is found', () => { before(async () => { + expectedResult = { + id: version.id, + fetchDate: toISODateWithoutMilliseconds(version.fetchDate), + content: version.content, + }; response = await request.get(`${basePath}/v1/version/service-1/Terms%20of%20Service/${encodeURIComponent(toISODateWithoutMilliseconds(FETCH_DATE))}`); }); From f1752660731364c11da2442ff6689e6f54ae6172 Mon Sep 17 00:00:00 2001 From: Nicolas Dupont Date: Tue, 12 Dec 2023 10:10:29 +0100 Subject: [PATCH 2/2] Add changelog entry --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbaac1ac5..7ef0663be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,15 @@ All changes that impact users of this module are documented in this file, in the [Common Changelog](https://common-changelog.org) format with some additional specifications defined in the CONTRIBUTING file. This codebase adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## Unreleased [minor] + +_Full changeset and discussions: [#1034](https://github.com/OpenTermsArchive/engine/pull/1034)._ + +> Development of this release was [supported](https://nlnet.nl/project/TOSDR-OTA/) by the [NGI0 Entrust Fund](https://nlnet.nl/entrust), a fund established by [NLnet](https://nlnet.nl/) with financial support from the European Commission's [Next Generation Internet](https://www.ngi.eu) programme, under the aegis of DG CNECT under grant agreement N°101069594. + +### Added + +- Provide a dedicated route in the Version API to obtain the latest version of some terms ([#1028](https://github.com/OpenTermsArchive/engine/pull/1028#issuecomment-1847188776)) ## 0.34.0 - 2023-12-11 @@ -12,7 +20,7 @@ _Full changeset and discussions: [#1033](https://github.com/OpenTermsArchive/eng ### Added -- Expose versions data through the collection API ([#1003](https://github.com/OpenTermsArchive/engine/pull/1028)). When using Git as storage for versions, this API relies on the assumption that the commit date matches the author date, introduced in the engine in June 2022 ([#875](https://github.com/OpenTermsArchive/engine/pull/875)). If your collection was created before this date, inconsistencies in the API results may arise. You can verify if your version history includes commits with commit dates differing from author dates by executing the following command at the root of your versions repository: `git log --format="%H %ad %cI" --date=iso-strict | awk '{if ($2 != $3) print "Author date", $2, "and commit date", $3, "mismatch for commit", $1 }'`. You can correct the history with the command: `git rebase --committer-date-is-author-date $(git rev-list --max-parents=0 HEAD)`. Since the entire history will be rewritten, a force push may be required for distributed repositories +- Expose versions data through the collection API ([#1028](https://github.com/OpenTermsArchive/engine/pull/1028)). When using Git as storage for versions, this API relies on the assumption that the commit date matches the author date, introduced in the engine in June 2022 ([#875](https://github.com/OpenTermsArchive/engine/pull/875)). If your collection was created before this date, inconsistencies in the API results may arise. You can verify if your version history includes commits with commit dates differing from author dates by executing the following command at the root of your versions repository: `git log --format="%H %ad %cI" --date=iso-strict | awk '{if ($2 != $3) print "Author date", $2, "and commit date", $3, "mismatch for commit", $1 }'`. You can correct the history with the command: `git rebase --committer-date-is-author-date $(git rev-list --max-parents=0 HEAD)`. Since the entire history will be rewritten, a force push may be required for distributed repositories ### Changed