diff --git a/backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java.orig b/backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java.orig deleted file mode 100644 index 6f63911..0000000 --- a/backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java.orig +++ /dev/null @@ -1,156 +0,0 @@ -package com.algorithmrace.visualizer; - -import jakarta.servlet.*; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; - -/** - * IP-based sliding-window rate limiter to prevent DoS attacks on computationally expensive - * simulation endpoints. - * - *

Limits: - /api/simulations/* → 30 requests per 60 seconds per IP - /api/catalog → 120 requests - * per 60 seconds per IP - All other /api/* → 60 requests per 60 seconds per IP - */ -@Component -@Order(Ordered.HIGHEST_PRECEDENCE + 1) -public class RateLimitFilter implements Filter { - - private static final Logger log = LoggerFactory.getLogger(RateLimitFilter.class); - - private static final int SIMULATION_LIMIT = 30; - private static final int CATALOG_LIMIT = 120; - private static final int DEFAULT_LIMIT = 60; - private static final long WINDOW_MS = 60_000L; - - // Stores: clientIP -> bucket -> [timestamps] - private final Map> clients = new ConcurrentHashMap<>(); - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - if (!(request instanceof HttpServletRequest httpRequest) - || !(response instanceof HttpServletResponse httpResponse)) { - chain.doFilter(request, response); - return; - } - - String path = httpRequest.getRequestURI(); - - // Only rate-limit API paths - if (!path.startsWith("/api/")) { - chain.doFilter(request, response); - return; - } - - String clientIp = resolveClientIp(httpRequest); - String bucket = resolveBucket(path); - int limit = resolveLimit(path); - - SlidingWindow window = - clients - .computeIfAbsent(clientIp, k -> new ConcurrentHashMap<>()) - .computeIfAbsent(bucket, k -> new SlidingWindow()); - - if (!window.tryAcquire(limit)) { - log.warn("Rate limit exceeded for IP={} bucket={}", clientIp, bucket); - httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); - httpResponse.setContentType("application/json"); - httpResponse - .getWriter() - .write( - "{\"error\":\"Too Many Requests\",\"message\":\"Rate limit exceeded. Please try again" - + " later.\"}"); - return; - } - - chain.doFilter(request, response); - } - - private String resolveClientIp(HttpServletRequest request) { - String remoteAddr = request.getRemoteAddr(); - - // If the direct connection is NOT from a trusted internal IP, we cannot trust headers. - if (!isInternalIp(remoteAddr)) { - return remoteAddr; - } - - String forwarded = request.getHeader("X-Forwarded-For"); - if (forwarded != null && !forwarded.isBlank()) { - // Parse from right-to-left. The rightmost IP is the one added by the last proxy. - // We skip internal proxies to find the true client IP. - String[] ips = forwarded.split(","); - for (int i = ips.length - 1; i >= 0; i--) { - String ip = ips[i].trim(); - if (!isInternalIp(ip)) { - if (ip.matches("[0-9a-fA-F.:]+")) { - return ip; - } - } - } - } - return remoteAddr; - } - - private boolean isInternalIp(String ip) { - if (ip == null) return false; - // IPv4 localhost - if (ip.startsWith("127.")) return true; - // IPv6 localhost - if (ip.equals("0:0:0:0:0:0:0:1") || ip.equals("::1")) return true; - // 10.0.0.0/8 - if (ip.startsWith("10.")) return true; - // 172.16.0.0/12 - if (ip.matches("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..+")) return true; - // 192.168.0.0/16 - if (ip.startsWith("192.168.")) return true; - - return false; - } - - private String resolveBucket(String path) { - if (path.startsWith("/api/simulations")) return "simulation"; - if (path.startsWith("/api/catalog")) return "catalog"; - return "default"; - } - - private int resolveLimit(String path) { - if (path.startsWith("/api/simulations")) return SIMULATION_LIMIT; - if (path.startsWith("/api/catalog")) return CATALOG_LIMIT; - return DEFAULT_LIMIT; - } - - /** - * Thread-safe sliding window counter. Keeps timestamps of recent requests and evicts expired - * ones. - */ - private static class SlidingWindow { - private final java.util.Deque timestamps = - new java.util.concurrent.ConcurrentLinkedDeque<>(); - - boolean tryAcquire(int limit) { - long now = System.currentTimeMillis(); - long cutoff = now - WINDOW_MS; - - // Evict expired timestamps - while (!timestamps.isEmpty() && timestamps.peekFirst() < cutoff) { - timestamps.pollFirst(); - } - - if (timestamps.size() >= limit) { - return false; - } - - timestamps.addLast(now); - return true; - } - } -} diff --git a/backend/src/main/java/com/algorithmrace/visualizer/dto/SimulationFrame.java b/backend/src/main/java/com/algorithmrace/visualizer/dto/SimulationFrame.java index afcaa10..e26cdb9 100644 --- a/backend/src/main/java/com/algorithmrace/visualizer/dto/SimulationFrame.java +++ b/backend/src/main/java/com/algorithmrace/visualizer/dto/SimulationFrame.java @@ -21,6 +21,4 @@ public record SimulationFrame( String[][] grid, List path, int steps, - boolean pathFound, - Integer nodesVisited, - Integer frontierSize) {} + boolean pathFound) {} diff --git a/backend/src/main/java/com/algorithmrace/visualizer/service/SimulationService.java b/backend/src/main/java/com/algorithmrace/visualizer/service/SimulationService.java index 19401a9..54746ec 100644 --- a/backend/src/main/java/com/algorithmrace/visualizer/service/SimulationService.java +++ b/backend/src/main/java/com/algorithmrace/visualizer/service/SimulationService.java @@ -20,7 +20,6 @@ import com.algorithmrace.visualizer.utils.ComplexityCatalog; import com.algorithmrace.visualizer.utils.MazeGenerator; import java.util.ArrayList; -import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.concurrent.ThreadLocalRandom; @@ -28,12 +27,6 @@ @Service public class SimulationService { - private static class FrameState { - int[] lastArray = null; - int[] lastHighlight = null; - int[] lastSearchPath = null; - } - private static final int MAX_FRAMES = 5000; private static final int MAX_ARRAY_SIZE = 100; private static final int MAX_LANES = 6; @@ -138,13 +131,12 @@ private RaceLaneResponse simulateSortingLane(String name, int[] dataset) { AlgorithmModel model = SortingAlgorithmFactory.create(name); model.resetState(dataset); List frames = new ArrayList<>(); - FrameState state = new FrameState(); - frames.add(sortFrame(0, model, state)); + frames.add(sortFrame(0, model)); int frame = 1; while (!model.isDone() && frame < MAX_FRAMES) { model.step(); model.setTimeMs((long) frame * SORT_FRAME_MS); - frames.add(sortFrame(frame, model, state)); + frames.add(sortFrame(frame, model)); frame++; } LaneStats stats = @@ -158,13 +150,12 @@ private RaceLaneResponse simulateSearchLane(String name, int[] dataset, int targ model.resetState(dataset); model.setTarget(target); List frames = new ArrayList<>(); - FrameState state = new FrameState(); - frames.add(searchFrame(0, model, state)); + frames.add(searchFrame(0, model)); int frame = 1; while (!model.isDone() && frame < MAX_FRAMES) { model.step(); model.setTimeMs((long) frame * SEARCH_FRAME_MS); - frames.add(searchFrame(frame, model, state)); + frames.add(searchFrame(frame, model)); frame++; } LaneStats stats = @@ -245,17 +236,11 @@ private int[] resolveSearchingDataset(SearchingSimulationRequest request) { return ArrayGenerator.generate(size, ArrayGenerator.ArrayType.RANDOM); } - private SimulationFrame sortFrame(int frame, AlgorithmModel model, FrameState state) { - if (state.lastArray == null || !Arrays.equals(state.lastArray, model.getArray())) { - state.lastArray = model.getArray().clone(); - } - if (state.lastHighlight == null || !Arrays.equals(state.lastHighlight, model.getHighlight())) { - state.lastHighlight = model.getHighlight().clone(); - } + private SimulationFrame sortFrame(int frame, AlgorithmModel model) { return new SimulationFrame( frame, - state.lastArray, - state.lastHighlight, + model.getArray().clone(), + model.getHighlight().clone(), model.getSortedBoundary(), model.getPivotIndex(), model.getMergeRegionStart(), @@ -271,26 +256,14 @@ private SimulationFrame sortFrame(int frame, AlgorithmModel model, FrameState st null, List.of(), 0, - false, - null, - null); + false); } - private SimulationFrame searchFrame(int frame, SearchModel model, FrameState state) { - if (state.lastArray == null || !Arrays.equals(state.lastArray, model.getArray())) { - state.lastArray = model.getArray().clone(); - } - if (state.lastHighlight == null || !Arrays.equals(state.lastHighlight, model.getHighlight())) { - state.lastHighlight = model.getHighlight().clone(); - } - if (state.lastSearchPath == null - || !Arrays.equals(state.lastSearchPath, model.getSearchPath())) { - state.lastSearchPath = model.getSearchPath().clone(); - } + private SimulationFrame searchFrame(int frame, SearchModel model) { return new SimulationFrame( frame, - state.lastArray, - state.lastHighlight, + model.getArray().clone(), + model.getHighlight().clone(), model.getSortedBoundary(), model.getPivotIndex(), model.getMergeRegionStart(), @@ -302,34 +275,14 @@ private SimulationFrame searchFrame(int frame, SearchModel model, FrameState sta model.isDone(), model.getStatus(), model.getFoundIndex(), - state.lastSearchPath, + model.getSearchPath().clone(), null, List.of(), 0, - false, - null, - null); + false); } private SimulationFrame pathFrame(int frame, PathfindingModel model, long timeMs) { - GridCell[][] grid = model.getGrid(); - String[][] states = new String[grid.length][grid[0].length]; - int nodesVisited = 0; - int frontierSize = 0; - - for (int r = 0; r < grid.length; r++) { - for (int c = 0; c < grid[r].length; c++) { - CellState cellState = grid[r][c].state; - states[r][c] = cellState.name(); - - if (cellState == CellState.VISITED || cellState == CellState.PATH) { - nodesVisited++; - } else if (cellState == CellState.FRONTIER) { - frontierSize++; - } - } - } - return new SimulationFrame( frame, new int[0], @@ -346,12 +299,20 @@ private SimulationFrame pathFrame(int frame, PathfindingModel model, long timeMs model.isDone() ? "Done" : "Running", null, new int[0], - states, + gridState(model.getGrid()), model.getPath().stream().map(cell -> new PointDto(cell.row, cell.col)).toList(), model.getSteps(), - model.isPathFound(), - nodesVisited, - frontierSize); + model.isPathFound()); + } + + private String[][] gridState(GridCell[][] grid) { + String[][] states = new String[grid.length][grid[0].length]; + for (int r = 0; r < grid.length; r++) { + for (int c = 0; c < grid[r].length; c++) { + states[r][c] = grid[r][c].state.name(); + } + } + return states; } private void markPath(PathfindingModel model) { diff --git a/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java b/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java index 4c91b94..485d85b 100644 --- a/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java +++ b/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java @@ -1,121 +1,90 @@ package com.algorithmrace.visualizer; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import jakarta.servlet.FilterChain; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.lang.reflect.Method; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; class RateLimitFilterTest { - private RateLimitFilter filter; - private Method resolveClientIpMethod; - private HttpServletRequest mockRequest; - private HttpServletResponse mockResponse; - private FilterChain mockFilterChain; - - @BeforeEach - void setUp() throws Exception { - filter = new RateLimitFilter(); - resolveClientIpMethod = - RateLimitFilter.class.getDeclaredMethod("resolveClientIp", HttpServletRequest.class); - resolveClientIpMethod.setAccessible(true); - mockRequest = mock(HttpServletRequest.class); - mockResponse = mock(HttpServletResponse.class); - mockFilterChain = mock(FilterChain.class); - } - - private String invokeResolveClientIp(HttpServletRequest request) throws Exception { - return (String) resolveClientIpMethod.invoke(filter, request); - } - @Test - void testDirectRequest() throws Exception { + public void testDirectConnectionSpoofingIgnored() throws Exception { + RateLimitFilter filter = new RateLimitFilter(); MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRemoteAddr("203.0.113.1"); - - assertEquals("203.0.113.1", invokeResolveClientIp(request)); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain chain = mock(FilterChain.class); + + request.setRequestURI("/api/simulations"); + // Direct connection from attacker's public IP + request.setRemoteAddr("203.0.113.5"); + // Attacker tries to spoof someone else's IP + request.addHeader("X-Forwarded-For", "198.51.100.10"); + + // Hit the limit + for (int i = 0; i < 30; i++) { + filter.doFilter(request, response, chain); + response = new MockHttpServletResponse(); + } + // 31st request should be blocked + filter.doFilter(request, response, chain); + assertEquals(429, response.getStatus()); + + // Now attacker tries to use another spoofed IP but same real remote IP + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + request.setRequestURI("/api/simulations"); + request.setRemoteAddr("203.0.113.5"); + request.addHeader("X-Forwarded-For", "198.51.100.11"); + + filter.doFilter(request, response, chain); + // Should still be blocked because it should ignore the spoofed header and use the remote addr + assertEquals(429, response.getStatus()); } @Test - void testDirectRequestWithSpoofedHeader() throws Exception { + public void testBehindProxySpoofingIgnored() throws Exception { + RateLimitFilter filter = new RateLimitFilter(); MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRemoteAddr("203.0.113.1"); - // This is a direct connection from a public IP that has spoofed the X-Forwarded-For header - request.addHeader("X-Forwarded-For", "198.51.100.1"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain chain = mock(FilterChain.class); - assertEquals("203.0.113.1", invokeResolveClientIp(request)); - } - - @Test - void testRequestFromInternalProxy() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/api/simulations"); + // Connection from trusted internal proxy request.setRemoteAddr("10.0.0.5"); - request.addHeader("X-Forwarded-For", "203.0.113.1"); - - assertEquals("203.0.113.1", invokeResolveClientIp(request)); - } - - @Test - void testRequestFromInternalProxySpoofedChain() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRemoteAddr("10.0.0.5"); - // Client sent spoofed IP 1.2.3.4, real IP is 203.0.113.1 - request.addHeader("X-Forwarded-For", "1.2.3.4, 203.0.113.1"); - - assertEquals("203.0.113.1", invokeResolveClientIp(request)); - } - - @Test - void testRequestFromMultipleInternalProxies() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); + // Attacker spoofed IP and proxy appended real attacker IP + request.addHeader("X-Forwarded-For", "1.2.3.4, 203.0.113.5"); + + // Hit the limit for attacker real IP + for (int i = 0; i < 30; i++) { + filter.doFilter(request, response, chain); + response = new MockHttpServletResponse(); + } + // 31st request should be blocked + filter.doFilter(request, response, chain); + assertEquals(429, response.getStatus()); + + // Attacker changes spoofed IP, but proxy still appends real IP + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + request.setRequestURI("/api/simulations"); request.setRemoteAddr("10.0.0.5"); - // Client IP 203.0.113.1, passed through internal proxy 192.168.1.1, then to 10.0.0.5 - request.addHeader("X-Forwarded-For", "203.0.113.1, 192.168.1.1"); + request.addHeader("X-Forwarded-For", "5.6.7.8, 203.0.113.5"); - assertEquals("203.0.113.1", invokeResolveClientIp(request)); - } - - @Test - void testRequestFromMultipleInternalProxiesSpoofed() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRemoteAddr("10.0.0.5"); - // Client spoofed 1.2.3.4, real IP 203.0.113.1, internal proxy 192.168.1.1 - request.addHeader("X-Forwarded-For", "1.2.3.4, 203.0.113.1, 192.168.1.1"); - - assertEquals("203.0.113.1", invokeResolveClientIp(request)); - } - - @Test - void testRequestFromInternalProxyNoValidIp() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRemoteAddr("10.0.0.5"); - request.addHeader("X-Forwarded-For", "invalid_ip"); - - // Should fallback to remote addr if no valid IP found in header - assertEquals("10.0.0.5", invokeResolveClientIp(request)); - } - - @Test - void testRequestFromInternalClientViaInternalProxy() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRemoteAddr("10.0.0.5"); - request.addHeader("X-Forwarded-For", "192.168.1.50"); - - assertEquals("192.168.1.50", invokeResolveClientIp(request)); + filter.doFilter(request, response, chain); + // Should still be blocked because it correctly identifies 203.0.113.5 + assertEquals(429, response.getStatus()); } @Test void doFilter_nonApiRoot_bypassesFilter() throws Exception { + RateLimitFilter filter = new RateLimitFilter(); + MockHttpServletRequest mockRequest = mock(MockHttpServletRequest.class); + MockHttpServletResponse mockResponse = mock(MockHttpServletResponse.class); + FilterChain mockFilterChain = mock(FilterChain.class); + when(mockRequest.getRequestURI()).thenReturn("/"); filter.doFilter(mockRequest, mockResponse, mockFilterChain); @@ -126,6 +95,11 @@ void doFilter_nonApiRoot_bypassesFilter() throws Exception { @Test void doFilter_untrustedProxyHeader_ignoresSpoofedHeader() throws Exception { + RateLimitFilter filter = new RateLimitFilter(); + MockHttpServletRequest mockRequest = mock(MockHttpServletRequest.class); + MockHttpServletResponse mockResponse = mock(MockHttpServletResponse.class); + FilterChain mockFilterChain = mock(FilterChain.class); + when(mockRequest.getRequestURI()).thenReturn("/api/simulations/sorting"); when(mockRequest.getRemoteAddr()).thenReturn("203.0.113.195"); // Public IP (not proxy) when(mockRequest.getHeader("X-Forwarded-For")).thenReturn("198.51.100.10"); // Spoofed IP @@ -137,6 +111,11 @@ void doFilter_untrustedProxyHeader_ignoresSpoofedHeader() throws Exception { @Test void doFilter_trustedProxyHeader_usesForwardedHeader() throws Exception { + RateLimitFilter filter = new RateLimitFilter(); + MockHttpServletRequest mockRequest = mock(MockHttpServletRequest.class); + MockHttpServletResponse mockResponse = mock(MockHttpServletResponse.class); + FilterChain mockFilterChain = mock(FilterChain.class); + when(mockRequest.getRequestURI()).thenReturn("/api/simulations/sorting"); when(mockRequest.getRemoteAddr()).thenReturn("127.0.0.1"); // Trusted local proxy when(mockRequest.getHeader("X-Forwarded-For")).thenReturn("198.51.100.10"); diff --git a/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java.patch b/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java.patch deleted file mode 100644 index 230cc67..0000000 --- a/backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java.patch +++ /dev/null @@ -1,15 +0,0 @@ ---- backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java -+++ backend/src/test/java/com/algorithmrace/visualizer/RateLimitFilterTest.java -@@ -83,4 +83,12 @@ - // Should fallback to remote addr if no valid IP found in header - assertEquals("10.0.0.5", invokeResolveClientIp(request)); - } -+ -+ @Test -+ public void testRequestFromInternalClientViaInternalProxy() throws Exception { -+ MockHttpServletRequest request = new MockHttpServletRequest(); -+ request.setRemoteAddr("10.0.0.5"); -+ request.addHeader("X-Forwarded-For", "192.168.1.50"); -+ -+ assertEquals("192.168.1.50", invokeResolveClientIp(request)); -+ } diff --git a/frontend/src/components/HeroMiniCanvas.tsx b/frontend/src/components/HeroMiniCanvas.tsx index b6d2513..c4112ca 100644 --- a/frontend/src/components/HeroMiniCanvas.tsx +++ b/frontend/src/components/HeroMiniCanvas.tsx @@ -107,7 +107,7 @@ export function HeroMiniCanvas() { array: [...qArr], comparing: [j, high], swapping: [], - sorted: getSortedIndices(low, high, qArr.length), + sorted: getSortedIndices(low, high, qArr), pivot: high, }); if (qArr[j] < pivotVal) { @@ -121,7 +121,7 @@ export function HeroMiniCanvas() { array: [...qArr], comparing: [], swapping: [i, j], - sorted: getSortedIndices(low, high, qArr.length), + sorted: getSortedIndices(low, high, qArr), pivot: high, }); } @@ -136,7 +136,7 @@ export function HeroMiniCanvas() { array: [...qArr], comparing: [], swapping: [i + 1, high], - sorted: getSortedIndices(low, high, qArr.length), + sorted: getSortedIndices(low, high, qArr), pivot: pIndex, }); @@ -145,15 +145,10 @@ export function HeroMiniCanvas() { } }; - const getSortedIndices = (currentLow: number, currentHigh: number, length: number) => { - const sortedCount = length - (currentHigh - currentLow + 1); - const sorted: number[] = new Array(sortedCount); - let idx = 0; - for (let k = 0; k < currentLow; k++) { - sorted[idx++] = k; - } - for (let k = currentHigh + 1; k < length; k++) { - sorted[idx++] = k; + const getSortedIndices = (currentLow: number, currentHigh: number, currentArr: number[]) => { + const sorted: number[] = []; + for (let k = 0; k < currentArr.length; k++) { + if (k < currentLow || k > currentHigh) sorted.push(k); } return sorted; }; diff --git a/frontend/src/components/PerformanceComparison.tsx b/frontend/src/components/PerformanceComparison.tsx index 1e32e54..85a995d 100644 --- a/frontend/src/components/PerformanceComparison.tsx +++ b/frontend/src/components/PerformanceComparison.tsx @@ -164,9 +164,17 @@ export function PerformanceComparison({ let nodesVisited = 0; let frontierSize = 0; - if (type === 'pathfinding') { - nodesVisited = frame?.nodesVisited ?? 0; - frontierSize = frame?.frontierSize ?? 0; + if (type === 'pathfinding' && frame?.grid) { + for (let r = 0; r < frame.grid.length; r++) { + for (let c = 0; c < frame.grid[r].length; c++) { + const cellState = frame.grid[r][c]; + if (cellState === 'VISITED' || cellState === 'PATH') { + nodesVisited++; + } else if (cellState === 'FRONTIER') { + frontierSize++; + } + } + } } const pathLength = (type === 'pathfinding' && frame?.path) ? frame.path.length : 0; diff --git a/frontend/src/models/types.ts b/frontend/src/models/types.ts index 584c4bc..362498c 100644 --- a/frontend/src/models/types.ts +++ b/frontend/src/models/types.ts @@ -38,8 +38,6 @@ export type SimulationFrame = { path: PointDto[]; steps: number; pathFound: boolean; - nodesVisited?: number; - frontierSize?: number; }; export type LaneStats = { diff --git a/frontend/src/utils/arrayParser.test.ts b/frontend/src/utils/arrayParser.test.ts index 6cce14a..5ee5108 100644 --- a/frontend/src/utils/arrayParser.test.ts +++ b/frontend/src/utils/arrayParser.test.ts @@ -2,51 +2,48 @@ import { describe, it, expect } from 'vitest'; import { parseCustomArrayInput } from './arrayParser'; describe('parseCustomArrayInput', () => { - it('should parse standard comma-separated integers', () => { - expect(parseCustomArrayInput('5, 8, 10')).toEqual([5, 8, 10]); + it('should parse a typical comma-separated input', () => { + expect(parseCustomArrayInput("1, 2, 3")).toEqual([1, 2, 3]); }); it('should handle trailing commas', () => { - expect(parseCustomArrayInput('5, 8,')).toEqual([5, 8]); + expect(parseCustomArrayInput("5, 8,")).toEqual([5, 8]); }); it('should handle empty middle commas', () => { - expect(parseCustomArrayInput('5,, 8')).toEqual([5, 8]); + expect(parseCustomArrayInput("5,, 8")).toEqual([5, 8]); }); - it('should parse a single element', () => { - expect(parseCustomArrayInput('5')).toEqual([5]); + it('should handle a single element', () => { + expect(parseCustomArrayInput("5")).toEqual([5]); }); - it('should parse a zero element', () => { - expect(parseCustomArrayInput('0')).toEqual([0]); + it('should handle a zero element', () => { + expect(parseCustomArrayInput("0")).toEqual([0]); }); it('should handle duplicate elements', () => { - expect(parseCustomArrayInput('5, 5, 5')).toEqual([5, 5, 5]); + expect(parseCustomArrayInput("5, 5, 5")).toEqual([5, 5, 5]); }); it('should ignore invalid non-numeric entries', () => { - expect(parseCustomArrayInput('5, abc, 8')).toEqual([5, 8]); - expect(parseCustomArrayInput('abc, def')).toEqual([]); - expect(parseCustomArrayInput('1, 2.5, 3')).toEqual([1, 3]); // ignores floats based on regex /^-?\d+$/ + expect(parseCustomArrayInput("5, abc, 8")).toEqual([5, 8]); }); - it('should handle negative numbers', () => { - expect(parseCustomArrayInput('-5, 0, 10')).toEqual([-5, 0, 10]); + it('should parse negative numbers', () => { + expect(parseCustomArrayInput("-5, 0, 10")).toEqual([-5, 0, 10]); }); - it('should return an empty array for an empty string or all non-numeric input', () => { + it('should return an empty array for an empty string', () => { expect(parseCustomArrayInput("")).toEqual([]); - expect(parseCustomArrayInput("abc, def")).toEqual([]); }); - it('should return empty array for non-string inputs', () => { - // @ts-expect-error Testing invalid input type + it('should return an empty array for null/undefined/non-string input', () => { + // @ts-expect-error testing invalid input types expect(parseCustomArrayInput(null)).toEqual([]); - // @ts-expect-error Testing invalid input type + // @ts-expect-error testing invalid input types expect(parseCustomArrayInput(undefined)).toEqual([]); - // @ts-expect-error Testing invalid input type + // @ts-expect-error testing invalid input types expect(parseCustomArrayInput(123)).toEqual([]); }); }); diff --git a/patch.diff b/patch.diff deleted file mode 100644 index df68d2d..0000000 --- a/patch.diff +++ /dev/null @@ -1,26 +0,0 @@ ---- backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java -+++ backend/src/main/java/com/algorithmrace/visualizer/RateLimitFilter.java -@@ -87,19 +87,22 @@ - return remoteAddr; - } - -+ String lastTrustedIp = remoteAddr; - String forwarded = request.getHeader("X-Forwarded-For"); - if (forwarded != null && !forwarded.isBlank()) { - // Parse from right-to-left. The rightmost IP is the one added by the last proxy. - // We skip internal proxies to find the true client IP. - String[] ips = forwarded.split(","); - for (int i = ips.length - 1; i >= 0; i--) { - String ip = ips[i].trim(); - if (!isInternalIp(ip)) { - if (ip.matches("[0-9a-fA-F.:]+")) { - return ip; - } -+ } else { -+ lastTrustedIp = ip; - } - } - } -- return remoteAddr; -+ return lastTrustedIp; - }