From 7b81880c3c48b1b1c9850593d5a4948e75f04cc8 Mon Sep 17 00:00:00 2001 From: Justyn Spooner Date: Tue, 14 Feb 2023 11:06:19 +0000 Subject: [PATCH] [Feature] Add support for General Supply Documentation MRV submission (#19) * Add support for General Supply Documentation * Add full tests for General Supply Documentation MRV * Renam file * Use KGs for the test mint * Add validation for KGs in the General Supply Documentation MRV * OCD --- .github/workflows/ci-checks.yml | 1 + ...llGeneralSupplyDocumentationPolicy.test.ts | 286 ++++++++++++++++++ .../validateMrvDocumentSubmission.test.ts | 78 +++++ env.example | 3 +- src/config/guardianTags.ts | 1 + src/config/index.ts | 4 + src/spec/openapi.json | 37 ++- src/spec/openapi.ts | 17 +- .../validateMrvDocumentSubmission.ts | 28 +- 9 files changed, 451 insertions(+), 4 deletions(-) create mode 100644 __tests__/e2e/fullGeneralSupplyDocumentationPolicy.test.ts create mode 100644 __tests__/unit/validators/validateMrvDocumentSubmission.test.ts diff --git a/.github/workflows/ci-checks.yml b/.github/workflows/ci-checks.yml index b314495..527628a 100644 --- a/.github/workflows/ci-checks.yml +++ b/.github/workflows/ci-checks.yml @@ -22,6 +22,7 @@ jobs: TEST_AUTH_URL: ${{ secrets.TEST_AUTH_URL }} TEST_COOL_FARM_POLICY_ID: ${{ secrets.TEST_COOL_FARM_POLICY_ID }} TEST_AGRECALC_POLICY_ID: ${{ secrets.TEST_AGRECALC_POLICY_ID }} + TEST_GENERAL_SUPPLY_DOCUMENTATION_POLICY_ID: ${{ secrets.TEST_GENERAL_SUPPLY_DOCUMENTATION_POLICY_ID }} steps: - uses: actions/checkout@v2 diff --git a/__tests__/e2e/fullGeneralSupplyDocumentationPolicy.test.ts b/__tests__/e2e/fullGeneralSupplyDocumentationPolicy.test.ts new file mode 100644 index 0000000..ee0a9d0 --- /dev/null +++ b/__tests__/e2e/fullGeneralSupplyDocumentationPolicy.test.ts @@ -0,0 +1,286 @@ +import StatusCode from 'src/constants/status' +import hmacAxios from 'src/apiClient/hmacApiClient' +import config from 'src/config' + +const SECONDS = 1000 +const TEN_SECONDS = 10 * SECONDS +const ONE_MINUTE = 60 * SECONDS + +const loginResponseData = async (username, password) => { + const data = { username, password } + + const response = await hmacAxios.post( + `${config.apiUrl}/api/accounts/login`, + data + ) + + return response.data.data +} + +describe('Test General Supply Documentation policy flow', () => { + const isoDate = new Date().toISOString() + const registrant = `ci_registrant_${isoDate}` + const verifier = `ci_verifier_${isoDate}` + const password = 'secret' + const policyId = config.testGeneralSupplyDocumentationPolicyId + const { registryUsername, registryPassword } = config + + it('should have the correct environment variables', () => { + const { + apiUrl, + testGeneralSupplyDocumentationPolicyId, + hmacAuthKey, + hmacEnabled, + network, + registryUsername, + registryPassword, + guardianApiUrl, + } = config + + expect(apiUrl).toBeDefined() + expect(testGeneralSupplyDocumentationPolicyId).toBeDefined() + expect(hmacAuthKey).toBeDefined() + expect(hmacEnabled).toBeDefined() + expect(network).toBeDefined() + expect(registryUsername).toBeDefined() + expect(registryPassword).toBeDefined() + expect(guardianApiUrl).toBeDefined() + }) + it( + 'creates a new registrant account', + async () => { + const data = { username: registrant, password } + + const response = await hmacAxios.post( + `${config.apiUrl}/api/accounts`, + data + ) + + expect(response.status).toBe(StatusCode.OK) + }, + ONE_MINUTE + ) + it( + 'creates a new verifier account', + async () => { + const data = { username: verifier, password } + + const response = await hmacAxios.post( + `${config.apiUrl}/api/accounts`, + data + ) + + expect(response.status).toBe(StatusCode.OK) + }, + ONE_MINUTE + ) + it( + 'assigns the policy REGISTRANT role to the registrant account', + async () => { + const { accessToken } = await loginResponseData( + registrant, + password + ) + + const response = await hmacAxios.post( + `${config.apiUrl}/api/policies/${policyId}/role/registrant`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + + expect(response.status).toBe(StatusCode.OK) + }, + ONE_MINUTE + ) + it( + 'assigns the policy VERIFIER role to the verifier account', + async () => { + const { accessToken } = await loginResponseData(verifier, password) + + const response = await hmacAxios.post( + `${config.apiUrl}/api/policies/${policyId}/role/verifier`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + + expect(response.status).toBe(StatusCode.OK) + }, + ONE_MINUTE + ) + it( + 'submits a new application', + async () => { + const { accessToken } = await loginResponseData( + registrant, + password + ) + + const data = { + field0: `Matt Farm ${isoDate}`, + field1: 'Paignton', + field2: 420, + field3: 'Fancy Soil', + field4: 'A Cat', + field5: 'We grow grass', + field6: 'No we do not', + field7: 'No, we do not...', + field8: 'No', + } + + const response = await hmacAxios.post( + `${config.apiUrl}/api/policies/${policyId}/register`, + data, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + expect(response.status).toBe(StatusCode.OK) + }, + ONE_MINUTE + ) + it( + 'approves the application', + async () => { + await new Promise((r) => setTimeout(r, TEN_SECONDS)) + const { did } = await loginResponseData(registrant, password) + const { accessToken } = await loginResponseData( + registryUsername, + registryPassword + ) + + const response = await hmacAxios.put( + `${config.apiUrl}/api/policies/${policyId}/approve/application/${did}`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + expect(response.status).toBe(StatusCode.OK) + }, + ONE_MINUTE + ) + it( + 'submits an ecological project', + async () => { + await new Promise((r) => setTimeout(r, TEN_SECONDS)) + const { accessToken } = await loginResponseData( + registrant, + password + ) + + const data = { + field0: '1234', + field1: `Matt's Farm ${isoDate}`, + field2: "This is a description about Matt's farm", + field3: 'Matt Smithies', + field4: 'Ecological Project Info - Link to Project Data', + field5: 'Ecological Project Info - Country: The host country for the project', + field6: 'Ecological Project Info - Project Scale: One from the list of - Micro, Small, Medium, or Large', + field7: 'Modular Benefit Project - Unique identifier ', + field8: 'Modular Benefit Project - Geographic Location', + field9: 'Modular Benefit Project - Targeted Benefit Type', + field10: 'Modular Benefit Project - Developer(s)', + field11: 'Modular Benefit Project - Sponsor(s)', + field12: 'Modular Benefit Project - Claim Tokens', + } + + const response = await hmacAxios.post( + `${config.apiUrl}/api/policies/${policyId}/project`, + data, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + expect(response.status).toBe(StatusCode.OK) + }, + ONE_MINUTE + ) + it( + 'approves the ecological project', + async () => { + await new Promise((r) => setTimeout(r, TEN_SECONDS)) + const { did } = await loginResponseData(registrant, password) + const { accessToken } = await loginResponseData( + registryUsername, + registryPassword + ) + + const response = await hmacAxios.put( + `${config.apiUrl}/api/policies/${policyId}/approve/project/${did}`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + expect(response.status).toBe(StatusCode.OK) + }, + ONE_MINUTE + ) + it( + 'submits an MRV request', + async () => { + await new Promise((r) => setTimeout(r, TEN_SECONDS)) + const { accessToken } = await loginResponseData( + registrant, + password + ) + + const data = { + field0: `Field 0 ${isoDate}`, + field1: 'Field 1', + field2: 'Field 2', + field3: '10000', + field4: 'Field 4', + } + + const response = await hmacAxios.post( + `${config.apiUrl}/api/policies/${policyId}/mrv/general-supply-documentation`, + data, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + + expect(response.status).toBe(StatusCode.OK) + }, + ONE_MINUTE + ) + it( + 'approves the MRV request', + async () => { + await new Promise((r) => setTimeout(r, TEN_SECONDS)) + const { did } = await loginResponseData(registrant, password) + const { accessToken } = await loginResponseData(verifier, password) + + const response = await hmacAxios.put( + `${config.apiUrl}/api/policies/${policyId}/approve/mrv/${did}`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + expect(response.status).toBe(StatusCode.OK) + }, + ONE_MINUTE + ) +}) diff --git a/__tests__/unit/validators/validateMrvDocumentSubmission.test.ts b/__tests__/unit/validators/validateMrvDocumentSubmission.test.ts new file mode 100644 index 0000000..3ad0e5f --- /dev/null +++ b/__tests__/unit/validators/validateMrvDocumentSubmission.test.ts @@ -0,0 +1,78 @@ +import { MRV } from 'src/config/guardianTags' +import validate from 'src/validators/validateMrvDocumentSubmission' + +describe(`Validate Measurement Reporting Verification Document Submission`, () => { + describe('General Supply Documentation', () => { + it(`should return an error if the payload is not complete`, () => { + const payload = { + field0: 'field0', + field1: 'field1', + field2: 'field2', + field3: '1000', + field4: 'field4', + } + + const result = validate('BAD_TYPE', payload) + + expect(result).toEqual([ + `The policy can only process MRV types of "${MRV.AGRECALC}", "${MRV.COOL_FARM_TOOL}" or "${MRV.GENERAL_SUPPLY_DOCUMENTATION}". Please update your request.`, + ]) + }) + it(`should return no errors if the payload is complete`, () => { + const payload = { + field0: 'field0', + field1: 'field1', + field2: 'field2', + field3: '1000', + field4: 'field4', + } + + const result = validate(MRV.GENERAL_SUPPLY_DOCUMENTATION, payload) + + expect(result).toEqual(null) + }) + it(`should return error if KGs are not divisible by 1000`, () => { + const payload = { + field0: 'field0', + field1: 'field1', + field2: 'field2', + field3: '1500', + field4: 'field4', + } + + const result = validate(MRV.GENERAL_SUPPLY_DOCUMENTATION, payload) + + expect(result).toEqual([ + 'Must be a string representing a positive integer divisible by 1,000', + ]) + }) + it(`should return error if KGs are a fraction`, () => { + const payload = { + field0: 'field0', + field1: 'field1', + field2: 'field2', + field3: '1000.1000', + field4: 'field4', + } + + const result = validate(MRV.GENERAL_SUPPLY_DOCUMENTATION, payload) + + expect(result).toEqual([ + 'Must be a string representing a positive integer divisible by 1,000', + ]) + }) + it(`should return error if KGs are empty`, () => { + const payload = { + field0: 'field0', + field1: 'field1', + field2: 'field2', + field3: '', + field4: 'field4', + } + + const result = validate(MRV.GENERAL_SUPPLY_DOCUMENTATION, payload) + + expect(result).toEqual(['"field3" is not allowed to be empty']) + }) + }) +}) diff --git a/env.example b/env.example index cdea2d5..b0a35cc 100644 --- a/env.example +++ b/env.example @@ -24,4 +24,5 @@ PUBLIC_TRUST_CHAIN_ACCESS=TRUE # These are used for end to end tests of the API. Make sure to add these to your .env.test.local file TEST_COOL_FARM_POLICY_ID=62f2365b7fe3fa7b888e846d -TEST_AGRECALC_POLICY_ID=62f27a007fe3fa7b888e8496 \ No newline at end of file +TEST_AGRECALC_POLICY_ID=62f27a007fe3fa7b888e8496 +TEST_GENERAL_SUPPLY_DOCUMENTATION_POLICY_ID=62f27a007fe3fa7b888e8497 \ No newline at end of file diff --git a/src/config/guardianTags.ts b/src/config/guardianTags.ts index 5ee64ca..81476d3 100644 --- a/src/config/guardianTags.ts +++ b/src/config/guardianTags.ts @@ -53,4 +53,5 @@ export enum Role { export enum MRV { AGRECALC = 'agrecalc', COOL_FARM_TOOL = 'cool-farm-tool', + GENERAL_SUPPLY_DOCUMENTATION = 'general-supply-documentation', } diff --git a/src/config/index.ts b/src/config/index.ts index 0ff915a..a14193f 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -10,6 +10,7 @@ interface Config { testAuthUrl: string testCoolFarmPolicyId: string testAgreCalcPolicyId: string + testGeneralSupplyDocumentationPolicyId: string guardianApiUrl: string registryUsername: string registryPassword: string @@ -27,6 +28,7 @@ const { TEST_AUTH_URL, TEST_COOL_FARM_POLICY_ID, TEST_AGRECALC_POLICY_ID, + TEST_GENERAL_SUPPLY_DOCUMENTATION_POLICY_ID, GUARDIAN_API_URL, STANDARD_REGISTRY_USERNAME, STANDARD_REGISTRY_PASSWORD, @@ -52,6 +54,8 @@ const config: Config = { testAuthUrl: TEST_AUTH_URL, testCoolFarmPolicyId: TEST_COOL_FARM_POLICY_ID, testAgreCalcPolicyId: TEST_AGRECALC_POLICY_ID, + testGeneralSupplyDocumentationPolicyId: + TEST_GENERAL_SUPPLY_DOCUMENTATION_POLICY_ID, guardianApiUrl: GUARDIAN_API_URL, registryUsername: STANDARD_REGISTRY_USERNAME, registryPassword: STANDARD_REGISTRY_PASSWORD, diff --git a/src/spec/openapi.json b/src/spec/openapi.json index 056ea6a..ac1931b 100644 --- a/src/spec/openapi.json +++ b/src/spec/openapi.json @@ -361,6 +361,37 @@ "field3", "field4" ] + }, + { + "properties": { + "field0": { + "type": "string", + "description": "Documentation Name" + }, + "field1": { + "type": "string", + "description": "Documentation Description" + }, + "field2": { + "type": "string", + "description": "Documentation Link" + }, + "field3": { + "type": "string", + "description": "Carbon Mintable (Mintable KGs)" + }, + "field4": { + "type": "string", + "description": "Documentation CID (IPFS Provenance)" + } + }, + "required": [ + "field0", + "field1", + "field2", + "field3", + "field4" + ] } ] }, @@ -979,7 +1010,11 @@ "name": "mrv_type", "in": "path", "schema": { - "type": "string" + "enum": [ + "agrecalc", + "cool-farm-tool", + "general-supply-documentation" + ] }, "required": true, "example": "agrecalc" diff --git a/src/spec/openapi.ts b/src/spec/openapi.ts index d9ecb33..8bbddbf 100644 --- a/src/spec/openapi.ts +++ b/src/spec/openapi.ts @@ -142,7 +142,10 @@ export interface paths { parameters: { path: { policyId: string - mrv_type: string + mrv_type: + | 'agrecalc' + | 'cool-farm-tool' + | 'general-supply-documentation' } } responses: { @@ -429,6 +432,18 @@ export interface components { /** @description Negative GHG KG Emissions (Mintable KG) */ field4: number } + | { + /** @description Documentation Name */ + field0: string + /** @description Documentation Description */ + field1: string + /** @description Documentation Link */ + field2: string + /** @description Carbon Mintable (Mintable KGs) */ + field3: string + /** @description Documentation CID (IPFS Provenance) */ + field4: string + } TrustChains: components['schemas']['TrustChainDocument'][] /** Trust Chain Document */ TrustChainDocument: { diff --git a/src/validators/validateMrvDocumentSubmission.ts b/src/validators/validateMrvDocumentSubmission.ts index b44176f..06a4536 100644 --- a/src/validators/validateMrvDocumentSubmission.ts +++ b/src/validators/validateMrvDocumentSubmission.ts @@ -22,6 +22,26 @@ const agrecalcSchema = Joi.object({ field5: Joi.number().required(), }) +// Regular expression for a positive integer ending in 3 0s +const positiveIntegerDivisibleByThousandRegex = /^([1-9]\d*)(000)$/ + +const generalSupplyDocumentationSchema = + Joi.object({ + field0: Joi.string().required(), + field1: Joi.string().required(), + field2: Joi.string().required(), + field3: Joi.string() + .regex(positiveIntegerDivisibleByThousandRegex) + .required() + .messages({ + 'string.pattern.base': + 'Must be a string representing a positive integer divisible by 1,000', + }), + field4: Joi.string().required(), + }) + +// Regex for a positive integer divisible by 1,000 + const getSchemaFromPath = (mrvType: string) => { if (mrvType.toLowerCase() === MRV.AGRECALC.toLowerCase()) { return agrecalcSchema @@ -31,6 +51,12 @@ const getSchemaFromPath = (mrvType: string) => { return cftSchema } + if ( + mrvType.toLowerCase() === MRV.GENERAL_SUPPLY_DOCUMENTATION.toLowerCase() + ) { + return generalSupplyDocumentationSchema + } + return false } @@ -42,7 +68,7 @@ export default ( if (!schema) { return [ - `The policy can only process MRV types of "${MRV.AGRECALC}" or "${MRV.COOL_FARM_TOOL}" please update your request.`, + `The policy can only process MRV types of "${MRV.AGRECALC}", "${MRV.COOL_FARM_TOOL}" or "${MRV.GENERAL_SUPPLY_DOCUMENTATION}". Please update your request.`, ] }