Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Orcid Api test #320

Draft
wants to merge 9 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@ VSCODE_ACCESS_TOKEN=
NODES_MEDIA_SERVER_URL=http://host.docker.internal:5454

REPO_SERVICE_SECRET_KEY="m8sIy5BPygBcX3+ZmMVuAA10k6w59BSCZd+Z5+VLYm4="
REPO_SERVER_URL=http://host.docker.internal:5485
REPO_SERVER_URL=http://host.docker.internal:5485
DEBUG=nock.desci-server
1 change: 1 addition & 0 deletions desci-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
"eslint-plugin-prettier": "^5.0.1",
"lint-staged": "^11.1.2",
"mocha": "^10.2.0",
"nock": "^13.5.4",
"nodemon": "^2.0.22",
"nyc": "^15.1.0",
"prettier": "^3.1.1",
Expand Down
114 changes: 102 additions & 12 deletions desci-server/src/services/orcid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ const ORCID_DOMAIN = process.env.ORCID_API_DOMAIN || 'sandbox.orcid.org';
type Claim = Awaited<ReturnType<typeof attestationService.getProtectedNodeClaims>>[number];
const logger = parentLogger.child({ module: 'ORCIDApiService' });

/**
* Service class for interfacing with ORCID /works API
* Handles updating orcid work profile entries for users with orcid
* linked to their profiles
*/
class OrcidApiService {
baseUrl: string;

Expand All @@ -25,6 +30,12 @@ class OrcidApiService {
logger.info({ url: this.baseUrl }, 'Init ORCID Service');
}

/**
* Query user orcid access token from the database and refreshes
* tokens if needed and update database entry valid token
* @param userId unique user identifier
* @returns a valid access token string
*/
private async getAccessToken(userId: number) {
const authTokens = await prisma.authToken.findMany({
where: {
Expand Down Expand Up @@ -76,18 +87,28 @@ class OrcidApiService {
});
logger.info({ status: response.status, statusText: response.statusText, data }, 'REFRESH TOKEN RESPONSE');
} else {
logger.info(
logger.error(
{ status: response.status, statusText: response.statusText, BODY: await response.json() },
'REFRESH TOKEN ERROR',
);
}
} catch (err) {
logger.info({ err }, 'ORCID REFRESH TOKEN ERROR');
logger.error({ err }, 'ORCID REFRESH TOKEN ERROR');
}

return authToken.accessToken;
}

/**
* Remove an attestation from user's ORCID work profile
* If user has no verified protected attestations, remove research node
* work entry
* @param {Object} argument - The claim argument to process
* @param {number} argument.claimId - The ID of the node attestation to remove
* @param {string} argument.nodeUuid - The uuid of the research node
* @param {string} argument.orcid - The ORCID identifier of the user
* @returns
*/
async removeClaimRecord({ claimId, nodeUuid, orcid }: { claimId: number; nodeUuid: string; orcid: string }) {
const putCode = await prisma.orcidPutCodes.findFirst({
where: {
Expand Down Expand Up @@ -132,6 +153,15 @@ class OrcidApiService {
logger.info({ userId: user.id, CLAIMS: claims.length, nodeUuid }, '[ORCID::DELETE]:: FINISH');
}

/**
* Execute http request to remove ORCID work entry
* and remove the associated putCode from the database
* @param {Object} argument - The claim argument to process
* @param {string} argument.orcid - The ORCID identifier of the user
* @param {number} argument.putCode - The ORCID /work record putCode
* @param {string} argument.authToken - A valid user orcid access token
* @returns
*/
async removeWorkRecord({ putCode, authToken, orcid }: { orcid: string; putCode: OrcidPutCodes; authToken: string }) {
const code = putCode.putcode;
const url = `${this.baseUrl}/${orcid}/work${code ? '/' + code : ''}`;
Expand Down Expand Up @@ -172,6 +202,14 @@ class OrcidApiService {
);
}

/**
* Update ORCID work summary of a user
* Retrieve a validated protected attestations and post each as a work entry
* Retrieve Research Node with uuid {nodeUuid} and post a work entry
* @param nodeUuid - Research node uuid
* @param orcid - ORCID identifier
* @returns
*/
async postWorkRecord(nodeUuid: string, orcid: string) {
try {
const user = await prisma.user.findUnique({ where: { orcid } });
Expand Down Expand Up @@ -225,6 +263,18 @@ class OrcidApiService {
}
}

/**
* Execute http request to post/update ORCID work entry for a node
* and insert/update the associated putCode in the database
* @param {Object} argument - The Research Node details object
* @param {Object} argument.manifest - The node's manifest
* @param {string} argument.publicationDate - The last publish datetime string in rfc3339 format
* @param {string} argument.uuid - Unique uuid identifier of the node to update
* @param {number} argument.userId - ID of the user (node owner)
* @param {string} argument.authToken - A valid user orcid access token
* @param {string} argument.orcid - The ORCID identifier of the user
* @param {number} argument.nodeVersion - The latest version of the research node
*/
async putNodeWorkRecord({
manifest,
publicationDate,
Expand All @@ -248,7 +298,7 @@ class OrcidApiService {
});
const putCode = orcidPutCode?.putcode;

let data = generateRootWorkRecord({ manifest, publicationDate, nodeVersion, putCode });
let data = generateNodeWorkRecord({ manifest, publicationDate, nodeVersion, putCode });
data = data.replace(/\\"/g, '"');

const url = `${this.baseUrl}/${orcid}/work${putCode ? '/' + putCode : ''}`;
Expand Down Expand Up @@ -312,10 +362,23 @@ class OrcidApiService {
);
}
} catch (err) {
logger.info({ err }, '[ORCID_API_SERVICE]::NODE API Error Response');
logger.error({ err }, '[ORCID_API_SERVICE]::NODE API Error Response');
}
}

/**
* Execute http request to post/update ORCID work entry for an attestation
* and insert/update the associated putCode in the database
* @param {Object} argument - The Research Node details object
* @param {string} argument.authToken - A valid user orcid access token
* @param {Object} argument.claim - The claim object retrieved from the database
* @param {Object} argument.manifest - The node's manifest
* @param {number} argument.nodeVersion - The latest version of the research node
* @param {string} argument.orcid - The ORCID identifier of the user
* @param {string} argument.publicationDate - The last publish datetime string in rfc3339 format
* @param {string} argument.uuid - Unique uuid identifier of the node to update
* @param {number} argument.userId - ID of the user (node owner)
*/
async putClaimWorkRecord({
manifest,
publicationDate,
Expand Down Expand Up @@ -410,20 +473,27 @@ class OrcidApiService {
'ORCID CLAIM RECORD UPDATED',
);
} else {
logger.info(
logger.error(
{ status: response.status, response, body: await response.text() },
'[ORCID_API_SERVICE]::ORCID CLAIM API ERROR',
);
}
} catch (err) {
logger.info({ err }, '[ORCID_API_SERVICE]::CLAIM API Error Response');
logger.error({ err }, '[ORCID_API_SERVICE]::CLAIM API Error Response');
}
}
}

const orcidApiService = new OrcidApiService();
export default orcidApiService;

/**
* Generate an ORCID work summary xml string based for an attestation/claim
* Model Reference https://github.com/ORCID/orcid-model/blob/master/src/main/resources/record_3.0/work-3.0.xsd
* @param {Object} argument - The Research Node details object
* @param {Object} argument.claim - The claim object retrieved from the database
* @param {Object} argument.manifest - The node's manifest
* @param {number} argument.nodeVersion - The latest version of the research node
* @param {string} argument.publicationDate - The last publish datetime string in rfc3339 format
* @param {number=} argument.putCode - The ORCID /work record putCode
*/
const generateClaimWorkRecord = ({
manifest,
putCode,
Expand Down Expand Up @@ -483,9 +553,20 @@ const generateClaimWorkRecord = ({
`
);
};

const zeropad = (data: string) => (data.length < 2 ? `0${data}` : data);

const generateRootWorkRecord = ({
/**
* Generate an ORCID work summary xml string based for a research Node
* Model Reference https://github.com/ORCID/orcid-model/blob/master/src/main/resources/record_3.0/work-3.0.xsd
* @param {Object} argument - The Research Node details object
* @param {Object} argument.manifest - The node's manifest
* @param {number} argument.nodeVersion - The latest version of the research node
* @param {string} argument.publicationDate - The last publish datetime string in rfc3339 format
* @param {number=} argument.putCode - The ORCID /work record putCode
* @returns {string} xml string of the constructed work summary data
*/
const generateNodeWorkRecord = ({
manifest,
nodeVersion,
putCode,
Expand All @@ -495,7 +576,7 @@ const generateRootWorkRecord = ({
nodeVersion: number;
putCode?: string;
publicationDate: string;
}) => {
}): string => {
const codeAttr = putCode ? 'put-code="' + putCode + '"' : '';
const workType = 'preprint';
const [month, day, year] = publicationDate.split('-');
Expand Down Expand Up @@ -530,7 +611,13 @@ const generateRootWorkRecord = ({
);
};

const generateContributors = (authors: ResearchObjectV1Author[]) => {
/**
* Generate an ORCID work contributors xml string
* Model Reference https://github.com/ORCID/orcid-model/blob/master/src/main/resources/record_3.0/work-3.0.xsd#L160
* @param authors[] - A list of ResearchObjectV1Author entries
* @returns {string} xml string of the constructed contributor data
*/
const generateContributors = (authors: ResearchObjectV1Author[]): string => {
const contributors =
authors?.length > 0
? `<work:contributors>
Expand Down Expand Up @@ -558,3 +645,6 @@ const generateContributors = (authors: ResearchObjectV1Author[]) => {
: ``;
return contributors;
};

const orcidApiService = new OrcidApiService();
export default orcidApiService;
61 changes: 60 additions & 1 deletion desci-server/test/integration/Attestation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Annotation,
Attestation,
AttestationVersion,
AuthTokenSource,
CommunityMember,
CommunityMembershipRole,
DesciCommunity,
Expand All @@ -18,6 +19,7 @@ import {
} from '@prisma/client';
import { assert, expect } from 'chai';
import jwt from 'jsonwebtoken';
import nock from 'nock';
import request from 'supertest';

import { prisma } from '../../src/client.js';
Expand Down Expand Up @@ -2088,7 +2090,7 @@ describe('Attestations Service', async () => {
});
});

describe('Protected Attestation Verification', async () => {
describe.only('Protected Attestation Verification', async () => {
let openCodeClaim: NodeAttestation;
let openDataClaim: NodeAttestation;
let node: Node;
Expand All @@ -2105,6 +2107,9 @@ describe('Attestations Service', async () => {
MemberJwtToken2: string,
memberAuthHeaderVal1: string,
memberAuthHeaderVal2: string;
const mockPutCode = '1926486';
const ORCID_ID = '0000-0000-1111-090X';
const NOCK_URL = new URL('https://api.sandbox.orcid.org');

before(async () => {
node = nodes[0];
Expand All @@ -2121,6 +2126,26 @@ describe('Attestations Service', async () => {
claimerId: author.id,
});

// add oricd to user profile
await prisma.user.update({
where: {
id: author.id,
},
data: {
orcid: ORCID_ID,
},
});

// insert mock orcid auth token
await prisma.authToken.create({
data: {
source: AuthTokenSource.ORCID,
accessToken: 'mock-access-token',
userId: author.id,
refreshToken: 'refresh-token',
},
});

members = await communityService.getAllMembers(desciCommunity.id);
MemberJwtToken1 = jwt.sign({ email: members[0].user.email }, process.env.JWT_SECRET!, {
expiresIn: '1y',
Expand All @@ -2143,9 +2168,27 @@ describe('Attestations Service', async () => {
await prisma.$queryRaw`TRUNCATE TABLE "Annotation" CASCADE;`;
await prisma.$queryRaw`TRUNCATE TABLE "NodeAttestationVerification" CASCADE;`;
// await prisma.$queryRaw`TRUNCATE TABLE "CommunityMember" CASCADE;`;

nock.restore();
});

it('should allow only members verify a node attestation(claim)', async () => {
const scope1 = nock(NOCK_URL)
.post(`/v3.0/${ORCID_ID}/work`)
.once()
.reply(201, '', { location: `https://api.sandbox.orcid.org/v3.0/${ORCID_ID}/work/${mockPutCode}` });

const scope2 = nock(new URL('https://sandbox.orcid.org')).post(`/oauth/token`).twice().reply(200, {
access_token: 'access-token',
token_type: 'auth',
refresh_token: 'refresh-token',
expires_in: 3599,
scope: '',
name: '',
orcid: ORCID_ID,
});

console.log('ACTIVE MOCKS', nock.activeMocks());
let res = await request(app)
.post(`/v1/attestations/verification`)
.set('authorization', memberAuthHeaderVal1)
Expand All @@ -2154,15 +2197,31 @@ describe('Attestations Service', async () => {
});
expect(res.statusCode).to.equal(200);

// setTimeout(() => {
// }, 100);

const scope3 = nock(NOCK_URL).put(`/v3.0/${ORCID_ID}/work/${mockPutCode}`).once().reply(200);

res = await request(app).post(`/v1/attestations/verification`).set('authorization', memberAuthHeaderVal2).send({
claimId: openCodeClaim.id,
});
expect(res.statusCode).to.equal(200);

// setTimeout(() => {
// }, 100);

const verifications = await attestationService.getAllClaimVerfications(openCodeClaim.id);
expect(verifications.length).to.equal(2);
expect(verifications.some((v) => v.userId === members[0].userId)).to.equal(true);
expect(verifications.some((v) => v.userId === members[1].userId)).to.equal(true);

console.log('Pending MOCKS', nock.pendingMocks());
scope1.done();
scope2.done();
scope3.done();
console.log('INTERCEPTOR SCOPE', scope1);
console.log('INTERCEPTOR SCOPE', scope2);
console.log('INTERCEPTOR SCOPE', scope3);
});

it('should prevent non-authorized users from verifying a protected attestation(claim)', async () => {
Expand Down
Loading
Loading