Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .env.jwt-testing
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Example environment configuration for local JWT testing
# Copy this file to .env and uncomment the JWT public key to enable JWT authentication tests

# JWT Test Public Key - Use this for local testing with generated test tokens
# This key matches the private key in cypress/fixtures/jwt-keys.json
# WARNING: This is a TEST KEY ONLY - DO NOT use in production!
#NEXT_PUBLIC_JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
#MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj760PNWDk5AWonv/G63Q
#08b1XAqUdCVttXLxr6AEXcJeYWXwDPRZAGKBpMTVcu0SIl7I958ebVx2A1I4dNAZ
#6xCku2bgOzoOiFJqNF1EzaxhHbk2gBQt6q92X5RaPFZh3UUkmvISACoiDH+Mja2W
#kW3o8o4iRWaRUvo0sRpbv+O7PSx+3FBABGZSSz1wV7rz7YMjDUjHCF2gsS3XKeA3
#ZzmwYlLmpxM1kD6h/XloO9OHgH2h2IlOyhm7VkhRYYc1auj5zJYKzKkWfCvbozF+
#rufZNFqMGjlUzmH5KYr4CcnuzYFTN0RxUJrCs1UDh/KbI2wZx3ZXXt4zp4QQNAO0
#RQIDAQAB
#-----END PUBLIC KEY-----"

# Other environment variables (see .env.example for complete list)
3 changes: 3 additions & 0 deletions .github/workflows/cypress_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ on:
required: true
CYPRESS_USER_COOKIE:
required: true
NEXT_PUBLIC_JWT_PUBLIC_KEY:
required: false

