Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into tysn/sync-upstream
Browse files Browse the repository at this point in the history
  • Loading branch information
tphalp committed Nov 21, 2024
2 parents a518c40 + f531fa1 commit f982205
Show file tree
Hide file tree
Showing 49 changed files with 7,993 additions and 354 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/e2e-cypress-alt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
sudo sysctl -w vm.max_map_count=262144
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
node-version: 'lts/Iron'
check-latest: true
- run: npm ci

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e-cypress-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
node-version: 'lts/Iron'
check-latest: true
- run: npm ci

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e-cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
node-version: "lts/Iron"
check-latest: true
- run: npm ci

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/e2e-template-adapter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
config-file: cypress.config.ts
project: packages/template-adapter
project: packages/template-adapter # Use for own package ${{ vars.PACKAGE_DIR }}
start: npm run dev:e2e
wait-on: "http://localhost:8080/health, http://localhost:9200"

Expand All @@ -78,4 +78,4 @@ jobs:
if: failure()
with:
name: cypress-screenshots
path: ./packages/template-adapter/cypress/screenshots
path: ./packages/template-adapter/cypress/screenshots # Use ${{ vars.PACKAGE_DIR }} in place of "packages/template-adapter" for own package
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Set up NodeJS
uses: actions/setup-node@v4
with:
node-version: "lts/*"
node-version: "lts/Iron"
check-latest: true

- name: Install all dependencies
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ apps/server/cypress/screenshots
/playwright/.cache/

apps/server/cachedDefaults/preferences.json
apps/server/cypress.env.json
*.rdb

.github/workflows/docker-publish.yml
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@ See [PREFERENCES.md](PREFERENCES.md) for details.

## Authentication

The Express.js endpoints that are exposed in these repositories do not provide any authentication. You will need to fork the repo if you want to add your own authentication.
We have an optional [authentication](./apps/server/src/authentication.ts) system built in and enabled by .env variables. If AUTHENTICATION_ENABLE=true and the other required variables are provided, then all express endpoints defined after the useAuthentication call will require a Bearer token and optionally a set of scopes. This system assumes that you have an authorization system such as auth0 to point to. If you need more control over your authentication, then you may fork the repository and implement your own.

When authentication is enabled the widget endpoint will require authorization. There is a token endpoint that can be used to retrieve a one time use token that can be passed into the widget url for use in an iframe. When this is used the server will set an authorization cookie that the widget UI will pass to the server for all of its requests.
6 changes: 6 additions & 0 deletions apps/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,9 @@ ELASTIC_SEARCH_URL=http://localhost:9200
INSTITUTION_POLLING_INTERVAL=1

INSTITUTION_CACHE_LIST_URL=http://localhost:8088/institutions/cacheList

AUTHENTICATION_ENABLE=false
AUTHENTICATION_AUDIENCE="ucp-hosted-apps"
AUTHENTICATION_ISSUER_BASE_URL="https://dev-d23wau8o0uc5hw8n.us.auth0.com"
AUTHENTICATION_TOKEN_SIGNING_ALG="RS256"
AUTHENTICATION_SCOPES="widget:demo"
2 changes: 1 addition & 1 deletion apps/server/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = {
jest: true,
},
parserOptions: {
project: "./tsconfig.json",
project: true,
},
ignorePatterns: [
".eslintrc.cjs",
Expand Down
11 changes: 11 additions & 0 deletions apps/server/cypress.config.authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from "cypress";

import baseCypressConfig from "./baseCypressConfig";

export default defineConfig({
...baseCypressConfig,
e2e: {
...baseCypressConfig.e2e,
specPattern: "cypress/e2e/authenticationSuite/**/*.{js,jsx,ts,tsx}",
},
});
8 changes: 8 additions & 0 deletions apps/server/cypress.example.env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"auth_audience": "ucp-hosted-apps",
"auth_scope": "widget:demo",
"auth_client_id": "osS8CuafkPsJlfz5mfKRgYH942Pmwpxd",
"auth_domain": "dev-d23wau8o0uc5hw8n.us.auth0.com",
"auth_username": "testname",
"auth_password": "testpassword"
}
43 changes: 43 additions & 0 deletions apps/server/cypress/e2e/authenticationSuite/authentication.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
clickContinue,
expectConnectionSuccess,
visitAgg,
} from "@repo/utils-dev-dependency";
import {
enterTestExampleACredentials,
searchAndSelectTestExampleA,
} from "../../shared/utils/testExample";

