diff --git a/.github/workflows/e2e-node.yml b/.github/workflows/e2e-node.yml new file mode 100644 index 0000000..bb7352b --- /dev/null +++ b/.github/workflows/e2e-node.yml @@ -0,0 +1,42 @@ +name: End-to-end Tests (Node) + +on: + push: + # Allow manual triggering, e.g. to run end-to-end tests against Dependabot PRs: + workflow_dispatch: + +env: + CI: true +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + e2e-node: + runs-on: ${{ matrix.os }} + environment: + name: ${{ matrix.environment-name }} + continue-on-error: ${{ matrix.experimental }} + strategy: + matrix: + os: [ubuntu-latest] + node-version: ["20.x", "18.x"] + # PodSpaces doesn't support error descriptions yet. + environment-name: ["ESS Dev-2-3"] + experimental: [false] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + cache-dependency-path: "**/package-lock.json" + + - name: Install dependencies + run: npm ci + + - run: npm run test:e2e:node + env: + E2E_TEST_IDP: ${{ secrets.E2E_TEST_IDP }} + E2E_TEST_ENVIRONMENT: ${{ matrix.environment-name }} + E2E_TEST_OWNER_CLIENT_ID: ${{ secrets.E2E_TEST_OWNER_CLIENT_ID }} + E2E_TEST_OWNER_CLIENT_SECRET: ${{ secrets.E2E_TEST_OWNER_CLIENT_SECRET }} diff --git a/e2e/env/.env.example b/e2e/env/.env.example new file mode 100644 index 0000000..3542fde --- /dev/null +++ b/e2e/env/.env.example @@ -0,0 +1,8 @@ +# Select environment (see @inrupt/internal-test-env for a list of available environments) +E2E_TEST_ENVIRONMENT="ESS PodSpaces" +E2E_TEST_IDP=https://login.inrupt.com + +# The following environment variables should be kept in a .env.test.local file +# Obtain credentials via https://login.inrupt.com/registration.html +E2E_TEST_CLIENT_ID="" +E2E_TEST_CLIENT_SECRET="" diff --git a/e2e/node/error.test.ts b/e2e/node/error.test.ts new file mode 100644 index 0000000..24c87ef --- /dev/null +++ b/e2e/node/error.test.ts @@ -0,0 +1,124 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +import { + describe, + it, + expect, + jest, + beforeEach, + afterEach, +} from "@jest/globals"; +import { custom } from "openid-client"; +import { config } from "dotenv"; +import { join } from "path"; + +import { + getNodeTestingEnvironment, + getPodRoot, +} from "@inrupt/internal-test-env"; +import { Session } from "@inrupt/solid-client-authn-node"; + +import { + NotAcceptableError, + NotFoundError, + UnauthorizedError, + handleErrorResponse, +} from "../../src/index"; + +custom.setHttpOptionsDefaults({ + timeout: 15000, +}); + +if (process.env.CI === "true") { + // Tests running in the CI runners tend to be more flaky. + jest.retryTimes(3, { logErrorsBeforeRetry: true }); +} else { + // Load .env.local file + config({ path: join("..", "env", ".env.local") }); +} + +const ENV = getNodeTestingEnvironment(); +const { owner } = ENV.clientCredentials; + +describe(`End-to-end error description test for ${ENV.environment}`, () => { + const authenticatedSession = new Session({ keepAlive: false }); + + beforeEach(async () => { + await authenticatedSession.login({ + clientId: owner.id, + clientSecret: owner.secret, + oidcIssuer: ENV.idp, + }); + }); + + afterEach(async () => { + await authenticatedSession.logout(); + }); + + it("returns an RFC9457 error response for unauthenticated requests", async () => { + const podRoot = await getPodRoot(authenticatedSession); + // This unauthenticated fetch should get a 401 response. + const response = await fetch(podRoot); + const responseBody = await response.text(); + const error = handleErrorResponse( + response, + responseBody, + "Some error message", + ); + expect(error).toBeInstanceOf(UnauthorizedError); + expect(error.problemDetails.detail).toBeDefined(); + expect(error.problemDetails.instance).toBeDefined(); + }); + + it("returns an RFC9457 error response for not found resources", async () => { + const podRoot = await getPodRoot(authenticatedSession); + const response = await authenticatedSession.fetch( + new URL("some-missing-resource", podRoot), + ); + const responseBody = await response.text(); + const error = handleErrorResponse( + response, + responseBody, + "Some error message", + ); + expect(error).toBeInstanceOf(NotFoundError); + expect(error.problemDetails.detail).toBeDefined(); + expect(error.problemDetails.instance).toBeDefined(); + }); + + it("returns an RFC9457 error response for not acceptable content negotiation", async () => { + const podRoot = await getPodRoot(authenticatedSession); + const response = await authenticatedSession.fetch(podRoot, { + headers: { + Accept: "text/csv", + }, + }); + const responseBody = await response.text(); + const error = handleErrorResponse( + response, + responseBody, + "Some error message", + ); + expect(error).toBeInstanceOf(NotAcceptableError); + expect(error.problemDetails.detail).toBeDefined(); + expect(error.problemDetails.instance).toBeDefined(); + }); +}); diff --git a/package-lock.json b/package-lock.json index 51fb7c3..88767c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,15 +5,17 @@ "requires": true, "packages": { "": { - "name": "@inrupt/solid-client-error-js", + "name": "@inrupt/solid-client-errors-js", "version": "0.0.1", "devDependencies": { "@inrupt/base-rollup-config": "^3.0.1", "@inrupt/eslint-config-lib": "^3.0.1", "@inrupt/internal-test-env": "^3.2.1", "@inrupt/jest-jsdom-polyfills": "^3.2.1", + "@inrupt/solid-client-authn-node": "^2.2.4", "@typescript-eslint/eslint-plugin": "^7.14.1", "@typhonjs-typedoc/ts-lib-docs": "^2023.7.12", + "dotenv": "^16.4.5", "eslint": "^8.57.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^28.6.0", diff --git a/package.json b/package.json index 5c3f6b8..8ab2ee3 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,10 @@ "@inrupt/eslint-config-lib": "^3.0.1", "@inrupt/internal-test-env": "^3.2.1", "@inrupt/jest-jsdom-polyfills": "^3.2.1", + "@inrupt/solid-client-authn-node": "^2.2.4", "@typescript-eslint/eslint-plugin": "^7.14.1", "@typhonjs-typedoc/ts-lib-docs": "^2023.7.12", + "dotenv": "^16.4.5", "eslint": "^8.57.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^28.6.0",