From a5d47c8be0ccd1520f09c12525db572c7922d3cc Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Fri, 8 May 2026 17:07:56 +0200 Subject: [PATCH] build(ci): report test coverage (#1241) --- .github/workflows/build.yml | 14 ++++++++++---- .gitignore | 1 + src/lib/hash.slow.test.ts | 12 ++++++++++++ src/lib/hash.test.ts | 33 ++++++++++++++++++--------------- src/lib/hash.ts | 7 +++---- vitest.config.ts | 14 +++++++++++++- 6 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 src/lib/hash.slow.test.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b47d9667..609eb8ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,10 +15,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup (Node.js ${{ matrix.node-version }}) - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} @@ -30,7 +30,7 @@ jobs: run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package-lock.json').packages['node_modules/@playwright/test'].version)")" >> $GITHUB_ENV - name: Cache Playwright - uses: actions/cache@v3 + uses: actions/cache@v5 id: playwright-cache with: path: | @@ -50,7 +50,13 @@ jobs: run: npm run format:check - name: Test - run: npm test + run: npm test -- --coverage.enabled - name: Build run: npm run build + + - name: Upload coverage report + # Set if: always() to also generate the report if tests are failing + # Only works if you set `reportOnFailure: true` in vite config + if: always() + uses: davelosert/vitest-coverage-report-action@v2 diff --git a/.gitignore b/.gitignore index 3c031b22..138c7e10 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ dist-ssr *storybook.log storybook-static/ +coverage/ diff --git a/src/lib/hash.slow.test.ts b/src/lib/hash.slow.test.ts new file mode 100644 index 00000000..1cf3795f --- /dev/null +++ b/src/lib/hash.slow.test.ts @@ -0,0 +1,12 @@ +import { describe, it, expect } from 'vitest' +import { DEFAULT_PBKDF_ITERATIONS, hashPassword } from './hash' + +// **NOTE**: Seems there are issues generating code coverage via v8 for long running/blocking tests. +// Externalized to own file for exclusion in coverage settings. +describe('hash (slow)', () => { + it('hashPassword', { timeout: 20_000 }, async () => { + expect(await hashPassword('test', 'Satoshi.jmdat', DEFAULT_PBKDF_ITERATIONS)).toBe( + 'da41454ecc40c48499decbca7b1df4595f0a856caada3f182d47293fbad03004', + ) + }) +}) diff --git a/src/lib/hash.test.ts b/src/lib/hash.test.ts index 867e5b30..b50b853a 100644 --- a/src/lib/hash.test.ts +++ b/src/lib/hash.test.ts @@ -2,20 +2,23 @@ import { describe, it, expect } from 'vitest' import { DEFAULT_PBKDF_ITERATIONS, hashPassword } from './hash' describe('hash', () => { - it('DEFAULT_PBKDF_ITERATIONS', () => { - expect(DEFAULT_PBKDF_ITERATIONS).toBe(210_000) - }) - - it('hashPassword', { timeout: 20_000 }, async () => { - expect(await hashPassword('', '', 1)).toBe('6d2ecbbbfb2e6dcd7056faf9af6aa06eae594391db983279a6bf27e0eb228614') - expect(await hashPassword('password', 'salt', 1)).toBe( - '867f70cf1ade02cff3752599a3a53dc4af34c7a669815ae5d513554e1c8cf252', - ) - expect(await hashPassword('test', 'Satoshi.jmdat', 21)).toBe( - '1acb29f6e7c841823a9a2369d2f2cc7e9ee19c78621c4d7194d1f45eb0d5e8ed', - ) - expect(await hashPassword('test', 'Satoshi.jmdat', DEFAULT_PBKDF_ITERATIONS)).toBe( - 'da41454ecc40c48499decbca7b1df4595f0a856caada3f182d47293fbad03004', - ) + describe('hashPassword', () => { + it('DEFAULT_PBKDF_ITERATIONS', () => { + expect(DEFAULT_PBKDF_ITERATIONS).toBe(210_000) + }) + it('hashPassword success', { timeout: 20_000 }, async () => { + expect(await hashPassword('', '', 1)).toBe('6d2ecbbbfb2e6dcd7056faf9af6aa06eae594391db983279a6bf27e0eb228614') + expect(await hashPassword('password', 'salt', 1)).toBe( + '867f70cf1ade02cff3752599a3a53dc4af34c7a669815ae5d513554e1c8cf252', + ) + expect(await hashPassword('test', 'Satoshi.jmdat', 21)).toBe( + '1acb29f6e7c841823a9a2369d2f2cc7e9ee19c78621c4d7194d1f45eb0d5e8ed', + ) + }) + it('hashPassword error', { timeout: 20_000 }, async () => { + await expect(async () => { + return await hashPassword('', '', -1) + }).rejects.toThrowError('Failed to hash password: "c" expected integer >= 0, got -1') + }) }) }) diff --git a/src/lib/hash.ts b/src/lib/hash.ts index a1b3b9c7..65a6a68d 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -1,9 +1,9 @@ -import { pbkdf2Async } from '@noble/hashes/pbkdf2.js' +import { pbkdf2Async, type Pbkdf2Opt } from '@noble/hashes/pbkdf2.js' import { sha512 } from '@noble/hashes/sha2.js' import { bytesToHex } from '@noble/hashes/utils.js' // see https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 (last check: 2026-01) -export const DEFAULT_PBKDF_ITERATIONS = 210_000 +export const DEFAULT_PBKDF_ITERATIONS: Pbkdf2Opt['c'] = 210_000 /** * Securely hashes a password using PBKDF2 with SHA-512. @@ -16,7 +16,7 @@ export const DEFAULT_PBKDF_ITERATIONS = 210_000 export async function hashPassword( password: string, salt: string, - iterations = DEFAULT_PBKDF_ITERATIONS, + iterations: Pbkdf2Opt['c'] = DEFAULT_PBKDF_ITERATIONS, ): Promise { try { const passwordBuffer = new TextEncoder().encode(password) @@ -24,7 +24,6 @@ export async function hashPassword( const derivedKey = await pbkdf2Async(sha512, passwordBuffer, saltBuffer, { c: iterations, dkLen: 32 }) return bytesToHex(derivedKey) } catch (error: unknown) { - console.error('Password hashing failed:', error) const reason = (error instanceof Error ? (error.message ?? '') : '') || 'Unknown error' throw new Error(`Failed to hash password: ${reason}`) } diff --git a/vitest.config.ts b/vitest.config.ts index f350a29d..0b801f39 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -11,6 +11,18 @@ export default defineConfig((args: ConfigEnv): ViteUserConfig => { env: { LC_ALL: 'en_US.UTF-8', }, + coverage: { + // 'json-summary' is required for ci coverage report + reporter: ['text', 'json', 'json-summary'], + // If you want a coverage reports even if your tests are failing, include the reportOnFailure option + reportOnFailure: true, + thresholds: { + lines: 70, + functions: 60, + branches: 60, + statements: 70, + }, + }, projects: [ { extends: true, @@ -37,7 +49,7 @@ export default defineConfig((args: ConfigEnv): ViteUserConfig => { environment: 'jsdom', setupFiles: './vitest.setup.ts', include: ['**/*.test.{ts,tsx}'], - exclude: ['node_modules', '.storybook'], + exclude: ['src/lib/hash.slow.test.ts', 'node_modules', '.storybook'], }, resolve: { alias: {