Repository navigation
Feat/optimize evidence images #482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e29ad5e
8f3990c
ca6da25
23f60b0
1765a39
aa2a7a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,25 @@ export default function ClaimDetailPage() { | |
|
|
||
| const [stakeAmount, setStakeAmount] = useState(0); | ||
| const [claimNotFound, setClaimNotFound] = useState(false); | ||
| const [claim, setClaim] = useState<Claim | null>(null); | ||
| const [isLoading, setIsLoading] = useState(true); | ||
|
|
||
| useEffect(() => { | ||
| if (params.id) { | ||
| setIsLoading(true); | ||
| getClaimById(params.id) | ||
| .then((data) => { | ||
| setClaim(data); | ||
| setIsLoading(false); | ||
| }) | ||
| .catch((err) => { | ||
| if (err.message === 'CLAIM_NOT_FOUND') { | ||
| setClaimNotFound(true); | ||
| } | ||
| setIsLoading(false); | ||
| }); | ||
| } | ||
| }, [params.id]); | ||
|
Comment on lines
+26
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🌐 Web query:
💡 Result: <source_evidence> Citations:
Use the asynchronous Next.js 16 requires dynamic route 🧰 Tools🪛 React Doctor (0.9.13)[warning] 24-24: This synchronous effect update causes an extra render: Calling setState synchronously within an effect can trigger cascading renders. Prefer deriving or initializing the value before render. If the effect must read a browser API after mount, treat this as advisory or suppress it with Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). (set-state-in-effect) 🤖 Prompt for AI Agents |
||
|
|
||
| const handleStakeChange = (stake: string) => { | ||
| const value = parseFloat(stake) || 0; | ||
|
|
@@ -298,5 +317,4 @@ export default function ClaimDetailPage() { | |
| </div> | ||
| </MainLayout> | ||
| ); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -138,4 +138,4 @@ export function EvidenceViewer({ | |
| ); | ||
| } | ||
|
|
||
| export default EvidenceViewer; | ||
| export default EvidenceViewer; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,181 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { render, screen, fireEvent, waitFor } from '@testing-library/react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { EvidenceViewer } from '../EvidenceViewer'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { Evidence } from '@/app/types/claim'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Mock the lucide-react icons to avoid rendering issues in tests | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jest.mock('lucide-react', () => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AlertCircle: () => <div data-testid="alert-circle" />, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ImageOff: () => <div data-testid="image-off" />, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Link2: () => <div data-testid="link-2" />, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileText: () => <div data-testid="file-text" />, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('EvidenceViewer - new functionality', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mockClaimId = 'claim-123'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('shows no evidence state when evidence array is empty', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render(<EvidenceViewer claimId={mockClaimId} evidence={[]} />); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByTestId('no-evidence-state')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('No evidence submitted for this claim')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('renders valid image with loading state', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validEvidence: Evidence[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: '1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'image', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: '/test-image.jpg', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: '2024-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render(<EvidenceViewer claimId={mockClaimId} evidence={validEvidence} />); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Skeleton should be present initially (loading state) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByTestId('image-skeleton-0')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // After image loads, skeleton should be hidden and image visible | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const img = screen.getByAltText('Evidence image 1'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fireEvent.load(img); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await waitFor(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.queryByTestId('image-skeleton-0')).not.toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('shows error state when image fails to load', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const brokenImageEvidence: Evidence[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: '1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'image', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: '/broken-image.jpg', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: '2024-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render(<EvidenceViewer claimId={mockClaimId} evidence={brokenImageEvidence} />); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const img = screen.getByAltText('Evidence image 1'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fireEvent.error(img); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await waitFor(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByTestId('image-error-0')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('Failed to load image')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('blocks invalid URLs for security', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const maliciousEvidence: Evidence[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: '1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'link', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: 'javascript:alert(1)', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: '2024-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: '2', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'image', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: 'data:image/png;base64,invalid', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: '2024-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render(<EvidenceViewer claimId={mockClaimId} evidence={maliciousEvidence} />); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Should show security error for invalid link | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('Invalid link source blocked for security')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Should show security error for invalid image | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('Invalid image source')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+68
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win This test expects text that cannot render. "Invalid image source" appears only inside the branch on Line 55 of With the current component, the The tests that use 💚 Proposed fix- expect(screen.getByText('Invalid image source')).toBeInTheDocument();
+ expect(screen.getByText('Invalid or unsupported image format')).toBeInTheDocument();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('allows valid HTTPS links and blocks invalid schemes', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mixedEvidence: Evidence[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: '1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'link', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: 'https://trusted-source.com/evidence.pdf', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: '2024-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: '2', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'link', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: 'http://insecure-source.com', // HTTP should be blocked for external links | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: '2024-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render(<EvidenceViewer claimId={mockClaimId} evidence={mixedEvidence} />); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Valid HTTPS link should render normally | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('https://trusted-source.com/evidence.pdf')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Insecure HTTP link should be blocked | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('Invalid link source blocked for security')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('renders text evidence properly', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const textEvidence: Evidence[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: '1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'text', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: 'Witness statement about the incident', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: '2024-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render(<EvidenceViewer claimId={mockClaimId} evidence={textEvidence} />); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('Witness statement about the incident')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByTestId('file-text')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('handles video evidence with warning state', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const videoEvidence: Evidence[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: '1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'video', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: 'https://example.com/video.mp4', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: '2024-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render(<EvidenceViewer claimId={mockClaimId} evidence={videoEvidence} />); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('Video evidence requires manual verification - link available upon request')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('validates image file extensions', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const invalidFormatEvidence: Evidence[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: '1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'image', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: 'https://example.com/image.exe', // Executable, not an image | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: '2024-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render(<EvidenceViewer claimId={mockClaimId} evidence={invalidFormatEvidence} />); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByText('Invalid or unsupported image format')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('maintains accessibility attributes', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const testEvidence: Evidence[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: '1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'image', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: '/test.jpg', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createdAt: '2024-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| render(<EvidenceViewer claimId={mockClaimId} evidence={testEvidence} />); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const toggleButton = screen.getByRole('button', { name: /evidence/i }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(toggleButton).toHaveAttribute('aria-expanded', 'true'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(toggleButton).toHaveAttribute('aria-controls', 'evidence-content'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Error states have proper role="alert" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fireEvent.error(screen.getByAltText('Evidence image 1')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(screen.getByRole('alert')).toBeInTheDocument(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| /** | ||
| * Safe evidence media validation utilities | ||
| * Validates remote sources, dimensions, formats, and ensures safe loading behavior | ||
| */ | ||
|
|
||
| // Allowed image MIME types | ||
| const ALLOWED_IMAGE_MIME_TYPES = new Set([ | ||
| 'image/jpeg', | ||
| 'image/png', | ||
| 'image/gif', | ||
| 'image/webp', | ||
| 'image/avif', | ||
| ]); | ||
|
|
||
| // Allowed image file extensions | ||
| const ALLOWED_IMAGE_EXTENSIONS = new Set([ | ||
| '.jpg', '.jpeg', '.png', '.gif', '.webp', '.avif' | ||
| ]); | ||
|
|
||
| // Allowed video MIME types | ||
| const ALLOWED_VIDEO_MIME_TYPES = new Set([ | ||
| 'video/mp4', | ||
| 'video/webm', | ||
| ]); | ||
|
|
||
| // Allowed video file extensions | ||
| const ALLOWED_VIDEO_EXTENSIONS = new Set(['.mp4', '.webm']); | ||
|
|
||
| // Allowed document MIME types (only PDFs for safety) | ||
| const ALLOWED_DOCUMENT_MIME_TYPES = new Set(['application/pdf']); | ||
| const ALLOWED_DOCUMENT_EXTENSIONS = new Set(['.pdf']); | ||
|
|
||
| // Blocked URL schemes to prevent XSS | ||
| const BLOCKED_SCHEMES = new Set(['javascript:', 'data:', 'vbscript:', 'file:']); | ||
|
|
||
| // Validate that a URL is safe to use | ||
| export function isValidMediaUrl(url: string): boolean { | ||
| try { | ||
| // Check for blocked schemes first | ||
| const lowerUrl = url.toLowerCase(); | ||
| for (const scheme of BLOCKED_SCHEMES) { | ||
| if (lowerUrl.startsWith(scheme)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // For remote URLs, only allow HTTPS | ||
| const urlObj = new URL(url); | ||
| if (urlObj.protocol !== 'https:' && !url.startsWith('/')) { // Allow relative paths (local assets) | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } catch { | ||
| // If URL parsing fails, it's an invalid URL | ||
| return false; | ||
| } | ||
| } | ||
|
Comment on lines
+37
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
This has three effects:
Parse a relative path against a fixed base. Require the result to stay on that origin, so that 🐛 Proposed fix-// Blocked URL schemes to prevent XSS
-const BLOCKED_SCHEMES = new Set(['javascript:', 'data:', 'vbscript:', 'file:']);
+// Fixed base for resolving same-origin relative paths (no dependency on `window`)
+const RELATIVE_BASE = 'https://app.invalid';
+// C0 control characters, DEL, and backslash (browsers treat `/\host` as protocol-relative)
+const UNSAFE_URL_CHARS = /[\u0000-\u001F\u007F\\]/;
// Validate that a URL is safe to use
export function isValidMediaUrl(url: string): boolean {
- try {
- // Check for blocked schemes first
- const lowerUrl = url.toLowerCase();
- for (const scheme of BLOCKED_SCHEMES) {
- if (lowerUrl.startsWith(scheme)) {
- return false;
- }
- }
-
- // For remote URLs, only allow HTTPS
- const urlObj = new URL(url);
- if (urlObj.protocol !== 'https:' && !url.startsWith('/')) { // Allow relative paths (local assets)
- return false;
- }
-
- return true;
- } catch {
- // If URL parsing fails, it's an invalid URL
- return false;
- }
+ if (!url || UNSAFE_URL_CHARS.test(url)) return false;
+ try {
+ if (url.startsWith('/')) {
+ // Same-origin relative path only; rejects `//host`
+ return new URL(url, RELATIVE_BASE).origin === RELATIVE_BASE;
+ }
+ return new URL(url).protocol === 'https:';
+ } catch {
+ return false;
+ }
} export function isValidLinkUrl(url: string): boolean {
- if (!isValidMediaUrl(url)) return false;
- try {
- const urlObj = new URL(url);
- // Only allow HTTPS for external links
- return urlObj.protocol === 'https:';
- } catch {
- // If it's a relative URL, that's fine too
- return url.startsWith('/');
- }
+ return isValidMediaUrl(url);
}Based on learnings: "strip or reject raw C0 control characters (0x00-0x1F) and DEL (0x7F) BEFORE scheme validation". 🤖 Prompt for AI AgentsSource: Learnings |
||
|
|
||
| // Get file extension from URL | ||
| function getFileExtension(url: string): string { | ||
| try { | ||
| const pathname = new URL(url, window.location.origin).pathname; | ||
| const lastDot = pathname.lastIndexOf('.'); | ||
| if (lastDot === -1) return ''; | ||
| return pathname.slice(lastDot).toLowerCase(); | ||
| } catch { | ||
| return ''; | ||
| } | ||
| } | ||
|
|
||
| // Validate image URL and format | ||
| export function isValidImageUrl(url: string): boolean { | ||
| if (!isValidMediaUrl(url)) return false; | ||
| const ext = getFileExtension(url); | ||
| return ALLOWED_IMAGE_EXTENSIONS.has(ext); | ||
| } | ||
|
|
||
| // Validate video URL and format | ||
| export function isValidVideoUrl(url: string): boolean { | ||
| if (!isValidMediaUrl(url)) return false; | ||
| const ext = getFileExtension(url); | ||
| return ALLOWED_VIDEO_EXTENSIONS.has(ext); | ||
| } | ||
|
|
||
| // Validate document URL and format | ||
| export function isValidDocumentUrl(url: string): boolean { | ||
| if (!isValidMediaUrl(url)) return false; | ||
| const ext = getFileExtension(url); | ||
| return ALLOWED_DOCUMENT_EXTENSIONS.has(ext); | ||
| } | ||
|
|
||
| // Validate link URL (for external links) | ||
| export function isValidLinkUrl(url: string): boolean { | ||
| if (!isValidMediaUrl(url)) return false; | ||
| try { | ||
| const urlObj = new URL(url); | ||
| // Only allow HTTPS for external links | ||
| return urlObj.protocol === 'https:'; | ||
| } catch { | ||
| // If it's a relative URL, that's fine too | ||
| return url.startsWith('/'); | ||
| } | ||
| } | ||
|
|
||
| // Media loading state hook return type | ||
| export interface MediaLoadingState { | ||
| isLoading: boolean; | ||
| hasError: boolean; | ||
| isLoaded: boolean; | ||
| } | ||
|
|
||
| // Create initial media state | ||
| export function createInitialMediaState(): MediaLoadingState { | ||
| return { | ||
| isLoading: true, | ||
| hasError: false, | ||
| isLoaded: false, | ||
| }; | ||
| } | ||
|
|
||
| // Helper to check if evidence type can contain media | ||
| export function isMediaType(type: string): boolean { | ||
| return ['image', 'video'].includes(type); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
A failed or in-progress fetch shows "No evidence submitted for this claim".
The page sets
isLoading, but no code reads it. When the error is notCLAIM_NOT_FOUND, the page keeps no error state. In both casesclaimstaysnull, andclaim?.evidence || []makesEvidenceViewerrender its empty state.A verifier can then stake and submit a possibly irreversible decision while believing the claim has no evidence. Render a loading state and an accessible error state instead of the empty state.
The effect also does not ignore stale responses when
params.idchanges. Add a cancellation flag.🐛 Proposed fix
As per path instructions: "Prioritize truthful transaction lifecycle … accessible failure states".
Also applies to: 167-170
🤖 Prompt for AI Agents
Source: Path instructions