describe("authentication", () => {
it("fails if not authorized", () => {
visitAgg({ failOnStatusCode: false });

cy.findByText("Unauthorized").should("exist");
});

it("is able to connect with the token flow", () => {
const accessToken = Cypress.env("accessToken");

const userId = crypto.randomUUID();

cy.request({
headers: {
authorization: `Bearer ${accessToken}`,
},
method: "GET",
url: `/api/token?userId=${userId}`,
}).then((response) => {
const token = response.body.token;

visitAgg({
userId,
token,
});

searchAndSelectTestExampleA();
enterTestExampleACredentials();
clickContinue();
expectConnectionSuccess();
});
});
});
12 changes: 9 additions & 3 deletions apps/server/cypress/e2e/suite1/testExample.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { JobTypes } from "@repo/utils";
import {
clickContinue,
expectConnectionSuccess,
generateVcDataTests,
generateDataTests,
} from "@repo/utils-dev-dependency";
import {
enterTestExampleACredentials,
Expand Down Expand Up @@ -39,6 +39,12 @@ const makeABConnection = async (jobType) => {
};

describe("testExampleA and B aggregators", () => {
generateVcDataTests({ makeAConnection: makeAnAConnection });
generateVcDataTests({ makeAConnection: makeABConnection });
generateDataTests({
makeAConnection: makeAnAConnection,
shouldTestVcEndpoint: true,
});
generateDataTests({
makeAConnection: makeABConnection,
shouldTestVcEndpoint: true,
});
});
6 changes: 3 additions & 3 deletions apps/server/cypress/e2e/suite2/sophtron.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { JobTypes } from "@repo/utils";
import { generateVcDataTests, visitAgg } from "@repo/utils-dev-dependency";
import { generateDataTests, visitAgg } from "@repo/utils-dev-dependency";
import {
expectConnectionSuccess,
clickContinue,
Expand Down Expand Up @@ -33,7 +33,7 @@ describe("Sophtron aggregator", () => {
});

it("Connects to Sophtron Bank with all MFA options", () => {
visitAgg();
visitAgg({});
searchByText("Sophtron Bank");
cy.findByLabelText("Add account with Sophtron Bank").first().click();
cy.findByLabelText("User ID").type("asdfg12X");
Expand Down Expand Up @@ -63,5 +63,5 @@ describe("Sophtron aggregator", () => {
expectConnectionSuccess();
});

generateVcDataTests({ makeAConnection });
generateDataTests({ makeAConnection, shouldTestVcEndpoint: true });
});
57 changes: 41 additions & 16 deletions apps/server/cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,61 @@
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'
import "./commands";

import { configure } from '@testing-library/cypress'
import { configure } from "@testing-library/cypress";
import { JwtPayload } from "jsonwebtoken";

configure({ testIdAttribute: 'data-test' })
configure({ testIdAttribute: "data-test" });

Cypress.on('uncaught:exception', () => {
Cypress.on("uncaught:exception", () => {
// returning false here prevents Cypress from
// failing the test
return false
})
return false;
});

before(() => {
if (Cypress.env("auth_audience")) {
cy.request({
method: "POST",
url: `https://${Cypress.env("auth_domain")}/oauth/token`,
body: {
audience: Cypress.env("auth_audience"),
client_id: Cypress.env("auth_client_id"),
grant_type: "password",
password: Cypress.env("auth_password") as string,
username: Cypress.env("auth_username") as string,
scope: Cypress.env("auth_scope"),
},
}).then((response: Cypress.Response<JwtPayload>) => {
const accessToken = response.body.access_token;

Cypress.env("accessToken", accessToken);
});
}
});

beforeEach(() => {
Cypress.env('userId', crypto.randomUUID())
})
Cypress.env("userId", crypto.randomUUID());
});

