Skip to content

Commit 5d54c25

Browse files
Unit tests and bug fixes (#11)
* Add event and resolver unit tests Add a suite of unit tests and small test controllers to improve coverage around HTTP events and error handling. New tests include additional-details, endpoint-exists flag, base event helpers, HTTP method enum, HTTP response event helpers, non-existent endpoint request body preservation, response event request data, and runtime-exception-resolver behavior (default and include-message=ALWAYS). Also add ResponseEventDataTestController and RuntimeExceptionResolverTestController. Update ErrorTestControllerTest to include assertions for server/client error response helpers. * Rename endpointExists to endpointCalled Replace the endpointExists property with endpointCalled throughout the codebase and update related logic and tests. - Rename HttpRequestEvent/BaseHttpEvent field and builder usage to endpointCalled. - Update CachedBodyHttpServletRequest, captor (RequestEndpointCalledCaptor), registry and publishers to set/check endpointCalled. - Change property name to web-captor.event-details.include-endpoint-called in examples and test resources and update tests accordingly (renamed test packages/files where applicable). - Update frontend types and UI (label and value) to reflect endpointCalled. - Fix request wrapper unwrapping in HttpServletUtils by using WebUtils.getNativeRequest to handle decorated requests (e.g. multipart wrappers). - Ensure HttpResponseEventPublisher publishes request/response wrapper objects and simplify RuntimeExceptionResolver to always include the error reason phrase. - Add comprehensive EndpointCalledFlagTest to cover various HTTP scenarios (including multipart behavior) and add IDE run configurations for demo/frontend. * Eagerly serialize multipart files & add previews Eagerly capture multipart file bytes on the backend and add download/preview support in the frontend. - Backend: add serializedFiles field and eager serialization in BodyPayload to capture base64 content before multipart temp-file cleanup, plus a safe getter that returns an empty map when absent. - Frontend: enhance CapturedResult to show human-friendly sizes, provide a Download link for files, enlarge image previews, and add text/JSON preview (with pretty-printing). Also import new icons used for download/preview. - Demo: add demo-landscape.jpg and update demoData to fetch and send an image file in the multipart demo. - Tests: add MultipartFileContentTest to verify base64 content is captured and matches original file bytes. These changes ensure multipart file contents are reliably recorded and usable in the UI (download/preview) and covered by tests.
1 parent f0e8037 commit 5d54c25

36 files changed

Lines changed: 1178 additions & 43 deletions

.run/DemoApplication.run.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="DemoApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
3+
<option name="FRAME_DEACTIVATION_UPDATE_POLICY" value="UnknownPolicyInFreeMode" />
4+
<module name="spring-web-captor-demo" />
5+
<option name="SPRING_BOOT_MAIN_CLASS" value="com.davidrandoll.webcaptor.demo.DemoApplication" />
6+
<extension name="coverage">
7+
<pattern>
8+
<option name="PATTERN" value="com.davidrandoll.webcaptor.demo.*" />
9+
<option name="ENABLED" value="true" />
10+
</pattern>
11+
</extension>
12+
<method v="2">
13+
<option name="Make" enabled="true" />
14+
</method>
15+
</configuration>
16+
</component>

.run/Run Example.run.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="Run Example" type="Multirun" separateTabs="false" reuseTabsWithFailures="false" startOneByOne="true" markFailedProcess="true" hideSuccessProcess="false" delayTime="0.0">
3+
<runConfiguration name="DemoApplication" type="Spring Boot" />
4+
<runConfiguration name="Run Frontend" type="Shell Script" />
5+
<method v="2" />
6+
</configuration>
7+
</component>

.run/Run Frontend.run.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="Run Frontend" type="ShConfigurationType">
3+
<option name="SCRIPT_TEXT" value="npm run dev" />
4+
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
5+
<option name="SCRIPT_PATH" value="" />
6+
<option name="SCRIPT_OPTIONS" value="" />
7+
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
8+
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$/examples/frontend" />
9+
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
10+
<option name="INTERPRETER_PATH" value="powershell.exe" />
11+
<option name="INTERPRETER_OPTIONS" value="" />
12+
<option name="EXECUTE_IN_TERMINAL" value="true" />
13+
<option name="EXECUTE_SCRIPT_FILE" value="false" />
14+
<envs />
15+
<method v="2" />
16+
</configuration>
17+
</component>

examples/backend/src/main/resources/application.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ spring:
1313
web-captor:
1414
enabled: true
1515
event-details:
16-
include-endpoint-exists: true
16+
include-endpoint-called: true
1717
include-full-url: true
1818
include-path: true
1919
include-method: true
20.9 KB
Loading

examples/frontend/src/components/CapturedResult.tsx

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import JsonViewer from './JsonViewer';
77
import {
88
Clock, Globe, Monitor, MapPin, FileText, AlertTriangle,
99
Server, ArrowDown, Layers, Search, GitBranch, Upload,
10-
RotateCcw, Sparkles, Puzzle, Info,
10+
RotateCcw, Sparkles, Puzzle, Info, Download, Eye,
1111
} from 'lucide-react';
1212

1313
function parseDuration(d: unknown): string {
@@ -224,10 +224,10 @@ export default function CapturedResult({ event, onTryAnother }: Props) {
224224
</div>
225225
<div className="bg-slate-900 rounded-xl p-4 border border-slate-800">
226226
<div className="flex items-center gap-1 text-[10px] text-slate-500 uppercase tracking-wider mb-2">
227-
<MapPin className="w-3 h-3" /> Endpoint Exists
227+
<MapPin className="w-3 h-3" /> Endpoint Called
228228
</div>
229-
<span className={`font-mono text-sm font-medium ${event.endpointExists ? 'text-emerald-400' : 'text-red-400'}`}>
230-
{String(event.endpointExists)}
229+
<span className={`font-mono text-sm font-medium ${event.endpointCalled ? 'text-emerald-400' : 'text-red-400'}`}>
230+
{String(event.endpointCalled)}
231231
</span>
232232
</div>
233233
</div>
@@ -354,18 +354,49 @@ export default function CapturedResult({ event, onTryAnother }: Props) {
354354
<div key={`${field}-${i}`} className="bg-slate-900 border border-slate-800 rounded-xl p-3 sm:p-4 mb-2">
355355
<div className="flex items-center justify-between mb-2">
356356
<span className="font-mono text-sm text-sky-400 font-medium">{f.filename}</span>
357-
<span className="text-xs text-slate-500">{(f.size / 1024).toFixed(1)} KB</span>
357+
<div className="flex items-center gap-2">
358+
<span className="text-xs text-slate-500">{f.size < 1024 ? `${f.size} B` : `${(f.size / 1024).toFixed(1)} KB`}</span>
359+
{f.base64Content && (
360+
<a
361+
href={`data:${f.contentType || 'application/octet-stream'};base64,${f.base64Content}`}
362+
download={f.filename}
363+
className="flex items-center gap-1 text-xs text-sky-400 hover:text-sky-300 transition-colors"
364+
>
365+
<Download className="w-3 h-3" /> Download
366+
</a>
367+
)}
368+
</div>
358369
</div>
359-
<div className="text-xs text-slate-500">
370+
<div className="text-xs text-slate-500 mb-2">
360371
Field: <span className="text-slate-400">{field}</span> | Type: <span className="text-slate-400">{f.contentType}</span>
361372
</div>
362373
{f.contentType?.startsWith('image/') && f.base64Content && (
363374
<img
364375
src={`data:${f.contentType};base64,${f.base64Content}`}
365376
alt={f.filename}
366-
className="mt-2 max-h-32 rounded border border-slate-700"
377+
className="mt-2 max-h-48 rounded border border-slate-700"
367378
/>
368379
)}
380+
{f.contentType?.startsWith('text/') && f.base64Content && (
381+
<div className="mt-2">
382+
<div className="flex items-center gap-1 text-[10px] text-slate-500 uppercase tracking-wider mb-1">
383+
<Eye className="w-3 h-3" /> Preview
384+
</div>
385+
<pre className="bg-slate-950 border border-slate-800 rounded-lg p-3 text-xs text-slate-300 font-mono overflow-x-auto max-h-40 whitespace-pre-wrap break-all">
386+
{atob(f.base64Content)}
387+
</pre>
388+
</div>
389+
)}
390+
{f.contentType === 'application/json' && f.base64Content && (
391+
<div className="mt-2">
392+
<div className="flex items-center gap-1 text-[10px] text-slate-500 uppercase tracking-wider mb-1">
393+
<Eye className="w-3 h-3" /> Preview
394+
</div>
395+
<pre className="bg-slate-950 border border-slate-800 rounded-lg p-3 text-xs text-slate-300 font-mono overflow-x-auto max-h-40 whitespace-pre-wrap break-all">
396+
{(() => { try { return JSON.stringify(JSON.parse(atob(f.base64Content)), null, 2); } catch { return atob(f.base64Content); } })()}
397+
</pre>
398+
</div>
399+
)}
369400
</div>
370401
))
371402
)}

