From 995dc70a40127c0a792b193bb33c11a4ff52fc61 Mon Sep 17 00:00:00 2001 From: "Pedro Carneiro Jr." Date: Mon, 1 Jun 2026 20:42:47 +0200 Subject: [PATCH 1/3] Add comprehensive API test documentation and testing guide --- API_TEST_GUIDE.md | 1018 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1018 insertions(+) create mode 100644 API_TEST_GUIDE.md diff --git a/API_TEST_GUIDE.md b/API_TEST_GUIDE.md new file mode 100644 index 0000000..6231c75 --- /dev/null +++ b/API_TEST_GUIDE.md @@ -0,0 +1,1018 @@ +# CryptoFlash API Testing Guide + +This document provides comprehensive testing documentation for all CryptoFlash REST API endpoints. + +## Table of Contents + +1. [Test Setup](#test-setup) +2. [Test Structure](#test-structure) +3. [Market Data Endpoints Tests](#market-data-endpoints-tests) +4. [User Endpoints Tests](#user-endpoints-tests) +5. [Order Endpoints Tests](#order-endpoints-tests) +6. [Admin Endpoints Tests](#admin-endpoints-tests) +7. [Running Tests](#running-tests) +8. [Manual Testing with cURL](#manual-testing-with-curl) + +--- + +## Test Setup + +### Prerequisites + +- Java 25 +- Maven +- Running MongoDB instance +- Running Redis instance +- JUnit 5 +- Spring Boot Test Framework +- MockMvc for testing REST endpoints + +### Dependencies (in pom.xml) + +```xml + + org.springframework.boot + spring-boot-starter-test + test + +``` + +--- + +## Test Structure + +Tests are organized by controller: + +``` +src/test/java/jar/ +├── controller/ +│ ├── MarketControllerTest.java +│ ├── UserControllerTest.java +│ ├── OrderControllerTest.java +│ └── AdminControllerTest.java +├── integration/ +│ └── EndToEndTest.java +└── CryptoflashApplicationTests.java +``` + +--- + +## Market Data Endpoints Tests + +### Endpoint: `GET /api/market/symbols` + +**Test Class:** `MarketControllerTest` + +**Test Case 1: Get All Symbols** +```java +@Test +@DisplayName("Should return all supported trading symbols") +void testGetSymbols() throws Exception { + mockMvc.perform(get("/api/market/symbols")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(5))) + .andExpect(jsonPath("$[0]", is("BTCUSD"))) + .andExpect(jsonPath("$[1]", is("ETHUSD"))); +} +``` + +**Expected Response:** +```json +["BTCUSD", "ETHUSD", "LTCUSD", "XRPUSD", "BCHUSD"] +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Response contains 5 symbols +- ✅ All symbols are valid cryptocurrency pairs + +--- + +### Endpoint: `GET /api/market/candles/{symbol}` + +**Test Case 2: Get Candles for Valid Symbol** +```java +@Test +@DisplayName("Should return market candles for valid symbol") +void testGetCandlesForBTCUSD() throws Exception { + mockMvc.perform(get("/api/market/candles/BTCUSD") + .param("limit", "50")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", instanceOf(List.class))) + .andExpect(jsonPath("$[0].symbol", is("BTCUSD"))) + .andExpect(jsonPath("$[0].open").exists()) + .andExpect(jsonPath("$[0].high").exists()) + .andExpect(jsonPath("$[0].low").exists()) + .andExpect(jsonPath("$[0].close").exists()) + .andExpect(jsonPath("$[0].volume").exists()); +} +``` + +**Test Case 3: Get Candles with Custom Limit** +```java +@Test +@DisplayName("Should return candles with custom limit") +void testGetCandlesWithCustomLimit() throws Exception { + mockMvc.perform(get("/api/market/candles/ETHUSD") + .param("limit", "100")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()", lessThanOrEqualTo(100))); +} +``` + +**Test Case 4: Get Candles with Default Limit** +```java +@Test +@DisplayName("Should return default 50 candles when limit not specified") +void testGetCandlesDefaultLimit() throws Exception { + mockMvc.perform(get("/api/market/candles/LTCUSD")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()", lessThanOrEqualTo(50))); +} +``` + +**Expected Response Structure:** +```json +[ + { + "symbol": "BTCUSD", + "timestamp": "2026-06-01T10:30:00Z", + "open": 67500.00, + "high": 68000.00, + "low": 67200.00, + "close": 67800.00, + "volume": 1250.50 + } +] +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Response is an array of candles +- ✅ Each candle has all required fields +- ✅ Candles are in reverse chronological order +- ✅ Limit parameter is respected + +--- + +## User Endpoints Tests + +### Endpoint: `GET /api/users/{userId}` + +**Test Case 1: Get User Profile - Valid User** +```java +@Test +@DisplayName("Should return user profile for valid userId") +void testGetUserProfileValid() throws Exception { + mockMvc.perform(get("/api/users/SIM_USER_001")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userId", is("SIM_USER_001"))) + .andExpect(jsonPath("$.balance").exists()) + .andExpect(jsonPath("$.totalTrades").exists()) + .andExpect(jsonPath("$.joinDate").exists()); +} +``` + +**Test Case 2: Get User Profile - Invalid User** +```java +@Test +@DisplayName("Should return 404 for non-existent userId") +void testGetUserProfileNotFound() throws Exception { + mockMvc.perform(get("/api/users/INVALID_USER_999")) + .andExpect(status().isNotFound()); +} +``` + +**Expected Response (Success):** +```json +{ + "userId": "SIM_USER_001", + "balance": 100000.00, + "totalTrades": 156, + "joinDate": "2026-04-10T00:00:00Z", + "portfolioValue": 125000.00 +} +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK for valid user +- ✅ Status code: 404 NOT FOUND for invalid user +- ✅ Response contains all required user fields +- ✅ Balance is a positive number + +--- + +### Endpoint: `GET /api/users/{userId}/trades` + +**Test Case 3: Get User Trades - Default Limit** +```java +@Test +@DisplayName("Should return user trades with default limit of 50") +void testGetUserTradesDefaultLimit() throws Exception { + mockMvc.perform(get("/api/users/SIM_USER_001/trades")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", instanceOf(List.class))) + .andExpect(jsonPath("$.length()", lessThanOrEqualTo(50))) + .andExpect(jsonPath("$[0].symbol").exists()) + .andExpect(jsonPath("$[0].price").exists()) + .andExpect(jsonPath("$[0].quantity").exists()) + .andExpect(jsonPath("$[0].timestamp").exists()); +} +``` + +**Test Case 4: Get User Trades - Custom Limit** +```java +@Test +@DisplayName("Should return user trades with custom limit") +void testGetUserTradesCustomLimit() throws Exception { + mockMvc.perform(get("/api/users/SIM_USER_001/trades") + .param("limit", "100")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()", lessThanOrEqualTo(100))); +} +``` + +**Test Case 5: Get User Trades - Most Recent First** +```java +@Test +@DisplayName("Should return trades sorted by timestamp (most recent first)") +void testGetUserTradesOrdering() throws Exception { + MvcResult result = mockMvc.perform(get("/api/users/SIM_USER_001/trades")) + .andExpect(status().isOk()) + .andReturn(); + + // Verify trades are sorted newest to oldest + String jsonResponse = result.getResponse().getContentAsString(); + List trades = objectMapper.readValue(jsonResponse, List.class); + + for (int i = 1; i < trades.size(); i++) { + assertTrue(trades.get(i-1).get("timestamp").toString() + .compareTo(trades.get(i).get("timestamp").toString()) >= 0); + } +} +``` + +**Expected Response:** +```json +[ + { + "buyerId": "SIM_USER_001", + "sellerId": "SIM_USER_002", + "symbol": "BTCUSD", + "price": 67800.00, + "quantity": 0.5, + "timestamp": "2026-06-01T10:35:22Z" + }, + { + "buyerId": "SIM_USER_002", + "sellerId": "SIM_USER_001", + "symbol": "ETHUSD", + "price": 3102.50, + "quantity": 2.0, + "timestamp": "2026-06-01T10:30:15Z" + } +] +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Response is array of trades +- ✅ Each trade has all required fields +- ✅ Trades are sorted by timestamp (most recent first) +- ✅ Limit parameter is respected + +--- + +### Endpoint: `GET /api/users/{userId}/pnl` + +**Test Case 6: Get User P&L** +```java +@Test +@DisplayName("Should return user profit/loss analytics") +void testGetUserPnL() throws Exception { + mockMvc.perform(get("/api/users/SIM_USER_001/pnl")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalPnL").exists()) + .andExpect(jsonPath("$.percentageGain").exists()) + .andExpect(jsonPath("$.realizedPnL").exists()) + .andExpect(jsonPath("$.unrealizedPnL").exists()) + .andExpect(jsonPath("$.winRate").exists()); +} +``` + +**Expected Response:** +```json +{ + "userId": "SIM_USER_001", + "totalPnL": 25000.00, + "percentageGain": 25.00, + "realizedPnL": 20000.00, + "unrealizedPnL": 5000.00, + "winRate": 62.5, + "totalTrades": 156 +} +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ All P&L metrics are present +- ✅ P&L values are reasonable +- ✅ Win rate is between 0-100% + +--- + +## Order Endpoints Tests + +### Endpoint: `POST /api/orders` + +**Test Case 1: Place Valid Order** +```java +@Test +@DisplayName("Should place a valid limit order") +void testPlaceValidOrder() throws Exception { + OrderRequest orderRequest = new OrderRequest( + "BTCUSD", + "BUY", + 67500.00, + 0.5 + ); + + mockMvc.perform(post("/api/orders") + .header("X-User-ID", "SIM_USER_001") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(orderRequest))) + .andExpect(status().isCreated()); +} +``` + +**Test Case 2: Place Order Without User Header** +```java +@Test +@DisplayName("Should fail when X-User-ID header is missing") +void testPlaceOrderMissingUserId() throws Exception { + OrderRequest orderRequest = new OrderRequest( + "BTCUSD", + "BUY", + 67500.00, + 0.5 + ); + + mockMvc.perform(post("/api/orders") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(orderRequest))) + .andExpect(status().isBadRequest()); +} +``` + +**Test Case 3: Place Order with Invalid Quantity** +```java +@Test +@DisplayName("Should fail when quantity is invalid") +void testPlaceOrderInvalidQuantity() throws Exception { + OrderRequest orderRequest = new OrderRequest( + "BTCUSD", + "BUY", + 67500.00, + -0.5 // Invalid negative quantity + ); + + mockMvc.perform(post("/api/orders") + .header("X-User-ID", "SIM_USER_001") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(orderRequest))) + .andExpect(status().isBadRequest()); +} +``` + +**Test Case 4: Place Order with Invalid Side** +```java +@Test +@DisplayName("Should fail when order side is invalid") +void testPlaceOrderInvalidSide() throws Exception { + OrderRequest orderRequest = new OrderRequest( + "BTCUSD", + "INVALID_SIDE", + 67500.00, + 0.5 + ); + + mockMvc.perform(post("/api/orders") + .header("X-User-ID", "SIM_USER_001") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(orderRequest))) + .andExpect(status().isBadRequest()); +} +``` + +**Valid Order Request:** +```json +{ + "symbol": "BTCUSD", + "side": "BUY", + "price": 67500.00, + "quantity": 0.5 +} +``` + +**Pass Criteria:** +- ✅ Status code: 201 CREATED for valid orders +- ✅ Status code: 400 BAD REQUEST for invalid orders +- ✅ Missing X-User-ID header returns error +- ✅ Negative quantities are rejected +- ✅ Invalid sides (not BUY/SELL) are rejected + +--- + +### Endpoint: `GET /api/orders/book/{symbol}` + +**Test Case 5: Get Order Book for Valid Symbol** +```java +@Test +@DisplayName("Should return order book for valid symbol") +void testGetOrderBook() throws Exception { + mockMvc.perform(get("/api/orders/book/BTCUSD")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.buyBook", instanceOf(List.class))) + .andExpect(jsonPath("$.sellBook", instanceOf(List.class))) + .andExpect(jsonPath("$.buyBook[0].price").exists()) + .andExpect(jsonPath("$.buyBook[0].orderInfo").exists()) + .andExpect(jsonPath("$.sellBook[0].price").exists()) + .andExpect(jsonPath("$.sellBook[0].orderInfo").exists()); +} +``` + +**Test Case 6: Get Order Book - Buy Orders Sorted Descending** +```java +@Test +@DisplayName("Should return buy book sorted by price descending") +void testGetOrderBookBuySorting() throws Exception { + MvcResult result = mockMvc.perform(get("/api/orders/book/BTCUSD")) + .andExpect(status().isOk()) + .andReturn(); + + String jsonResponse = result.getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(jsonResponse); + ArrayNode buyBook = (ArrayNode) jsonNode.get("buyBook"); + + // Verify buy prices are in descending order + for (int i = 1; i < buyBook.size(); i++) { + double prevPrice = buyBook.get(i-1).get("price").asDouble(); + double currPrice = buyBook.get(i).get("price").asDouble(); + assertTrue(prevPrice >= currPrice, "Buy book not in descending order"); + } +} +``` + +**Test Case 7: Get Order Book - Sell Orders Sorted Ascending** +```java +@Test +@DisplayName("Should return sell book sorted by price ascending") +void testGetOrderBookSellSorting() throws Exception { + MvcResult result = mockMvc.perform(get("/api/orders/book/BTCUSD")) + .andExpect(status().isOk()) + .andReturn(); + + String jsonResponse = result.getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(jsonResponse); + ArrayNode sellBook = (ArrayNode) jsonNode.get("sellBook"); + + // Verify sell prices are in ascending order + for (int i = 1; i < sellBook.size(); i++) { + double prevPrice = sellBook.get(i-1).get("price").asDouble(); + double currPrice = sellBook.get(i).get("price").asDouble(); + assertTrue(prevPrice <= currPrice, "Sell book not in ascending order"); + } +} +``` + +**Expected Response:** +```json +{ + "buyBook": [ + { + "price": 67500.00, + "orderInfo": "SIM_USER_001:0.5" + }, + { + "price": 67400.00, + "orderInfo": "SIM_USER_003:1.0" + } + ], + "sellBook": [ + { + "price": 67800.00, + "orderInfo": "SIM_USER_002:0.75" + }, + { + "price": 67900.00, + "orderInfo": "SIM_USER_004:0.25" + } + ] +} +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Both buyBook and sellBook are present +- ✅ Buy orders sorted by price (highest first) +- ✅ Sell orders sorted by price (lowest first) +- ✅ Each order contains price and orderInfo +- ✅ Order book contains up to 50 orders per side + +--- + +## Admin Endpoints Tests + +### Endpoint: `GET /api/admin/health` + +**Test Case 1: System Health Check** +```java +@Test +@DisplayName("Should return system health status") +void testSystemHealth() throws Exception { + mockMvc.perform(get("/api/admin/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.redisStatus", is("UP"))) + .andExpect(jsonPath("$.mongoStatus", is("UP"))) + .andExpect(jsonPath("$.memory.total").exists()) + .andExpect(jsonPath("$.memory.free").exists()) + .andExpect(jsonPath("$.memory.max").exists()) + .andExpect(jsonPath("$.activeReplay").isBoolean()); +} +``` + +**Expected Response:** +```json +{ + "redisStatus": "UP", + "mongoStatus": "UP", + "memory": { + "total": 2147483648, + "free": 1024000000, + "max": 4294967296 + }, + "activeReplay": true +} +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Redis status is UP +- ✅ MongoDB status is UP +- ✅ Memory values are positive +- ✅ activeReplay is a boolean + +--- + +### Endpoint: `GET /api/admin/db-info` + +**Test Case 2: Database Information** +```java +@Test +@DisplayName("Should return database information") +void testDatabaseInfo() throws Exception { + mockMvc.perform(get("/api/admin/db-info")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.database", is("cryptoflash_db"))) + .andExpect(jsonPath("$.collections", instanceOf(List.class))) + .andExpect(jsonPath("$.market_candles_count").isNumber()); +} +``` + +**Expected Response:** +```json +{ + "database": "cryptoflash_db", + "collections": ["market_candles", "trade_history", "users"], + "market_candles_count": 12500 +} +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Database name is correct +- ✅ Collections list is not empty +- ✅ Candle count is a non-negative number + +--- + +### Endpoint: `GET /api/admin/users` + +**Test Case 3: List All Users** +```java +@Test +@DisplayName("Should return list of all users") +void testListAllUsers() throws Exception { + mockMvc.perform(get("/api/admin/users")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", instanceOf(List.class))) + .andExpect(jsonPath("$[0].userId").exists()) + .andExpect(jsonPath("$[0].balance").exists()) + .andExpect(jsonPath("$.length()", greaterThan(0))); +} +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Response is an array +- ✅ Each user has userId and balance fields +- ✅ At least one user exists + +--- + +### Endpoint: `GET /api/admin/volume/{symbol}` + +**Test Case 4: Get 24h Volume** +```java +@Test +@DisplayName("Should return 24h trading volume for symbol") +void testGet24hVolume() throws Exception { + mockMvc.perform(get("/api/admin/volume/BTCUSD")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.symbol", is("BTCUSD"))) + .andExpect(jsonPath("$.metric", is("24h Volume"))) + .andExpect(jsonPath("$.value").isNumber()) + .andExpect(jsonPath("$.value", greaterThanOrEqualTo(0.0))); +} +``` + +**Expected Response:** +```json +{ + "symbol": "BTCUSD", + "metric": "24h Volume", + "value": 5847.25 +} +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Symbol matches requested symbol +- ✅ Value is non-negative +- ✅ Metric label is correct + +--- + +### Endpoint: `GET /api/admin/vwap/{symbol}` + +**Test Case 5: Get Volume-Weighted Average Price** +```java +@Test +@DisplayName("Should return VWAP for symbol") +void testGetVWAP() throws Exception { + mockMvc.perform(get("/api/admin/vwap/ETHUSD")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.symbol", is("ETHUSD"))) + .andExpect(jsonPath("$.metric", is("24h VWAP"))) + .andExpect(jsonPath("$.value").isNumber()) + .andExpect(jsonPath("$.value", greaterThan(0.0))); +} +``` + +**Expected Response:** +```json +{ + "symbol": "ETHUSD", + "metric": "24h VWAP", + "value": 3102.45 +} +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Symbol matches requested symbol +- ✅ VWAP value is positive +- ✅ Value is reasonable for the symbol + +--- + +### Endpoint: `GET /api/admin/whales` + +**Test Case 6: Get Top Traders (Whales)** +```java +@Test +@DisplayName("Should return top traders (whales)") +void testGetWhales() throws Exception { + mockMvc.perform(get("/api/admin/whales")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].userId").exists()) + .andExpect(jsonPath("$[0].wealthUsd").exists()) + .andExpect(jsonPath("$[0].portfolioValue").exists()); +} +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Response is sorted by wealth (highest first) +- ✅ Each entry has userId and wealth fields +- ✅ Wealth values are positive + +--- + +### Endpoint: `GET /api/admin/leaderboard/top` + +**Test Case 7: Get Top 10 Leaderboard** +```java +@Test +@DisplayName("Should return top 10 traders") +void testGetLeaderboardTop() throws Exception { + mockMvc.perform(get("/api/admin/leaderboard/top")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", instanceOf(List.class))) + .andExpect(jsonPath("$.length()", lessThanOrEqualTo(10))) + .andExpect(jsonPath("$[0].rank", is(1))) + .andExpect(jsonPath("$[0].userId").exists()) + .andExpect(jsonPath("$[0].wealthUsd").exists()); +} +``` + +**Expected Response:** +```json +[ + { + "rank": 1, + "userId": "SIM_USER_001", + "wealthUsd": 250000.00 + }, + { + "rank": 2, + "userId": "SIM_USER_003", + "wealthUsd": 225000.00 + } +] +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Max 10 entries returned +- ✅ Sorted by rank (1 is first) +- ✅ Ranks are sequential + +--- + +### Endpoint: `GET /api/admin/leaderboard/rank/{userId}` + +**Test Case 8: Get User Ranking** +```java +@Test +@DisplayName("Should return user's leaderboard rank") +void testGetLeaderboardRank() throws Exception { + mockMvc.perform(get("/api/admin/leaderboard/rank/SIM_USER_001")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userId", is("SIM_USER_001"))) + .andExpect(jsonPath("$.rank").isNumber()) + .andExpect(jsonPath("$.rank", greaterThan(0))) + .andExpect(jsonPath("$.wealthUsd").isNumber()) + .andExpect(jsonPath("$.wealthUsd", greaterThanOrEqualTo(0.0))); +} +``` + +**Expected Response:** +```json +{ + "userId": "SIM_USER_001", + "rank": 3, + "wealthUsd": 125000.00 +} +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Rank is a positive integer +- ✅ Wealth is non-negative +- ✅ User ID matches requested user + +--- + +### Endpoint: `GET /api/admin/trigger-replay` + +**Test Case 9: Trigger Market Replay** +```java +@Test +@DisplayName("Should manually trigger market replay") +void testTriggerReplay() throws Exception { + mockMvc.perform(get("/api/admin/trigger-replay")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Replay triggered"))) + .andExpect(content().string(containsString("candles found"))); +} +``` + +**Expected Response:** +``` +"Replay triggered. Total candles found in 'market_candles' collection: 12500" +``` + +**Pass Criteria:** +- ✅ Status code: 200 OK +- ✅ Response contains "Replay triggered" +- ✅ Candle count is reported + +--- + +## Running Tests + +### Run All Tests + +```bash +# Using Maven +./mvnw test + +# Run with verbose output +./mvnw test -X + +# Run specific test class +./mvnw test -Dtest=MarketControllerTest + +# Run specific test method +./mvnw test -Dtest=MarketControllerTest#testGetSymbols +``` + +### Run Tests with Coverage + +```bash +# Generate coverage report +./mvnw jacoco:report + +# View coverage at target/site/jacoco/index.html +``` + +### Run Integration Tests + +```bash +# Run end-to-end tests (requires running services) +./mvnw test -Dtest=EndToEndTest +``` + +--- + +## Manual Testing with cURL + +### Market Data Testing + +```bash +# Get all symbols +curl -X GET http://localhost:8080/api/market/symbols + +# Get BTCUSD candles (50) +curl -X GET http://localhost:8080/api/market/candles/BTCUSD + +# Get ETHUSD candles (100) +curl -X GET http://localhost:8080/api/market/candles/ETHUSD?limit=100 +``` + +### User Testing + +```bash +# Get user profile +curl -X GET http://localhost:8080/api/users/SIM_USER_001 + +# Get user trades +curl -X GET http://localhost:8080/api/users/SIM_USER_001/trades + +# Get user trades (100 limit) +curl -X GET "http://localhost:8080/api/users/SIM_USER_001/trades?limit=100" + +# Get user P&L +curl -X GET http://localhost:8080/api/users/SIM_USER_001/pnl +``` + +### Order Testing + +```bash +# Place a buy order +curl -X POST http://localhost:8080/api/orders \ + -H "Content-Type: application/json" \ + -H "X-User-ID: SIM_USER_001" \ + -d '{ + "symbol": "BTCUSD", + "side": "BUY", + "price": 67500.00, + "quantity": 0.5 + }' + +# Place a sell order +curl -X POST http://localhost:8080/api/orders \ + -H "Content-Type: application/json" \ + -H "X-User-ID: SIM_USER_001" \ + -d '{ + "symbol": "ETHUSD", + "side": "SELL", + "price": 3100.00, + "quantity": 2.0 + }' + +# Get order book for BTCUSD +curl -X GET http://localhost:8080/api/orders/book/BTCUSD +``` + +### Admin Testing + +```bash +# System health check +curl -X GET http://localhost:8080/api/admin/health + +# Database info +curl -X GET http://localhost:8080/api/admin/db-info + +# List all users +curl -X GET http://localhost:8080/api/admin/users + +# Get 24h volume +curl -X GET http://localhost:8080/api/admin/volume/BTCUSD + +# Get VWAP +curl -X GET http://localhost:8080/api/admin/vwap/ETHUSD + +# Get whales +curl -X GET http://localhost:8080/api/admin/whales + +# Get top 10 leaderboard +curl -X GET http://localhost:8080/api/admin/leaderboard/top + +# Get user rank +curl -X GET http://localhost:8080/api/admin/leaderboard/rank/SIM_USER_001 + +# Trigger replay +curl -X GET http://localhost:8080/api/admin/trigger-replay +``` + +### Testing with jq for JSON Pretty Print + +```bash +# Get symbols with pretty formatting +curl -X GET http://localhost:8080/api/market/symbols | jq + +# Get candles with filtering +curl -X GET http://localhost:8080/api/market/candles/BTCUSD | jq '.[0]' + +# Get user profile and extract specific field +curl -X GET http://localhost:8080/api/users/SIM_USER_001 | jq '.balance' +``` + +--- + +## Success Criteria Summary + +| Endpoint | Method | Status | Response Type | +|----------|--------|--------|---------------| +| `/api/market/symbols` | GET | 200 | Array of strings | +| `/api/market/candles/{symbol}` | GET | 200 | Array of candles | +| `/api/users/{userId}` | GET | 200/404 | User object | +| `/api/users/{userId}/trades` | GET | 200 | Array of trades | +| `/api/users/{userId}/pnl` | GET | 200 | PnL object | +| `/api/orders` | POST | 201/400 | Void | +| `/api/orders/book/{symbol}` | GET | 200 | Order book object | +| `/api/admin/health` | GET | 200 | Health status | +| `/api/admin/db-info` | GET | 200 | Database info | +| `/api/admin/users` | GET | 200 | Array of users | +| `/api/admin/volume/{symbol}` | GET | 200 | Volume object | +| `/api/admin/vwap/{symbol}` | GET | 200 | VWAP object | +| `/api/admin/whales` | GET | 200 | Array of traders | +| `/api/admin/leaderboard/top` | GET | 200 | Array of rankings | +| `/api/admin/leaderboard/rank/{userId}` | GET | 200 | User rank object | +| `/api/admin/trigger-replay` | GET | 200 | String message | + +--- + +## Troubleshooting + +### Test Failures + +**Issue:** Tests fail with "Connection refused" to MongoDB/Redis +- **Solution:** Ensure Docker containers are running: `docker-compose -f docker-compose-dev.yml up -d` + +**Issue:** MockMvc tests fail with 404 +- **Solution:** Verify endpoint paths match exactly, check spelling of URL + +**Issue:** JSON parsing errors in assertions +- **Solution:** Print actual response to debug: add `.andDo(print())` + +### Common Test Patterns + +```java +// Print response for debugging +.andDo(print()) + +// Check if response is an array +.andExpect(jsonPath("$", instanceOf(List.class))) + +// Check array length +.andExpect(jsonPath("$.length()", equalTo(5))) + +// Check nested value exists +.andExpect(jsonPath("$[0].symbol").exists()) + +// Check numeric comparison +.andExpect(jsonPath("$.balance", greaterThan(0.0))) +``` + +--- + +## Next Steps + +1. Implement these test cases in their respective controller test files +2. Add integration tests for multi-endpoint workflows +3. Add performance/load tests for high-frequency trading scenarios +4. Set up continuous integration to run tests on each commit + From 17b70bce922c339db4db998b29623dcce6f44a01 Mon Sep 17 00:00:00 2001 From: Pedro Carneiro Junior Date: Thu, 4 Jun 2026 16:20:04 +0200 Subject: [PATCH 2/3] Tests --- API_TEST_EXECUTION_SUMMARY.md | 38 +++++ API_TEST_EXECUTION_SUMMARY_RUN2.md | 47 ++++++ API_TEST_EXECUTION_SUMMARY_RUN3.md | 38 +++++ API_TEST_EXECUTION_SUMMARY_RUN4.md | 44 ++++++ API_TEST_GUIDE.md | 14 ++ pom.xml | 9 ++ .../java/jar/controller/OrderController.java | 15 +- .../jar/controller/AdminControllerTest.java | 146 ++++++++++++++++++ .../jar/controller/MarketControllerTest.java | 83 ++++++++++ .../jar/controller/OrderControllerTest.java | 120 ++++++++++++++ .../jar/controller/UserControllerTest.java | 87 +++++++++++ .../java/jar/integration/EndToEndTest.java | 84 ++++++++++ 12 files changed, 720 insertions(+), 5 deletions(-) create mode 100644 API_TEST_EXECUTION_SUMMARY.md create mode 100644 API_TEST_EXECUTION_SUMMARY_RUN2.md create mode 100644 API_TEST_EXECUTION_SUMMARY_RUN3.md create mode 100644 API_TEST_EXECUTION_SUMMARY_RUN4.md create mode 100644 src/test/java/jar/controller/AdminControllerTest.java create mode 100644 src/test/java/jar/controller/MarketControllerTest.java create mode 100644 src/test/java/jar/controller/OrderControllerTest.java create mode 100644 src/test/java/jar/controller/UserControllerTest.java create mode 100644 src/test/java/jar/integration/EndToEndTest.java diff --git a/API_TEST_EXECUTION_SUMMARY.md b/API_TEST_EXECUTION_SUMMARY.md new file mode 100644 index 0000000..6cfa9b1 --- /dev/null +++ b/API_TEST_EXECUTION_SUMMARY.md @@ -0,0 +1,38 @@ +# CryptoFlash API Test Execution Summary + +## Overview +This file captures the execution of the API test plan for the CryptoFlash project. + +## Changes made +- Added controller tests for API endpoints: + - `src/test/java/jar/controller/MarketControllerTest.java` + - `src/test/java/jar/controller/UserControllerTest.java` + - `src/test/java/jar/controller/OrderControllerTest.java` + - `src/test/java/jar/controller/AdminControllerTest.java` +- Updated `pom.xml` to include: + - `spring-boot-starter-test` + - `spring-boot-starter-validation` +- Added request validation for `OrderController.OrderRequest` in `src/main/java/jar/controller/OrderController.java`. +- Documented execution summary in `API_TEST_GUIDE.md`. + +## Commands executed +```bash +./mvnw test -q +./mvnw -Djava.version=21 test -q +./mvnw -Djava.version=21 -Dtest=MarketControllerTest test -q +./mvnw -Djava.version=21 -Dtest=MarketControllerTest,UserControllerTest,OrderControllerTest,AdminControllerTest test -q +``` + +## Environment notes +- Local environment uses Java 21. +- The project `pom.xml` originally specified Java 25, so tests were executed with `-Djava.version=21`. +- Full application test execution still requires a running MongoDB and Redis instance. + +## Result +- The new controller tests passed successfully when run together: + - `./mvnw -Djava.version=21 -Dtest=MarketControllerTest,UserControllerTest,OrderControllerTest,AdminControllerTest test -q` +- Exit status: `0` + +## Notes +- Existing `CryptoflashApplicationTests` may fail in the current environment when MongoDB or Redis are not available. +- The execution summary has been added to `API_TEST_GUIDE.md` and also saved in this standalone file. diff --git a/API_TEST_EXECUTION_SUMMARY_RUN2.md b/API_TEST_EXECUTION_SUMMARY_RUN2.md new file mode 100644 index 0000000..25b4256 --- /dev/null +++ b/API_TEST_EXECUTION_SUMMARY_RUN2.md @@ -0,0 +1,47 @@ +# CryptoFlash API Test Execution Summary (Run 2) + +## Overview + +This document captures a second execution of the API test plan for CryptoFlash. +It preserves the existing summary file by writing a new record under `API_TEST_EXECUTION_SUMMARY_RUN2.md`. + +## Execution Date + +- June 1, 2026 + +## Scope + +- Verified the focused controller test suite for Market, User, Order, and Admin APIs. +- Used the same targeted Maven command as the prior run. +- Did not start application containers for this execution. + +## Commands Used + +```bash +wsl -d Ubuntu -- bash -lc 'cd /home/pedro/projects/lm-aide/CryptoFlash && ./mvnw -Djava.version=21 -Dtest=MarketControllerTest,UserControllerTest,OrderControllerTest,AdminControllerTest test -q; echo EXIT_CODE:$?' +``` + +## Execution Result + +- Controller tests executed successfully. +- Maven returned `EXIT_CODE:0`. +- The run produced Spring MockMvc initialization logs and validation warnings from Mockito, but no test failures. + +## Verified Test Coverage + +- `MarketControllerTest` +- `UserControllerTest` +- `OrderControllerTest` +- `AdminControllerTest` + +## Notes + +- This run validates the standalone controller API tests in the current development environment. +- Full application tests still require MongoDB and Redis to be available for end-to-end or integration scenarios. +- The actual command output included expected validation handling for invalid order requests and missing headers. + +## Next Steps + +1. If you want to validate the full app behavior, start the containers from `docker-compose-dev.yml`. +2. Run the full Maven suite or the integration tests once MongoDB and Redis are available. +3. Preserve this run record alongside the existing summary for comparison. diff --git a/API_TEST_EXECUTION_SUMMARY_RUN3.md b/API_TEST_EXECUTION_SUMMARY_RUN3.md new file mode 100644 index 0000000..9a0d820 --- /dev/null +++ b/API_TEST_EXECUTION_SUMMARY_RUN3.md @@ -0,0 +1,38 @@ +# CryptoFlash API Test Execution Summary (Run 3 - Integration) + +## Overview + +This document captures an integration run that starts the full application and verifies basic end-to-end endpoints against running MongoDB and Redis containers. + +## Execution Date + +- June 1, 2026 + +## Commands Used + +```bash +wsl -d Ubuntu -- bash -lc 'cd /home/pedro/projects/lm-aide/CryptoFlash && ./mvnw -Djava.version=21 -Dtest=EndToEndTest test -q; echo EXIT_CODE:$?' +``` + +## Execution Result + +- The application started inside the test (Spring Boot v4.0.2) and connected to the running MongoDB replica set and Redis cluster. +- `EndToEndTest` executed successfully; Maven returned `EXIT_CODE:0`. +- Logs indicate successful seeding of users and Redis consumer group initialization. + +## Verified Endpoints + +- `GET /api/market/symbols` returned a non-empty array. +- `GET /api/admin/health` returned 200 and a non-empty body. + +## Notes + +- This run confirms the application can start and reach MongoDB and Redis configured by `docker-compose-dev.yml`. +- The integration test is minimal by design; expand with additional end-to-end flows if you want broader coverage. + +## Next Steps + +1. Expand `EndToEndTest` with additional scenarios (order placement, replay trigger, user trades). +2. Add a CI job to run integration tests against an ephemeral Docker environment. +3. Save additional run artifacts (logs, jacoco) if needed. + diff --git a/API_TEST_EXECUTION_SUMMARY_RUN4.md b/API_TEST_EXECUTION_SUMMARY_RUN4.md new file mode 100644 index 0000000..1ab397f --- /dev/null +++ b/API_TEST_EXECUTION_SUMMARY_RUN4.md @@ -0,0 +1,44 @@ +# CryptoFlash API Test Execution Summary (Run 4 - Expanded Integration) + +## Overview + +This record captures an expanded integration run that exercises additional end-to-end scenarios: +- user profile +- place order +- order book +- candles +- trigger replay + +## Execution Date + +- June 1, 2026 + +## Commands Used + +```bash +wsl -d Ubuntu -- bash -lc 'cd /home/pedro/projects/lm-aide/CryptoFlash && ./mvnw -Djava.version=21 -Dtest=EndToEndTest test' +``` + +## Execution Result + +- Tests executed: 7 +- Failures: 0 +- Errors: 0 +- Build: SUCCESS +- Observations: Application started, connected to MongoDB and Redis; users already seeded; market replay engine ran but found no candles (expected if collection empty); order placement created a resting order in the buy book. + +## Verified Scenarios + +- `GET /api/market/symbols` returned a non-empty list. +- `GET /api/admin/health` returned 200. +- `GET /api/users/SIM_USER_001` returned seeded user data. +- `POST /api/orders` placed a valid order with header `X-User-ID` and returned 201. +- `GET /api/orders/book/BTCUSD` returned an order book structure. +- `GET /api/market/candles/BTCUSD?limit=5` returned an array (may be empty). +- `GET /api/admin/trigger-replay` returned a success message. + +## Next Steps + +- Expand tests to cover negative scenarios (invalid orders, missing headers). +- Add CI job that spins up ephemeral Docker services and runs `EndToEndTest`. +- Optionally persist run logs and artifacts to `target/` for traceability. diff --git a/API_TEST_GUIDE.md b/API_TEST_GUIDE.md index 6231c75..a54a64d 100644 --- a/API_TEST_GUIDE.md +++ b/API_TEST_GUIDE.md @@ -950,6 +950,20 @@ curl -X GET http://localhost:8080/api/market/candles/BTCUSD | jq '.[0]' curl -X GET http://localhost:8080/api/users/SIM_USER_001 | jq '.balance' ``` +## Automated Test Execution Summary + +- Added controller tests under `src/test/java/jar/controller` for Market, User, Order, and Admin APIs. +- Updated `pom.xml` with `spring-boot-starter-test` and `spring-boot-starter-validation`. +- Added request validation for `OrderController.OrderRequest` to enforce valid `BUY/SELL` sides and positive quantities/prices. +- Verified new tests with: + +```bash +./mvnw -Djava.version=21 -Dtest=MarketControllerTest,UserControllerTest,OrderControllerTest,AdminControllerTest test -q +``` + +- Result: new controller tests passed successfully (`EXIT_CODE:0`). +- Note: full application test execution still requires MongoDB and Redis to be available in the environment. + --- ## Success Criteria Summary diff --git a/pom.xml b/pom.xml index 3fd43bd..9956e17 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,15 @@ spring-boot-starter-websocket-test test + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-validation + diff --git a/src/main/java/jar/controller/OrderController.java b/src/main/java/jar/controller/OrderController.java index c8b9c56..abd6087 100644 --- a/src/main/java/jar/controller/OrderController.java +++ b/src/main/java/jar/controller/OrderController.java @@ -1,5 +1,10 @@ package jar.controller; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; import jar.service.TradingService; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ZSetOperations; @@ -27,7 +32,7 @@ public OrderController(TradingService tradingService, StringRedisTemplate redisT @PostMapping public ResponseEntity placeOrder( @RequestHeader("X-User-ID") String userId, - @RequestBody OrderRequest request) { + @Valid @RequestBody OrderRequest request) { tradingService.placeLimitOrder( request.symbol(), @@ -69,9 +74,9 @@ private Object formatBook(Set> book) { } public record OrderRequest( - String symbol, - String side, - Double price, - Double quantity + @NotBlank String symbol, + @Pattern(regexp = "BUY|SELL") String side, + @NotNull @Positive Double price, + @NotNull @Positive Double quantity ) {} } diff --git a/src/test/java/jar/controller/AdminControllerTest.java b/src/test/java/jar/controller/AdminControllerTest.java new file mode 100644 index 0000000..b089515 --- /dev/null +++ b/src/test/java/jar/controller/AdminControllerTest.java @@ -0,0 +1,146 @@ +package jar.controller; + +import com.mongodb.client.MongoDatabase; +import jar.model.User; +import jar.service.AnalyticsService; +import jar.service.LeaderboardService; +import jar.service.MarketReplayService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class AdminControllerTest { + + private MockMvc mockMvc; + private final AnalyticsService analyticsService = Mockito.mock(AnalyticsService.class); + private final MarketReplayService marketReplayService = Mockito.mock(MarketReplayService.class); + private final LeaderboardService leaderboardService = Mockito.mock(LeaderboardService.class); + private final MongoTemplate mongoTemplate = Mockito.mock(MongoTemplate.class); + + @BeforeEach + void setup() { + AdminController controller = new AdminController(analyticsService, marketReplayService, leaderboardService, mongoTemplate); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + void testDatabaseInfo() throws Exception { + MongoDatabase database = org.mockito.Mockito.mock(MongoDatabase.class); + when(mongoTemplate.getDb()).thenReturn(database); + when(database.getName()).thenReturn("cryptoflash_db"); + when(mongoTemplate.getCollectionNames()).thenReturn(Set.of("market_candles", "trade_history", "users")); + when(mongoTemplate.count(any(Query.class), eq("market_candles"))).thenReturn(12500L); + + mockMvc.perform(get("/api/admin/db-info") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.database").value("cryptoflash_db")) + .andExpect(jsonPath("$.collections").isArray()) + .andExpect(jsonPath("$.market_candles_count").value(12500)); + } + + @Test + void testTriggerReplay() throws Exception { + when(mongoTemplate.count(any(Query.class), eq("market_candles"))).thenReturn(12500L); + Mockito.doNothing().when(marketReplayService).triggerReplay(); + + mockMvc.perform(get("/api/admin/trigger-replay") + .accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").value("Replay triggered. Total candles found in 'market_candles' collection: 12500")); + } + + @Test + void testSystemHealth() throws Exception { + when(marketReplayService.isReplayInProgress()).thenReturn(true); + + mockMvc.perform(get("/api/admin/health") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.redisStatus").value("UP")) + .andExpect(jsonPath("$.mongoStatus").value("UP")) + .andExpect(jsonPath("$.memory.total").exists()) + .andExpect(jsonPath("$.memory.free").exists()) + .andExpect(jsonPath("$.memory.max").exists()) + .andExpect(jsonPath("$.activeReplay").value(true)); + } + + @Test + void testGet24hVolume() throws Exception { + when(analyticsService.get24hVolume("BTCUSD")).thenReturn(5847.25); + + mockMvc.perform(get("/api/admin/volume/BTCUSD") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.symbol").value("BTCUSD")) + .andExpect(jsonPath("$.metric").value("24h Volume")) + .andExpect(jsonPath("$.value").value(5847.25)); + } + + @Test + void testGetVWAP() throws Exception { + when(analyticsService.getVWAP("ETHUSD")).thenReturn(3102.45); + + mockMvc.perform(get("/api/admin/vwap/ETHUSD") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.symbol").value("ETHUSD")) + .andExpect(jsonPath("$.metric").value("24h VWAP")) + .andExpect(jsonPath("$.value").value(3102.45)); + } + + @Test + void testListAllUsers() throws Exception { + User user = new User("1", "SIM_USER_001", "Test User", new User.Wallet(100000.0, 0.0, 0.0, 0.0, 0.0, 0.0)); + when(mongoTemplate.findAll(User.class)).thenReturn(List.of(user)); + + mockMvc.perform(get("/api/admin/users") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].userId").value("SIM_USER_001")) + .andExpect(jsonPath("$[0].wallet.usd").value(100000.0)); + } + + @Test + void testGetLeaderboardTop() throws Exception { + when(leaderboardService.getTop10()).thenReturn(List.of( + Map.of("rank", 1, "userId", "SIM_USER_001", "wealthUsd", 250000.0) + )); + + mockMvc.perform(get("/api/admin/leaderboard/top") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()" ).value(1)) + .andExpect(jsonPath("$[0].rank").value(1)) + .andExpect(jsonPath("$[0].userId").value("SIM_USER_001")); + } + + @Test + void testGetLeaderboardRank() throws Exception { + when(leaderboardService.getUserRank("SIM_USER_001")).thenReturn(1); + when(leaderboardService.getUserWealth("SIM_USER_001")).thenReturn(125000.0); + + mockMvc.perform(get("/api/admin/leaderboard/rank/SIM_USER_001") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userId").value("SIM_USER_001")) + .andExpect(jsonPath("$.rank").value(1)) + .andExpect(jsonPath("$.wealthUsd").value(125000.0)); + } +} diff --git a/src/test/java/jar/controller/MarketControllerTest.java b/src/test/java/jar/controller/MarketControllerTest.java new file mode 100644 index 0000000..854fe59 --- /dev/null +++ b/src/test/java/jar/controller/MarketControllerTest.java @@ -0,0 +1,83 @@ +package jar.controller; + +import jar.model.MarketCandle; +import jar.service.MarketReplayService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class MarketControllerTest { + + private MockMvc mockMvc; + private final MarketReplayService marketReplayService = Mockito.mock(MarketReplayService.class); + private final MongoTemplate mongoTemplate = Mockito.mock(MongoTemplate.class); + + @BeforeEach + void setup() { + MarketController controller = new MarketController(marketReplayService, mongoTemplate); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + void testGetSymbols() throws Exception { + mockMvc.perform(get("/api/market/symbols")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()" ).value(5)) + .andExpect(jsonPath("$[0]").value("BTCUSD")) + .andExpect(jsonPath("$[1]").value("ETHUSD")); + } + + @Test + void testGetCandlesForBTCUSD() throws Exception { + Instant older = Instant.parse("2026-06-01T10:30:00Z"); + Instant newer = Instant.parse("2026-06-01T10:31:00Z"); + + when(marketReplayService.getCurrentTimestamp("BTCUSD")).thenReturn(Optional.of(newer)); + when(mongoTemplate.find(any(Query.class), eq(MarketCandle.class), eq("market_candles"))) + .thenReturn(new java.util.ArrayList<>(List.of( + new MarketCandle("1", "BTCUSD", 67500.0, 68000.0, 67200.0, 67800.0, 1250.5, newer), + new MarketCandle("2", "BTCUSD", 67400.0, 67600.0, 67300.0, 67500.0, 1020.0, older) + ))); + + mockMvc.perform(get("/api/market/candles/BTCUSD") + .param("limit", "50") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()" ).value(2)) + .andExpect(jsonPath("$[0].symbol").value("BTCUSD")) + .andExpect(jsonPath("$[0].open").value(67400.0)) + .andExpect(jsonPath("$[0].high").value(67600.0)) + .andExpect(jsonPath("$[0].low").value(67300.0)) + .andExpect(jsonPath("$[0].close").value(67500.0)) + .andExpect(jsonPath("$[0].volume").value(1020.0)); + } + + @Test + void testGetCandlesDefaultLimit() throws Exception { + Instant now = Instant.parse("2026-06-01T11:00:00Z"); + when(marketReplayService.getCurrentTimestamp("LTCUSD")).thenReturn(Optional.of(now)); + when(mongoTemplate.find(any(Query.class), eq(MarketCandle.class), eq("market_candles"))) + .thenReturn(List.of()); + + mockMvc.perform(get("/api/market/candles/LTCUSD") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()" ).value(0)); + } +} diff --git a/src/test/java/jar/controller/OrderControllerTest.java b/src/test/java/jar/controller/OrderControllerTest.java new file mode 100644 index 0000000..64f7e58 --- /dev/null +++ b/src/test/java/jar/controller/OrderControllerTest.java @@ -0,0 +1,120 @@ +package jar.controller; + +import jar.service.TradingService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class OrderControllerTest { + + private MockMvc mockMvc; + private final TradingService tradingService = Mockito.mock(TradingService.class); + private final StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class); + + @BeforeEach + void setup() { + OrderController controller = new OrderController(tradingService, redisTemplate); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + void testPlaceValidOrder() throws Exception { + String body = "{\"symbol\":\"BTCUSD\",\"side\":\"BUY\",\"price\":67500.0,\"quantity\":0.5}"; + + mockMvc.perform(post("/api/orders") + .header("X-User-ID", "SIM_USER_001") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isCreated()); + } + + @Test + void testPlaceOrderMissingUserId() throws Exception { + String body = "{\"symbol\":\"BTCUSD\",\"side\":\"BUY\",\"price\":67500.0,\"quantity\":0.5}"; + + mockMvc.perform(post("/api/orders") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isBadRequest()); + } + + @Test + void testPlaceOrderInvalidQuantity() throws Exception { + String body = "{\"symbol\":\"BTCUSD\",\"side\":\"BUY\",\"price\":67500.0,\"quantity\":-0.5}"; + + mockMvc.perform(post("/api/orders") + .header("X-User-ID", "SIM_USER_001") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isBadRequest()); + } + + @Test + void testPlaceOrderInvalidSide() throws Exception { + String body = "{\"symbol\":\"BTCUSD\",\"side\":\"INVALID_SIDE\",\"price\":67500.0,\"quantity\":0.5}"; + + mockMvc.perform(post("/api/orders") + .header("X-User-ID", "SIM_USER_001") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isBadRequest()); + } + + @Test + void testGetOrderBook() throws Exception { + ZSetOperations zSetOperations = mock(ZSetOperations.class); + when(redisTemplate.opsForZSet()).thenReturn(zSetOperations); + + ZSetOperations.TypedTuple buy1 = mock(ZSetOperations.TypedTuple.class); + ZSetOperations.TypedTuple buy2 = mock(ZSetOperations.TypedTuple.class); + when(buy1.getValue()).thenReturn("SIM_USER_001:0.5"); + when(buy1.getScore()).thenReturn(67500.0); + when(buy2.getValue()).thenReturn("SIM_USER_003:1.0"); + when(buy2.getScore()).thenReturn(67400.0); + + ZSetOperations.TypedTuple sell1 = mock(ZSetOperations.TypedTuple.class); + ZSetOperations.TypedTuple sell2 = mock(ZSetOperations.TypedTuple.class); + when(sell1.getValue()).thenReturn("SIM_USER_002:0.75"); + when(sell1.getScore()).thenReturn(67800.0); + when(sell2.getValue()).thenReturn("SIM_USER_004:0.25"); + when(sell2.getScore()).thenReturn(67900.0); + + Set> buyBook = new LinkedHashSet<>(); + buyBook.add(buy1); + buyBook.add(buy2); + + Set> sellBook = new LinkedHashSet<>(); + sellBook.add(sell1); + sellBook.add(sell2); + + when(zSetOperations.reverseRangeWithScores(eq("order_book:BTCUSD:BUY"), eq(0L), eq(49L))).thenReturn(buyBook); + when(zSetOperations.rangeWithScores(eq("order_book:BTCUSD:SELL"), eq(0L), eq(49L))).thenReturn(sellBook); + + mockMvc.perform(get("/api/orders/book/BTCUSD") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.buyBook").isArray()) + .andExpect(jsonPath("$.sellBook").isArray()) + .andExpect(jsonPath("$.buyBook[0].price").value(67500.0)) + .andExpect(jsonPath("$.buyBook[0].orderInfo").value("SIM_USER_001:0.5")) + .andExpect(jsonPath("$.sellBook[0].price").value(67800.0)) + .andExpect(jsonPath("$.sellBook[0].orderInfo").value("SIM_USER_002:0.75")); + } +} diff --git a/src/test/java/jar/controller/UserControllerTest.java b/src/test/java/jar/controller/UserControllerTest.java new file mode 100644 index 0000000..0f0a6d9 --- /dev/null +++ b/src/test/java/jar/controller/UserControllerTest.java @@ -0,0 +1,87 @@ +package jar.controller; + +import jar.model.TradeHistory; +import jar.model.User; +import jar.service.AnalyticsService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class UserControllerTest { + + private MockMvc mockMvc; + private final MongoTemplate mongoTemplate = Mockito.mock(MongoTemplate.class); + private final AnalyticsService analyticsService = Mockito.mock(AnalyticsService.class); + + @BeforeEach + void setup() { + UserController controller = new UserController(mongoTemplate, analyticsService); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + void testGetUserProfileValid() throws Exception { + User user = new User("1", "SIM_USER_001", "Test User", new User.Wallet(100000.0, 1.0, 2.0, 3.0, 4.0, 5.0)); + when(mongoTemplate.findOne(any(Query.class), eq(User.class))).thenReturn(user); + + mockMvc.perform(get("/api/users/SIM_USER_001") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userId").value("SIM_USER_001")) + .andExpect(jsonPath("$.wallet.usd").value(100000.0)); + } + + @Test + void testGetUserProfileNotFound() throws Exception { + when(mongoTemplate.findOne(any(Query.class), eq(User.class))).thenReturn(null); + + mockMvc.perform(get("/api/users/INVALID_USER_999") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void testGetUserTradesDefaultLimit() throws Exception { + TradeHistory trade = new TradeHistory("1", "t1", "BTCUSD", "SIM_USER_001", "SIM_USER_002", 67800.0, 0.5, Instant.parse("2026-06-01T10:35:22Z")); + when(mongoTemplate.find(any(Query.class), eq(TradeHistory.class))).thenReturn(List.of(trade)); + + mockMvc.perform(get("/api/users/SIM_USER_001/trades") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()" ).value(1)) + .andExpect(jsonPath("$[0].symbol").value("BTCUSD")) + .andExpect(jsonPath("$[0].price").value(67800.0)) + .andExpect(jsonPath("$[0].quantity").value(0.5)) + .andExpect(jsonPath("$[0].timestamp").value("2026-06-01T10:35:22Z")); + } + + @Test + void testGetUserPnL() throws Exception { + List> pnlResponse = List.of( + Map.of("symbol", "BTCUSD", "realizedPnL", 25000.0) + ); + when(analyticsService.getUserPnL("SIM_USER_001")).thenReturn(pnlResponse); + + mockMvc.perform(get("/api/users/SIM_USER_001/pnl") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].symbol").value("BTCUSD")) + .andExpect(jsonPath("$[0].realizedPnL").value(25000.0)); + } +} diff --git a/src/test/java/jar/integration/EndToEndTest.java b/src/test/java/jar/integration/EndToEndTest.java new file mode 100644 index 0000000..a19dea1 --- /dev/null +++ b/src/test/java/jar/integration/EndToEndTest.java @@ -0,0 +1,84 @@ +package jar.integration; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class EndToEndTest { + + @LocalServerPort + private int port; + + private final RestTemplate rest = new RestTemplate(); + + private String base(String path) { + return "http://localhost:" + port + path; + } + + @Test + void symbolsEndpointReturnsList() { + ResponseEntity resp = rest.getForEntity(base("/api/market/symbols"), String[].class); + assertEquals(200, resp.getStatusCode().value()); + assertNotNull(resp.getBody()); + assertTrue(resp.getBody().length > 0); + } + + @Test + void adminHealthIsAccessible() { + ResponseEntity resp = rest.getForEntity(base("/api/admin/health"), String.class); + assertEquals(200, resp.getStatusCode().value()); + assertNotNull(resp.getBody()); + } + + @Test + void getUserProfile() { + ResponseEntity resp = rest.getForEntity(base("/api/users/SIM_USER_001"), String.class); + assertEquals(200, resp.getStatusCode().value()); + assertNotNull(resp.getBody()); + assertTrue(resp.getBody().contains("SIM_USER_001") || resp.getBody().contains("userId")); + } + + @Test + void placeValidOrder() { + String url = base("/api/orders"); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("X-User-ID", "SIM_USER_001"); + String body = "{\"symbol\":\"BTCUSD\",\"side\":\"BUY\",\"price\":67500.0,\"quantity\":0.5}"; + HttpEntity req = new HttpEntity<>(body, headers); + ResponseEntity resp = rest.postForEntity(url, req, Void.class); + assertEquals(201, resp.getStatusCode().value()); + } + + @Test + void getOrderBook() { + ResponseEntity resp = rest.getForEntity(base("/api/orders/book/BTCUSD"), String.class); + assertEquals(200, resp.getStatusCode().value()); + assertNotNull(resp.getBody()); + assertTrue(resp.getBody().contains("buyBook") || resp.getBody().contains("sellBook")); + } + + @Test + void getCandles() { + ResponseEntity resp = rest.getForEntity(base("/api/market/candles/BTCUSD?limit=5"), String.class); + assertEquals(200, resp.getStatusCode().value()); + assertNotNull(resp.getBody()); + assertTrue(resp.getBody().trim().startsWith("[")); + } + + @Test + void triggerReplay() { + ResponseEntity resp = rest.getForEntity(base("/api/admin/trigger-replay"), String.class); + assertEquals(200, resp.getStatusCode().value()); + assertNotNull(resp.getBody()); + assertTrue(resp.getBody().toLowerCase().contains("replay triggered") || resp.getBody().toLowerCase().contains("candles found")); + } +} From fbd373c449e45346129a5e2d5f7309a133ac3133 Mon Sep 17 00:00:00 2001 From: Pedro Carneiro Junior Date: Thu, 11 Jun 2026 16:17:19 +0200 Subject: [PATCH 3/3] Added UI mockups to documentation --- ...ockup 1 Trader Trading Terminal Layout.txt | 49 +++++++++++++++++++ ...up 2 Trader Portfolio and Analytics Layout | 39 +++++++++++++++ ...Mockup 3 Admin Simulation Dashboard Layout | 36 ++++++++++++++ ... Admin Infrastructure & Users Panel Layout | 32 ++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 frontend/mockups/UI Mockup 1 Trader Trading Terminal Layout.txt create mode 100644 frontend/mockups/UI Mockup 2 Trader Portfolio and Analytics Layout create mode 100644 frontend/mockups/UI Mockup 3 Admin Simulation Dashboard Layout create mode 100644 frontend/mockups/UI Mockup 4 Admin Infrastructure & Users Panel Layout diff --git a/frontend/mockups/UI Mockup 1 Trader Trading Terminal Layout.txt b/frontend/mockups/UI Mockup 1 Trader Trading Terminal Layout.txt new file mode 100644 index 0000000..3c81e99 --- /dev/null +++ b/frontend/mockups/UI Mockup 1 Trader Trading Terminal Layout.txt @@ -0,0 +1,49 @@ ++--------------------------------------------------------------------------------------------+ +| CryptoFlash | [Dashboard] [Portfolio & History] User: SIM_USER_001 [Logout] | ++--------------------------------------------------------------------------------------------+ +| SELECT ASSET (UC1) | +| [X] BTCUSD [ ] ETHUSD [ ] LTCUSD [ ] XRPUSD [ ] BCHUSD | ++----------------------------------------------------------+---------------------------------+ +| | ORDER BOOK (UC5) | +| MARKET CANDLES CHART (UC2) | Pair: BTCUSD | +| +---------------------------------+ +| Price ($) | ASKS (Sell Orders) | +| ^ | Price | Quantity | Total | +| | _ _ | --------- | -------- | -------- | +| | | || | _ | 67,200.50 | 0.4500 | 30,240.2 | +| | _ | || | | | | 67,150.00 | 1.2000 | 80,580.0 | +| | | || | | _ | | | 67,100.25 | 0.0550 | 3,690.5 | +| | |__| |__|__| | --------- | -------- | -------- | +| +---------------------------------------> Time | SPREAD: $50.25 | +| 12:00 12:15 12:30 12:45 13:00 | --------- | -------- | -------- | +| | BIDS (Buy Orders) | +| Volume | 67,050.00 | 0.8500 | 56,992.5 | +| | █ █ █ | 67,000.00 | 2.1000 | 140,700 | +| +───────────────────────────────────────> | 66,950.75 | 0.1500 | 10,042.6 | ++----------------------------------------------------------+---------------------------------+ +| ORDER ENTRY FORM (UC3 / UC4) | +| | +| [ BUY ] (UC3) [ SELL ] (UC4) Wallet Balance: | +| Available USD: $500,000.00 | +| Order Type: Limit Order Available BTC: 5.50000000 | +| | +| Price (USD): [ 67050.00 ] Estimated Total: | +| Quantity: [ 0.50 ] $33,525.00 USD | +| | +| [ PLACE LIMIT ORDER ] Status: [ Ready ] | ++--------------------------------------------------------------------------------------------+ + +Component Specification for Documentation: + +Top Bar (Navigation & Asset Selector): + Associated Use Case: UC1: View Trading Symbols + Source Code Reference / Connection: Handled by MarketController.getSymbols(). Displays the available pairs hardcoded in your system. +Center Left (Interactive Candlestick Chart): + Associated Use Case: UC2: View Market Candles + Source Code Reference / Connection: Fetches rows chronologically sorted via MarketController.getRecentCandles(). Backed by the MongoDB market_candles collection. +Center Right (Order Book Split Grid): + Associated Use Case: UC5: View Order Book + Source Code Reference / Connection: Powered by OrderController.getOrderBook(). Reads the Ask (SELL) and Bid (BUY) Redis Sorted Sets (ZSET) in real-time. +Bottom (Order Ticket Form): + Associated Use Case: UC3 / UC4: Place Buy/Sell Order + Source Code Reference / Connection: A form that fires a POST request to OrderController.placeOrder(). Transmits X-User-ID via header to evaluate transaction logic in TradingService. diff --git a/frontend/mockups/UI Mockup 2 Trader Portfolio and Analytics Layout b/frontend/mockups/UI Mockup 2 Trader Portfolio and Analytics Layout new file mode 100644 index 0000000..d69e654 --- /dev/null +++ b/frontend/mockups/UI Mockup 2 Trader Portfolio and Analytics Layout @@ -0,0 +1,39 @@ ++--------------------------------------------------------------------------------------------+ +| CryptoFlash | [Dashboard] *[Portfolio & History]* User: SIM_USER_001 [Logout] | ++--------------------------------------------------------------------------------------------+ +| USER PROFILE & WALLET BALANCES (UC6) | +| User ID: SIM_USER_001 | Account Status: ACTIVE | +| | +| Asset | Total Balance | Available Balance | Locked in Orders | Est. Value (USD) | +| ----- | ------------- | ----------------- | ---------------- | ---------------- | +| USD | $500,000.00 | $466,475.00 | $33,525.00 | $500,000.00 | +| BTC | 5.50000000 | 5.50000000 | 0.00000000 | $368,775.00 | +| ETH | 10.00000000 | 10.00000000 | 0.00000000 | $32,400.00 | ++--------------------------------------------------------------------------------------------+ +| PERSONAL P&L ANALYTICS (UC8) | +| | +| Net Profit / Loss: +$14,250.75 USD [▲ 3.85%] | +| | +| P&L Realized: $18,100.00 USD Total Fees Paid: $120.50 USD | +| P&L Unrealized: -$3,849.25 USD Total Trades: 42 | ++--------------------------------------------------------------------------------------------+ +| PERSONAL TRADE HISTORY LOG (UC7) | +| | +| Timestamp | Pair | Side | Price (USD) | Quantity | Total (USD) | Status | +| ------------------- | ------ | ---- | ----------- | -------- | ------------ | ------------ | +| 2026-06-05 14:32:10 | BTCUSD | BUY | 67,050.00 | 0.5000 | $33,525.00 | SETTLED | +| 2026-06-05 11:15:04 | ETHUSD | SELL | 3,240.00 | 2.0000 | $6,480.00 | SETTLED | +| 2026-06-04 18:22:51 | BTCUSD | BUY | 66,900.00 | 0.1500 | $10,035.00 | SETTLED | ++--------------------------------------------------------------------------------------------+ + +Component Specification for Documentation: + +User Profile & Wallet Balances Grid: + Associated Use Case: UC6: View User Profile + Source Code Reference / Connection: Fetches real-time portfolio metrics from UserController.getProfile(). Displays the fields mapped to the Wallet model instance belonging to the specific logged-in user. +Personal P&L Analytics Panel: + Associated Use Case: UC8: View P&L Analytics + Source Code Reference / Connection: Derived from metrics computed via AnalyticsService. It cross-references current real-time market prices with the user's average asset entry costs to derive realized and unrealized performance. +Personal Trade History Log Table: + Associated Use Case: UC7: View Trade History + Source Code Reference / Connection: Populated by calling OrderController.getUserTrades(). This queries the MongoDB trade_history collection, filtering exclusively by the user's userId to list past executions chronologically. diff --git a/frontend/mockups/UI Mockup 3 Admin Simulation Dashboard Layout b/frontend/mockups/UI Mockup 3 Admin Simulation Dashboard Layout new file mode 100644 index 0000000..dd98637 --- /dev/null +++ b/frontend/mockups/UI Mockup 3 Admin Simulation Dashboard Layout @@ -0,0 +1,36 @@ ++--------------------------------------------------------------------------------------------+ +| CryptoFlash Admin | *[Simulation Panel]* [Infra & Users] Role: SYSTEM_ADMIN | ++--------------------------------------------------------------------------------------------+ +| SIMULATION ENGINE CONTROL (UC15) | +| | +| Current Replay Status: [ RUNNING ] Simulation Speed: [ 5x (1 candle / min) ]| +| Target Asset Pair: [ BTCUSD ] Active Session ID: REPLAY_2026_06_05 | +| | +| [ START REPLAY ] [|| PAUSE REPLAY ] [■ STOP / RESET ] [ Configure Data Range... ]| ++--------------------------------------------------------------------------------------------+ +| GLOBAL MARKET METRICS (UC11 / UC12) | +| | +| Global 24h Volume: $12,450,800.00 USD Total Active Orders: 1,420 | +| Current Market VWAP: $67,025.40 USD Total System Matches: 842 trades | ++--------------------------------------------------------------------------------------------+ +| COMPETITIVE LEADERBOARD & WHALE REPORT (UC13 / UC14) | +| | +| [▲ GLOBAL LEADERBOARD] (UC14) [ WHALE REPORT - TOP VOLUME] (UC13) | +| Rank | User ID | Total P&L | Return % | Rank | User ID | 24h Vol | Trades| +| ---- | ------------ | ----------- | -------- | ---- | ------------ | --------- | ------| +| 1st | SIM_USER_042 | +$45,210.00 | +9.04% | 1st | SIM_USER_011 | $850,200 | 114 | +| 2nd | SIM_USER_001 | +$14,250.75 | +2.85% | 2nd | SIM_USER_087 | $620,450 | 92 | +| 3rd | SIM_USER_015 | +$8,900.20 | +1.78% | 3rd | SIM_USER_001 | $540,110 | 42 | ++--------------------------------------------------------------------------------------------+ + +Component Specification for Documentation: + +Simulation Engine Control Panel: + Associated Use Case: UC15: Trigger Market Replay Session + Source Code Reference / Connection: Interacts with the backend via AdminController.triggerReplay() or a similar administrative configuration route. This signals the internal scheduled runner to start drawing historical intervals and generating corresponding execution events. +Global Market Metrics Grid: + Associated Use Case: UC11: View Global Trading Volume (24h) & UC12: View VWAP Analytics + Source Code Reference / Connection: Connected to the system's analytical aggregation layer. Volume calculations and the Volume-Weighted Average Price (VWAP) formula are derived globally by monitoring current transaction events processed in memory. +Leaderboard & Whale Analysis Split Tables: + Associated Use Case: UC13: View Top Traders Report (Whales) & UC14: View Global Leaderboard + Source Code Reference / Connection: Powered by dedicated leaderboards endpoints. The global leaderboard ranks active participants by net balance/ROI updates, while the Whale Report sorts profiles by aggregate trade volume processed over a rolling 24-hour window. diff --git a/frontend/mockups/UI Mockup 4 Admin Infrastructure & Users Panel Layout b/frontend/mockups/UI Mockup 4 Admin Infrastructure & Users Panel Layout new file mode 100644 index 0000000..828b6f8 --- /dev/null +++ b/frontend/mockups/UI Mockup 4 Admin Infrastructure & Users Panel Layout @@ -0,0 +1,32 @@ ++---------------------------------------------------------------------------------------------+ +| CryptoFlash Admin | [Simulation Panel] *[Infra & Users]* Role: SYSTEM_ADMIN | ++---------------------------------------------------------------------------------------------+ +| INFRASTRUCTURE & SYSTEM HEALTH STATUS (UC9) | +| | +| System Status: [ OPTIMAL ] Up-time: 14 days, 06 hours, 22 min | +| | +| Service Name | Connection Status | Latency / Response Time | Storage Util. | +| ------------------------- | ------------------- | ----------------------- | -------------- | +| Spring Boot Core API | [ ONLINE ] | 4 ms | -- | +| Redis Hot Store (In-Mem) | [ ONLINE ] | 0.8 ms | 142 MB Used | +| MongoDB Cold Store (Disk) | [ ONLINE ] | 12 ms | 2.4 GB Used | ++---------------------------------------------------------------------------------------------+ +| USER REGISTRATION INDEX (UC10) | +| | +| Total Registered Profiles: 102 Active Simulated Sessions: 100 | +| | +| User ID | Full Name | Registered Timestamp | Base Currency | Acc. Status | +| ------------ | ------------------- | -------------------- | ------------- | ------------- | +| SIM_USER_001 | Trader Profile 001 | 2026-06-01 09:00:15 | USD | ACTIVE | +| SIM_USER_002 | Trader Profile 002 | 2026-06-01 09:02:44 | USD | ACTIVE | +| SIM_USER_100 | Trader Profile 100 | 2026-06-02 14:11:03 | USD | ACTIVE | ++---------------------------------------------------------------------------------------------+ + +Component Specification for Documentation: + +Infrastructure & System Health Status Grid: + Associated Use Case: UC9: Monitor System Health + Source Code Reference / Connection: Displays health checks and connection state monitoring. It reflects database ping confirmations and connectivity heartbeats to confirm operational integrity for both the Redis in-memory tables and MongoDB persistent records. +User Registration Index Table: + Associated Use Case: UC10: List All Registered Users + Source Code Reference / Connection: Connected to the user management endpoint. This table executes an administrative fetch query to compile, count, and list every active profile block stored inside the core user registry.