afterEach(() => {
const testAggregators = ['mx_int', 'sophtron']
const userId = Cypress.env('userId')
const testAggregators = ["mx_int", "sophtron"];
const userId = Cypress.env("userId");

testAggregators.forEach((aggregator) => {
cy.request({
method: 'DELETE',
headers: {
authorization: `Bearer ${Cypress.env("accessToken")}`,
},
method: "DELETE",
url: `/api/aggregator/${aggregator}/user/${userId}`,
failOnStatusCode: false
failOnStatusCode: false,
}).should((response) => {
expect(response.status).to.be.oneOf([200, 204, 400])
})
})
})
expect(response.status).to.be.oneOf([200, 204, 400]);
});
});
});

// Alternatively you can use CommonJS syntax:
// require('./commands')
5 changes: 5 additions & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"scripts": {
"cypress:open": "cypress open --config-file cypress.config.ts",
"cypress:open:alternate": "cypress open --config-file cypress.config.alternate.ts",
"cypress:open:authentication": "cypress open --config-file cypress.config.authentication.ts",
"cypress:open:prod": "cypress open --config-file cypress.config.prod.ts",
"cypress:run": "cypress run --config-file cypress.config.ts",
"cypress:run:alternate": "cypress run --config-file cypress.config.alternate.ts",
"cypress:run:authentication": "cypress run --config-file cypress.config.authentication.ts",
"cypress:run:prod": "cypress run --config-file cypress.config.prod.ts",
"elasticsearch": "docker run --rm --name elasticsearch_container -p 9200:9200 -p 9300:9300 -e \"discovery.type=single-node\" -e \"xpack.security.enabled=false\" elasticsearch:8.13.4",
"dev": "concurrently --kill-others \"npm run elasticsearch\" \"npm run server\" \"npm run redisserver\"",
Expand Down Expand Up @@ -40,11 +42,13 @@
"assert-browserify": "^2.0.0",
"axios": "^1.6.8",
"buffer-browserify": "^0.2.5",
"cookie-parser": "^1.4.7",
"crypto-browserify": "^3.12.0",
"crypto-js": "^4.2.0",
"dotenv": "^16.3.1",
"express": "^4.19.2",
"express-async-errors": "^3.1.1",
"express-oauth2-jwt-bearer": "^1.6.0",
"express-rate-limit": "^7.0.2",
"he": "^1.2.0",
"joi": "^17.13.3",
Expand Down Expand Up @@ -97,6 +101,7 @@
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-promise": "^6.1.1",
"jsonwebtoken": "^9.0.2",
"msw": "^2.2.13",
"nodemon": "^3.1.0",
"ts-jest": "^29.1.2",
Expand Down
30 changes: 28 additions & 2 deletions apps/server/src/adapterIndex.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { VCDataTypes } from "@repo/utils";
import { getAggregatorAdapter, getVC } from "./adapterIndex";
import { getDataFromVCJwt, VCDataTypes } from "@repo/utils";
import { getAggregatorAdapter, getData, getVC } from "./adapterIndex";
import type { Aggregator } from "./adapterSetup";
import { sophtronVcAccountsData } from "./test/testData/sophtronVcData";
import { TEST_EXAMPLE_A_AGGREGATOR_STRING, TestAdapter } from "./test-adapter";
import { testVcAccountsData } from "./test/testData/testVcData";

const connectionId = "testConectionId";
const type = VCDataTypes.ACCOUNTS;
Expand Down Expand Up @@ -34,6 +35,31 @@ describe("adapterSetup", () => {
});
});

describe("getData", () => {
it("uses testExample if the aggregator is testExampleA", async () => {
const response = await getData({
aggregator: TEST_EXAMPLE_A_AGGREGATOR_STRING,
connectionId,
type,
userId,
});

expect(response).toEqual(getDataFromVCJwt(testVcAccountsData));
});

it("throws an error if the aggregator doesnt have a handler", async () => {
await expect(
async () =>
await getData({
aggregator: "junk" as Aggregator,
connectionId,
type,
userId,
}),
).rejects.toThrow("Unsupported aggregator junk");
});
});

describe("getAggregatorAdapter", () => {
it("throws an error if its an unsupported aggregator", async () => {
expect(() => getAggregatorAdapter("junk" as Aggregator)).toThrow(
Expand Down
Loading

0 comments on commit f982205

Please sign in to comment.