build(ci): report test coverage (#1241)

This commit is contained in:
Thebora Kompanioni 2026-05-08 17:07:56 +02:00 committed by GitHub
parent 1b37ad3169
commit a5d47c8be0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 57 additions and 24 deletions

View file

@ -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

1
.gitignore vendored
View file

@ -29,3 +29,4 @@ dist-ssr
*storybook.log
storybook-static/
coverage/

12
src/lib/hash.slow.test.ts Normal file
View file

@ -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',
)
})
})

View file

@ -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')
})
})
})

View file

@ -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<string> {
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}`)
}

View file

@ -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: {