jobs:
cypress:
Expand Down Expand Up @@ -40,6 +42,7 @@ jobs:
CYPRESS_NODE_ENV: test
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
NEXT_PUBLIC_ORCID_API_URL: ${{ secrets.NEXT_PUBLIC_ORCID_API_URL }}
NEXT_PUBLIC_JWT_PUBLIC_KEY: ${{ secrets.NEXT_PUBLIC_JWT_PUBLIC_KEY }}
SITEMAPS_URL: ${{ secrets.SITEMAPS_URL }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
CYPRESS_USER_COOKIE: ${{ secrets.CYPRESS_USER_COOKIE }}
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ Note: `yarn dev` only runs the frontend. If you need the API, use `yarn dev-all`
* `yarn cy:run`: Runs Cypress tests in headless mode.
* `yarn cy:open`: Opens the Cypress Test Runner for interactive testing.

#### JWT Authentication Tests

To run JWT authentication tests with valid tokens:
1. Copy `.env.jwt-testing` to `.env` and uncomment the JWT public key
2. See `cypress/fixtures/JWT_TEST_SETUP.md` for detailed instructions
3. The test keys in `cypress/fixtures/jwt-keys.json` are for testing only

For more details on JWT testing, see: `docs/JWT_TESTING.md`

### Linting and Formatting

* `yarn lint`: Runs ESLint to check for code quality issues.
Expand Down
29 changes: 27 additions & 2 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,42 @@
import { defineConfig } from 'cypress'
import * as fs from 'fs'
import * as path from 'path'

export default defineConfig({
experimentalFetchPolyfill: false,
video: false,
projectId: 'yur1cf',
retries: 2,
e2e: {
setupNodeEvents(on, config) {},
setupNodeEvents(on, config) {
// Map CYPRESS_USER_COOKIE environment variable to Cypress.env('userCookie')
if (process.env.CYPRESS_USER_COOKIE) {
config.env.userCookie = process.env.CYPRESS_USER_COOKIE
}

// Load JWT public key from fixture for tests
const jwtKeysPath = path.join(__dirname, 'cypress', 'fixtures', 'jwt-keys.json')
if (fs.existsSync(jwtKeysPath)) {
const jwtKeys = JSON.parse(fs.readFileSync(jwtKeysPath, 'utf8'))
config.env.jwtPublicKey = jwtKeys.publicKey
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the cypress.config.ts file to see full context
fd cypress.config.ts -type f | head -5

Repository: datacite/akita

Length of output: 228


🏁 Script executed:

# Find and read the full cypress.config.ts file
cat -n cypress.config.ts 2>/dev/null || echo "File not found at root"

Repository: datacite/akita

Length of output: 1848


🏁 Script executed:

# Search for JWT-related skip patterns in test files
rg -n 'describe\.skip|it\.skip|xit\(|xdescribe\(|skip\(|jwtPublicKey|JWT_PUBLIC_KEY|NEXT_PUBLIC_JWT_PUBLIC_KEY' --type ts --type js | head -50

Repository: datacite/akita

Length of output: 1724


🏁 Script executed:

# Look for how Cypress.env is used with JWT configuration in tests
rg -B3 -A3 "Cypress\.env\('jwtPublicKey'\)|skipIf|skip.*jwt|jwt.*skip" --type ts --type js | head -60

Repository: datacite/akita

Length of output: 1719


Gate JWT test enablement on NEXT_PUBLIC_JWT_PUBLIC_KEY instead of local fixture (prevents test mismatch with unconfigurated app).

The fixture (cypress/fixtures/jwt-keys.json) is unconditionally loaded if it exists, which always sets config.env.jwtPublicKey. Tests skip only when both Cypress.env('jwtPublicKey') AND Cypress.env('NEXT_PUBLIC_JWT_PUBLIC_KEY') are absent—so with the fixture present, the skip condition never triggers even when the app isn't configured with JWT. This causes tests to run with a test key while the app may be using a different or missing key, creating false positives.

Instead, check process.env.NEXT_PUBLIC_JWT_PUBLIC_KEY and set the Cypress env only when that variable is present, or expose an explicit jwtPublicKeyConfigured flag to indicate whether JWT is ready for testing.

Suggested adjustment (illustrative)
-      const jwtKeysPath = path.join(__dirname, 'cypress', 'fixtures', 'jwt-keys.json')
-      if (fs.existsSync(jwtKeysPath)) {
-        const jwtKeys = JSON.parse(fs.readFileSync(jwtKeysPath, 'utf8'))
-        config.env.jwtPublicKey = jwtKeys.publicKey
-      }
+      const envPublicKey = process.env.NEXT_PUBLIC_JWT_PUBLIC_KEY
+      if (envPublicKey) {
+        config.env.jwtPublicKey = envPublicKey
+        config.env.jwtPublicKeyConfigured = true
+      } else {
+        config.env.jwtPublicKeyConfigured = false
+      }

Also applies to: 31-36

🤖 Prompt for AI Agents
In `@cypress.config.ts` around lines 17 - 22, The current fixture-loading block
(jwtKeysPath / jwtKeys / config.env.jwtPublicKey) unconditionally sets Cypress
env from a local file; change it to only set config.env.jwtPublicKey when the
app is actually configured by checking process.env.NEXT_PUBLIC_JWT_PUBLIC_KEY
(or set an explicit flag like config.env.jwtPublicKeyConfigured) and use that to
gate setting config.env.jwtPublicKey; update the same logic in the other similar
block (lines referenced) so tests rely on NEXT_PUBLIC_JWT_PUBLIC_KEY presence
rather than a local fixture.


return config
},
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.test.*',
},
component: {
setupNodeEvents(on, config) { },
setupNodeEvents(on, config) {
// Load JWT public key from fixture for component tests
const jwtKeysPath = path.join(__dirname, 'cypress', 'fixtures', 'jwt-keys.json')
if (fs.existsSync(jwtKeysPath)) {
const jwtKeys = JSON.parse(fs.readFileSync(jwtKeysPath, 'utf8'))
config.env.jwtPublicKey = jwtKeys.publicKey
}

return config
},
specPattern: 'src/components/**/*.test.*',
devServer: {
bundler: 'webpack',
Expand Down
166 changes: 166 additions & 0 deletions cypress/e2e/jwtAuth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/// <reference types="cypress" />

import { setAuthenticatedSession } from '../support/jwt-helper'

describe('JWT Authentication', () => {
beforeEach(() => {
cy.setCookie('_consent', 'true')
})

describe('Unauthenticated User', () => {
it('should show sign in link when not authenticated', () => {
cy.visit('/')
cy.get('a[href*="sign_in"]', { timeout: 30000 }).should('be.visible')
})

it('should not show user menu when not authenticated', () => {
cy.visit('/')
cy.get('#sign-in').should('not.exist')
})
})

describe('Authenticated User (with valid JWT)', () => {
beforeEach(() => {
// Set up authenticated session with valid JWT token using test fixtures
// This will work if NEXT_PUBLIC_JWT_PUBLIC_KEY is set to the test public key
setAuthenticatedSession({ uid: 'test-user-123', name: 'Test User' })
})

it('should display user name when authenticated with valid JWT', () => {
// Skip if JWT public key is not configured for tests
if (!Cypress.env('jwtPublicKey') && !Cypress.env('NEXT_PUBLIC_JWT_PUBLIC_KEY')) {
cy.log('Skipping: NEXT_PUBLIC_JWT_PUBLIC_KEY not configured for tests')
return
}

cy.visit('/')
cy.get('#sign-in', { timeout: 30000 }).should('be.visible')
})

it('should show user dropdown menu when authenticated with valid JWT', () => {
// Skip if JWT public key is not configured for tests
if (!Cypress.env('jwtPublicKey') && !Cypress.env('NEXT_PUBLIC_JWT_PUBLIC_KEY')) {
cy.log('Skipping: NEXT_PUBLIC_JWT_PUBLIC_KEY not configured for tests')
return
}

cy.visit('/')
cy.get('#sign-in', { timeout: 30000 }).click()
cy.get('[data-cy=settings]').should('be.visible')
})
})

describe('Invalid JWT Token', () => {
it('should handle invalid token gracefully', () => {
// Set an invalid JWT token
const invalidCookie = JSON.stringify({
authenticated: {
access_token: 'invalid.jwt.token'
}
})
cy.setCookie('_datacite', invalidCookie)

cy.visit('/')
// Should behave like unauthenticated user
cy.get('a[href*="sign_in"]', { timeout: 30000 }).should('be.visible')
})

it('should handle malformed cookie gracefully', () => {
// Set a malformed cookie
cy.setCookie('_datacite', 'not-valid-json')

cy.visit('/')
// Should behave like unauthenticated user
cy.get('a[href*="sign_in"]', { timeout: 30000 }).should('be.visible')
})

it('should handle missing access_token in cookie', () => {
// Set a cookie without access_token
const incompleteCookie = JSON.stringify({
authenticated: {}
})
cy.setCookie('_datacite', incompleteCookie)

cy.visit('/')
// Should behave like unauthenticated user
cy.get('a[href*="sign_in"]', { timeout: 30000 }).should('be.visible')
})
})

describe('JWT Verification Error Handling', () => {
it('should not crash the app with expired token', () => {
// Set an expired JWT token (this will be caught by JWT verification)
const expiredCookie = JSON.stringify({
authenticated: {
access_token: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJ0ZXN0LXVzZXIiLCJuYW1lIjoiVGVzdCBVc2VyIiwiZXhwIjoxfQ.invalid'
}
})
cy.setCookie('_datacite', expiredCookie)

cy.visit('/')
// App should still load
cy.get('body').should('be.visible')
cy.get('a[href*="sign_in"]', { timeout: 30000 }).should('be.visible')
})

it('should not crash the app with corrupted token', () => {
// Set a corrupted JWT token
const corruptedCookie = JSON.stringify({
authenticated: {
access_token: 'corrupted-token-that-is-not-valid'
}
})
cy.setCookie('_datacite', corruptedCookie)

cy.visit('/')
// App should still load
cy.get('body').should('be.visible')
cy.get('a[href*="sign_in"]', { timeout: 30000 }).should('be.visible')
})
})

describe('Session Persistence', () => {
it('should maintain session across page navigations', () => {
// Set a cookie (even though token verification may fail without proper JWT setup)
const testCookie = JSON.stringify({
authenticated: {
access_token: 'test.token.value'
}
})
cy.setCookie('_datacite', testCookie)

cy.visit('/')
// Cookie should be present on first page
cy.getCookie('_datacite').should('exist')

// Navigate to different pages
cy.visit('/about')
cy.get('body').should('be.visible')

// Cookie should still be present after navigation
cy.getCookie('_datacite').should('exist')

// Authentication state should remain consistent (signed out in this case)
cy.get('a[href*="sign_in"]', { timeout: 30000 }).should('be.visible')
})

it('should handle session when NEXT_PUBLIC_JWT_PUBLIC_KEY is not configured', () => {
// This test verifies that when NEXT_PUBLIC_JWT_PUBLIC_KEY env var is not set,
// the app doesn't crash but handles it gracefully
const testCookie = JSON.stringify({
authenticated: {
access_token: 'test.token.value'
}
})
cy.setCookie('_datacite', testCookie)

cy.visit('/')
// App should still be functional
cy.get('body').should('be.visible')
// Should show sign in link since JWT verification fails without the key
cy.get('a[href*="sign_in"]', { timeout: 30000 }).should('be.visible')
})
})
})

export {}
Loading
Loading