diff --git a/package-lock.json b/package-lock.json index cb8b2ca8..b7cf8ba2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dc-api-build", - "version": "2.1.1", + "version": "2.1.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "dc-api-build", - "version": "2.1.1", + "version": "2.1.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -5603,7 +5603,7 @@ }, "src": { "name": "dc-api", - "version": "2.1.1", + "version": "2.1.2", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "^2.0.1", diff --git a/package.json b/package.json index 103af0fa..1914ae76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dc-api-build", - "version": "2.1.1", + "version": "2.1.2", "description": "NUL Digital Collections API Build Environment", "repository": "https://github.com/nulib/dc-api-v2", "author": "nulib", diff --git a/src/api/request/pipeline.js b/src/api/request/pipeline.js index 264b7919..d60fdd1f 100644 --- a/src/api/request/pipeline.js +++ b/src/api/request/pipeline.js @@ -5,9 +5,12 @@ function filterFor(query, event) { const beUnpublished = { term: { published: false } }; const beRestricted = { term: { visibility: "Private" } }; - const filter = event.userToken.isReadingRoom() - ? { must: [matchTheQuery], must_not: [beUnpublished] } - : { must: [matchTheQuery], must_not: [beUnpublished, beRestricted] }; + let filter = { must: [matchTheQuery] }; + if (!event.userToken.isSuperUser()) { + filter.must_not = event.userToken.isReadingRoom() + ? [beUnpublished] + : [beUnpublished, beRestricted]; + } return { bool: filter }; } diff --git a/src/handlers/get-collection-by-id.js b/src/handlers/get-collection-by-id.js index 3961dcb2..fb723b4b 100644 --- a/src/handlers/get-collection-by-id.js +++ b/src/handlers/get-collection-by-id.js @@ -8,8 +8,11 @@ const getOpts = (event) => { const id = event.pathParameters.id; const allowPrivate = - event.userToken.isReadingRoom() || event.userToken.hasEntitlement(id); - const allowUnpublished = event.userToken.hasEntitlement(id); + event.userToken.isSuperUser() || + event.userToken.isReadingRoom() || + event.userToken.hasEntitlement(id); + const allowUnpublished = + event.userToken.isSuperUser() || event.userToken.hasEntitlement(id); return { allowPrivate, allowUnpublished }; }; diff --git a/src/handlers/get-file-set-by-id.js b/src/handlers/get-file-set-by-id.js index 3f35c37b..48d6e3e5 100644 --- a/src/handlers/get-file-set-by-id.js +++ b/src/handlers/get-file-set-by-id.js @@ -7,7 +7,9 @@ const opensearchResponse = require("../api/response/opensearch"); */ exports.handler = wrap(async (event) => { const id = event.pathParameters.id; - const allowPrivate = event.userToken.isReadingRoom(); - const esResponse = await getFileSet(id, { allowPrivate }); + const allowPrivate = + event.userToken.isSuperUser() || event.userToken.isReadingRoom(); + const allowUnpublished = event.userToken.isSuperUser(); + const esResponse = await getFileSet(id, { allowPrivate, allowUnpublished }); return await opensearchResponse.transform(esResponse); }); diff --git a/src/handlers/get-thumbnail.js b/src/handlers/get-thumbnail.js index 03182289..11efb52a 100644 --- a/src/handlers/get-thumbnail.js +++ b/src/handlers/get-thumbnail.js @@ -31,7 +31,8 @@ function validateRequest(event) { } const getThumbnail = async (id, aspect, size, event) => { - const allowUnpublished = event.userToken.hasEntitlement(id); + const allowUnpublished = + event.userToken.isSuperUser() || event.userToken.hasEntitlement(id); const allowPrivate = allowUnpublished || event.userToken.isReadingRoom(); let esResponse; diff --git a/src/handlers/get-work-by-id.js b/src/handlers/get-work-by-id.js index 12c4f7f1..5cc2ef88 100644 --- a/src/handlers/get-work-by-id.js +++ b/src/handlers/get-work-by-id.js @@ -10,8 +10,11 @@ exports.handler = wrap(async (event) => { const id = event.pathParameters.id; const allowPrivate = - event.userToken.isReadingRoom() || event.userToken.hasEntitlement(id); - const allowUnpublished = event.userToken.hasEntitlement(id); + event.userToken.isSuperUser() || + event.userToken.isReadingRoom() || + event.userToken.hasEntitlement(id); + const allowUnpublished = + event.userToken.isSuperUser() || event.userToken.hasEntitlement(id); const esResponse = await getWork(id, { allowPrivate, allowUnpublished }); diff --git a/src/handlers/oai.js b/src/handlers/oai.js index da98b4b1..d4909ad8 100644 --- a/src/handlers/oai.js +++ b/src/handlers/oai.js @@ -28,7 +28,7 @@ function invalidDateParameters(verb, dates) { } /** - * A function to support the OAI-PMH harvesting specfication + * A function to support the "OAI-PMH" harvesting specfication */ exports.handler = wrap(async (event) => { const url = `${baseUrl(event)}oai`; diff --git a/src/handlers/oai/search.js b/src/handlers/oai/search.js index 74088375..5dd349e6 100644 --- a/src/handlers/oai/search.js +++ b/src/handlers/oai/search.js @@ -44,9 +44,10 @@ async function oaiSearch(dates, set, size = 250) { { term: { visibility: "Public" } }, range, ], - ...(set && { must: [{ term: { "collection.id": set } }] }), }, }; + if (set) query.bool.must.push({ term: { "collection.id": set } }); + const body = { size, query, diff --git a/src/handlers/oai/verbs.js b/src/handlers/oai/verbs.js index 9a11fbbf..7f982a56 100644 --- a/src/handlers/oai/verbs.js +++ b/src/handlers/oai/verbs.js @@ -23,7 +23,7 @@ const oaiAttributes = { xmlns: "http://www.openarchives.org/OAI/2.0/", "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", "xsi:schemaLocation": - "http://www.openarchives.org/OAI/2.0/\nhttp://www.openarchives.org/OAI/2.0/OAI-PMH.xsd", + "http://www.openarchives.org/OAI/2.0/\nhttp://www.openarchives.org/OAI/2.0/OAI_PMH.xsd", }; function header(work) { @@ -32,11 +32,10 @@ function header(work) { datestamp: work.modified_date, }; - if (Object.keys(work.collection).length > 0) { + if (work?.collection && Object.keys(work.collection).length > 0) { fields = { ...fields, setSpec: work.collection.id, - setName: work.collection.title, }; } @@ -80,7 +79,7 @@ const getRecord = async (url, id) => { const work = JSON.parse(esResponse.body)._source; const record = transform(work); const document = { - OAI_PMH: { + "OAI-PMH": { _attributes: oaiAttributes, responseDate: new Date().toISOString(), request: { @@ -91,7 +90,7 @@ const getRecord = async (url, id) => { }, _text: url, }, - GetRecord: { ...record }, + GetRecord: { record: record }, }, }; return output(document); @@ -107,7 +106,7 @@ const getRecord = async (url, id) => { const identify = async (url) => { let earliestDatestamp = await earliestRecord(); const obj = { - OAI_PMH: { + "OAI-PMH": { _attributes: oaiAttributes, responseDate: new Date().toISOString(), request: { @@ -120,9 +119,10 @@ const identify = async (url) => { repositoryName: "Northwestern University Libraries", baseURL: url, protocolVersion: "2.0", + adminEmail: "repository@northwestern.edu", earliestDatestamp: earliestDatestamp, deletedRecord: "no", - granularity: "YYYY-MM-DDThh:mm:ss.ffffffZ", + granularity: "YYYY-MM-DDThh:mm:ssZ", }, }, }; @@ -166,7 +166,7 @@ const listIdentifiers = async ( _text: scrollId, }; const obj = { - OAI_PMH: { + "OAI-PMH": { _attributes: oaiAttributes, responseDate: new Date().toISOString(), request: { @@ -177,7 +177,7 @@ const listIdentifiers = async ( _text: url, }, ListIdentifiers: { - headers: { header: headers }, + header: headers, resumptionToken: resumptionTokenElement, }, }, @@ -203,7 +203,7 @@ const listIdentifiers = async ( const listMetadataFormats = (url) => { const obj = { - OAI_PMH: { + "OAI-PMH": { _attributes: oaiAttributes, responseDate: new Date().toISOString(), request: { @@ -261,7 +261,7 @@ const listRecords = async ( _text: scrollId, }; const obj = { - OAI_PMH: { + "OAI-PMH": { _attributes: oaiAttributes, responseDate: new Date().toISOString(), request: { @@ -303,11 +303,14 @@ const listSets = async (url) => { const sets = hits.map((hit) => { const collection = hit._source; - return { setSpec: collection.id, setName: collection.title }; + return { + setSpec: collection.id, + setName: collection.title, + }; }); const obj = { - OAI_PMH: { + "OAI-PMH": { _attributes: oaiAttributes, responseDate: new Date().toISOString(), request: { diff --git a/src/handlers/oai/xml-transformer.js b/src/handlers/oai/xml-transformer.js index f9960346..c9d2323f 100644 --- a/src/handlers/oai/xml-transformer.js +++ b/src/handlers/oai/xml-transformer.js @@ -8,12 +8,12 @@ const declaration = { const invalidOaiRequest = (oaiCode, message, statusCode = 400) => { const obj = { - OAI_PMH: { + "OAI-PMH": { _attributes: { xmlns: "http://www.openarchives.org/OAI/2.0/", "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", "xsi:schemaLocation": - "http://www.openarchives.org/OAI/2.0/\nhttp://www.openarchives.org/OAI/2.0/OAI-PMH.xsd", + "http://www.openarchives.org/OAI/2.0/\nhttp://www.openarchives.org/OAI/2.0/OAI_PMH.xsd", }, responseDate: new Date().toISOString(), error: { diff --git a/src/helpers.js b/src/helpers.js index 839b632e..7a5d6bf3 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -91,8 +91,14 @@ function decodeEventBody(event) { return event; } +function getEventToken(event) { + const bearerRe = /^Bearer (?.+)$/; + const result = bearerRe.exec(event.headers.authorization); + return result?.groups?.token || event.cookieObject[apiTokenName()]; +} + function decodeToken(event) { - const existingToken = event.cookieObject[apiTokenName()]; + const existingToken = getEventToken(event); try { event.userToken = new ApiToken(existingToken); diff --git a/src/package-lock.json b/src/package-lock.json index 8d0ac1b9..3783889b 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "dc-api", - "version": "2.1.1", + "version": "2.1.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "dc-api", - "version": "2.1.1", + "version": "2.1.2", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "^2.0.1", diff --git a/src/package.json b/src/package.json index 98233f04..daf92f36 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,6 @@ { "name": "dc-api", - "version": "2.1.1", + "version": "2.1.2", "description": "NUL Digital Collections API", "repository": "https://github.com/nulib/dc-api-v2", "author": "nulib", diff --git a/test/fixtures/mocks/unpublished-work-1234.json b/test/fixtures/mocks/unpublished-work-1234.json index 6dd41a07..2498055b 100644 --- a/test/fixtures/mocks/unpublished-work-1234.json +++ b/test/fixtures/mocks/unpublished-work-1234.json @@ -8,6 +8,10 @@ "id": "1234", "api_model": "Work", "published": false, - "visibility": "Public" + "visibility": "Public", + "representative_file_set": { + "fileSetId": "5678", + "url": "https://index.test.library.northwestern.edu/iiif/2/mbk-dev/5678" + } } } diff --git a/test/integration/get-doc.test.js b/test/integration/get-doc.test.js index 3bb992a2..ff9203a6 100644 --- a/test/integration/get-doc.test.js +++ b/test/integration/get-doc.test.js @@ -4,6 +4,8 @@ const chai = require("chai"); const expect = chai.expect; chai.use(require("chai-http")); +const ApiToken = requireSource("api/api-token"); + describe("Doc retrieval routes", () => { helpers.saveEnvironment(); const mock = helpers.mockIndex(); @@ -109,6 +111,23 @@ describe("Doc retrieval routes", () => { expect(resultBody.data.api_model).to.eq("Collection"); expect(resultBody.data.id).to.eq("1234"); }); + + it("403s a private collection", async () => { + const event = helpers + .mockEvent("GET", "/collections/{id}") + .pathParams({ id: 1234 }) + .render(); + + mock + .get("/dc-v2-collection/_doc/1234") + .reply( + 200, + helpers.testFixture("mocks/collection-1234-private-published.json") + ); + + const result = await handler(event); + expect(result.statusCode).to.eq(403); + }); }); describe("GET /file-sets/{id}", () => { @@ -134,5 +153,110 @@ describe("Doc retrieval routes", () => { expect(resultBody.data.api_model).to.eq("FileSet"); expect(resultBody.data.id).to.eq("1234"); }); + + it("403s a private file-set", async () => { + const event = helpers + .mockEvent("GET", "/file-sets/{id}") + .pathParams({ id: 1234 }) + .render(); + + mock + .get("/dc-v2-file-set/_doc/1234") + .reply(200, helpers.testFixture("mocks/fileset-restricted-1234.json")); + + const result = await handler(event); + expect(result.statusCode).to.eq(403); + }); + }); + + describe("Superuser", () => { + helpers.saveEnvironment(); + let event; + let token; + + beforeEach(() => { + process.env.API_TOKEN_SECRET = "abcdef"; + token = new ApiToken().superUser().sign(); + }); + + describe("works", () => { + const { handler } = requireSource("handlers/get-work-by-id"); + + beforeEach(() => { + event = helpers + .mockEvent("GET", "/works/{id}") + .headers({ + authorization: `Bearer ${token}`, + }) + .pathParams({ id: 1234 }) + .render(); + }); + + it("returns an unpublished work", async () => { + mock + .get("/dc-v2-work/_doc/1234") + .reply(200, helpers.testFixture("mocks/unpublished-work-1234.json")); + + const result = await handler(event); + expect(result.statusCode).to.eq(200); + }); + + it("returns a private work", async () => { + mock + .get("/dc-v2-work/_doc/1234") + .reply(200, helpers.testFixture("mocks/private-work-1234.json")); + + const result = await handler(event); + expect(result.statusCode).to.eq(200); + }); + }); + + describe("collections", () => { + const { handler } = requireSource("handlers/get-collection-by-id"); + + it("returns a private collection", async () => { + const event = helpers + .mockEvent("GET", "/collections/{id}") + .headers({ + authorization: `Bearer ${token}`, + }) + .pathParams({ id: 1234 }) + .render(); + + mock + .get("/dc-v2-collection/_doc/1234") + .reply( + 200, + helpers.testFixture("mocks/collection-1234-private-published.json") + ); + + const result = await handler(event); + expect(result.statusCode).to.eq(200); + }); + }); + + describe("file sets", () => { + const { handler } = requireSource("handlers/get-file-set-by-id"); + + it("returns a private file-set", async () => { + const event = helpers + .mockEvent("GET", "/file-sets/{id}") + .headers({ + authorization: `Bearer ${token}`, + }) + .pathParams({ id: 1234 }) + .render(); + + mock + .get("/dc-v2-file-set/_doc/1234") + .reply( + 200, + helpers.testFixture("mocks/fileset-restricted-1234.json") + ); + + const result = await handler(event); + expect(result.statusCode).to.eq(200); + }); + }); }); }); diff --git a/test/integration/get-thumbnail.test.js b/test/integration/get-thumbnail.test.js index a3a073b3..92d3cfd8 100644 --- a/test/integration/get-thumbnail.test.js +++ b/test/integration/get-thumbnail.test.js @@ -198,6 +198,49 @@ describe("Thumbnail routes", () => { }); }); + describe("Superuser", () => { + let event; + + beforeEach(() => { + const token = new ApiToken().superUser().sign(); + event = helpers + .mockEvent("GET", "/works/{id}/thumbnail") + .headers({ + authorization: `Bearer ${token}`, + origin: "https://test.example.edu/", + }) + .pathParams({ id: 1234 }); + }); + + it("retrieves thumbnail even if the work is private", async () => { + mock + .get("/dc-v2-work/_doc/1234") + .reply(200, helpers.testFixture("mocks/private-work-1234.json")); + mock + .get("/iiif/2/mbk-dev/5678/full/!300,300/0/default.jpg") + .reply(200, helpers.testFixture("mocks/thumbnail_full.jpg"), { + "Content-Type": "image/jpeg", + }); + + const result = await handler(event.render()); + expect(result.statusCode).to.eq(200); + }); + + it("retrieves thumbnail even if the work is unpublished", async () => { + mock + .get("/dc-v2-work/_doc/1234") + .reply(200, helpers.testFixture("mocks/unpublished-work-1234.json")); + mock + .get("/iiif/2/mbk-dev/5678/full/!300,300/0/default.jpg") + .reply(200, helpers.testFixture("mocks/thumbnail_full.jpg"), { + "Content-Type": "image/jpeg", + }); + + const result = await handler(event.render()); + expect(result.statusCode).to.eq(200); + }); + }); + describe("QueryString parameters", () => { const event = helpers .mockEvent("GET", "/works/{id}/thumbnail") diff --git a/test/integration/oai.test.js b/test/integration/oai.test.js index 866e44d2..82e26345 100644 --- a/test/integration/oai.test.js +++ b/test/integration/oai.test.js @@ -29,16 +29,12 @@ describe("Oai routes", () => { expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - const header = resultBody.OAI_PMH.GetRecord.header; + const header = resultBody["OAI-PMH"].GetRecord.record.header; expect(header) .to.be.an("object") - .and.to.deep.include.keys( - "identifier", - "datestamp", - "setSpec", - "setName" - ); - const metadata = resultBody.OAI_PMH.GetRecord.metadata["oai_dc:dc"]; + .and.to.deep.include.keys("identifier", "datestamp", "setSpec"); + const metadata = + resultBody["OAI-PMH"].GetRecord.record.metadata["oai_dc:dc"]; expect(metadata) .to.be.an("object") .and.to.deep.include.keys( @@ -67,10 +63,10 @@ describe("Oai routes", () => { expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error["_attributes"]["code"]).to.eq( + expect(resultBody["OAI-PMH"].error["_attributes"]["code"]).to.eq( "badArgument" ); - expect(resultBody.OAI_PMH.error["_text"]).to.eq( + expect(resultBody["OAI-PMH"].error["_text"]).to.eq( "You must supply an identifier for GetRecord requests" ); }); @@ -86,10 +82,10 @@ describe("Oai routes", () => { expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error["_attributes"]["code"]).to.eq( + expect(resultBody["OAI-PMH"].error["_attributes"]["code"]).to.eq( "idDoesNotExist" ); - expect(resultBody.OAI_PMH.error["_text"]).to.eq( + expect(resultBody["OAI-PMH"].error["_text"]).to.eq( "The specified record does not exist" ); }); @@ -104,7 +100,7 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(200); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.ListRecords.record) + expect(resultBody["OAI-PMH"].ListRecords.record) .to.be.an("array") .and.to.have.lengthOf(12); }); @@ -117,10 +113,10 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(400); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error["_attributes"]["code"]).to.eq( + expect(resultBody["OAI-PMH"].error["_attributes"]["code"]).to.eq( "badArgument" ); - expect(resultBody.OAI_PMH.error["_text"]).to.eq( + expect(resultBody["OAI-PMH"].error["_text"]).to.eq( "Invalid date -- make sure that 'from' or 'until' parameters are formatted as: 'YYYY-MM-DDThh:mm:ss.ffffffZ'" ); }); @@ -136,7 +132,7 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(200); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.ListRecords.record) + expect(resultBody["OAI-PMH"].ListRecords.record) .to.be.an("array") .and.to.have.lengthOf(12); }); @@ -161,7 +157,7 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(200); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - const resumptionToken = resultBody.OAI_PMH.ListRecords.resumptionToken; + const resumptionToken = resultBody["OAI-PMH"].ListRecords.resumptionToken; expect(resumptionToken).to.not.haveOwnProperty("_text"); }); @@ -179,10 +175,10 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(401); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error["_attributes"]["code"]).to.eq( + expect(resultBody["OAI-PMH"].error["_attributes"]["code"]).to.eq( "badResumptionToken" ); - expect(resultBody.OAI_PMH.error["_text"]).to.eq( + expect(resultBody["OAI-PMH"].error["_text"]).to.eq( "Your resumptionToken is no longer valid" ); }); @@ -201,10 +197,10 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(400); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error["_attributes"]["code"]).to.eq( + expect(resultBody["OAI-PMH"].error["_attributes"]["code"]).to.eq( "badRequest" ); - expect(resultBody.OAI_PMH.error["_text"]).to.eq( + expect(resultBody["OAI-PMH"].error["_text"]).to.eq( "An error occurred processing the ListRecords request" ); }); @@ -219,10 +215,10 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(400); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error["_attributes"]["code"]).to.eq( + expect(resultBody["OAI-PMH"].error["_attributes"]["code"]).to.eq( "badArgument" ); - expect(resultBody.OAI_PMH.error["_text"]).to.eq( + expect(resultBody["OAI-PMH"].error["_text"]).to.eq( "Missing required metadataPrefix argument" ); }); @@ -237,7 +233,7 @@ describe("Oai routes", () => { expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); const listMetadataFormatsElement = - resultBody.OAI_PMH.ListMetadataFormats.metadataFormat; + resultBody["OAI-PMH"].ListMetadataFormats.metadataFormat; expect(listMetadataFormatsElement.metadataNamespace._text).to.eq( "http://www.openarchives.org/OAI/2.0/oai_dc/" ); @@ -254,10 +250,10 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(400); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error._attributes).to.include({ + expect(resultBody["OAI-PMH"].error._attributes).to.include({ code: "badArgument", }); - expect(resultBody.OAI_PMH.error._text).to.eq("Missing required verb"); + expect(resultBody["OAI-PMH"].error._text).to.eq("Missing required verb"); }); it("supports the Identify verb", async () => { @@ -286,14 +282,12 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(200); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - const identifyElement = resultBody.OAI_PMH.Identify; + const identifyElement = resultBody["OAI-PMH"].Identify; expect(identifyElement.earliestDatestamp._text).to.eq( "2022-11-22T20:36:00.581418Z" ); expect(identifyElement.deletedRecord._text).to.eq("no"); - expect(identifyElement.granularity._text).to.eq( - "YYYY-MM-DDThh:mm:ss.ffffffZ" - ); + expect(identifyElement.granularity._text).to.eq("YYYY-MM-DDThh:mm:ssZ"); }); it("supports the ListRecords verb", async () => { @@ -308,7 +302,7 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(200); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.ListRecords.record) + expect(resultBody["OAI-PMH"].ListRecords.record) .to.be.an("array") .to.have.lengthOf(12); }); @@ -325,7 +319,7 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(200); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.ListSets.set) + expect(resultBody["OAI-PMH"].ListSets.set) .to.be.an("array") .and.to.have.lengthOf(3); }); @@ -342,10 +336,10 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(500); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error._attributes).to.include({ + expect(resultBody["OAI-PMH"].error._attributes).to.include({ code: "badRequest", }); - expect(resultBody.OAI_PMH.error._text).to.eq( + expect(resultBody["OAI-PMH"].error._text).to.eq( "An error occurred processing the ListSets request" ); }); @@ -363,7 +357,7 @@ describe("Oai routes", () => { expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); const resumptionToken = - resultBody.OAI_PMH.ListIdentifiers.resumptionToken; + resultBody["OAI-PMH"].ListIdentifiers.resumptionToken; expect(resumptionToken["_text"]).to.have.lengthOf(120); }); @@ -376,10 +370,10 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(400); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error._attributes).to.include({ + expect(resultBody["OAI-PMH"].error._attributes).to.include({ code: "badArgument", }); - expect(resultBody.OAI_PMH.error._text).to.eq( + expect(resultBody["OAI-PMH"].error._text).to.eq( "Missing required metadataPrefix argument" ); }); @@ -404,9 +398,10 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(200); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.ListIdentifiers.headers) - .to.be.an("array") - .to.have.lengthOf(1); + console.log(resultBody["OAI-PMH"].ListIdentifiers.header); + expect(resultBody["OAI-PMH"].ListIdentifiers.header) + .to.be.an("object") + .to.have.keys(["identifier", "datestamp", "setSpec"]); }); it("uses an empty resumptionToken to tell harvesters that list requests are complete", async () => { @@ -436,7 +431,7 @@ describe("Oai routes", () => { expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); const resumptionToken = - resultBody.OAI_PMH.ListIdentifiers.resumptionToken; + resultBody["OAI-PMH"].ListIdentifiers.resumptionToken; expect(resumptionToken).to.not.haveOwnProperty("_text"); }); @@ -460,10 +455,10 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(401); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error["_attributes"]["code"]).to.eq( + expect(resultBody["OAI-PMH"].error["_attributes"]["code"]).to.eq( "badResumptionToken" ); - expect(resultBody.OAI_PMH.error["_text"]).to.eq( + expect(resultBody["OAI-PMH"].error["_text"]).to.eq( "Your resumptionToken is no longer valid" ); }); @@ -488,10 +483,10 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(400); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error["_attributes"]["code"]).to.eq( + expect(resultBody["OAI-PMH"].error["_attributes"]["code"]).to.eq( "badRequest" ); - expect(resultBody.OAI_PMH.error["_text"]).to.eq( + expect(resultBody["OAI-PMH"].error["_text"]).to.eq( "An error occurred processing the ListIdentifiers request" ); }); @@ -505,7 +500,7 @@ describe("Oai routes", () => { expect(result.statusCode).to.eq(400); expect(result).to.have.header("content-type", /application\/xml/); const resultBody = convert.xml2js(result.body, xmlOpts); - expect(resultBody.OAI_PMH.error._attributes).to.include({ + expect(resultBody["OAI-PMH"].error._attributes).to.include({ code: "badVerb", }); }); diff --git a/test/unit/api/helpers.test.js b/test/unit/api/helpers.test.js index bf9833f0..15bac641 100644 --- a/test/unit/api/helpers.test.js +++ b/test/unit/api/helpers.test.js @@ -300,23 +300,57 @@ describe("helpers", () => { }); describe("decodeToken", async () => { - it("adds the decoded token to the event", () => { + it("identifies a cookie token", () => { const token = new ApiToken().user({ uid: "abc123" }).sign(); - - const event = helpers + let result = helpers .mockEvent("GET", "/works/{id}/") .pathParams({ id: 1234 }) .headers({ - Cookie: `${process.env.API_TOKEN_NAME}=${token}`, + cookie: `${process.env.API_TOKEN_NAME}=${token}`, }) .render(); - let result = objectifyCookies(event); - result = decodeToken(event); + result = objectifyCookies(result); + result = decodeToken(result); + expect(result.userToken.token).to.include({ + sub: "abc123", + }); + expect(result.userToken.token).to.not.have.property("isReadingRoom"); + }); + it("identifies a bearer token", () => { + const token = new ApiToken().user({ uid: "abc123" }).sign(); + let result = helpers + .mockEvent("GET", "/works/{id}/") + .pathParams({ id: 1234 }) + .headers({ + authorization: `Bearer ${token}`, + }) + .render(); + result = objectifyCookies(result); + result = decodeToken(result); expect(result.userToken.token).to.include({ sub: "abc123", }); expect(result.userToken.token).to.not.have.property("isReadingRoom"); }); + it("prioritizes a bearer token over a cookie token", () => { + const cookieToken = new ApiToken().user({ uid: "abc123" }).sign(); + const bearerToken = new ApiToken().user({ uid: "def456" }).sign(); + + let result = helpers + .mockEvent("GET", "/works/{id}/") + .pathParams({ id: 1234 }) + .headers({ + authorization: `Bearer ${bearerToken}`, + cookie: `${process.env.API_TOKEN_NAME}=${cookieToken}`, + }) + .render(); + result = objectifyCookies(result); + result = decodeToken(result); + expect(result.userToken.token).to.include({ + sub: "def456", + }); + expect(result.userToken.token).to.not.have.property("isReadingRoom"); + }); it("adds an anonymous token to the event if the token is expired", () => { const payload = { iss: "https://example.com", @@ -327,16 +361,15 @@ describe("helpers", () => { email: "user@example.com", }; const token = jwt.sign(payload, process.env.API_TOKEN_SECRET); - - const event = helpers + let result = helpers .mockEvent("GET", "/works/{id}/") .pathParams({ id: 1234 }) .headers({ - Cookie: `${process.env.API_TOKEN_NAME}=${token}`, + cookie: `${process.env.API_TOKEN_NAME}=${token}`, }) .render(); - let result = objectifyCookies(event); - result = decodeToken(event); + result = objectifyCookies(result); + result = decodeToken(result); expect(result.userToken.token).to.not.include({ sub: "abc123", }); diff --git a/test/unit/api/request/pipeline.test.js b/test/unit/api/request/pipeline.test.js index 63f03157..f7e039f4 100644 --- a/test/unit/api/request/pipeline.test.js +++ b/test/unit/api/request/pipeline.test.js @@ -73,4 +73,32 @@ describe("RequestPipeline", () => { }); }); }); + + describe("superuser", () => { + it("filters out private results by default", () => { + event.userToken = new ApiToken(); + + // process.env.READING_ROOM_IPS = "192.168.0.1,172.16.10.2"; + const result = pipeline.authFilter(helpers.preprocess(event)); + expect(result.searchContext.size).to.eq(50); + expect(result.searchContext.query.bool.must).to.include( + requestBody.query + ); + expect(result.searchContext.query.bool.must_not).to.deep.include( + { term: { visibility: "Private" } }, + { term: { published: false } } + ); + }); + + it("includes private results if the user is in the reading room", () => { + event.userToken = new ApiToken().superUser(); + + const result = pipeline.authFilter(helpers.preprocess(event)); + expect(result.searchContext.size).to.eq(50); + expect(result.searchContext.query.bool.must).to.include( + requestBody.query + ); + expect(result.searchContext.query.bool).not.to.have.any.keys("must_not"); + }); + }); });