examples/frontend/src/components/demoData.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,14 @@ export const DEMOS: DemoScenario[] = [
104104
method: 'POST',
105105
url: '/demo/upload',
106106
headers: {},
107-
body: '(multipart: demo.txt + description)',
107+
body: '(multipart: image + description)',
108108
tags: ['POST', 'Multipart', 'Files'],
109-
run() {
109+
async run() {
110+
const res = await fetch('/demo-landscape.jpg');
111+
const blob = await res.blob();
110112
const fd = new FormData();
111-
fd.append('file', new Blob(['Hello from Spring Web Captor!'], { type: 'text/plain' }), 'demo.txt');
112-
fd.append('description', 'Demo file upload');
113+
fd.append('file', new File([blob], 'landscape.jpg', { type: 'image/jpeg' }), 'landscape.jpg');
114+
fd.append('description', 'Beautiful landscape photo');
113115
return sendRequest('POST', '/demo/upload', { body: fd });
114116
},
115117
},

examples/frontend/src/types/events.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface BodyPayload {
1111
}
1212

1313
export interface HttpRequestEvent {
14-
endpointExists: boolean;
14+
endpointCalled: boolean;
1515
fullUrl: string;
1616
path: string;
1717
method: string;

spring-web-captor/src/main/java/com/davidrandoll/spring_web_captor/event/BaseHttpEvent.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
@AllArgsConstructor
2929
@SuperBuilder(toBuilder = true)
3030
public class BaseHttpEvent {
31-
private boolean endpointExists;
31+
private boolean endpointCalled;
3232
private String fullUrl;
3333
private String path;
3434
private HttpMethodEnum method;

spring-web-captor/src/main/java/com/davidrandoll/spring_web_captor/event/BodyPayload.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,27 @@ public class BodyPayload {
2121
@JsonIgnore
2222
private MultiValueMap<String, MultipartFile> files;
2323

24+
private Map<String, List<SerializedFile>> serializedFiles;
25+
2426
public BodyPayload(JsonNode body) {
2527
this.body = body;
2628
}
2729

2830
public BodyPayload(JsonNode body, MultiValueMap<String, MultipartFile> files) {
2931
this.body = body;
3032
this.files = files;
33+
// Eagerly serialize files so that base64 content is captured before
34+
// Spring cleans up multipart temp files
35+
this.serializedFiles = serializeFiles(files);
3136
}
3237

3338
@JsonProperty("files")
3439
public Map<String, List<SerializedFile>> getSerializedFiles() {
40+
if (serializedFiles != null) return serializedFiles;
41+
return Collections.emptyMap();
42+
}
43+
44+
private static Map<String, List<SerializedFile>> serializeFiles(MultiValueMap<String, MultipartFile> files) {
3545
if (files == null || files.isEmpty()) return Collections.emptyMap();
3646
Map<String, List<SerializedFile>> result = new LinkedHashMap<>();
3747
files.forEach((key, multipartFiles) ->

0 commit comments

Comments
 (0)