diff --git a/.github/workflows/production.yaml b/.github/workflows/production.yaml index 371423c2..ab6f8cf0 100644 --- a/.github/workflows/production.yaml +++ b/.github/workflows/production.yaml @@ -8,7 +8,37 @@ on: - master - production jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + run: npm ci + + - name: Run Vitest + run: npx vitest run + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright Tests + run: npx playwright test + + - name: Upload Playwright Traces + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-traces + path: playwright-report/**/trace.zip + Deploy-Production: + needs: test runs-on: labels: ubuntu-latest steps: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..7627c212 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,39 @@ +name: Run Tests + +on: + pull_request: + branches: [master] + push: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + run: npm ci + + - name: Run Vitest + run: npx vitest run + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright Tests + run: npx playwright test + + - name: Upload Playwright Traces + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-traces + path: playwright-report/**/trace.zip + + \ No newline at end of file diff --git a/e2e/decoder.spec.ts b/e2e/decoder.spec.ts index 7ecfd1c9..27963f7d 100644 --- a/e2e/decoder.spec.ts +++ b/e2e/decoder.spec.ts @@ -53,10 +53,11 @@ test.describe("Can interact with JWT Decoder JWT editor", () => { await expect(jwtEditorInput).toHaveValue(inputValue); }); - test("can copy value in JWT editor", async ({ page, context }) => { + test("can copy value in JWT editor", async ({ page, context, browserName }) => { + const permissions = browserName === 'firefox' ? [] : ["clipboard-read", "clipboard-write"] const inputValue = (TestJwts.RS512 as JwtSignedWithDigitalModel).withPemKey .jwt; - await context.grantPermissions(["clipboard-read", "clipboard-write"]); + await context.grantPermissions(permissions); const lang = await getLang(page); expectToBeNonNull(lang); diff --git a/playwright.config.ts b/playwright.config.ts index efd9b88b..62cec27f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -44,11 +44,6 @@ export default defineConfig({ use: { ...devices['Desktop Firefox'] }, }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', @@ -71,9 +66,10 @@ export default defineConfig({ ], /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, + webServer: { + command: 'npm run dev', + port: 1234, + timeout: 60 * 1000, + reuseExistingServer: !process.env.CI, + }, }); diff --git a/src/features/common/services/utils.ts b/src/features/common/services/utils.ts index a6d69d8e..67833864 100644 --- a/src/features/common/services/utils.ts +++ b/src/features/common/services/utils.ts @@ -84,7 +84,7 @@ export const safeJsonStringify = fromThrowable(JSON.stringify, (e) => { }); export const safeNewUint8ArrayFromBuffer = fromThrowable( - (buffer: ArrayBufferLike) => new Uint8Array(buffer), + (buffer: Buffer) => new Uint8Array(buffer), (e) => { if (e instanceof Error) { return e.message; diff --git a/tests/jwt.service.test.ts b/tests/jwt.service.test.ts index 6b3a30c2..6eee1779 100644 --- a/tests/jwt.service.test.ts +++ b/tests/jwt.service.test.ts @@ -2,14 +2,16 @@ import { describe, expect, test } from "vitest"; import { DefaultTokensValues } from "@/features/common/values/default-tokens.values"; import { validateJwtFormat } from "@/features/common/services/jwt.service"; import { JwtTypeValues } from "@/features/common/values/jwt-type.values"; +import { DebuggerTaskValues } from "@/features/common/values/debugger-task.values"; +import { DebuggerInputValues } from "@/features/common/values/debugger-input.values"; describe("validateJwtFormat", () => { - const tokenHS256 = DefaultTokensValues.hs256.token; - const tokenHS384 = DefaultTokensValues.hs384.token; - const tokenHS512 = DefaultTokensValues.hs512.token; - const tokenRS256 = DefaultTokensValues.rs256.token; - const tokenRS384 = DefaultTokensValues.rs384.token; - const tokenRS512 = DefaultTokensValues.rs512.token; + const tokenHS256 = DefaultTokensValues.HS256.token; + const tokenHS384 = DefaultTokensValues.HS384.token; + const tokenHS512 = DefaultTokensValues.HS512.token; + const tokenRS256 = DefaultTokensValues.RS256.token; + const tokenRS384 = DefaultTokensValues.RS384.token; + const tokenRS512 = DefaultTokensValues.RS512.token; const unsecured = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ."; @@ -24,7 +26,7 @@ describe("validateJwtFormat", () => { const invalidToken8 = "eyJhbGciOiJIUzI1N9.dGVzdA.Yysa_W8n99vc_zcHxetNl4qo8gNx1qZu63I0H5UTYAI"; const invalidToken9 = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c.abc"; + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c.abc"; test("input is a valid JWT", () => { const result1 = validateJwtFormat(tokenHS256); @@ -32,40 +34,61 @@ describe("validateJwtFormat", () => { expect(result1.isOk()).toBe(true); result1.map((value) => expect(value).toStrictEqual({ - signingAlgorithm: "HS256", - type: JwtTypeValues.MACed, - encoded: { - token: - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", - header: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", - payload: - "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ", - signature: "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", - }, decoded: { header: { alg: "HS256", typ: "JWT", }, + payload: { + admin: true, + sub: "1234567890", + name: "John Doe", + iat: 1516239022, + }, + }, + signingAlgorithm: "HS256", + type: JwtTypeValues.MACed, + }) + ); + + const result2 = validateJwtFormat(tokenHS384); + expect(result2.isErr()).toBe(false); + expect(result2.isOk()).toBe(true); + result2.map((value) => + expect(value).toStrictEqual({ + type: JwtTypeValues.MACed, + signingAlgorithm: "HS384", + decoded: { + header: { alg: "HS384", typ: "JWT" }, + payload: { + sub: "1234567890", + name: "John Doe", + admin: true, + iat: 1516239022, + }, + }, + }) + ); + + const result3 = validateJwtFormat(tokenHS512); + expect(result3.isErr()).toBe(false); + expect(result3.isOk()).toBe(true); + result3.map((value) => + expect(value).toStrictEqual({ + type: JwtTypeValues.MACed, + signingAlgorithm: "HS512", + decoded: { + header: { alg: "HS512", typ: "JWT" }, payload: { sub: "1234567890", name: "John Doe", + admin: true, iat: 1516239022, }, }, - }), - ); - - // const result2 = validateJwtFormat(tokenHS384); - // expect(result2.isErr()).toBe(false); - // expect(result2.isOk()).toBe(true); - // result2.map((value) => expect(value).toBe(tokenHS384)); - // - // const result3 = validateJwtFormat(tokenHS512); - // expect(result3.isErr()).toBe(false); - // expect(result3.isOk()).toBe(true); - // result3.map((value) => expect(value).toBe(tokenHS512)); - // + }) + ); + const result4 = validateJwtFormat(tokenRS256); expect(result4.isErr()).toBe(false); expect(result4.isOk()).toBe(true); @@ -73,15 +96,6 @@ describe("validateJwtFormat", () => { expect(value).toStrictEqual({ signingAlgorithm: "RS256", type: JwtTypeValues.DigitallySigned, - encoded: { - token: - "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ", - header: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9", - payload: - "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0", - signature: - "NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ", - }, decoded: { header: { alg: "RS256", @@ -94,19 +108,47 @@ describe("validateJwtFormat", () => { iat: 1516239022, }, }, - }), - ); - - // const result5 = validateJwtFormat(tokenRS384); - // expect(result5.isErr()).toBe(false); - // expect(result5.isOk()).toBe(true); - // result5.map((value) => expect(value).toBe(tokenRS384)); - // - // const result6 = validateJwtFormat(tokenRS512); - // expect(result6.isErr()).toBe(false); - // expect(result6.isOk()).toBe(true); - // result6.map((value) => expect(value).toBe(tokenRS512)); - // + }) + ); + + const result5 = validateJwtFormat(tokenRS384); + expect(result5.isErr()).toBe(false); + expect(result5.isOk()).toBe(true); + result5.map((value) => + expect(value).toStrictEqual({ + type: JwtTypeValues.DigitallySigned, + signingAlgorithm: "RS384", + decoded: { + header: { alg: "RS384", typ: "JWT" }, + payload: { + sub: "1234567890", + name: "John Doe", + admin: true, + iat: 1516239022, + }, + }, + }) + ); + + const result6 = validateJwtFormat(tokenRS512); + expect(result6.isErr()).toBe(false); + expect(result6.isOk()).toBe(true); + result6.map((value) => + expect(value).toStrictEqual({ + type: JwtTypeValues.DigitallySigned, + signingAlgorithm: "RS512", + decoded: { + header: { alg: "RS512", typ: "JWT" }, + payload: { + sub: "1234567890", + name: "John Doe", + admin: true, + iat: 1516239022, + }, + }, + }) + ); + const result7 = validateJwtFormat(unsecured); expect(result7.isErr()).toBe(false); expect(result7.isOk()).toBe(true); @@ -114,14 +156,6 @@ describe("validateJwtFormat", () => { expect(value).toStrictEqual({ signingAlgorithm: "none", type: JwtTypeValues.Unsecured, - encoded: { - token: - "eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.", - header: "eyJhbGciOiJub25lIn0", - payload: - "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ", - signature: "", - }, decoded: { header: { alg: "none", @@ -132,7 +166,7 @@ describe("validateJwtFormat", () => { "http://example.com/is_root": true, }, }, - }), + }) ); }); @@ -141,81 +175,105 @@ describe("validateJwtFormat", () => { expect(result1.isErr()).toBe(true); expect(result1.isOk()).toBe(false); result1.mapErr((error) => - expect(error).toStrictEqual([ - "The first segment, the JWT header, and the second segment, the JWT payload, must represent a completely valid JSON object conforming to RFC 7159.", - ]), + expect(error).toStrictEqual({ + task: DebuggerTaskValues.DECODE, + input: DebuggerInputValues.JWT, + message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, + }) ); const result2 = validateJwtFormat(invalidToken2); expect(result2.isErr()).toBe(true); expect(result2.isOk()).toBe(false); result2.mapErr((error) => - expect(error).toStrictEqual([ - "The first segment, the JWT header, and the second segment, the JWT payload, must represent a completely valid JSON object conforming to RFC 7159.", - ]), + expect(error).toStrictEqual({ + task: DebuggerTaskValues.DECODE, + input: DebuggerInputValues.JWT, + message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, + }) ); const result3 = validateJwtFormat(invalidToken3); expect(result3.isErr()).toBe(true); expect(result3.isOk()).toBe(false); result3.mapErr((error) => - expect(error).toStrictEqual([ - "The second (payload) segment cannot be an empty string.", - ]), + expect(error).toStrictEqual({ + task: DebuggerTaskValues.DECODE, + input: DebuggerInputValues.JWT, + message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, + }) ); const result4 = validateJwtFormat(invalidToken4); expect(result4.isErr()).toBe(true); expect(result4.isOk()).toBe(false); result4.mapErr((error) => - expect(error).toStrictEqual([ - "The JWT must contain at least one period ('.') character. Source: https://datatracker.ietf.org/doc/html/rfc7519#section-7.2", - ]), + expect(error).toStrictEqual({ + task: DebuggerTaskValues.DECODE, + input: DebuggerInputValues.JWT, + message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, + }) ); const result5 = validateJwtFormat(invalidToken5); expect(result5.isErr()).toBe(true); expect(result5.isOk()).toBe(false); result5.mapErr((error) => - expect(error).toStrictEqual([ - "The JWT must contain at least one period ('.') character. Source: https://datatracker.ietf.org/doc/html/rfc7519#section-7.2", - ]), + expect(error).toStrictEqual({ + task: DebuggerTaskValues.DECODE, + input: DebuggerInputValues.JWT, + message: `JWT must not be empty.`, + }) ); const result6 = validateJwtFormat(invalidToken6); expect(result6.isErr()).toBe(true); expect(result6.isOk()).toBe(false); result6.mapErr((error) => - expect(error).toStrictEqual([ - "Each JWT segment must be a base64url-encoded. The third (signature) segment isn't.", - ]), + expect(error).toStrictEqual({ + task: DebuggerTaskValues.DECODE, + input: DebuggerInputValues.JWT, + message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, + }) ); const result7 = validateJwtFormat(invalidToken7); expect(result7.isErr()).toBe(true); expect(result7.isOk()).toBe(false); result7.mapErr((error) => - expect(error).toStrictEqual([ - "The second segment, the JWT payload, must represent a completely valid JSON object conforming to RFC 7159.", - ]), + expect(error).toStrictEqual({ + task: DebuggerTaskValues.DECODE, + input: DebuggerInputValues.JWT, + message: `The second segment, the JWT payload, must represent a completely valid JSON object conforming to [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-3).`, + data: { + header: { + alg: "HS256", + }, + payload: "test", + }, + }) ); const result8 = validateJwtFormat(invalidToken8); expect(result8.isErr()).toBe(true); expect(result8.isOk()).toBe(false); result8.mapErr((error) => - expect(error).toStrictEqual([ - "The first segment, the JWT header, and the second segment, the JWT payload, must represent a completely valid JSON object conforming to RFC 7159.", - ]), + expect(error).toStrictEqual({ + task: DebuggerTaskValues.DECODE, + input: DebuggerInputValues.JWT, + message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, + }) ); const result9 = validateJwtFormat(invalidToken9); expect(result9.isErr()).toBe(true); expect(result9.isOk()).toBe(false); result9.mapErr((error) => - expect(error).toStrictEqual([ - "This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters. Source: https://datatracker.ietf.org/doc/html/rfc7516#section-9", - ]), + expect(error).toStrictEqual({ + task: DebuggerTaskValues.DECODE, + input: DebuggerInputValues.JWT, + message: `This tool only supports a JWT that uses the JWS Compact Serialization, which must have three base64url-encoded segments separated by two period ('.') characters as defined on [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-3.3)`, + }) ); }); }); diff --git a/vitest.config.ts b/vitest.config.ts index b8aa5767..e278344a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ coverage: { provider: "istanbul", }, + exclude: ['e2e', 'node_modules'] }, plugins: [tsconfigPaths()], });