Backend tests

This commit is contained in:
Ludy87
2026-07-03 19:51:06 +02:00
parent 11ba3814e5
commit 51950a618b
12 changed files with 556 additions and 72 deletions
+56 -2
View File
@@ -109,28 +109,82 @@ tasks:
platforms: [linux, darwin]
test:
desc: "Run backend tests"
desc: "Run backend tests with and without frontend bundling"
# Cover the backend with the main build/property combinations that change
# Gradle behavior in this repo.
cmds:
- cmd: cmd /c ".\gradlew.bat test"
- cmd: cmd /c ".\gradlew.bat test" >> log-spotlessCheck.txt
platforms: [windows]
- cmd: ./gradlew test
platforms: [linux, darwin]
- cmd: cmd /c ".\gradlew.bat test -PbuildWithFrontend=true" >> log-spotlessCheck-buildWithFrontend.txt
platforms: [windows]
- cmd: ./gradlew test -PbuildWithFrontend=true
platforms: [linux, darwin]
- cmd: cmd /c ".\gradlew.bat test -PbuildWithFrontend=true -PprototypesMode=true" >> log-spotlessCheck-buildWithFrontend-prototypesMode.txt
platforms: [windows]
- cmd: ./gradlew test -PbuildWithFrontend=true -PprototypesMode=true
platforms: [linux, darwin]
- cmd: cmd /c "set STIRLING_FLAVOR=core&& .\gradlew.bat test" >> log-spotlessCheck-STIRLING_FLAVOR-core.txt
platforms: [windows]
- cmd: STIRLING_FLAVOR=core ./gradlew test
platforms: [linux, darwin]
- cmd: cmd /c "set STIRLING_FLAVOR=saas&& .\gradlew.bat test" >> log-spotlessCheck-STIRLING_FLAVOR-saas.txt
platforms: [windows]
- cmd: STIRLING_FLAVOR=saas ./gradlew test
platforms: [linux, darwin]
format:
desc: "Auto-fix code formatting"
# Mirror the test matrix so formatting is validated under the same backend
# configuration variants.
cmds:
- cmd: cmd /c ".\gradlew.bat spotlessApply"
platforms: [windows]
- cmd: ./gradlew spotlessApply
platforms: [linux, darwin]
- cmd: cmd /c ".\gradlew.bat spotlessApply -PbuildWithFrontend=true"
platforms: [windows]
- cmd: ./gradlew spotlessApply -PbuildWithFrontend=true
platforms: [linux, darwin]
- cmd: cmd /c ".\gradlew.bat spotlessApply -PbuildWithFrontend=true -PprototypesMode=true"
platforms: [windows]
- cmd: ./gradlew spotlessApply -PbuildWithFrontend=true -PprototypesMode=true
platforms: [linux, darwin]
- cmd: cmd /c "set STIRLING_FLAVOR=core&& .\gradlew.bat spotlessApply"
platforms: [windows]
- cmd: STIRLING_FLAVOR=core ./gradlew spotlessApply
platforms: [linux, darwin]
- cmd: cmd /c "set STIRLING_FLAVOR=saas&& .\gradlew.bat spotlessApply"
platforms: [windows]
- cmd: STIRLING_FLAVOR=saas ./gradlew spotlessApply
platforms: [linux, darwin]
format:check:
desc: "Check code formatting"
# Mirror the test matrix so formatting checks use the same backend
# configuration variants.
cmds:
- cmd: cmd /c ".\gradlew.bat spotlessCheck"
platforms: [windows]
- cmd: ./gradlew spotlessCheck
platforms: [linux, darwin]
- cmd: cmd /c ".\gradlew.bat spotlessCheck -PbuildWithFrontend=true"
platforms: [windows]
- cmd: ./gradlew spotlessCheck -PbuildWithFrontend=true
platforms: [linux, darwin]
- cmd: cmd /c ".\gradlew.bat spotlessCheck -PbuildWithFrontend=true -PprototypesMode=true"
platforms: [windows]
- cmd: ./gradlew spotlessCheck -PbuildWithFrontend=true -PprototypesMode=true
platforms: [linux, darwin]
- cmd: cmd /c "set STIRLING_FLAVOR=core&& .\gradlew.bat spotlessCheck"
platforms: [windows]
- cmd: STIRLING_FLAVOR=core ./gradlew spotlessCheck
platforms: [linux, darwin]
- cmd: cmd /c "set STIRLING_FLAVOR=saas&& .\gradlew.bat spotlessCheck"
platforms: [windows]
- cmd: STIRLING_FLAVOR=saas ./gradlew spotlessCheck
platforms: [linux, darwin]
fix:
desc: "Auto-fix backend"
+11 -11
View File
@@ -6,7 +6,7 @@ spotless {
java {
target 'src/**/java/**/*.java'
targetExclude 'src/main/java/org/apache/**'
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false)
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
suppressLintsFor { setStep('google-java-format') }
@@ -29,19 +29,19 @@ spotless {
}
}
dependencies {
api "com.google.guava:guava:${guavaVersion}"
api "com.google.guava:guava:${rootProject.ext.guavaVersion}"
api 'org.springframework.boot:spring-boot-starter-webmvc'
api 'org.springframework.boot:spring-boot-starter-aspectj'
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260313.1'
api 'com.fathzer:javaluator:3.0.6'
api 'com.posthog.java:posthog:1.2.0'
api "org.apache.commons:commons-lang3:${commonsLang3}"
api "org.apache.commons:commons-lang3:${rootProject.ext.commonsLang3}"
api 'com.drewnoakes:metadata-extractor:2.20.0' // Image metadata extractor
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion"
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
api "org.apache.pdfbox:preflight:$pdfboxVersion"
api "org.apache.pdfbox:pdfbox:${rootProject.ext.pdfboxVersion}"
api "org.apache.pdfbox:pdfbox-io:${rootProject.ext.pdfboxVersion}"
api "org.apache.pdfbox:xmpbox:${rootProject.ext.pdfboxVersion}"
api "org.apache.pdfbox:preflight:${rootProject.ext.pdfboxVersion}"
api 'com.github.junrar:junrar:7.5.10' // RAR archive support for CBR files
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
@@ -60,7 +60,7 @@ dependencies {
exclude group: 'com.google.code.gson', module: 'gson'
}
api "com.stirling:jpdfium:${jpdfiumVersion}"
api "com.stirling:jpdfium:${rootProject.ext.jpdfiumVersion}"
// -PjpdfiumPlatforms=all|<csv of linux-x64,linux-arm64,darwin-x64,darwin-arm64,windows-x64>
def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim()
@@ -75,12 +75,12 @@ dependencies {
}
logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms.join(', ')}")
jpdfiumPlatforms.each { platform ->
runtimeOnly "com.stirling:jpdfium-natives-${platform}:${jpdfiumVersion}"
runtimeOnly "com.stirling:jpdfium-natives-${platform}:${rootProject.ext.jpdfiumVersion}"
}
// Bucket4j (local in-process token bucket for RateLimitStore default impl)
implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}"
implementation "com.bucket4j:bucket4j_jdk17-core:${rootProject.ext.bucket4jVersion}"
// ArchUnit: enforces module dependency direction (see ArchitectureTest)
testImplementation "com.tngtech.archunit:archunit-junit5:${archunitVersion}"
testImplementation "com.tngtech.archunit:archunit-junit5:${rootProject.ext.archunitVersion}"
}
+2
View File
@@ -0,0 +1,2 @@
# Auto-generated by MSW (`msw init`); regenerated verbatim, not hand-formatted.
src/main/resources/static/mockServiceWorker.js
+21 -19
View File
@@ -13,7 +13,7 @@ spotless {
java {
target 'src/**/java/**/*.java'
targetExclude 'src/main/resources/static/**', 'src/main/java/org/apache/**'
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false)
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
suppressLintsFor { setStep('google-java-format') }
@@ -67,21 +67,21 @@ dependencies {
exclude group: 'com.fasterxml.jackson.jaxrs'
exclude group: 'com.fasterxml.jackson.module', module: 'jackson-module-jaxb-annotations'
}
implementation "commons-io:commons-io:$commonsIoVersion"
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "commons-io:commons-io:${rootProject.ext.commonsIoVersion}"
implementation "org.bouncycastle:bcprov-jdk18on:${rootProject.ext.bouncycastleVersion}"
implementation "org.bouncycastle:bcpkix-jdk18on:${rootProject.ext.bouncycastleVersion}"
implementation 'io.micrometer:micrometer-core'
implementation 'com.google.zxing:core:3.5.4'
implementation "org.commonmark:commonmark:$commonmarkVersion" // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion"
implementation "org.commonmark:commonmark:${rootProject.ext.commonmarkVersion}" // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark-ext-gfm-tables:${rootProject.ext.commonmarkVersion}"
// General PDF dependencies
implementation "org.apache.pdfbox:preflight:$pdfboxVersion"
implementation "org.apache.pdfbox:xmpbox:$pdfboxVersion"
implementation "org.apache.pdfbox:preflight:${rootProject.ext.pdfboxVersion}"
implementation "org.apache.pdfbox:xmpbox:${rootProject.ext.pdfboxVersion}"
implementation 'org.verapdf:validation-model:1.28.2'
// CVE-2025-66453: Explicit rhino 1.7.15 to override verapdf's 1.7.13
implementation "org.mozilla:rhino:${rhinoVersion}"
implementation "org.mozilla:rhino:${rootProject.ext.rhinoVersion}"
// veraPDF still uses javax.xml.bind, not the new jakarta namespace
implementation 'javax.xml.bind:jaxb-api:2.3.1'
@@ -89,33 +89,33 @@ dependencies {
implementation 'com.sun.xml.bind:jaxb-core:4.0.7'
// CVE-2022-25647: Explicit gson to prevent unsafe deserialization (tabula would pull 2.8.7)
implementation "com.google.code.gson:gson:${gsonVersion}"
implementation "com.google.code.gson:gson:${rootProject.ext.gsonVersion}"
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation 'org.apache.poi:poi-ooxml:5.5.1'
// Batik only bridge module needed (transitively pulls anim, gvt, util, css, dom, svg-dom)
// Replaces batik-all which included unused codec, svggen, transcoder, script modules
implementation "org.apache.xmlgraphics:batik-bridge:${batikVersion}"
implementation "org.apache.xmlgraphics:batik-bridge:${rootProject.ext.batikVersion}"
// Required by TwelveMonkeys imageio-batik SPI (SVGImageReaderSpi) during ImageIO init
runtimeOnly "org.apache.xmlgraphics:batik-transcoder:${batikVersion}"
runtimeOnly "org.apache.xmlgraphics:batik-transcoder:${rootProject.ext.batikVersion}"
// PDFBox Graphics2D bridge for Batik SVG to PDF conversion
implementation 'de.rototor.pdfbox:graphics2d:3.0.5'
// TwelveMonkeys
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:${rootProject.ext.imageioVersion}"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-hdr:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-icns:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-iff:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:${rootProject.ext.imageioVersion}"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
@@ -284,7 +284,9 @@ tasks.register('npmBuild', Exec) {
group = 'frontend'
description = 'Build editor frontend application'
workingDir file('../..')
commandLine = ['task', frontendBuildTask]
commandLine = Os.isFamily(Os.FAMILY_WINDOWS) ?
['powershell', '-NoProfile', '-Command', "task ${frontendBuildTask}"] :
['task', frontendBuildTask]
inputs.dir(new File(frontendEditorDir, 'src'))
inputs.dir(new File(frontendEditorDir, 'public'))
inputs.file(new File(frontendDir, 'package.json'))
@@ -20,17 +20,18 @@
--cc-separator-border-color: #e0e0e0;
--cc-toggle-on-bg: #007bff;
--cc-toggle-off-bg: #667481;
--cc-toggle-on-knob-bg: #ffffff;
--cc-toggle-off-knob-bg: #ffffff;
/* Toggle colors mirror Mantine Switch (light scheme) */
--cc-toggle-on-bg: var(--mantine-primary-color-filled, #007bff);
--cc-toggle-off-bg: var(--mantine-color-gray-3, #dee2e6);
--cc-toggle-on-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-off-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-enabled-icon-color: #ffffff;
--cc-toggle-disabled-icon-color: #ffffff;
--cc-toggle-readonly-bg: #f1f3f4;
--cc-toggle-readonly-knob-bg: #79747e;
--cc-toggle-readonly-knob-icon-color: #f1f3f4;
--cc-toggle-readonly-bg: var(--mantine-color-disabled, #f1f3f4);
--cc-toggle-readonly-knob-bg: var(--mantine-color-gray-0, #f8f9fa);
--cc-toggle-readonly-knob-icon-color: transparent;
--cc-section-category-border: #e0e0e0;
@@ -69,17 +70,18 @@
--cc-separator-border-color: #555555;
--cc-toggle-on-bg: #4dabf7;
--cc-toggle-off-bg: #667481;
--cc-toggle-on-knob-bg: #2d2d2d;
--cc-toggle-off-knob-bg: #2d2d2d;
/* Toggle colors mirror Mantine Switch (dark scheme) */
--cc-toggle-on-bg: var(--mantine-primary-color-filled, #4dabf7);
--cc-toggle-off-bg: var(--mantine-color-dark-5, #555555);
--cc-toggle-on-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-off-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-enabled-icon-color: #2d2d2d;
--cc-toggle-disabled-icon-color: #2d2d2d;
--cc-toggle-readonly-bg: #555555;
--cc-toggle-readonly-knob-bg: #8e8e8e;
--cc-toggle-readonly-knob-icon-color: #555555;
--cc-toggle-readonly-bg: var(--mantine-color-disabled, #555555);
--cc-toggle-readonly-knob-bg: var(--mantine-color-dark-3, #8e8e8e);
--cc-toggle-readonly-knob-icon-color: transparent;
--cc-section-category-border: #555555;
@@ -176,9 +178,16 @@
color: var(--cc-primary-color) !important;
}
/* Lower z-index so cookie banner appears behind onboarding modals */
/* Banner sits above the chat FAB but behind all modals and onboarding; value
is Z_INDEX_COOKIE_CONSENT_BANNER, set as this variable by useCookieConsent */
#cc-main {
z-index: 100 !important;
z-index: var(--z-index-cookie-consent) !important;
}
/* Preferences dialog sits above the settings modal it opens from; value is
Z_INDEX_COOKIE_PREFERENCES_MODAL, set as this variable by useCookieConsent */
.show--preferences #cc-main {
z-index: var(--z-index-cookie-preferences) !important;
}
/* Ensure consent modal text is visible in both themes */
@@ -203,3 +212,63 @@
#cc-main .cm__link {
color: var(--cc-primary-color) !important;
}
/* ── Category toggles restyled to match Mantine Switch (size sm) ──────────
Mantine sm metrics: 38×20 track, 14px plain thumb, 2.5px inline padding,
150ms ease transitions, no icon inside the thumb. Colors come from the
--cc-toggle-* variables above, which point at the Mantine palette. */
#cc-main .section__toggle,
#cc-main .section__toggle-wrapper,
#cc-main .toggle__icon,
#cc-main .toggle__label {
width: 38px !important;
height: 20px !important;
border-radius: 1000px !important;
}
/* Track: flat fill, no outline ring or border */
#cc-main .toggle__icon {
border: none !important;
box-shadow: none !important;
transition: background-color 150ms ease !important;
}
#cc-main .section__toggle:checked ~ .toggle__icon {
border: none !important;
box-shadow: none !important;
}
/* Always-enabled categories = Mantine disabled switch (must out-prioritise
the !important checked-track rule above) */
#cc-main .section__toggle:checked:disabled ~ .toggle__icon {
background: var(--cc-toggle-readonly-bg) !important;
border: none !important;
box-shadow: none !important;
}
#cc-main .section__toggle:disabled {
cursor: not-allowed !important;
}
/* Thumb: small plain circle, vertically centred, no drop shadow */
#cc-main .toggle__icon-circle {
width: 14px !important;
height: 14px !important;
top: 3px !important;
left: 2.5px !important;
box-shadow: none !important;
transition:
transform 150ms ease,
background-color 150ms ease !important;
}
/* Checked thumb travel: 38 − 14 − 2.5 = 21.5px end position */
#cc-main .section__toggle:checked ~ .toggle__icon .toggle__icon-circle {
transform: translateX(19px) !important;
}
/* Mantine switches have no check/cross glyph inside the thumb */
#cc-main .toggle__icon-on,
#cc-main .toggle__icon-off {
display: none !important;
}
@@ -0,0 +1,349 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.14.6'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
addEventListener('install', function () {
self.skipWaiting()
})
addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id')
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now()
// Bypass navigation requests.
if (event.request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (
event.request.cache === 'only-if-cached' &&
event.request.mode !== 'same-origin'
) {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
})
/**
* @param {FetchEvent} event
* @param {string} requestId
* @param {number} requestInterceptedAt
*/
async function handleRequest(event, requestId, requestInterceptedAt) {
const client = await resolveMainClient(event)
const requestCloneForEvents = event.request.clone()
const response = await getResponse(
event,
client,
requestId,
requestInterceptedAt,
)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
)
}
return response
}
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const serializedRequest = await serializeRequest(event.request)
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
interceptedAt: requestInterceptedAt,
...serializedRequest,
},
},
[serializedRequest.body],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
])
})
}
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
}
}
@@ -147,11 +147,17 @@ class SvgToPdfTest {
ImageIO.write(red, "png", external.toFile());
try {
String rootRelativeExternal =
"/"
+ external.toAbsolutePath()
.toString()
.replace('\\', '/')
.replaceFirst("^/+", "");
String svg =
"<svg xmlns=\"http://www.w3.org/2000/svg\" "
+ "xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"100\" height=\"100\">"
+ "<image x=\"0\" y=\"0\" width=\"100\" height=\"100\" xlink:href=\""
+ external.toUri()
+ rootRelativeExternal
+ "\"/></svg>";
byte[] pdf;
+17 -17
View File
@@ -11,7 +11,7 @@ spotless {
java {
target 'src/**/java/**/*.java'
targetExclude 'src/main/java/org/apache/**'
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false)
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
suppressLintsFor { setStep('google-java-format') }
@@ -35,13 +35,13 @@ spotless {
}
dependencies {
implementation project(':common')
api "com.google.guava:guava:${guavaVersion}"
api "com.google.guava:guava:${rootProject.ext.guavaVersion}"
api 'org.springframework:spring-jdbc'
api 'org.springframework:spring-webmvc'
api 'org.springframework.session:spring-session-core'
api "org.springframework.security:spring-security-core:$springSecuritySamlVersion"
api "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
api "org.springframework.security:spring-security-core:${rootProject.ext.springSecuritySamlVersion}"
api "org.springframework.security:spring-security-saml2-service-provider:${rootProject.ext.springSecuritySamlVersion}"
api 'org.springframework.boot:spring-boot-starter-jetty'
api 'org.springframework.boot:spring-boot-starter-security'
api 'org.springframework.boot:spring-boot-starter-data-jpa'
@@ -55,30 +55,30 @@ dependencies {
api 'com.github.ben-manes.caffeine:caffeine'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.46'
implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}"
implementation "com.bucket4j:bucket4j_jdk17-core:${rootProject.ext.bucket4jVersion}"
// Lettuce-backed Bucket4j ProxyManager used by ValkeyRateLimitStore for cluster-wide
// token-bucket rate limiting (parity with in-process Bucket4j semantics; no fixed-window
// boundary doubling).
implementation "com.bucket4j:bucket4j_jdk17-lettuce:${bucket4jVersion}"
implementation "com.bucket4j:bucket4j_jdk17-lettuce:${rootProject.ext.bucket4jVersion}"
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcprov-jdk18on:${rootProject.ext.bouncycastleVersion}"
implementation "com.google.code.gson:gson:${gsonVersion}"
implementation "com.google.code.gson:gson:${rootProject.ext.gsonVersion}"
api 'io.micrometer:micrometer-registry-prometheus'
api "io.jsonwebtoken:jjwt-api:${jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-impl:${jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jwtVersion}"
api "io.jsonwebtoken:jjwt-api:${rootProject.ext.jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-impl:${rootProject.ext.jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${rootProject.ext.jwtVersion}"
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database - file format incompatible with 2.4.x, would break existing user databases
runtimeOnly 'org.postgresql:postgresql:42.7.11'
implementation('com.coveo:saml-client:5.0.0') {
exclude group: 'org.opensaml', module: 'opensaml-core'
}
implementation "software.amazon.awssdk:s3:${awsSdkVersion}"
implementation "software.amazon.awssdk:url-connection-client:${awsSdkVersion}"
implementation "software.amazon.awssdk:s3:${rootProject.ext.awsSdkVersion}"
implementation "software.amazon.awssdk:url-connection-client:${rootProject.ext.awsSdkVersion}"
// @DataJpaTest slice (Boot 4 ships test slices as separate starters, like webmvc-test at the
// root) so policy.source repositories can be exercised against embedded H2.
@@ -86,10 +86,10 @@ dependencies {
// Testcontainers: real MinIO/LocalStack (S3) and Valkey for integration tests in CI without
// manually-started instances. Tests skip cleanly when Docker is unavailable.
testImplementation "org.testcontainers:testcontainers:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:minio:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:localstack:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:junit-jupiter:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:testcontainers:${rootProject.ext.testcontainersMinioVersion}"
testImplementation "org.testcontainers:minio:${rootProject.ext.testcontainersMinioVersion}"
testImplementation "org.testcontainers:localstack:${rootProject.ext.testcontainersMinioVersion}"
testImplementation "org.testcontainers:junit-jupiter:${rootProject.ext.testcontainersMinioVersion}"
}
tasks.register('prepareKotlinBuildScriptModel') {}
@@ -24,7 +24,7 @@ import stirling.software.common.model.ApplicationProperties;
* the full production bean method {@code valkeyConnectionFactory()} so the parse, credential
* wiring, and eager-handshake all run exactly as at boot.
*/
@Testcontainers
@Testcontainers(disabledWithoutDocker = true)
@EnabledIf("isDockerAvailable")
class LiveValkeyAuthIntegrationTest {
@@ -28,7 +28,7 @@ import stirling.software.common.model.ApplicationProperties;
* to reproduce a partition rather than {@code stop} (which would fail fast with
* connection-refused).
*/
@Testcontainers
@Testcontainers(disabledWithoutDocker = true)
@EnabledIf("isDockerAvailable")
class LiveValkeyChaosTest {
@@ -39,7 +39,7 @@ import stirling.software.common.model.ApplicationProperties;
* unavailable - without that guard, {@code @Testcontainers} would throw {@code initializationError}
* (test FAILURE, not skip) on CI runners without Docker.
*/
@Testcontainers
@Testcontainers(disabledWithoutDocker = true)
@EnabledIf("isDockerAvailable")
class LiveValkeyIntegrationTest {
+5 -3
View File
@@ -276,6 +276,7 @@ subprojects {
def jacocoReport = tasks.named("jacocoTestReport")
tasks.withType(Test).configureEach {
jvmArgs "--enable-native-access=ALL-UNNAMED"
useJUnitPlatform()
finalizedBy(jacocoReport)
}
@@ -288,7 +289,8 @@ subprojects {
html.required.set(true)
}
doLast {
def xmlReport = reports.xml.outputLocation.get().asFile
def reportBaseDir = layout.buildDirectory.dir("reports/jacoco/test").get().asFile
def xmlReport = new File(reportBaseDir, "jacocoTestReport.xml")
if (!xmlReport.exists()) {
logger.lifecycle("Jacoco coverage report not found at ${xmlReport}")
return
@@ -367,7 +369,7 @@ subprojects {
}
logger.lifecycle(separator)
def htmlReport = reports.html.outputLocation.get().asFile
def htmlReport = new File(reportBaseDir, "html")
logger.lifecycle("Detailed HTML report available at: ${htmlReport}")
if (rows.any { it[3] == "FAIL" }) {
logger.lifecycle("Some coverage targets were missed. Please review the detailed report above.")
@@ -657,7 +659,7 @@ tasks.register('compileRestartHelper', JavaCompile) {
source = fileTree(dir: 'scripts', include: 'RestartHelper.java')
classpath = files()
destinationDirectory = layout.buildDirectory.dir("restart-helper-classes")
def restartMajorVersion = project.ext.modernJavaVersion
def restartMajorVersion = rootProject.ext.modernJavaVersion
def restartCompatibility = JavaVersion.toVersion(restartMajorVersion.toString())
sourceCompatibility = restartCompatibility
targetCompatibility = restartCompatibility