diff --git a/package-lock.json b/package-lock.json index 720e01f..020a0bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "globals": "^15.14.0", "husky": "^9.1.7", "jest": "^29.7.0", + "jest-mock-extended": "^4.0.0", "lint-staged": "^15.4.3", "prettier": "^3.4.2", "prisma": "^6.4.1", @@ -8494,6 +8495,21 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-mock-extended": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-4.0.0.tgz", + "integrity": "sha512-7BZpfuvLam+/HC+NxifIi9b+5VXj/utUDMPUqrDJehGWVuXPtLS9Jqlob2mJLrI/pg2k1S8DMfKDvEB88QNjaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-essentials": "^10.0.2" + }, + "peerDependencies": { + "@jest/globals": "^28.0.0 || ^29.0.0 || ^30.0.0", + "jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0 || ^30.0.0", + "typescript": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -12441,6 +12457,21 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-essentials": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-10.1.1.tgz", + "integrity": "sha512-4aTB7KLHKmUvkjNj8V+EdnmuVTiECzn3K+zIbRthumvHu+j44x3w63xpfs0JL3NGIzGXqoQ7AV591xHO+XrOTw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ts-jest": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.1.tgz", diff --git a/package.json b/package.json index fa03156..750ca58 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "globals": "^15.14.0", "husky": "^9.1.7", "jest": "^29.7.0", + "jest-mock-extended": "^4.0.0", "lint-staged": "^15.4.3", "prettier": "^3.4.2", "prisma": "^6.4.1", diff --git a/scripts/test-logging.ts b/scripts/test-logging.ts index a0d41af..acdd9cf 100644 --- a/scripts/test-logging.ts +++ b/scripts/test-logging.ts @@ -1,15 +1,18 @@ #!/usr/bin/env ts-node import "dotenv/config"; -import express from 'express'; -import { traceIdMiddleware } from '../src/middlewares/traceId.middleware'; -import { requestLoggerMiddleware } from '../src/middlewares/requestLogger.middleware'; -import { globalLogger, createLogger } from '../src/services/logger.service'; -import fs from 'fs'; -import path from 'path'; +import express from "express"; +import { traceIdMiddleware } from "../src/middlewares/traceId.middleware"; +import { requestLoggerMiddleware } from "../src/middlewares/requestLogger.middleware"; +import { + globalLogger, + createLogger, +} from "../src/modules/shared/application/services/LoggerService"; +import fs from "fs"; +import path from "path"; // Ensure logs directory exists -const logsDir = path.join(process.cwd(), 'logs'); +const logsDir = path.join(process.cwd(), "logs"); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir, { recursive: true }); } @@ -18,7 +21,7 @@ const app = express(); const PORT = 3001; // Use different port to avoid conflicts // Create a logger for this test -const testLogger = createLogger('LOGGING_TEST'); +const testLogger = createLogger("LOGGING_TEST"); // Middleware setup app.use(express.json()); @@ -26,59 +29,63 @@ app.use(traceIdMiddleware); app.use(requestLoggerMiddleware); // Test routes -app.get('/', (req, res) => { - testLogger.info('Health check endpoint accessed', req); - res.json({ - message: 'Winston logging test server is running!', +app.get("/", (req, res) => { + testLogger.info("Health check endpoint accessed", req); + res.json({ + message: "Winston logging test server is running!", traceId: req.traceId, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), }); }); -app.post('/test-log', (req, res) => { - testLogger.info('Test log endpoint accessed', req, { +app.post("/test-log", (req, res) => { + testLogger.info("Test log endpoint accessed", req, { requestBody: req.body, - customData: 'This is a test log entry' + customData: "This is a test log entry", }); - - res.json({ - message: 'Log entry created successfully', + + res.json({ + message: "Log entry created successfully", traceId: req.traceId, - loggedData: req.body + loggedData: req.body, }); }); -app.get('/test-error', (req, res) => { - const testError = new Error('This is a test error for logging'); - testLogger.error('Test error occurred', testError, req, { - errorContext: 'Intentional test error', - additionalInfo: 'This error was generated for testing purposes' +app.get("/test-error", (req, res) => { + const testError = new Error("This is a test error for logging"); + testLogger.error("Test error occurred", testError, req, { + errorContext: "Intentional test error", + additionalInfo: "This error was generated for testing purposes", }); - - res.status(500).json({ - error: 'Test error generated', + + res.status(500).json({ + error: "Test error generated", traceId: req.traceId, - message: 'Check the logs for error details' + message: "Check the logs for error details", }); }); -app.post('/test-sensitive-data', (req, res) => { +app.post("/test-sensitive-data", (req, res) => { // This will test our data sanitization - testLogger.info('Testing sensitive data sanitization', req); - - res.json({ - message: 'Sensitive data logged (check logs for sanitization)', - traceId: req.traceId + testLogger.info("Testing sensitive data sanitization", req); + + res.json({ + message: "Sensitive data logged (check logs for sanitization)", + traceId: req.traceId, }); }); // Start the test server app.listen(PORT, () => { - globalLogger.info(`Winston logging test server started on port ${PORT}`, undefined, { - environment: process.env.NODE_ENV || 'development', - testMode: true - }); - + globalLogger.info( + `Winston logging test server started on port ${PORT}`, + undefined, + { + environment: process.env.NODE_ENV || "development", + testMode: true, + } + ); + console.log(`\nšŸš€ Winston Logging Test Server Started!`); console.log(`šŸ“ Server running at: http://localhost:${PORT}`); console.log(`šŸ“ Logs directory: ${logsDir}`); @@ -89,7 +96,9 @@ app.listen(PORT, () => { console.log(` POST /test-sensitive-data - Test data sanitization`); console.log(`\nšŸ“Š Example requests:`); console.log(` curl http://localhost:${PORT}/`); - console.log(` curl -X POST http://localhost:${PORT}/test-log -H "Content-Type: application/json" -d '{"username":"test","password":"secret123","data":"normal"}'`); + console.log( + ` curl -X POST http://localhost:${PORT}/test-log -H "Content-Type: application/json" -d '{"username":"test","password":"secret123","data":"normal"}'` + ); console.log(` curl http://localhost:${PORT}/test-error`); console.log(`\nšŸ“ Check the logs directory for output files:`); console.log(` - logs/combined.log (all logs)`); @@ -98,8 +107,8 @@ app.listen(PORT, () => { }); // Graceful shutdown -process.on('SIGINT', () => { - globalLogger.info('Shutting down Winston logging test server'); - console.log('\nšŸ‘‹ Winston logging test server stopped'); +process.on("SIGINT", () => { + globalLogger.info("Shutting down Winston logging test server"); + console.log("\nšŸ‘‹ Winston logging test server stopped"); process.exit(0); }); diff --git a/scripts/validate-logging.ts b/scripts/validate-logging.ts index 7562923..d211c93 100644 --- a/scripts/validate-logging.ts +++ b/scripts/validate-logging.ts @@ -1,10 +1,13 @@ #!/usr/bin/env ts-node import "dotenv/config"; -import fs from 'fs'; -import path from 'path'; -import { createLogger, globalLogger } from '../src/services/logger.service'; -import { v4 as uuidv4 } from 'uuid'; +import fs from "fs"; +import path from "path"; +import { + createLogger, + globalLogger, +} from "../src/modules/shared/application/services/LoggerService"; +import { v4 as uuidv4 } from "uuid"; /** * Validation script for Winston logging implementation @@ -13,26 +16,30 @@ import { v4 as uuidv4 } from 'uuid'; interface ValidationResult { metric: string; - status: 'PASS' | 'FAIL'; + status: "PASS" | "FAIL"; details: string; } class LoggingValidator { private results: ValidationResult[] = []; - private testLogger = createLogger('VALIDATION_TEST'); + private testLogger = createLogger("VALIDATION_TEST"); constructor() { this.ensureLogsDirectory(); } private ensureLogsDirectory(): void { - const logsDir = path.join(process.cwd(), 'logs'); + const logsDir = path.join(process.cwd(), "logs"); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir, { recursive: true }); } } - private addResult(metric: string, status: 'PASS' | 'FAIL', details: string): void { + private addResult( + metric: string, + status: "PASS" | "FAIL", + details: string + ): void { this.results.push({ metric, status, details }); } @@ -43,35 +50,62 @@ class LoggingValidator { try { const mockReq = { traceId: uuidv4(), - method: 'GET', - url: '/test', - ip: '127.0.0.1', - get: () => 'test-agent' + method: "GET", + url: "/test", + ip: "127.0.0.1", + get: () => "test-agent", } as any; - this.testLogger.info('Test structured log', mockReq, { testData: 'validation' }); + this.testLogger.info("Test structured log", mockReq, { + testData: "validation", + }); // Check if log file contains JSON - const logFile = path.join(process.cwd(), 'logs', 'combined.log'); + const logFile = path.join(process.cwd(), "logs", "combined.log"); if (fs.existsSync(logFile)) { - const logContent = fs.readFileSync(logFile, 'utf8'); - const lastLine = logContent.trim().split('\n').pop(); - + const logContent = fs.readFileSync(logFile, "utf8"); + const lastLine = logContent.trim().split("\n").pop(); + if (lastLine) { const logEntry = JSON.parse(lastLine); - if (logEntry.timestamp && logEntry.level && logEntry.message && logEntry.traceId) { - this.addResult('Structured JSON Logging', 'PASS', 'Logs are properly formatted as JSON with required fields'); + if ( + logEntry.timestamp && + logEntry.level && + logEntry.message && + logEntry.traceId + ) { + this.addResult( + "Structured JSON Logging", + "PASS", + "Logs are properly formatted as JSON with required fields" + ); } else { - this.addResult('Structured JSON Logging', 'FAIL', 'Missing required fields in log entry'); + this.addResult( + "Structured JSON Logging", + "FAIL", + "Missing required fields in log entry" + ); } } else { - this.addResult('Structured JSON Logging', 'FAIL', 'No log entries found'); + this.addResult( + "Structured JSON Logging", + "FAIL", + "No log entries found" + ); } } else { - this.addResult('Structured JSON Logging', 'FAIL', 'Log file not created'); + this.addResult( + "Structured JSON Logging", + "FAIL", + "Log file not created" + ); } } catch (error) { - this.addResult('Structured JSON Logging', 'FAIL', `Error: ${error instanceof Error ? error.message : String(error)}`); + this.addResult( + "Structured JSON Logging", + "FAIL", + `Error: ${error instanceof Error ? error.message : String(error)}` + ); } } @@ -83,34 +117,55 @@ class LoggingValidator { const traceId = uuidv4(); const mockReq = { traceId, - method: 'POST', - url: '/test-trace', - ip: '127.0.0.1', - get: () => 'test-agent' + method: "POST", + url: "/test-trace", + ip: "127.0.0.1", + get: () => "test-agent", } as any; - this.testLogger.info('Test trace ID logging', mockReq); + this.testLogger.info("Test trace ID logging", mockReq); // Validate UUID v4 format - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (uuidRegex.test(traceId)) { - this.addResult('Trace ID Generation', 'PASS', 'Valid UUID v4 trace IDs generated'); + this.addResult( + "Trace ID Generation", + "PASS", + "Valid UUID v4 trace IDs generated" + ); } else { - this.addResult('Trace ID Generation', 'FAIL', 'Invalid trace ID format'); + this.addResult( + "Trace ID Generation", + "FAIL", + "Invalid trace ID format" + ); } // Check if trace ID appears in logs - const logFile = path.join(process.cwd(), 'logs', 'combined.log'); + const logFile = path.join(process.cwd(), "logs", "combined.log"); if (fs.existsSync(logFile)) { - const logContent = fs.readFileSync(logFile, 'utf8'); + const logContent = fs.readFileSync(logFile, "utf8"); if (logContent.includes(traceId)) { - this.addResult('Trace ID Correlation', 'PASS', 'Trace IDs properly included in log entries'); + this.addResult( + "Trace ID Correlation", + "PASS", + "Trace IDs properly included in log entries" + ); } else { - this.addResult('Trace ID Correlation', 'FAIL', 'Trace ID not found in log entries'); + this.addResult( + "Trace ID Correlation", + "FAIL", + "Trace ID not found in log entries" + ); } } } catch (error) { - this.addResult('Trace ID Implementation', 'FAIL', `Error: ${error instanceof Error ? error.message : String(error)}`); + this.addResult( + "Trace ID Implementation", + "FAIL", + `Error: ${error instanceof Error ? error.message : String(error)}` + ); } } @@ -121,26 +176,27 @@ class LoggingValidator { try { const mockReq = { traceId: uuidv4(), - method: 'PUT', - url: '/test-context', - ip: '192.168.1.1', - get: (header: string) => header === 'User-Agent' ? 'validation-agent' : undefined + method: "PUT", + url: "/test-context", + ip: "192.168.1.1", + get: (header: string) => + header === "User-Agent" ? "validation-agent" : undefined, } as any; - this.testLogger.info('Test context validation', mockReq, { - customContext: 'test', - userId: '12345' + this.testLogger.info("Test context validation", mockReq, { + customContext: "test", + userId: "12345", }); - const logFile = path.join(process.cwd(), 'logs', 'combined.log'); + const logFile = path.join(process.cwd(), "logs", "combined.log"); if (fs.existsSync(logFile)) { - const logContent = fs.readFileSync(logFile, 'utf8'); - const lastLine = logContent.trim().split('\n').pop(); - + const logContent = fs.readFileSync(logFile, "utf8"); + const lastLine = logContent.trim().split("\n").pop(); + if (lastLine) { const logEntry = JSON.parse(lastLine); - - const hasRequiredContext = + + const hasRequiredContext = logEntry.timestamp && logEntry.traceId && logEntry.context && @@ -149,14 +205,26 @@ class LoggingValidator { logEntry.ip; if (hasRequiredContext) { - this.addResult('Context Information', 'PASS', 'All required context fields present'); + this.addResult( + "Context Information", + "PASS", + "All required context fields present" + ); } else { - this.addResult('Context Information', 'FAIL', 'Missing required context fields'); + this.addResult( + "Context Information", + "FAIL", + "Missing required context fields" + ); } } } } catch (error) { - this.addResult('Context Information', 'FAIL', `Error: ${error instanceof Error ? error.message : String(error)}`); + this.addResult( + "Context Information", + "FAIL", + `Error: ${error instanceof Error ? error.message : String(error)}` + ); } } @@ -165,10 +233,7 @@ class LoggingValidator { */ async validatePersistence(): Promise { try { - const logFiles = [ - 'logs/combined.log', - 'logs/error.log' - ]; + const logFiles = ["logs/combined.log", "logs/error.log"]; let allFilesExist = true; const missingFiles: string[] = []; @@ -182,30 +247,49 @@ class LoggingValidator { } if (allFilesExist) { - this.addResult('Log Persistence', 'PASS', 'All log files created and persistent'); + this.addResult( + "Log Persistence", + "PASS", + "All log files created and persistent" + ); } else { - this.addResult('Log Persistence', 'FAIL', `Missing log files: ${missingFiles.join(', ')}`); + this.addResult( + "Log Persistence", + "FAIL", + `Missing log files: ${missingFiles.join(", ")}` + ); } // Test error logging persistence - const testError = new Error('Validation test error'); - this.testLogger.error('Test error persistence', testError); + const testError = new Error("Validation test error"); + this.testLogger.error("Test error persistence", testError); // Check if error appears in error.log setTimeout(() => { - const errorLogPath = path.join(process.cwd(), 'logs', 'error.log'); + const errorLogPath = path.join(process.cwd(), "logs", "error.log"); if (fs.existsSync(errorLogPath)) { - const errorContent = fs.readFileSync(errorLogPath, 'utf8'); - if (errorContent.includes('Validation test error')) { - this.addResult('Error Log Persistence', 'PASS', 'Errors properly logged to error.log'); + const errorContent = fs.readFileSync(errorLogPath, "utf8"); + if (errorContent.includes("Validation test error")) { + this.addResult( + "Error Log Persistence", + "PASS", + "Errors properly logged to error.log" + ); } else { - this.addResult('Error Log Persistence', 'FAIL', 'Error not found in error.log'); + this.addResult( + "Error Log Persistence", + "FAIL", + "Error not found in error.log" + ); } } }, 100); - } catch (error) { - this.addResult('Log Persistence', 'FAIL', `Error: ${error instanceof Error ? error.message : String(error)}`); + this.addResult( + "Log Persistence", + "FAIL", + `Error: ${error instanceof Error ? error.message : String(error)}` + ); } } @@ -216,42 +300,54 @@ class LoggingValidator { try { const mockReq = { traceId: uuidv4(), - method: 'POST', - url: '/test-sanitization', - ip: '127.0.0.1', - get: () => 'test-agent', + method: "POST", + url: "/test-sanitization", + ip: "127.0.0.1", + get: () => "test-agent", body: { - username: 'testuser', - password: 'supersecret123', - token: 'jwt-token-value', - secret: 'api-secret', - normalField: 'normal-value' - } + username: "testuser", + password: "supersecret123", + token: "jwt-token-value", + secret: "api-secret", + normalField: "normal-value", + }, } as any; this.testLogger.logRequest(mockReq); - const logFile = path.join(process.cwd(), 'logs', 'combined.log'); + const logFile = path.join(process.cwd(), "logs", "combined.log"); if (fs.existsSync(logFile)) { - const logContent = fs.readFileSync(logFile, 'utf8'); - + const logContent = fs.readFileSync(logFile, "utf8"); + // Check that sensitive data is redacted - const hasSensitiveData = - logContent.includes('supersecret123') || - logContent.includes('jwt-token-value') || - logContent.includes('api-secret'); + const hasSensitiveData = + logContent.includes("supersecret123") || + logContent.includes("jwt-token-value") || + logContent.includes("api-secret"); - const hasRedactedData = logContent.includes('[REDACTED]'); - const hasNormalData = logContent.includes('normal-value'); + const hasRedactedData = logContent.includes("[REDACTED]"); + const hasNormalData = logContent.includes("normal-value"); if (!hasSensitiveData && hasRedactedData && hasNormalData) { - this.addResult('Data Sanitization', 'PASS', 'Sensitive data properly sanitized'); + this.addResult( + "Data Sanitization", + "PASS", + "Sensitive data properly sanitized" + ); } else { - this.addResult('Data Sanitization', 'FAIL', 'Sensitive data not properly sanitized'); + this.addResult( + "Data Sanitization", + "FAIL", + "Sensitive data not properly sanitized" + ); } } } catch (error) { - this.addResult('Data Sanitization', 'FAIL', `Error: ${error instanceof Error ? error.message : String(error)}`); + this.addResult( + "Data Sanitization", + "FAIL", + `Error: ${error instanceof Error ? error.message : String(error)}` + ); } } @@ -267,10 +363,10 @@ class LoggingValidator { for (let i = 0; i < iterations; i++) { const mockReq = { traceId: uuidv4(), - method: 'GET', + method: "GET", url: `/test-performance-${i}`, - ip: '127.0.0.1', - get: () => 'perf-test' + ip: "127.0.0.1", + get: () => "perf-test", } as any; this.testLogger.info(`Performance test ${i}`, mockReq); @@ -282,12 +378,24 @@ class LoggingValidator { // Consider performance acceptable if average log time < 1ms if (avgTime < 1) { - this.addResult('Performance', 'PASS', `Average log time: ${avgTime.toFixed(3)}ms per operation`); + this.addResult( + "Performance", + "PASS", + `Average log time: ${avgTime.toFixed(3)}ms per operation` + ); } else { - this.addResult('Performance', 'FAIL', `Performance degradation detected: ${avgTime.toFixed(3)}ms per operation`); + this.addResult( + "Performance", + "FAIL", + `Performance degradation detected: ${avgTime.toFixed(3)}ms per operation` + ); } } catch (error) { - this.addResult('Performance', 'FAIL', `Error: ${error instanceof Error ? error.message : String(error)}`); + this.addResult( + "Performance", + "FAIL", + `Error: ${error instanceof Error ? error.message : String(error)}` + ); } } @@ -295,7 +403,7 @@ class LoggingValidator { * Run all validations */ async runValidation(): Promise { - console.log('šŸ” Starting Winston Logging Validation...\n'); + console.log("šŸ” Starting Winston Logging Validation...\n"); await this.validateStructuredLogging(); await this.validateTraceId(); @@ -305,7 +413,7 @@ class LoggingValidator { await this.validatePerformance(); // Wait a bit for async operations - await new Promise(resolve => setTimeout(resolve, 200)); + await new Promise((resolve) => setTimeout(resolve, 200)); this.printResults(); } @@ -314,38 +422,42 @@ class LoggingValidator { * Print validation results */ private printResults(): void { - console.log('šŸ“Š Validation Results:\n'); + console.log("šŸ“Š Validation Results:\n"); let passCount = 0; let failCount = 0; - this.results.forEach(result => { - const icon = result.status === 'PASS' ? 'āœ…' : 'āŒ'; - const status = result.status === 'PASS' ? 'PASS' : 'FAIL'; - + this.results.forEach((result) => { + const icon = result.status === "PASS" ? "āœ…" : "āŒ"; + const status = result.status === "PASS" ? "PASS" : "FAIL"; + console.log(`${icon} ${result.metric}: ${status}`); console.log(` ${result.details}\n`); - if (result.status === 'PASS') { + if (result.status === "PASS") { passCount++; } else { failCount++; } }); - console.log('šŸ“ˆ Summary:'); + console.log("šŸ“ˆ Summary:"); console.log(` āœ… Passed: ${passCount}`); console.log(` āŒ Failed: ${failCount}`); console.log(` šŸ“Š Total: ${this.results.length}`); if (failCount === 0) { - console.log('\nšŸŽ‰ All validation tests passed! Winston logging implementation is successful.'); + console.log( + "\nšŸŽ‰ All validation tests passed! Winston logging implementation is successful." + ); } else { - console.log('\nāš ļø Some validation tests failed. Please review the implementation.'); + console.log( + "\nāš ļø Some validation tests failed. Please review the implementation." + ); } - console.log('\nšŸ“ Log files location: ./logs/'); - console.log('šŸ“ Check combined.log and error.log for detailed output'); + console.log("\nšŸ“ Log files location: ./logs/"); + console.log("šŸ“ Check combined.log and error.log for detailed output"); } } diff --git a/src/examples/sorobanExample.ts b/src/examples/sorobanExample.ts index 494e80a..00c3382 100644 --- a/src/examples/sorobanExample.ts +++ b/src/examples/sorobanExample.ts @@ -1,4 +1,4 @@ -import { sorobanService } from '../services/sorobanService'; +import { sorobanService } from "../modules/soroban/application/services/SorobanService"; /** * Example of how to use the SorobanService in a real application @@ -6,48 +6,45 @@ import { sorobanService } from '../services/sorobanService'; async function sorobanExample() { try { // Example 1: Submit a transaction - const transactionXDR = 'AAAA...'; // Your XDR-encoded transaction - const transactionHash = await sorobanService.submitTransaction(transactionXDR); - console.log('Transaction submitted successfully:', transactionHash); - + const transactionXDR = "AAAA..."; // Your XDR-encoded transaction + const transactionHash = + await sorobanService.submitTransaction(transactionXDR); + console.log("Transaction submitted successfully:", transactionHash); + // Example 2: Invoke a contract method (e.g., minting an NFT) - const contractId = 'your-contract-id'; - const methodName = 'mint_nft'; - const args = [ - 'user-wallet-address', - 'metadata-uri' - ]; - + const contractId = "your-contract-id"; + const methodName = "mint_nft"; + const args = ["user-wallet-address", "metadata-uri"]; + const result = await sorobanService.invokeContractMethod( contractId, methodName, args ); - - console.log('Contract method invoked successfully:', result); - + + console.log("Contract method invoked successfully:", result); + // Example 3: Invoke a contract method for budget management - const budgetContractId = 'your-budget-contract-id'; - const budgetMethodName = 'allocate_funds'; + const budgetContractId = "your-budget-contract-id"; + const budgetMethodName = "allocate_funds"; const budgetArgs = [ - 'project-id', - 1000 // amount in stroops + "project-id", + 1000, // amount in stroops ]; - + const budgetResult = await sorobanService.invokeContractMethod( budgetContractId, budgetMethodName, budgetArgs ); - - console.log('Budget allocation successful:', budgetResult); - + + console.log("Budget allocation successful:", budgetResult); } catch (error) { - console.error('Error in Soroban operations:', error); + console.error("Error in Soroban operations:", error); } } // Uncomment to run the example // sorobanExample(); -export { sorobanExample }; \ No newline at end of file +export { sorobanExample }; diff --git a/src/index.ts b/src/index.ts index ed8745f..e56d0f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,9 +20,9 @@ import certificateRoutes from "./routes/certificatesRoutes"; import volunteerRoutes from "./routes/VolunteerRoutes"; import projectRoutes from "./routes/ProjectRoutes"; import organizationRoutes from "./routes/OrganizationRoutes"; -import messageRoutes from './modules/messaging/routes/messaging.routes'; +import messageRoutes from "./modules/messaging/routes/messaging.routes"; import testRoutes from "./routes/testRoutes"; -import { globalLogger } from "./services/logger.service"; +import { globalLogger } from "./modules/shared/application/services/LoggerService"; import fs from "fs"; import path from "path"; @@ -31,7 +31,7 @@ const PORT = process.env.PORT || 3000; const ENV = process.env.NODE_ENV || "development"; // Ensure logs directory exists -const logsDir = path.join(process.cwd(), 'logs'); +const logsDir = path.join(process.cwd(), "logs"); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir, { recursive: true }); } @@ -39,7 +39,7 @@ if (!fs.existsSync(logsDir)) { globalLogger.info("Starting VolunChain API...", undefined, { environment: ENV, port: PORT, - nodeVersion: process.version + nodeVersion: process.version, }); // Trace ID middleware (must be first to ensure all requests have trace IDs) @@ -184,10 +184,14 @@ prisma globalLogger.info("Cron jobs initialized successfully!"); app.listen(PORT, () => { - globalLogger.info(`Server is running on http://localhost:${PORT}`, undefined, { - port: PORT, - environment: ENV - }); + globalLogger.info( + `Server is running on http://localhost:${PORT}`, + undefined, + { + port: PORT, + environment: ENV, + } + ); if (ENV === "development") { globalLogger.info( @@ -219,4 +223,4 @@ const initializeRedis = async () => { } }; -export default app; \ No newline at end of file +export default app; diff --git a/src/middleware/rateLimitMiddleware.ts b/src/middleware/rateLimitMiddleware.ts index 1077b71..c3dad9c 100644 --- a/src/middleware/rateLimitMiddleware.ts +++ b/src/middleware/rateLimitMiddleware.ts @@ -1,6 +1,6 @@ import express, { Request, Response, NextFunction, Router } from "express"; import { RateLimitUseCase } from "./../modules/shared/middleware/rate-limit/use-cases/rate-limit-use-case"; -import { createLogger } from "../services/logger.service"; +import { createLogger } from "../modules/shared/application/services/LoggerService"; export class RateLimitMiddleware { private rateLimitUseCase: RateLimitUseCase; @@ -30,7 +30,7 @@ export class RateLimitMiddleware { remaining, retryAfter, ip: req.ip, - path: req.path + path: req.path, } ); return res.status(429).json({ @@ -38,7 +38,7 @@ export class RateLimitMiddleware { message: "You have exceeded the rate limit. Please try again later.", retryAfter: retryAfter * 60 + " seconds", // Default retry after 1 minute - ...(req.traceId && { traceId: req.traceId }) + ...(req.traceId && { traceId: req.traceId }), }); } diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index 7d88ffb..c7d8cd5 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -1,8 +1,8 @@ import { NextFunction, Request, Response } from "express"; import { CustomError, InternalServerError } from "../modules/shared/application/errors"; -import { createLogger } from "../services/logger.service"; +import { createLogger } from "../modules/shared/application/services/LoggerService"; -const logger = createLogger('ERROR_HANDLER'); +const logger = createLogger("ERROR_HANDLER"); interface ErrorLogInfo { timestamp: string; @@ -36,17 +36,12 @@ export const errorHandler = ( } // Log error with Winston including trace ID and context - logger.error( - 'Unhandled error occurred', - err, - req, - { - path: req.path, - method: req.method, - timestamp: new Date().toISOString(), - errorType: err.constructor.name - } - ); + logger.error("Unhandled error occurred", err, req, { + path: req.path, + method: req.method, + timestamp: new Date().toISOString(), + errorType: err.constructor.name, + }); // Handle different types of errors if (err instanceof CustomError) { @@ -54,7 +49,7 @@ export const errorHandler = ( code: err.code, message: err.message, ...(err.details && { details: err.details }), - ...(req.traceId && { traceId: req.traceId }) + ...(req.traceId && { traceId: req.traceId }), }); } @@ -65,6 +60,6 @@ export const errorHandler = ( return res.status(internalError.statusCode).json({ ...internalError.toJSON(), - ...(req.traceId && { traceId: req.traceId }) + ...(req.traceId && { traceId: req.traceId }), }); }; diff --git a/src/middlewares/requestLogger.middleware.ts b/src/middlewares/requestLogger.middleware.ts index d708934..a4c4d4b 100644 --- a/src/middlewares/requestLogger.middleware.ts +++ b/src/middlewares/requestLogger.middleware.ts @@ -1,32 +1,36 @@ -import { Request, Response, NextFunction } from 'express'; -import { createLogger } from '../services/logger.service'; +import { Request, Response, NextFunction } from "express"; +import { createLogger } from "../modules/shared/application/services/LoggerService"; -const logger = createLogger('REQUEST_LOGGER'); +const logger = createLogger("REQUEST_LOGGER"); /** * Middleware for logging HTTP requests and responses * Captures request details, response status, and response time */ -export const requestLoggerMiddleware = (req: Request, res: Response, next: NextFunction): void => { +export const requestLoggerMiddleware = ( + req: Request, + res: Response, + next: NextFunction +): void => { const startTime = Date.now(); // Log incoming request logger.logRequest(req, { timestamp: new Date().toISOString(), - requestId: req.traceId + requestId: req.traceId, }); // Override res.end to capture response details const originalEnd = res.end; - res.end = function(chunk?: any, encoding?: any, cb?: any): any { + res.end = function (chunk?: any, encoding?: any, cb?: any): any { const responseTime = Date.now() - startTime; // Log response logger.logResponse(req, res.statusCode, responseTime, { timestamp: new Date().toISOString(), requestId: req.traceId, - contentLength: res.get('Content-Length') + contentLength: res.get("Content-Length"), }); // Call original end method @@ -40,13 +44,17 @@ export const requestLoggerMiddleware = (req: Request, res: Response, next: NextF * Middleware for logging only errors (lighter version) * Use this for high-traffic endpoints where full request logging might be too verbose */ -export const errorOnlyLoggerMiddleware = (req: Request, res: Response, next: NextFunction): void => { +export const errorOnlyLoggerMiddleware = ( + req: Request, + res: Response, + next: NextFunction +): void => { const startTime = Date.now(); // Override res.end to capture only error responses const originalEnd = res.end; - res.end = function(chunk?: any, encoding?: any, cb?: any): any { + res.end = function (chunk?: any, encoding?: any, cb?: any): any { // Only log if status code indicates an error (4xx or 5xx) if (res.statusCode >= 400) { const responseTime = Date.now() - startTime; @@ -54,8 +62,8 @@ export const errorOnlyLoggerMiddleware = (req: Request, res: Response, next: Nex logger.logResponse(req, res.statusCode, responseTime, { timestamp: new Date().toISOString(), requestId: req.traceId, - contentLength: res.get('Content-Length'), - errorResponse: true + contentLength: res.get("Content-Length"), + errorResponse: true, }); } diff --git a/src/modules/auth/__tests__/services/AuthService.test.ts b/src/modules/auth/__tests__/services/AuthService.test.ts new file mode 100644 index 0000000..8de8ec7 --- /dev/null +++ b/src/modules/auth/__tests__/services/AuthService.test.ts @@ -0,0 +1,367 @@ +import { describe, expect, it, beforeEach, jest } from "@jest/globals"; +import AuthService from "../../application/services/AuthService"; +import { PrismaUserRepository } from "../../../user/repositories/PrismaUserRepository"; +import { SendVerificationEmailUseCase } from "../../use-cases/send-verification-email.usecase"; +import { VerifyEmailUseCase } from "../../use-cases/verify-email.usecase"; +import { ResendVerificationEmailUseCase } from "../../use-cases/resend-verification-email.usecase"; +import { WalletService } from "../../../wallet/services/WalletService"; + +// Mock dependencies +jest.mock("../../../user/repositories/PrismaUserRepository"); +jest.mock("../../use-cases/send-verification-email.usecase"); +jest.mock("../../use-cases/verify-email.usecase"); +jest.mock("../../use-cases/resend-verification-email.usecase"); +jest.mock("../../../wallet/services/WalletService"); +jest.mock("../../../../config/prisma", () => ({ + prisma: { + user: { + findUnique: jest.fn(), + }, + }, +})); + +// Declare mockPrisma at the top level +let mockPrisma: any; + +describe("AuthService", () => { + let authService: AuthService; + let mockUserRepository: jest.Mocked; + let mockSendVerificationEmailUseCase: jest.Mocked; + let mockVerifyEmailUseCase: jest.Mocked; + let mockResendVerificationEmailUseCase: jest.Mocked; + let mockWalletService: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + + // Set up prisma mock at the top level + mockPrisma = { + user: { + findUnique: jest.fn(), + }, + }; + jest.doMock("../../../../config/prisma", () => ({ + prisma: mockPrisma, + })); + + // Mock all required methods for PrismaUserRepository + mockUserRepository = { + findByEmail: jest.fn(), + create: jest.fn(), + isUserVerified: jest.fn(), + findById: jest.fn(), + update: jest.fn(), + findAll: jest.fn(), + delete: jest.fn(), + } as unknown as jest.Mocked; + + mockSendVerificationEmailUseCase = { + execute: jest.fn(), + userRepository: {} as any, + } as unknown as jest.Mocked; + + mockVerifyEmailUseCase = { + execute: jest.fn(), + userRepository: {} as any, + } as unknown as jest.Mocked; + + mockResendVerificationEmailUseCase = { + execute: jest.fn(), + userRepository: {} as any, + } as unknown as jest.Mocked; + + mockWalletService = { + isWalletValid: jest.fn(), + verifyWallet: jest.fn(), + validateWalletFormat: jest.fn(), + walletRepository: {} as any, + verifyWalletUseCase: {} as any, + validateWalletFormatUseCase: {} as any, + } as unknown as jest.Mocked; + + // Use the correct constructor for AuthService (update this if the signature is different) + // If AuthService expects no arguments, just use: authService = new AuthService(); + // Otherwise, pass the correct mocks as per the actual constructor + authService = new AuthService(); + }); + + describe("authenticate", () => { + it("should authenticate user with valid wallet address", async () => { + const walletAddress = "test-wallet-address"; + const mockUser = { id: "user-id", wallet: walletAddress }; + + mockWalletService.isWalletValid.mockResolvedValue(true); + mockPrisma.user.findUnique.mockResolvedValue(mockUser); + + const result = await authService.authenticate(walletAddress); + + expect(result).toBeDefined(); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); // Ensure token is not empty + expect(mockWalletService.isWalletValid).toHaveBeenCalledWith( + walletAddress + ); + expect(mockWalletService.isWalletValid).toHaveBeenCalledTimes(1); + expect(mockPrisma.user.findUnique).toHaveBeenCalledWith({ + where: { wallet: walletAddress }, + }); + expect(mockPrisma.user.findUnique).toHaveBeenCalledTimes(1); + }); + + it("should throw error for invalid wallet address", async () => { + const walletAddress = "invalid-wallet-address"; + + mockWalletService.isWalletValid.mockResolvedValue(false); + + await expect(authService.authenticate(walletAddress)).rejects.toThrow( + "Invalid wallet address" + ); + expect(mockWalletService.isWalletValid).toHaveBeenCalledWith( + walletAddress + ); + }); + + it("should throw error when user not found", async () => { + const walletAddress = "test-wallet-address"; + + mockWalletService.isWalletValid.mockResolvedValue(true); + mockPrisma.user.findUnique.mockResolvedValue(null); + + await expect(authService.authenticate(walletAddress)).rejects.toThrow( + "User not found" + ); + }); + }); + + describe("register", () => { + it("should register new user successfully", async () => { + const userData = { + name: "Test", + lastName: "User", + email: "test@example.com", + password: "password123", + wallet: "test-wallet-address", + }; + + const mockWalletVerification = { + success: true, + isValid: true, + accountExists: true, + walletAddress: userData.wallet, + verifiedAt: new Date(), + message: "Wallet verified", + }; + + const mockUser = { + id: "user-id", + name: userData.name, + email: userData.email, + wallet: userData.wallet, + }; + + mockWalletService.verifyWallet.mockResolvedValue(mockWalletVerification); + mockUserRepository.findByEmail.mockResolvedValue(null); + mockPrisma.user.findUnique.mockResolvedValue(null); + mockUserRepository.create.mockResolvedValue(mockUser); + mockSendVerificationEmailUseCase.execute.mockResolvedValue({ + success: true, + message: "Verification email sent", + }); + + const result = await authService.register( + userData.name, + userData.lastName, + userData.email, + userData.password, + userData.wallet + ); + + expect(result).toEqual({ + id: mockUser.id, + name: mockUser.name, + email: mockUser.email, + wallet: mockUser.wallet, + walletVerified: mockWalletVerification.accountExists, + message: + "User registered successfully. Please check your email to verify your account.", + }); + }); + + it("should throw error when wallet verification fails", async () => { + const userData = { + name: "Test", + lastName: "User", + email: "test@example.com", + password: "password123", + wallet: "invalid-wallet", + }; + + const mockWalletVerification = { + success: false, + isValid: false, + walletAddress: userData.wallet, + accountExists: false, + verifiedAt: null as any, + message: "Invalid wallet address", + }; + + mockWalletService.verifyWallet.mockResolvedValue(mockWalletVerification); + + await expect( + authService.register( + userData.name, + userData.lastName, + userData.email, + userData.password, + userData.wallet + ) + ).rejects.toThrow("Wallet verification failed: Invalid wallet address"); + }); + + it("should throw error when user with email already exists", async () => { + const userData = { + name: "Test", + lastName: "User", + email: "existing@example.com", + password: "password123", + wallet: "test-wallet-address", + }; + + const mockWalletVerification = { + success: true, + isValid: true, + accountExists: true, + walletAddress: userData.wallet, + verifiedAt: new Date(), + message: "Wallet verified", + }; + + mockWalletService.verifyWallet.mockResolvedValue(mockWalletVerification); + mockUserRepository.findByEmail.mockResolvedValue({ + id: "existing-user", + } as any); + + await expect( + authService.register( + userData.name, + userData.lastName, + userData.email, + userData.password, + userData.wallet + ) + ).rejects.toThrow("User with this email already exists"); + }); + }); + + describe("verifyEmail", () => { + it("should verify email successfully", async () => { + const token = "verification-token"; + const expectedResult = { + success: true, + message: "Email verified", + verified: true, + }; + + mockVerifyEmailUseCase.execute.mockResolvedValue(expectedResult); + + const result = await authService.verifyEmail(token); + + expect(result).toEqual(expectedResult); + expect(mockVerifyEmailUseCase.execute).toHaveBeenCalledWith({ token }); + }); + }); + + describe("resendVerificationEmail", () => { + it("should resend verification email successfully", async () => { + const email = "test@example.com"; + const expectedResult = { + success: true, + message: "Verification email sent", + }; + + mockResendVerificationEmailUseCase.execute.mockResolvedValue( + expectedResult + ); + + const result = await authService.resendVerificationEmail(email); + + expect(result).toEqual(expectedResult); + expect(mockResendVerificationEmailUseCase.execute).toHaveBeenCalledWith({ + email, + }); + }); + }); + + describe("checkVerificationStatus", () => { + it("should return verification status for verified user", async () => { + const userId = "user-id"; + mockUserRepository.isUserVerified.mockResolvedValue(true); + + const result = await authService.checkVerificationStatus(userId); + + expect(result).toEqual({ + isVerified: true, + message: "Email is verified", + }); + expect(mockUserRepository.isUserVerified).toHaveBeenCalledWith(userId); + }); + + it("should return verification status for unverified user", async () => { + const userId = "user-id"; + mockUserRepository.isUserVerified.mockResolvedValue(false); + + const result = await authService.checkVerificationStatus(userId); + + expect(result).toEqual({ + isVerified: false, + message: "Email is not verified", + }); + }); + }); + + describe("verifyWalletAddress", () => { + it("should verify wallet address", async () => { + const walletAddress = "test-wallet-address"; + const expectedResult = { + success: true, + isValid: true, + walletAddress, + accountExists: true, + verifiedAt: new Date(), + message: "Wallet verified", + }; + + mockWalletService.verifyWallet.mockResolvedValue(expectedResult); + + const result = await authService.verifyWalletAddress(walletAddress); + + expect(result).toEqual(expectedResult); + expect(mockWalletService.verifyWallet).toHaveBeenCalledWith( + walletAddress + ); + }); + }); + + describe("validateWalletFormat", () => { + it("should validate wallet format", async () => { + const walletAddress = "test-wallet-address"; + const expectedResult = { + success: true, + isValid: true, + walletAddress, + accountExists: true, + verifiedAt: new Date(), + message: "Format valid", + }; + + mockWalletService.validateWalletFormat.mockResolvedValue(expectedResult); + + const result = await authService.validateWalletFormat(walletAddress); + + expect(result).toEqual(expectedResult); + expect(mockWalletService.validateWalletFormat).toHaveBeenCalledWith( + walletAddress + ); + }); + }); +}); diff --git a/src/services/AuthService.ts b/src/modules/auth/application/services/AuthService.ts similarity index 71% rename from src/services/AuthService.ts rename to src/modules/auth/application/services/AuthService.ts index a378d18..b6a7ec3 100644 --- a/src/services/AuthService.ts +++ b/src/modules/auth/application/services/AuthService.ts @@ -1,11 +1,11 @@ -import bcrypt from 'bcryptjs'; -import { prisma } from "../config/prisma"; -import jwt from 'jsonwebtoken'; -import { PrismaUserRepository } from '../modules/user/repositories/PrismaUserRepository'; -import { SendVerificationEmailUseCase } from '../modules/auth/use-cases/send-verification-email.usecase'; -import { VerifyEmailUseCase } from '../modules/auth/use-cases/verify-email.usecase'; -import { ResendVerificationEmailUseCase } from '../modules/auth/use-cases/resend-verification-email.usecase'; -import { WalletService } from '../modules/wallet/services/WalletService'; +import bcrypt from "bcryptjs"; +import { prisma } from "../../../../config/prisma"; +import jwt from "jsonwebtoken"; +import { PrismaUserRepository } from "../../../user/repositories/PrismaUserRepository"; +import { SendVerificationEmailUseCase } from "../../use-cases/send-verification-email.usecase"; +import { VerifyEmailUseCase } from "../../use-cases/verify-email.usecase"; +import { ResendVerificationEmailUseCase } from "../../use-cases/resend-verification-email.usecase"; +import { WalletService } from "../../../wallet/services/WalletService"; class AuthService { private userRepository: PrismaUserRepository; @@ -16,9 +16,13 @@ class AuthService { constructor() { this.userRepository = new PrismaUserRepository(); - this.sendVerificationEmailUseCase = new SendVerificationEmailUseCase(this.userRepository); + this.sendVerificationEmailUseCase = new SendVerificationEmailUseCase( + this.userRepository + ); this.verifyEmailUseCase = new VerifyEmailUseCase(this.userRepository); - this.resendVerificationEmailUseCase = new ResendVerificationEmailUseCase(this.userRepository); + this.resendVerificationEmailUseCase = new ResendVerificationEmailUseCase( + this.userRepository + ); this.walletService = new WalletService(); } @@ -42,17 +46,25 @@ class AuthService { return jwt.sign({ id: user.id }, SECRET_KEY, { expiresIn: "1h" }); } - async register(name: string, lastName: string, email: string, password: string, wallet: string) { + async register( + name: string, + lastName: string, + email: string, + password: string, + wallet: string + ) { // Verify wallet address first const walletVerification = await this.walletService.verifyWallet(wallet); if (!walletVerification.success || !walletVerification.isValid) { - throw new Error(`Wallet verification failed: ${walletVerification.message}`); + throw new Error( + `Wallet verification failed: ${walletVerification.message}` + ); } // Check if user already exists by email const existingUser = await this.userRepository.findByEmail(email); if (existingUser) { - throw new Error('User with this email already exists'); + throw new Error("User with this email already exists"); } // Check if wallet is already registered @@ -60,7 +72,7 @@ class AuthService { where: { wallet: wallet }, }); if (existingWalletUser) { - throw new Error('This wallet address is already registered'); + throw new Error("This wallet address is already registered"); } // Hash password @@ -69,7 +81,7 @@ class AuthService { // Create user const user = await this.userRepository.create({ - id: '', + id: "", name, lastName, email, @@ -87,7 +99,8 @@ class AuthService { email: user.email, wallet: user.wallet, walletVerified: walletVerification.accountExists, - message: 'User registered successfully. Please check your email to verify your account.', + message: + "User registered successfully. Please check your email to verify your account.", }; } @@ -103,9 +116,7 @@ class AuthService { const isVerified = await this.userRepository.isUserVerified(userId); return { isVerified, - message: isVerified - ? "Email is verified" - : "Email is not verified" + message: isVerified ? "Email is verified" : "Email is not verified", }; } diff --git a/src/modules/auth/presentation/controllers/Auth.controller.ts b/src/modules/auth/presentation/controllers/Auth.controller.ts index 797fba6..ae1e16c 100644 --- a/src/modules/auth/presentation/controllers/Auth.controller.ts +++ b/src/modules/auth/presentation/controllers/Auth.controller.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express"; -import AuthService from "../../../../services/AuthService"; +import AuthService from "../../../../modules/auth/application/services/AuthService"; import { AuthenticatedRequest } from "../../../../types/auth.types"; class AuthController { diff --git a/src/modules/messaging/repositories/implementations/message-prisma.repository.ts b/src/modules/messaging/repositories/implementations/message-prisma.repository.ts index fbe1d1f..afe2b36 100644 --- a/src/modules/messaging/repositories/implementations/message-prisma.repository.ts +++ b/src/modules/messaging/repositories/implementations/message-prisma.repository.ts @@ -1,6 +1,6 @@ -import { PrismaClient } from '@prisma/client'; -import { IMessageRepository } from '../interfaces/message-repository.interface'; -import { Message } from '../../domain/entities/message.entity'; +import { PrismaClient } from "@prisma/client"; +import { IMessageRepository } from "../interfaces/message-repository.interface"; +import { Message } from "../../domain/entities/message.entity"; export class MessagePrismaRepository implements IMessageRepository { constructor(private prisma: PrismaClient) {} @@ -38,17 +38,14 @@ export class MessagePrismaRepository implements IMessageRepository { limit: number = 50 ): Promise { const skip = (page - 1) * limit; - + const messages = await this.prisma.message.findMany({ where: { volunteerId, - OR: [ - { senderId: userId }, - { receiverId: userId } - ], + OR: [{ senderId: userId }, { receiverId: userId }], }, orderBy: { - sentAt: 'asc', + sentAt: "asc", }, skip, take: limit, @@ -69,7 +66,7 @@ export class MessagePrismaRepository implements IMessageRepository { }); return messages.map( - (msg) => + (msg: any) => new Message( msg.id, msg.content, @@ -144,13 +141,10 @@ export class MessagePrismaRepository implements IMessageRepository { const message = await this.prisma.message.findFirst({ where: { id: messageId, - OR: [ - { senderId: userId }, - { receiverId: userId } - ], + OR: [{ senderId: userId }, { receiverId: userId }], }, }); return message !== null; } -} \ No newline at end of file +} diff --git a/src/modules/nft/__tests__/services/NFTService.test.ts b/src/modules/nft/__tests__/services/NFTService.test.ts new file mode 100644 index 0000000..a351d78 --- /dev/null +++ b/src/modules/nft/__tests__/services/NFTService.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect, beforeEach, jest } from "@jest/globals"; +import NFTService from "../../application/services/NFTService"; + +describe("NFTService", () => { + let nftService: typeof NFTService; + + beforeEach(() => { + nftService = NFTService; + }); + + it("should be defined", () => { + expect(nftService).toBeDefined(); + }); +}); diff --git a/src/services/NFTService.ts b/src/modules/nft/application/services/NFTService.ts similarity index 65% rename from src/services/NFTService.ts rename to src/modules/nft/application/services/NFTService.ts index 38f9dcc..23bd162 100644 --- a/src/services/NFTService.ts +++ b/src/modules/nft/application/services/NFTService.ts @@ -1,9 +1,9 @@ -import { NFTRepository } from "../modules/nft/repositories/nft.repository"; -import { CreateNFT } from "../modules/nft/use-cases/createNFT"; -import { GetNFT } from "../modules/nft/use-cases/getNFT"; -import { GetNFTByUserId } from "../modules/nft/use-cases/getNFTByUserId"; -import { CreateNFTDto } from "../modules/nft/dto/create-nft.dto"; -import { DeleteNFT } from "../modules/nft/use-cases/deleteNFT"; +import { NFTRepository } from "../../repositories/nft.repository"; +import { CreateNFT } from "../../use-cases/createNFT"; +import { GetNFT } from "../../use-cases/getNFT"; +import { GetNFTByUserId } from "../../use-cases/getNFTByUserId"; +import { CreateNFTDto } from "../../dto/create-nft.dto"; +import { DeleteNFT } from "../../use-cases/deleteNFT"; export class NFTService { private nftRepository = new NFTRepository(); diff --git a/src/modules/nft/presentation/controllers/NFTController.ts b/src/modules/nft/presentation/controllers/NFTController.ts index 9fda7e4..3f61edc 100644 --- a/src/modules/nft/presentation/controllers/NFTController.ts +++ b/src/modules/nft/presentation/controllers/NFTController.ts @@ -1,6 +1,5 @@ import { Request, Response } from "express"; -import NFTService from "../../../../services/NFTService"; - +import NFTService from "../../../../modules/nft/application/services/NFTService"; class NFTController { // Creates_a_new_NFT_and_returns_the_created_NFT_data_OKK!! async createNFT(req: Request, res: Response) { diff --git a/src/modules/organization/__tests__/services/OrganizationService.test.ts b/src/modules/organization/__tests__/services/OrganizationService.test.ts new file mode 100644 index 0000000..ffabd49 --- /dev/null +++ b/src/modules/organization/__tests__/services/OrganizationService.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect, beforeEach, jest } from "@jest/globals"; +import { OrganizationService } from "../../application/services/OrganizationService"; + +describe("OrganizationService", () => { + let organizationService: OrganizationService; + + beforeEach(() => { + organizationService = new OrganizationService(); + }); + + it("should be defined", () => { + expect(organizationService).toBeDefined(); + }); +}); diff --git a/src/services/OrganizationService.ts b/src/modules/organization/application/services/OrganizationService.ts similarity index 87% rename from src/services/OrganizationService.ts rename to src/modules/organization/application/services/OrganizationService.ts index f0f4a0a..29db2fc 100644 --- a/src/services/OrganizationService.ts +++ b/src/modules/organization/application/services/OrganizationService.ts @@ -1,5 +1,5 @@ import { PrismaClient } from "@prisma/client"; -import { ValidationError } from "../modules/shared/application/errors"; +import { ValidationError } from "../modules/shared/application/../../../errors"; import { Prisma, Organization, NFT } from "@prisma/client"; type OrganizationWithNFTs = Prisma.OrganizationGetPayload<{ @@ -8,7 +8,6 @@ type OrganizationWithNFTs = Prisma.OrganizationGetPayload<{ type OrganizationUpdateData = Prisma.OrganizationUpdateInput; - interface PrismaOrganization { id: string; createdAt: Date; @@ -30,11 +29,6 @@ interface PrismaNFT { description: string; } -// type OrganizationWithNFTs = PrismaOrganization; -// type OrganizationUpdateData = Partial< -// Omit -// >; - export class OrganizationService { private prisma = new PrismaClient(); @@ -65,7 +59,7 @@ export class OrganizationService { data: { name, email, - password, + password, category, wallet, }, @@ -103,12 +97,13 @@ export class OrganizationService { if (!organization) { throw new ValidationError("Organization not found"); } - + // Extract the updated email value - const updatedEmail = typeof updateData.email === "object" && updateData.email !== null - ? updateData.email.set - : updateData.email; - + const updatedEmail = + typeof updateData.email === "object" && updateData.email !== null + ? updateData.email.set + : updateData.email; + if (updatedEmail && updatedEmail !== organization.email) { const existingOrgEmail = await this.prisma.organization.findUnique({ where: { email: updatedEmail }, @@ -119,12 +114,13 @@ export class OrganizationService { ); } } - + // Extract the updated wallet value - const updatedWallet = typeof updateData.wallet === "object" && updateData.wallet !== null - ? updateData.wallet.set - : updateData.wallet; - + const updatedWallet = + typeof updateData.wallet === "object" && updateData.wallet !== null + ? updateData.wallet.set + : updateData.wallet; + if (updatedWallet && updatedWallet !== organization.wallet) { const existingOrgWallet = await this.prisma.organization.findUnique({ where: { wallet: updatedWallet }, @@ -135,7 +131,7 @@ export class OrganizationService { ); } } - + return this.prisma.organization.update({ where: { id }, data: updateData, @@ -144,7 +140,6 @@ export class OrganizationService { }, }) as unknown as OrganizationWithNFTs; } - async deleteOrganization(id: string): Promise { const organization = await this.getOrganizationById(id); @@ -177,9 +172,9 @@ export class OrganizationService { this.prisma.organization.count(), ]); - return { - organizations: organizations as unknown as OrganizationWithNFTs[], - total + return { + organizations: organizations as unknown as OrganizationWithNFTs[], + total, }; } } diff --git a/src/modules/organization/presentation/controllers/OrganizationController.ts b/src/modules/organization/presentation/controllers/OrganizationController.ts index 2c8029d..2777a8c 100644 --- a/src/modules/organization/presentation/controllers/OrganizationController.ts +++ b/src/modules/organization/presentation/controllers/OrganizationController.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express"; -import { OrganizationService } from "../../../../services/OrganizationService"; +import { OrganizationService } from "../../../organization/application/services/OrganizationService"; import { asyncHandler } from "../../../../utils/asyncHandler"; class OrganizationController { diff --git a/src/modules/project/__tests__/services/ProjectService.test.ts b/src/modules/project/__tests__/services/ProjectService.test.ts new file mode 100644 index 0000000..f998654 --- /dev/null +++ b/src/modules/project/__tests__/services/ProjectService.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect, beforeEach, jest } from '@jest/globals'; +import ProjectService from '../../application/services/ProjectService'; + +describe('ProjectService', () => { + let projectService: ProjectService; + + beforeEach(() => { + projectService = new ProjectService(); + }); + + it('should be defined', () => { + expect(projectService).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/src/services/ProjectService.ts b/src/modules/project/application/services/ProjectService.ts similarity index 70% rename from src/services/ProjectService.ts rename to src/modules/project/application/services/ProjectService.ts index d5a2a4c..c392d3c 100644 --- a/src/services/ProjectService.ts +++ b/src/modules/project/application/services/ProjectService.ts @@ -1,72 +1,75 @@ -import { prisma } from "../config/prisma" -import { Project } from "../entities/Project" -import { Volunteer } from "../entities/Volunteer" -import { withTransaction } from "../utils/transaction.helper" -import { Logger } from "../utils/logger" -import { ValidationError, DatabaseError } from "../modules/shared/application/errors" +import { prisma } from "../../../../config/prisma"; +import { Project } from "../../../../entities/Project"; +import { Volunteer } from "../../../../entities/Volunteer"; +import { withTransaction } from "../../../../utils/transaction.helper"; +import { Logger } from "../../../../utils/logger"; +import { + ValidationError, + DatabaseError, +} from "../../../../modules/shared/application/errors"; // Define types based on the Prisma schema interface PrismaProject { - id: string - createdAt: Date - updatedAt: Date - name: string - description: string - location: string - startDate: Date - endDate: Date - organizationId: string - volunteers: PrismaVolunteer[] + id: string; + createdAt: Date; + updatedAt: Date; + name: string; + description: string; + location: string; + startDate: Date; + endDate: Date; + organizationId: string; + volunteers: PrismaVolunteer[]; } interface PrismaVolunteer { - id: string - createdAt: Date - updatedAt: Date - name: string - description: string - requirements: string - incentive: string | null - projectId: string + id: string; + createdAt: Date; + updatedAt: Date; + name: string; + description: string; + requirements: string; + incentive: string | null; + projectId: string; project: { - id: string - name: string - description: string - location: string - startDate: Date - endDate: Date - createdAt: Date - updatedAt: Date - organizationId: string - } + id: string; + name: string; + description: string; + location: string; + startDate: Date; + endDate: Date; + createdAt: Date; + updatedAt: Date; + organizationId: string; + }; } interface CreateProjectData { - name: string - description: string - location: string - startDate: Date - endDate: Date - organizationId: string + name: string; + description: string; + location: string; + startDate: Date; + endDate: Date; + organizationId: string; initialVolunteers?: Array<{ - name: string - description: string - requirements: string - incentive?: string - }> + name: string; + description: string; + requirements: string; + incentive?: string; + }>; } interface UpdateProjectData { - name?: string - description?: string - location?: string - startDate?: Date - endDate?: Date + name?: string; + description?: string; + location?: string; + startDate?: Date; + endDate?: Date; } class ProjectService { - private projectRepo = prisma.project - private logger = new Logger("ProjectService") + private projectRepo = prisma.project; + private logger = new Logger("ProjectService"); /** * Create a new project with optional initial volunteers @@ -74,13 +77,13 @@ class ProjectService { */ async createProject(data: CreateProjectData): Promise { try { - this.validateProjectData(data) + this.validateProjectData(data); return await withTransaction(async (tx) => { this.logger.info("Creating project with transaction", { projectName: data.name, organizationId: data.organizationId, - }) + }); // Create the project const project = await tx.project.create({ @@ -99,18 +102,18 @@ class ProjectService { }, }, }, - }) + }); // Create initial volunteers if provided if (data.initialVolunteers && data.initialVolunteers.length > 0) { const volunteersData = data.initialVolunteers.map((volunteer) => ({ ...volunteer, projectId: project.id, - })) + })); await tx.volunteer.createMany({ data: volunteersData, - }) + }); // Fetch the project with volunteers const projectWithVolunteers = await tx.project.findUnique({ @@ -122,30 +125,32 @@ class ProjectService { }, }, }, - }) + }); if (!projectWithVolunteers) { - throw new DatabaseError("Failed to fetch created project with volunteers") + throw new DatabaseError( + "Failed to fetch created project with volunteers" + ); } - return this.mapToProject(projectWithVolunteers) + return this.mapToProject(projectWithVolunteers); } - return this.mapToProject(project) - }) + return this.mapToProject(project); + }); } catch (error) { this.logger.error("Failed to create project", { error: error instanceof Error ? error.message : "Unknown error", projectData: data, - }) + }); if (error instanceof ValidationError || error instanceof DatabaseError) { - throw error + throw error; } throw new DatabaseError("Failed to create project", { originalError: error instanceof Error ? error.message : "Unknown error", - }) + }); } } @@ -158,7 +163,7 @@ class ProjectService { location: string, startDate: Date, endDate: Date, - organizationId: string, + organizationId: string ): Promise { return this.createProject({ name, @@ -167,7 +172,7 @@ class ProjectService { startDate, endDate, organizationId, - }) + }); } /** @@ -176,15 +181,17 @@ class ProjectService { async updateProject(id: string, data: UpdateProjectData): Promise { try { return await withTransaction(async (tx) => { - this.logger.info("Updating project with transaction", { projectId: id }) + this.logger.info("Updating project with transaction", { + projectId: id, + }); // Check if project exists const existingProject = await tx.project.findUnique({ where: { id }, - }) + }); if (!existingProject) { - throw new ValidationError("Project not found", { projectId: id }) + throw new ValidationError("Project not found", { projectId: id }); } // Update the project @@ -201,24 +208,24 @@ class ProjectService { }, }, }, - }) + }); - return this.mapToProject(updatedProject) - }) + return this.mapToProject(updatedProject); + }); } catch (error) { this.logger.error("Failed to update project", { error: error instanceof Error ? error.message : "Unknown error", projectId: id, updateData: data, - }) + }); if (error instanceof ValidationError || error instanceof DatabaseError) { - throw error + throw error; } throw new DatabaseError("Failed to update project", { originalError: error instanceof Error ? error.message : "Unknown error", - }) + }); } } @@ -229,7 +236,9 @@ class ProjectService { async deleteProject(id: string): Promise { try { await withTransaction(async (tx) => { - this.logger.info("Deleting project with transaction", { projectId: id }) + this.logger.info("Deleting project with transaction", { + projectId: id, + }); // Check if project exists const existingProject = await tx.project.findUnique({ @@ -237,44 +246,44 @@ class ProjectService { include: { volunteers: true, }, - }) + }); if (!existingProject) { - throw new ValidationError("Project not found", { projectId: id }) + throw new ValidationError("Project not found", { projectId: id }); } // Delete all volunteers first (due to foreign key constraints) if (existingProject.volunteers.length > 0) { await tx.volunteer.deleteMany({ where: { projectId: id }, - }) + }); this.logger.info("Deleted associated volunteers", { projectId: id, volunteerCount: existingProject.volunteers.length, - }) + }); } // Delete the project await tx.project.delete({ where: { id }, - }) + }); - this.logger.info("Project deleted successfully", { projectId: id }) - }) + this.logger.info("Project deleted successfully", { projectId: id }); + }); } catch (error) { this.logger.error("Failed to delete project", { error: error instanceof Error ? error.message : "Unknown error", projectId: id, - }) + }); if (error instanceof ValidationError || error instanceof DatabaseError) { - throw error + throw error; } throw new DatabaseError("Failed to delete project", { originalError: error instanceof Error ? error.message : "Unknown error", - }) + }); } } @@ -284,37 +293,37 @@ class ProjectService { async addVolunteersToProject( projectId: string, volunteers: Array<{ - name: string - description: string - requirements: string - incentive?: string - }>, + name: string; + description: string; + requirements: string; + incentive?: string; + }> ): Promise { try { return await withTransaction(async (tx) => { this.logger.info("Adding volunteers to project with transaction", { projectId, volunteerCount: volunteers.length, - }) + }); // Verify project exists const project = await tx.project.findUnique({ where: { id: projectId }, - }) + }); if (!project) { - throw new ValidationError("Project not found", { projectId }) + throw new ValidationError("Project not found", { projectId }); } // Create volunteers const volunteersData = volunteers.map((volunteer) => ({ ...volunteer, projectId, - })) + })); await tx.volunteer.createMany({ data: volunteersData, - }) + }); // Fetch created volunteers const createdVolunteers = await tx.volunteer.findMany({ @@ -325,24 +334,24 @@ class ProjectService { include: { project: true, }, - }) + }); - return createdVolunteers.map((v) => this.mapToVolunteer(v, project)) - }) + return createdVolunteers.map((v) => this.mapToVolunteer(v, project)); + }); } catch (error) { this.logger.error("Failed to add volunteers to project", { error: error instanceof Error ? error.message : "Unknown error", projectId, volunteers, - }) + }); if (error instanceof ValidationError || error instanceof DatabaseError) { - throw error + throw error; } throw new DatabaseError("Failed to add volunteers to project", { originalError: error instanceof Error ? error.message : "Unknown error", - }) + }); } } @@ -357,26 +366,26 @@ class ProjectService { }, }, }, - }) - return project ? this.mapToProject(project) : null + }); + return project ? this.mapToProject(project) : null; } catch (error) { this.logger.error("Failed to get project by id", { error: error instanceof Error ? error.message : "Unknown error", projectId: id, - }) + }); throw new DatabaseError("Failed to fetch project", { originalError: error instanceof Error ? error.message : "Unknown error", - }) + }); } } async getProjectsByOrganizationId( organizationId: string, page = 1, - pageSize = 10, + pageSize = 10 ): Promise<{ projects: Project[]; total: number }> { try { - const skip = (page - 1) * pageSize + const skip = (page - 1) * pageSize; const [projects, total] = await Promise.all([ this.projectRepo.findMany({ @@ -401,22 +410,24 @@ class ProjectService { organizationId, }, }), - ]) + ]); return { - projects: projects.map((project: PrismaProject) => this.mapToProject(project)), + projects: projects.map((project: PrismaProject) => + this.mapToProject(project) + ), total, - } + }; } catch (error) { this.logger.error("Failed to get projects by organization", { error: error instanceof Error ? error.message : "Unknown error", organizationId, page, pageSize, - }) + }); throw new DatabaseError("Failed to fetch projects", { originalError: error instanceof Error ? error.message : "Unknown error", - }) + }); } } @@ -425,58 +436,63 @@ class ProjectService { */ private validateProjectData(data: CreateProjectData): void { if (!data.name || data.name.trim().length === 0) { - throw new ValidationError("Project name is required") + throw new ValidationError("Project name is required"); } if (!data.description || data.description.trim().length === 0) { - throw new ValidationError("Project description is required") + throw new ValidationError("Project description is required"); } if (!data.location || data.location.trim().length === 0) { - throw new ValidationError("Project location is required") + throw new ValidationError("Project location is required"); } if (!data.organizationId || data.organizationId.trim().length === 0) { - throw new ValidationError("Organization ID is required") + throw new ValidationError("Organization ID is required"); } if (data.startDate >= data.endDate) { - throw new ValidationError("Start date must be before end date") + throw new ValidationError("Start date must be before end date"); } if (data.startDate < new Date()) { - throw new ValidationError("Start date cannot be in the past") + throw new ValidationError("Start date cannot be in the past"); } } private mapToProject(prismaProject: PrismaProject): Project { - const project = new Project() - project.id = prismaProject.id - project.name = prismaProject.name - project.description = prismaProject.description - project.location = prismaProject.location - project.startDate = prismaProject.startDate - project.endDate = prismaProject.endDate - project.createdAt = prismaProject.createdAt - project.updatedAt = prismaProject.updatedAt - - project.volunteers = prismaProject.volunteers.map((v: PrismaVolunteer) => this.mapToVolunteer(v, project)) - - return project + const project = new Project(); + project.id = prismaProject.id; + project.name = prismaProject.name; + project.description = prismaProject.description; + project.location = prismaProject.location; + project.startDate = prismaProject.startDate; + project.endDate = prismaProject.endDate; + project.createdAt = prismaProject.createdAt; + project.updatedAt = prismaProject.updatedAt; + + project.volunteers = prismaProject.volunteers.map((v: PrismaVolunteer) => + this.mapToVolunteer(v, project) + ); + + return project; } - private mapToVolunteer(prismaVolunteer: PrismaVolunteer, project: Project): Volunteer { - const volunteer = new Volunteer() - volunteer.id = prismaVolunteer.id - volunteer.name = prismaVolunteer.name - volunteer.description = prismaVolunteer.description - volunteer.requirements = prismaVolunteer.requirements - volunteer.incentive = prismaVolunteer.incentive || undefined - volunteer.project = project - volunteer.createdAt = prismaVolunteer.createdAt - volunteer.updatedAt = prismaVolunteer.updatedAt - return volunteer + private mapToVolunteer( + prismaVolunteer: PrismaVolunteer, + project: Project + ): Volunteer { + const volunteer = new Volunteer(); + volunteer.id = prismaVolunteer.id; + volunteer.name = prismaVolunteer.name; + volunteer.description = prismaVolunteer.description; + volunteer.requirements = prismaVolunteer.requirements; + volunteer.incentive = prismaVolunteer.incentive || undefined; + volunteer.project = project; + volunteer.createdAt = prismaVolunteer.createdAt; + volunteer.updatedAt = prismaVolunteer.updatedAt; + return volunteer; } } -export default ProjectService \ No newline at end of file +export default ProjectService; diff --git a/src/modules/project/presentation/controllers/Project.controller.ts b/src/modules/project/presentation/controllers/Project.controller.ts index 1d15d56..2534b07 100644 --- a/src/modules/project/presentation/controllers/Project.controller.ts +++ b/src/modules/project/presentation/controllers/Project.controller.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express"; -import ProjectService from "../../../../services/ProjectService"; +import ProjectService from "../../../../modules/project/application/services/ProjectService"; class ProjectController { private projectService = new ProjectService(); diff --git a/src/modules/shared/__tests__/services/LoggerService.test.ts b/src/modules/shared/__tests__/services/LoggerService.test.ts new file mode 100644 index 0000000..4f9a1d4 --- /dev/null +++ b/src/modules/shared/__tests__/services/LoggerService.test.ts @@ -0,0 +1,10 @@ +import { describe, it, expect } from "@jest/globals"; +import { LoggerService } from "../../application/services/LoggerService"; + +describe("LoggerService", () => { + it("should create a logger instance", () => { + const logger = new LoggerService("TEST"); + expect(logger).toBeDefined(); + expect(logger.child("CHILD")).toBeInstanceOf(LoggerService); + }); +}); diff --git a/src/services/logger.service.ts b/src/modules/shared/application/services/LoggerService.ts similarity index 76% rename from src/services/logger.service.ts rename to src/modules/shared/application/services/LoggerService.ts index aea6c9e..51d98f0 100644 --- a/src/services/logger.service.ts +++ b/src/modules/shared/application/services/LoggerService.ts @@ -1,6 +1,6 @@ -import { Request } from 'express'; -import { logger as winstonLogger } from '../config/winston.config'; -import { getTraceId } from '../middlewares/traceId.middleware'; +import { Request } from "express"; +import { logger as winstonLogger } from "../../../../config/winston.config"; +import { getTraceId } from "../../../../middlewares/traceId.middleware"; export interface LogContext { traceId?: string; @@ -25,7 +25,7 @@ export interface LogMeta { export class LoggerService { private context: string; - constructor(context: string = 'SYSTEM') { + constructor(context: string = "SYSTEM") { this.context = context; } @@ -43,8 +43,8 @@ export class LoggerService { method: req.method, url: req.url, ip: req.ip, - userAgent: req.get('User-Agent'), - userId: (req as any).user?.id?.toString() + userAgent: req.get("User-Agent"), + userId: (req as any).user?.id?.toString(), }; } @@ -54,7 +54,7 @@ export class LoggerService { private createLogMeta(context: LogContext, meta?: LogMeta): object { return { ...context, - ...(meta && { meta }) + ...(meta && { meta }), }; } @@ -64,7 +64,7 @@ export class LoggerService { info(message: string, req?: Request, meta?: LogMeta): void { const context = this.createRequestContext(req); const logMeta = this.createLogMeta(context, meta); - + winstonLogger.info(message, logMeta); } @@ -74,14 +74,19 @@ export class LoggerService { warn(message: string, req?: Request, meta?: LogMeta): void { const context = this.createRequestContext(req); const logMeta = this.createLogMeta(context, meta); - + winstonLogger.warn(message, logMeta); } /** * Log error level message */ - error(message: string, error?: Error | any, req?: Request, meta?: LogMeta): void { + error( + message: string, + error?: Error | any, + req?: Request, + meta?: LogMeta + ): void { const context = this.createRequestContext(req); const logMeta = this.createLogMeta(context, { ...meta, @@ -91,11 +96,11 @@ export class LoggerService { stack: error.stack, name: error.name, ...(error.code && { code: error.code }), - ...(error.statusCode && { statusCode: error.statusCode }) - } - }) + ...(error.statusCode && { statusCode: error.statusCode }), + }, + }), }); - + winstonLogger.error(message, logMeta); } @@ -105,7 +110,7 @@ export class LoggerService { debug(message: string, req?: Request, meta?: LogMeta): void { const context = this.createRequestContext(req); const logMeta = this.createLogMeta(context, meta); - + winstonLogger.debug(message, logMeta); } @@ -119,41 +124,52 @@ export class LoggerService { headers: req.headers, query: req.query, params: req.params, - body: this.sanitizeBody(req.body) + body: this.sanitizeBody(req.body), }); - winstonLogger.info('Incoming HTTP Request', logMeta); + winstonLogger.info("Incoming HTTP Request", logMeta); } /** * Log HTTP response */ - logResponse(req: Request, statusCode: number, responseTime?: number, meta?: LogMeta): void { + logResponse( + req: Request, + statusCode: number, + responseTime?: number, + meta?: LogMeta + ): void { const context = this.createRequestContext(req); const logMeta = this.createLogMeta(context, { ...meta, statusCode, - ...(responseTime && { responseTime: `${responseTime}ms` }) + ...(responseTime && { responseTime: `${responseTime}ms` }), }); - const level = statusCode >= 400 ? 'warn' : 'info'; - winstonLogger[level]('HTTP Response', logMeta); + const level = statusCode >= 400 ? "warn" : "info"; + winstonLogger[level]("HTTP Response", logMeta); } /** * Sanitize request body to remove sensitive information */ private sanitizeBody(body: any): any { - if (!body || typeof body !== 'object') { + if (!body || typeof body !== "object") { return body; } - const sensitiveFields = ['password', 'token', 'secret', 'key', 'authorization']; + const sensitiveFields = [ + "password", + "token", + "secret", + "key", + "authorization", + ]; const sanitized = { ...body }; for (const field of sensitiveFields) { if (sanitized[field]) { - sanitized[field] = '[REDACTED]'; + sanitized[field] = "[REDACTED]"; } } @@ -169,7 +185,7 @@ export class LoggerService { } // Export singleton instance for global use -export const globalLogger = new LoggerService('GLOBAL'); +export const globalLogger = new LoggerService("GLOBAL"); // Export factory function for creating context-specific loggers export const createLogger = (context: string): LoggerService => { diff --git a/src/modules/soroban/__tests__/services/SorobanService.test.ts b/src/modules/soroban/__tests__/services/SorobanService.test.ts new file mode 100644 index 0000000..cfdae4c --- /dev/null +++ b/src/modules/soroban/__tests__/services/SorobanService.test.ts @@ -0,0 +1,9 @@ +import { describe, it, expect } from "@jest/globals"; +import { SorobanService } from "../../application/services/SorobanService"; + +describe("SorobanService", () => { + it("should create a SorobanService instance", () => { + const service = new SorobanService(); + expect(service).toBeDefined(); + }); +}); diff --git a/src/services/soroban/sorobanService.ts b/src/modules/soroban/application/services/SorobanService.ts similarity index 84% rename from src/services/soroban/sorobanService.ts rename to src/modules/soroban/application/services/SorobanService.ts index 38f7dd4..df52697 100644 --- a/src/services/soroban/sorobanService.ts +++ b/src/modules/soroban/application/services/SorobanService.ts @@ -1,5 +1,5 @@ -import { Server, Transaction, xdr } from 'soroban-client'; -import { sorobanConfig } from '../../config/soroban.config'; +import { Server, Transaction, xdr } from "soroban-client"; +import { sorobanConfig } from "../../../../config/soroban.config"; export class SorobanService { private server: Server; @@ -9,7 +9,7 @@ export class SorobanService { this.server = new Server(sorobanConfig.rpcUrl); // Ensure serverSecret is not undefined if (!sorobanConfig.serverSecret) { - throw new Error('SOROBAN_SERVER_SECRET is required'); + throw new Error("SOROBAN_SERVER_SECRET is required"); } this.serverSecret = sorobanConfig.serverSecret; } @@ -26,7 +26,7 @@ export class SorobanService { const response = await this.server.sendTransaction(transaction); return response.hash; } catch (error: any) { - console.error('Error submitting transaction:', error); + console.error("Error submitting transaction:", error); throw new Error(`Failed to submit transaction: ${error.message}`); } } @@ -47,7 +47,7 @@ export class SorobanService { // Note: The actual method name may vary depending on the soroban-client version // This is a placeholder - you'll need to check the actual API documentation // or inspect the Server object to find the correct method - + // Example implementation - adjust based on actual API // @ts-ignore - Ignoring type checking until we know the exact API const result = await this.server.invokeContract( @@ -57,7 +57,7 @@ export class SorobanService { ); return result; } catch (error: any) { - console.error('Error invoking contract method:', error); + console.error("Error invoking contract method:", error); throw new Error( `Failed to invoke contract method ${methodName}: ${error.message}` ); @@ -66,4 +66,4 @@ export class SorobanService { } // Export a singleton instance -export const sorobanService = new SorobanService(); \ No newline at end of file +export const sorobanService = new SorobanService(); diff --git a/src/modules/user/__tests__/services/UserService.test.ts b/src/modules/user/__tests__/services/UserService.test.ts new file mode 100644 index 0000000..2dc9809 --- /dev/null +++ b/src/modules/user/__tests__/services/UserService.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeEach } from "@jest/globals"; +import { UserService } from "../../application/services/UserService"; +import { CreateUserDto } from "../../dto/CreateUserDto"; +import { UpdateUserDto } from "../../dto/UpdateUserDto"; + +describe("UserService", () => { + let userService: UserService; + + beforeEach(() => { + // Clear all mocks + jest.clearAllMocks(); + userService = new UserService(); + // Patch the internal repository instance with manual mocks + (userService as any).userRepository = { + create: jest.fn(), + findById: jest.fn(), + update: jest.fn(), + // Add other methods as needed + }; + }); + + it("should create a user", async () => { + const dto: CreateUserDto = { + name: "Test", + lastName: "User", + email: "test@example.com", + password: "pass", + wallet: "wallet", + }; + (userService as any).userRepository.create.mockResolvedValue({ + ...dto, + id: "1", + }); + const result = await userService.createUser(dto); + expect(result).toBeDefined(); + }); + + it("should get user by id", async () => { + (userService as any).userRepository.findById.mockResolvedValue({ id: "1" }); + const result = await userService.getUserById("1"); + expect(result).toBeDefined(); + }); + + it("should update user", async () => { + const dto: UpdateUserDto = { + id: "1", + name: "Updated", + lastName: "User", + email: "test@example.com", + password: "pass", + wallet: "wallet", + }; + (userService as any).userRepository.update.mockResolvedValue(undefined); + await expect(userService.updateUser(dto)).resolves.toBeUndefined(); + }); +}); diff --git a/src/services/UserService.ts b/src/modules/user/application/services/UserService.ts similarity index 82% rename from src/services/UserService.ts rename to src/modules/user/application/services/UserService.ts index 7959af7..1133853 100644 --- a/src/services/UserService.ts +++ b/src/modules/user/application/services/UserService.ts @@ -1,4 +1,4 @@ -import { CreateUserDto } from "../modules/user/dto/CreateUserDto"; +import { CreateUserDto } from "../../dto/CreateUserDto"; import { CreateUserUseCase, DeleteUserUseCase, @@ -6,9 +6,10 @@ import { GetUserByIdUseCase, GetUsersUseCase, UpdateUserUseCase, -} from "../modules/user/use-cases/userUseCase"; -import { UpdateUserDto } from "../modules/user/dto/UpdateUserDto"; -import { PrismaUserRepository } from "../modules/user/repositories/PrismaUserRepository"; +} from "../../use-cases/userUseCase"; +import { UpdateUserDto } from "../../dto/UpdateUserDto"; +import { PrismaUserRepository } from "../../repositories/PrismaUserRepository"; + export class UserService { private userRepository = new PrismaUserRepository(); private createUserUseCase = new CreateUserUseCase(this.userRepository); @@ -35,6 +36,7 @@ export class UserService { async getUsers(page: number = 1, pageSize: number = 10) { return this.GetUsersUseCase.execute(page, pageSize); } + async deleteUser(id: string): Promise { return this.DeleteUserUseCase.execute(id); } diff --git a/src/modules/user/presentation/controllers/UserController.ts b/src/modules/user/presentation/controllers/UserController.ts index 1ef2aa5..8670dde 100644 --- a/src/modules/user/presentation/controllers/UserController.ts +++ b/src/modules/user/presentation/controllers/UserController.ts @@ -1,5 +1,5 @@ +import { UserService } from "../../../../modules/user/application/services/UserService"; import { CreateUserDto } from "../../../../modules/user/dto/CreateUserDto"; -import { UserService } from "../../../../services/UserService"; import { Response } from "express"; import { UpdateUserDto } from "../../../../modules/user/dto/UpdateUserDto"; import { AuthenticatedRequest } from "../../../../types/auth.types"; diff --git a/src/modules/user/presentation/controllers/userVolunteer.controller.ts b/src/modules/user/presentation/controllers/userVolunteer.controller.ts index 8254504..af9a49d 100644 --- a/src/modules/user/presentation/controllers/userVolunteer.controller.ts +++ b/src/modules/user/presentation/controllers/userVolunteer.controller.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express"; -import { UserVolunteerService } from "../../../../services/userVolunteer.service"; +import { UserVolunteerService } from "../../../../modules/volunteer/application/services/UserVolunteerService"; import { prisma } from "../../../../config/prisma"; class UserVolunteerController { diff --git a/src/modules/volunteer/__tests__/services/VolunteerService.test.ts b/src/modules/volunteer/__tests__/services/VolunteerService.test.ts new file mode 100644 index 0000000..bbbe847 --- /dev/null +++ b/src/modules/volunteer/__tests__/services/VolunteerService.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, beforeEach, jest } from "@jest/globals"; +import VolunteerService from "../../application/services/VolunteerService"; +import { CreateVolunteerDTO } from "../../dto/volunteer.dto"; + +jest.mock("../../repositories/implementations/volunteer-prisma.repository"); + +describe("VolunteerService", () => { + let volunteerService: VolunteerService; + + beforeEach(() => { + jest.clearAllMocks(); + volunteerService = new VolunteerService(); + // Patch the internal repository instance with manual mocks + (volunteerService as any).volunteerRepository = { + create: jest.fn(), + findById: jest.fn(), + // Add other methods as needed + }; + }); + + it("should create a volunteer", async () => { + const dto: CreateVolunteerDTO = { + name: "Test", + description: "desc", + requirements: "req", + projectId: "1", + }; + const volunteer = { ...dto, id: "1" } as any; + (volunteerService as any).volunteerRepository.create.mockResolvedValue( + volunteer + ); + const result = await volunteerService.createVolunteer(dto); + expect(result).toBeDefined(); + }); + + it("should get volunteer by id", async () => { + const volunteer = { id: "1" } as any; + (volunteerService as any).volunteerRepository.findById.mockResolvedValue( + volunteer + ); + const result = await volunteerService.getVolunteerById("1"); + expect(result).toBeDefined(); + }); +}); diff --git a/src/services/__tests__/userVolunteer.service.test.ts b/src/modules/volunteer/__tests__/services/userVolunteer.service.test.ts similarity index 56% rename from src/services/__tests__/userVolunteer.service.test.ts rename to src/modules/volunteer/__tests__/services/userVolunteer.service.test.ts index 277992e..768c362 100644 --- a/src/services/__tests__/userVolunteer.service.test.ts +++ b/src/modules/volunteer/__tests__/services/userVolunteer.service.test.ts @@ -1,8 +1,18 @@ -import { describe, expect, it, beforeEach, jest } from '@jest/globals'; -import { UserVolunteerService } from '../userVolunteer.service'; -import { PrismaClient, Prisma, User, Volunteer, UserVolunteer } from '@prisma/client'; -import { DeepMockProxy, mockDeep } from 'jest-mock-extended'; -import { VolunteerAlreadyRegisteredError, VolunteerNotFoundError, VolunteerPositionFullError } from '../../modules/volunteer/application/errors'; +import { describe, expect, it, beforeEach, jest } from "@jest/globals"; +import { UserVolunteerService } from "../../application/services/UserVolunteerService"; +import { + PrismaClient, + Prisma, + User, + Volunteer, + UserVolunteer, +} from "@prisma/client"; +import { DeepMockProxy, mockDeep } from "jest-mock-extended"; +import { + VolunteerAlreadyRegisteredError, + VolunteerNotFoundError, + VolunteerPositionFullError, +} from "../../../../modules/volunteer/application/errors"; // Mock PrismaClient with proper type const mockPrisma: DeepMockProxy = mockDeep(); @@ -20,61 +30,63 @@ type UserVolunteerWithRelations = UserVolunteer & { volunteer: Volunteer; }; -describe('UserVolunteerService', () => { +describe("UserVolunteerService", () => { beforeEach(() => { jest.clearAllMocks(); }); - describe('registerVolunteerSafely', () => { + describe("registerVolunteerSafely", () => { const mockUser: User = { - id: 'user-id-1', + id: "user-id-1", createdAt: new Date(), updatedAt: new Date(), - name: 'Test User', - lastName: 'Test', - email: 'test@example.com', - password: 'password', - wallet: 'wallet-address', + name: "Test User", + lastName: "Test", + email: "test@example.com", + password: "password", + wallet: "wallet-address", isVerified: true, - verificationToken: null + verificationToken: null, + verificationTokenExpires: null, }; const mockVolunteer: Volunteer = { - id: 'volunteer-id-1', + id: "volunteer-id-1", createdAt: new Date(), updatedAt: new Date(), - name: 'Test Volunteer Position', - description: 'Test Description', - requirements: 'Test Requirements', - incentive: 'Test Incentive', - projectId: 'project-id-1', - maxVolunteers: 5 + name: "Test Volunteer Position", + description: "Test Description", + requirements: "Test Requirements", + incentive: "Test Incentive", + projectId: "project-id-1", + maxVolunteers: 5, } as Volunteer; const mockUserVolunteer: UserVolunteer = { - id: 'user-volunteer-1', + id: "user-volunteer-1", userId: mockUser.id, volunteerId: mockVolunteer.id, - joinedAt: new Date() + joinedAt: new Date(), + hoursContributed: 0, } as UserVolunteer; const mockVolunteerWithCount: VolunteerWithCount = { ...mockVolunteer, _count: { - userVolunteers: 0 - } + userVolunteers: 0, + }, }; const mockUserVolunteerWithRelations: UserVolunteerWithRelations = { ...mockUserVolunteer, user: mockUser, - volunteer: mockVolunteer + volunteer: mockVolunteer, }; - it('should successfully register a user to a volunteer position', async () => { + it("should successfully register a user to a volunteer position", async () => { // Mock the transaction mockPrisma.$transaction.mockImplementation(async (fn) => { - if (typeof fn === 'function') { + if (typeof fn === "function") { return fn(mockPrisma); } return Promise.resolve(fn); @@ -82,7 +94,9 @@ describe('UserVolunteerService', () => { mockPrisma.volunteer.findUnique.mockResolvedValue(mockVolunteerWithCount); mockPrisma.userVolunteer.findUnique.mockResolvedValue(null); - mockPrisma.userVolunteer.create.mockResolvedValue(mockUserVolunteerWithRelations); + mockPrisma.userVolunteer.create.mockResolvedValue( + mockUserVolunteerWithRelations + ); const result = await userVolunteerService.registerVolunteerSafely( mockUser.id, @@ -94,25 +108,25 @@ describe('UserVolunteerService', () => { where: { id: mockVolunteer.id }, include: { _count: { - select: { userVolunteers: true } - } - } + select: { userVolunteers: true }, + }, + }, }); expect(mockPrisma.userVolunteer.create).toHaveBeenCalledWith({ data: { userId: mockUser.id, - volunteerId: mockVolunteer.id + volunteerId: mockVolunteer.id, }, include: { user: true, - volunteer: true - } + volunteer: true, + }, }); }); - it('should throw an error when volunteer does not exist', async () => { + it("should throw an error when volunteer does not exist", async () => { mockPrisma.$transaction.mockImplementation(async (fn) => { - if (typeof fn === 'function') { + if (typeof fn === "function") { return fn(mockPrisma); } return Promise.resolve(fn); @@ -120,82 +134,108 @@ describe('UserVolunteerService', () => { mockPrisma.volunteer.findUnique.mockResolvedValue(null); await expect( - userVolunteerService.registerVolunteerSafely(mockUser.id, 'non-existent-id') + userVolunteerService.registerVolunteerSafely( + mockUser.id, + "non-existent-id" + ) ).rejects.toThrow(VolunteerNotFoundError); }); - it('should throw an error when user is already registered', async () => { + it("should throw an error when user is already registered", async () => { mockPrisma.$transaction.mockImplementation(async (fn) => { - if (typeof fn === 'function') { + if (typeof fn === "function") { return fn(mockPrisma); } return Promise.resolve(fn); }); mockPrisma.volunteer.findUnique.mockResolvedValue(mockVolunteerWithCount); - mockPrisma.userVolunteer.findUnique.mockResolvedValue(mockUserVolunteerWithRelations); + mockPrisma.userVolunteer.findUnique.mockResolvedValue( + mockUserVolunteerWithRelations + ); await expect( - userVolunteerService.registerVolunteerSafely(mockUser.id, mockVolunteer.id) + userVolunteerService.registerVolunteerSafely( + mockUser.id, + mockVolunteer.id + ) ).rejects.toThrow(VolunteerAlreadyRegisteredError); }); - it('should throw an error when volunteer position is full', async () => { + it("should throw an error when volunteer position is full", async () => { const maxVolunteers = 10; mockPrisma.$transaction.mockImplementation(async (fn) => { - if (typeof fn === 'function') { + if (typeof fn === "function") { return fn(mockPrisma); } return Promise.resolve(fn); }); mockPrisma.volunteer.findUnique.mockResolvedValue({ ...mockVolunteer, - _count: { userVolunteers: maxVolunteers } + _count: { userVolunteers: maxVolunteers }, } as VolunteerWithCount); mockPrisma.userVolunteer.findUnique.mockResolvedValue(null); await expect( - userVolunteerService.registerVolunteerSafely(mockUser.id, mockVolunteer.id) + userVolunteerService.registerVolunteerSafely( + mockUser.id, + mockVolunteer.id + ) ).rejects.toThrow(VolunteerPositionFullError); }); - it('should handle concurrent registration attempts safely', async () => { + it("should handle concurrent registration attempts safely", async () => { const maxVolunteers = 10; // First registration attempt - mockPrisma.volunteer.findUnique.mockResolvedValueOnce(mockVolunteerWithCount); + mockPrisma.volunteer.findUnique.mockResolvedValueOnce( + mockVolunteerWithCount + ); mockPrisma.userVolunteer.findUnique.mockResolvedValueOnce(null); - mockPrisma.userVolunteer.create.mockResolvedValueOnce(mockUserVolunteerWithRelations); + mockPrisma.userVolunteer.create.mockResolvedValueOnce( + mockUserVolunteerWithRelations + ); // Second concurrent registration attempt mockPrisma.volunteer.findUnique.mockResolvedValueOnce({ ...mockVolunteer, - _count: { userVolunteers: maxVolunteers } + _count: { userVolunteers: maxVolunteers }, } as VolunteerWithCount); mockPrisma.userVolunteer.findUnique.mockResolvedValueOnce(null); // First registration should succeed - const result1 = await userVolunteerService.registerVolunteerSafely(mockUser.id, mockVolunteer.id); + const result1 = await userVolunteerService.registerVolunteerSafely( + mockUser.id, + mockVolunteer.id + ); expect(result1).toBeDefined(); // Second registration should fail due to full capacity await expect( - userVolunteerService.registerVolunteerSafely('user-456', mockVolunteer.id) + userVolunteerService.registerVolunteerSafely( + "user-456", + mockVolunteer.id + ) ).rejects.toThrow(VolunteerPositionFullError); }); - it('should use serializable isolation level for transaction', async () => { + it("should use serializable isolation level for transaction", async () => { mockPrisma.volunteer.findUnique.mockResolvedValue(mockVolunteerWithCount); mockPrisma.userVolunteer.findUnique.mockResolvedValue(null); - mockPrisma.userVolunteer.create.mockResolvedValue(mockUserVolunteerWithRelations); + mockPrisma.userVolunteer.create.mockResolvedValue( + mockUserVolunteerWithRelations + ); - await userVolunteerService.registerVolunteerSafely(mockUser.id, mockVolunteer.id); + await userVolunteerService.registerVolunteerSafely( + mockUser.id, + mockVolunteer.id + ); expect(mockPrisma.$transaction).toHaveBeenCalledWith( expect.any(Function), { isolationLevel: Prisma.TransactionIsolationLevel.Serializable, - timeout: 5000 + timeout: 5000, } ); }); }); -}); \ No newline at end of file +}); diff --git a/src/services/userVolunteer.service.ts b/src/modules/volunteer/application/services/UserVolunteerService.ts similarity index 98% rename from src/services/userVolunteer.service.ts rename to src/modules/volunteer/application/services/UserVolunteerService.ts index eadb89b..b578086 100644 --- a/src/services/userVolunteer.service.ts +++ b/src/modules/volunteer/application/services/UserVolunteerService.ts @@ -3,7 +3,7 @@ import { VolunteerAlreadyRegisteredError, VolunteerNotFoundError, VolunteerPositionFullError, -} from "../modules/volunteer/application/errors"; +} from "../../../../modules/volunteer/application/errors"; export class UserVolunteerService { constructor(private prisma: PrismaClient) {} diff --git a/src/services/VolunteerService.ts b/src/modules/volunteer/application/services/VolunteerService.ts similarity index 77% rename from src/services/VolunteerService.ts rename to src/modules/volunteer/application/services/VolunteerService.ts index 56daade..fda88f1 100644 --- a/src/services/VolunteerService.ts +++ b/src/modules/volunteer/application/services/VolunteerService.ts @@ -1,11 +1,11 @@ -import { Volunteer } from "../modules/volunteer/domain/entities/volunteer.entity"; -import { VolunteerPrismaRepository } from "../modules/volunteer/repositories/implementations/volunteer-prisma.repository"; -import { CreateVolunteerUseCase } from "../modules/volunteer/use-cases/create-volunteer.use-case"; -import { GetVolunteersByProjectUseCase } from "../modules/volunteer/use-cases/get-volunteers-by-project.use-case"; +import { Volunteer } from "../../domain/entities/volunteer.entity"; +import { VolunteerPrismaRepository } from "../../repositories/implementations/volunteer-prisma.repository"; +import { CreateVolunteerUseCase } from "../../use-cases/create-volunteer.use-case"; +import { GetVolunteersByProjectUseCase } from "../../use-cases/get-volunteers-by-project.use-case"; import { CreateVolunteerDTO, UpdateVolunteerDTO, -} from "../modules/volunteer/dto/volunteer.dto"; +} from "../../dto/volunteer.dto"; export default class VolunteerService { private volunteerRepository: VolunteerPrismaRepository; diff --git a/src/modules/volunteer/presentation/controllers/VolunteerController.ts b/src/modules/volunteer/presentation/controllers/VolunteerController.ts index 1cdd09b..9174cf0 100644 --- a/src/modules/volunteer/presentation/controllers/VolunteerController.ts +++ b/src/modules/volunteer/presentation/controllers/VolunteerController.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express"; -import VolunteerService from "../../../../services/VolunteerService"; +import VolunteerService from "../../../../modules/volunteer/application/services/VolunteerService"; import { CreateVolunteerDTO } from "../../../../modules/volunteer/dto/volunteer.dto"; export default class VolunteerController { diff --git a/src/modules/wallet/__tests__/WalletAuthIntegration.test.ts b/src/modules/wallet/__tests__/WalletAuthIntegration.test.ts index bbad82e..02c0553 100644 --- a/src/modules/wallet/__tests__/WalletAuthIntegration.test.ts +++ b/src/modules/wallet/__tests__/WalletAuthIntegration.test.ts @@ -1,9 +1,9 @@ -import AuthService from '../../../services/AuthService'; -import { WalletService } from '../services/WalletService'; +import AuthService from "../../../modules/auth/application/services/AuthService"; +import { WalletService } from "../services/WalletService"; // Mock the wallet service -jest.mock('../services/WalletService'); -jest.mock('../../../config/prisma', () => ({ +jest.mock("../services/WalletService"); +jest.mock("../../../config/prisma", () => ({ prisma: { user: { findUnique: jest.fn(), @@ -13,98 +13,107 @@ jest.mock('../../../config/prisma', () => ({ })); // Mock other dependencies -jest.mock('../../../modules/user/repositories/PrismaUserRepository'); -jest.mock('../../../modules/auth/use-cases/send-verification-email.usecase'); -jest.mock('../../../modules/auth/use-cases/verify-email.usecase'); -jest.mock('../../../modules/auth/use-cases/resend-verification-email.usecase'); +jest.mock("../../../modules/user/repositories/PrismaUserRepository"); +jest.mock("../../../modules/auth/use-cases/send-verification-email.usecase"); +jest.mock("../../../modules/auth/use-cases/verify-email.usecase"); +jest.mock("../../../modules/auth/use-cases/resend-verification-email.usecase"); -describe('Wallet Auth Integration', () => { +describe("Wallet Auth Integration", () => { let authService: AuthService; let mockWalletService: jest.Mocked; - const validWalletAddress = 'GCKFBEIYTKP5RDBQMUTAPDCFZDFNVTQNXUCUZMAQYVWLQHTQBDKTQRQY'; - const invalidWalletAddress = 'invalid-wallet'; + const validWalletAddress = + "GCKFBEIYTKP5RDBQMUTAPDCFZDFNVTQNXUCUZMAQYVWLQHTQBDKTQRQY"; + const invalidWalletAddress = "invalid-wallet"; beforeEach(() => { mockWalletService = new WalletService() as jest.Mocked; authService = new AuthService(); - + // Replace the wallet service instance (authService as any).walletService = mockWalletService; - + jest.clearAllMocks(); }); - describe('authenticate', () => { - it('should authenticate user with valid wallet', async () => { - const { prisma } = require('../../../config/prisma'); - + describe("authenticate", () => { + it("should authenticate user with valid wallet", async () => { + const { prisma } = require("../../../config/prisma"); + mockWalletService.isWalletValid.mockResolvedValue(true); prisma.user.findUnique.mockResolvedValue({ - id: 'user-123', + id: "user-123", wallet: validWalletAddress, - email: 'test@example.com', + email: "test@example.com", }); const token = await authService.authenticate(validWalletAddress); - expect(mockWalletService.isWalletValid).toHaveBeenCalledWith(validWalletAddress); + expect(mockWalletService.isWalletValid).toHaveBeenCalledWith( + validWalletAddress + ); expect(prisma.user.findUnique).toHaveBeenCalledWith({ where: { wallet: validWalletAddress }, }); expect(token).toBeDefined(); - expect(typeof token).toBe('string'); + expect(typeof token).toBe("string"); }); - it('should reject authentication with invalid wallet', async () => { + it("should reject authentication with invalid wallet", async () => { mockWalletService.isWalletValid.mockResolvedValue(false); - await expect(authService.authenticate(invalidWalletAddress)) - .rejects.toThrow('Invalid wallet address'); + await expect( + authService.authenticate(invalidWalletAddress) + ).rejects.toThrow("Invalid wallet address"); - expect(mockWalletService.isWalletValid).toHaveBeenCalledWith(invalidWalletAddress); + expect(mockWalletService.isWalletValid).toHaveBeenCalledWith( + invalidWalletAddress + ); }); - it('should reject authentication for non-existent user', async () => { - const { prisma } = require('../../../config/prisma'); - + it("should reject authentication for non-existent user", async () => { + const { prisma } = require("../../../config/prisma"); + mockWalletService.isWalletValid.mockResolvedValue(true); prisma.user.findUnique.mockResolvedValue(null); - await expect(authService.authenticate(validWalletAddress)) - .rejects.toThrow('User not found'); + await expect( + authService.authenticate(validWalletAddress) + ).rejects.toThrow("User not found"); }); }); - describe('register', () => { + describe("register", () => { const registrationData = { - name: 'John', - lastName: 'Doe', - email: 'john@example.com', - password: 'password123', + name: "John", + lastName: "Doe", + email: "john@example.com", + password: "password123", wallet: validWalletAddress, }; - it('should register user with valid wallet', async () => { - const { prisma } = require('../../../config/prisma'); - const mockUserRepository = require('../../../modules/user/repositories/PrismaUserRepository').PrismaUserRepository; - const mockSendEmailUseCase = require('../../../modules/auth/use-cases/send-verification-email.usecase').SendVerificationEmailUseCase; + it("should register user with valid wallet", async () => { + const { prisma } = require("../../../config/prisma"); + const mockUserRepository = + require("../../../modules/user/repositories/PrismaUserRepository").PrismaUserRepository; + const mockSendEmailUseCase = + require("../../../modules/auth/use-cases/send-verification-email.usecase").SendVerificationEmailUseCase; mockWalletService.verifyWallet.mockResolvedValue({ success: true, isValid: true, accountExists: true, walletAddress: validWalletAddress, - message: 'Wallet verified successfully', + message: "Wallet verified successfully", verifiedAt: new Date(), }); const mockUserRepositoryInstance = { findByEmail: jest.fn().mockResolvedValue(null), create: jest.fn().mockResolvedValue({ - id: 'user-123', - name: 'John', - email: 'john@example.com', + id: "user-123", + name: "John", + email: "john@example.com", wallet: validWalletAddress, }), }; @@ -114,7 +123,9 @@ describe('Wallet Auth Integration', () => { }; mockUserRepository.mockImplementation(() => mockUserRepositoryInstance); - mockSendEmailUseCase.mockImplementation(() => mockSendEmailUseCaseInstance); + mockSendEmailUseCase.mockImplementation( + () => mockSendEmailUseCaseInstance + ); prisma.user.findUnique.mockResolvedValue(null); @@ -126,43 +137,50 @@ describe('Wallet Auth Integration', () => { registrationData.wallet ); - expect(mockWalletService.verifyWallet).toHaveBeenCalledWith(validWalletAddress); - expect(result.id).toBe('user-123'); + expect(mockWalletService.verifyWallet).toHaveBeenCalledWith( + validWalletAddress + ); + expect(result.id).toBe("user-123"); expect(result.wallet).toBe(validWalletAddress); expect(result.walletVerified).toBe(true); }); - it('should reject registration with invalid wallet', async () => { + it("should reject registration with invalid wallet", async () => { mockWalletService.verifyWallet.mockResolvedValue({ success: false, isValid: false, accountExists: false, walletAddress: invalidWalletAddress, - message: 'Invalid wallet format', + message: "Invalid wallet format", verifiedAt: new Date(), }); - await expect(authService.register( - registrationData.name, - registrationData.lastName, - registrationData.email, - registrationData.password, + await expect( + authService.register( + registrationData.name, + registrationData.lastName, + registrationData.email, + registrationData.password, + invalidWalletAddress + ) + ).rejects.toThrow("Wallet verification failed: Invalid wallet format"); + + expect(mockWalletService.verifyWallet).toHaveBeenCalledWith( invalidWalletAddress - )).rejects.toThrow('Wallet verification failed: Invalid wallet format'); - - expect(mockWalletService.verifyWallet).toHaveBeenCalledWith(invalidWalletAddress); + ); }); - it('should reject registration with already registered wallet', async () => { - const { prisma } = require('../../../config/prisma'); - const mockUserRepository = require('../../../modules/user/repositories/PrismaUserRepository').PrismaUserRepository; + it("should reject registration with already registered wallet", async () => { + const { prisma } = require("../../../config/prisma"); + const mockUserRepository = + require("../../../modules/user/repositories/PrismaUserRepository").PrismaUserRepository; mockWalletService.verifyWallet.mockResolvedValue({ success: true, isValid: true, accountExists: true, walletAddress: validWalletAddress, - message: 'Wallet verified successfully', + message: "Wallet verified successfully", verifiedAt: new Date(), }); @@ -173,17 +191,19 @@ describe('Wallet Auth Integration', () => { mockUserRepository.mockImplementation(() => mockUserRepositoryInstance); prisma.user.findUnique.mockResolvedValue({ - id: 'existing-user', + id: "existing-user", wallet: validWalletAddress, }); - await expect(authService.register( - registrationData.name, - registrationData.lastName, - registrationData.email, - registrationData.password, - registrationData.wallet - )).rejects.toThrow('This wallet address is already registered'); + await expect( + authService.register( + registrationData.name, + registrationData.lastName, + registrationData.email, + registrationData.password, + registrationData.wallet + ) + ).rejects.toThrow("This wallet address is already registered"); }); }); }); diff --git a/src/scripts/sorobanDemo.ts b/src/scripts/sorobanDemo.ts index 959bd3a..5e39e5a 100644 --- a/src/scripts/sorobanDemo.ts +++ b/src/scripts/sorobanDemo.ts @@ -1,83 +1,86 @@ -import { sorobanService } from '../services/sorobanService'; -import dotenv from 'dotenv'; +import { sorobanService } from "../modules/soroban/application/services/SorobanService"; +import dotenv from "dotenv"; // Load environment variables dotenv.config(); /** * Demo script to demonstrate how to use the SorobanService - * + * * To run this script: * 1. Make sure you have the required environment variables set in your .env file: * SOROBAN_RPC_URL=https://soroban-testnet.stellar.org * SOROBAN_SERVER_SECRET=your_secret_key - * + * * 2. Run the script: * npx ts-node src/scripts/sorobanDemo.ts */ async function sorobanDemo() { try { - console.log('Starting SorobanService demo...'); - + console.log("Starting SorobanService demo..."); + // Example 1: Submit a transaction - console.log('\n--- Example 1: Submit a transaction ---'); + console.log("\n--- Example 1: Submit a transaction ---"); // In a real application, you would generate this XDR from a transaction - const transactionXDR = 'AAAA...'; // Replace with a real XDR-encoded transaction - - console.log('Submitting transaction...'); - const transactionHash = await sorobanService.submitTransaction(transactionXDR); - console.log('Transaction submitted successfully!'); - console.log('Transaction hash:', transactionHash); - + const transactionXDR = "AAAA..."; // Replace with a real XDR-encoded transaction + + console.log("Submitting transaction..."); + const transactionHash = + await sorobanService.submitTransaction(transactionXDR); + console.log("Transaction submitted successfully!"); + console.log("Transaction hash:", transactionHash); + // Example 2: Invoke a contract method (e.g., minting an NFT) - console.log('\n--- Example 2: Invoke a contract method (NFT minting) ---'); - const contractId = 'your-contract-id'; // Replace with your actual contract ID - const methodName = 'mint_nft'; + console.log("\n--- Example 2: Invoke a contract method (NFT minting) ---"); + const contractId = "your-contract-id"; // Replace with your actual contract ID + const methodName = "mint_nft"; const args = [ - 'user-wallet-address', // Replace with a real wallet address - 'metadata-uri' // Replace with a real metadata URI + "user-wallet-address", // Replace with a real wallet address + "metadata-uri", // Replace with a real metadata URI ]; - - console.log('Invoking contract method...'); + + console.log("Invoking contract method..."); console.log(`Contract ID: ${contractId}`); console.log(`Method: ${methodName}`); console.log(`Args: ${JSON.stringify(args)}`); - + const result = await sorobanService.invokeContractMethod( contractId, methodName, args ); - - console.log('Contract method invoked successfully!'); - console.log('Result:', result); - + + console.log("Contract method invoked successfully!"); + console.log("Result:", result); + // Example 3: Invoke a contract method for budget management - console.log('\n--- Example 3: Invoke a contract method (budget management) ---'); - const budgetContractId = 'your-budget-contract-id'; // Replace with your actual contract ID - const budgetMethodName = 'allocate_funds'; + console.log( + "\n--- Example 3: Invoke a contract method (budget management) ---" + ); + const budgetContractId = "your-budget-contract-id"; // Replace with your actual contract ID + const budgetMethodName = "allocate_funds"; const budgetArgs = [ - 'project-id', // Replace with a real project ID - 1000 // amount in stroops + "project-id", // Replace with a real project ID + 1000, // amount in stroops ]; - - console.log('Invoking contract method...'); + + console.log("Invoking contract method..."); console.log(`Contract ID: ${budgetContractId}`); console.log(`Method: ${budgetMethodName}`); console.log(`Args: ${JSON.stringify(budgetArgs)}`); - + const budgetResult = await sorobanService.invokeContractMethod( budgetContractId, budgetMethodName, budgetArgs ); - - console.log('Contract method invoked successfully!'); - console.log('Result:', budgetResult); - - console.log('\nSorobanService demo completed successfully!'); + + console.log("Contract method invoked successfully!"); + console.log("Result:", budgetResult); + + console.log("\nSorobanService demo completed successfully!"); } catch (error) { - console.error('Error in SorobanService demo:', error); + console.error("Error in SorobanService demo:", error); } } @@ -86,9 +89,9 @@ if (require.main === module) { sorobanDemo() .then(() => process.exit(0)) .catch((error) => { - console.error('Unhandled error:', error); + console.error("Unhandled error:", error); process.exit(1); }); } -export { sorobanDemo }; \ No newline at end of file +export { sorobanDemo }; diff --git a/src/services/README.md b/src/services/README.md deleted file mode 100644 index f94eb20..0000000 --- a/src/services/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# SorobanService - -The SorobanService is a reusable service for secure interaction with Soroban smart contracts. It provides a central point for all smart contract operations related to minting and budget management. - -## Features - -- Secure connection to Soroban RPC endpoint -- Transaction submission -- Smart contract method invocation -- Error handling and logging - -## Installation - -1. Install the required dependencies: - -```bash -npm install soroban-client dotenv -``` - -2. Set up your environment variables in your `.env` file: - -``` -SOROBAN_RPC_URL=https://soroban-testnet.stellar.org -SOROBAN_SERVER_SECRET=your_secret_key -``` - -## Usage - -### Basic Usage - -```typescript -import { sorobanService } from '../services/sorobanService'; - -// Submit a transaction -const transactionXDR = 'AAAA...'; // Your XDR-encoded transaction -const transactionHash = await sorobanService.submitTransaction(transactionXDR); -console.log('Transaction hash:', transactionHash); - -// Invoke a contract method -const contractId = 'your-contract-id'; -const methodName = 'mint_nft'; -const args = ['user-wallet-address', 'metadata-uri']; - -const result = await sorobanService.invokeContractMethod( - contractId, - methodName, - args -); -console.log('Contract method result:', result); -``` - -### NFT Minting Example - -```typescript -import { sorobanService } from '../services/sorobanService'; - -async function mintNFT(userWallet: string, metadataURI: string) { - try { - const contractId = 'your-nft-contract-id'; - const result = await sorobanService.invokeContractMethod( - contractId, - 'mint_nft', - [userWallet, metadataURI] - ); - return result; - } catch (error) { - console.error('Failed to mint NFT:', error); - throw error; - } -} -``` - -### Budget Management Example - -```typescript -import { sorobanService } from '../services/sorobanService'; - -async function allocateProjectFunds(projectId: string, amount: number) { - try { - const contractId = 'your-budget-contract-id'; - const result = await sorobanService.invokeContractMethod( - contractId, - 'allocate_funds', - [projectId, amount] - ); - return result; - } catch (error) { - console.error('Failed to allocate funds:', error); - throw error; - } -} -``` - -## Testing - -The SorobanService includes unit tests to ensure it works correctly. To run the tests: - -```bash -npm test -- tests/sorobanService.test.ts -``` - -## Demo - -A demo script is available to demonstrate how to use the SorobanService: - -```bash -npx ts-node src/scripts/sorobanDemo.ts -``` - -## Error Handling - -The SorobanService includes robust error handling. All methods throw errors with descriptive messages when something goes wrong. It's recommended to use try/catch blocks when calling these methods: - -```typescript -try { - const result = await sorobanService.invokeContractMethod( - contractId, - methodName, - args - ); - // Handle success -} catch (error) { - // Handle error - console.error('Error:', error); -} -``` - -## Security Considerations - -- The SorobanService is intended for backend usage only. -- Never expose the SOROBAN_SERVER_SECRET in client-side code. -- Always validate inputs before passing them to the service. -- Consider implementing rate limiting for contract method invocations. \ No newline at end of file diff --git a/src/services/soroban/README.md b/src/services/soroban/README.md deleted file mode 100644 index 8b658de..0000000 --- a/src/services/soroban/README.md +++ /dev/null @@ -1,201 +0,0 @@ -# Soroban Integration - -This directory contains the Soroban integration for the VolunChain backend. The SorobanService provides a secure way to interact with Soroban smart contracts for minting NFTs and managing budgets. - -## Directory Structure - -``` -src/services/soroban/ -ā”œā”€ā”€ README.md # This file -ā”œā”€ā”€ sorobanService.ts # The main service for Soroban interactions -└── demo.ts # Demo script showing how to use the service - -src/config/ -└── soroban.config.ts # Configuration for Soroban - -tests/soroban/ -└── sorobanService.test.ts # Tests for the SorobanService -``` - -## Features - -- Secure connection to Soroban RPC endpoint -- Transaction submission -- Smart contract method invocation -- Error handling and logging - -## Installation - -1. Install the required dependencies: - -```bash -npm install soroban-client dotenv -``` - -2. Set up your environment variables in your `.env` file: - -``` -SOROBAN_RPC_URL=https://soroban-testnet.stellar.org -SOROBAN_SERVER_SECRET=your_secret_key -``` - -## Usage - -### Basic Usage - -```typescript -import { sorobanService } from '../services/soroban/sorobanService'; - -// Submit a transaction -const transactionXDR = 'AAAA...'; // Your XDR-encoded transaction -const transactionHash = await sorobanService.submitTransaction(transactionXDR); -console.log('Transaction hash:', transactionHash); - -// Invoke a contract method -const contractId = 'your-contract-id'; -const methodName = 'mint_nft'; -const args = ['user-wallet-address', 'metadata-uri']; - -const result = await sorobanService.invokeContractMethod( - contractId, - methodName, - args -); -console.log('Contract method result:', result); -``` - -### NFT Minting Example - -```typescript -import { sorobanService } from '../services/soroban/sorobanService'; - -async function mintNFT(userWallet: string, metadataURI: string) { - try { - const contractId = 'your-nft-contract-id'; - const result = await sorobanService.invokeContractMethod( - contractId, - 'mint_nft', - [userWallet, metadataURI] - ); - return result; - } catch (error) { - console.error('Failed to mint NFT:', error); - throw error; - } -} -``` - -### Budget Management Example - -```typescript -import { sorobanService } from '../services/soroban/sorobanService'; - -async function allocateProjectFunds(projectId: string, amount: number) { - try { - const contractId = 'your-budget-contract-id'; - const result = await sorobanService.invokeContractMethod( - contractId, - 'allocate_funds', - [projectId, amount] - ); - return result; - } catch (error) { - console.error('Failed to allocate funds:', error); - throw error; - } -} -``` - -## Testing - -The SorobanService includes unit tests to ensure it works correctly. To run the tests: - -```bash -npm test -- tests/soroban/sorobanService.test.ts -``` - -## Demo - -A demo script is available to demonstrate how to use the SorobanService: - -```bash -npx ts-node src/services/soroban/demo.ts -``` - -## Error Handling - -The SorobanService includes robust error handling. All methods throw errors with descriptive messages when something goes wrong. It's recommended to use try/catch blocks when calling these methods: - -```typescript -try { - const result = await sorobanService.invokeContractMethod( - contractId, - methodName, - args - ); - // Handle success -} catch (error) { - // Handle error - console.error('Error:', error); -} -``` - -## Security Considerations - -- The SorobanService is intended for backend usage only. -- Never expose the SOROBAN_SERVER_SECRET in client-side code. -- Always validate inputs before passing them to the service. -- Consider implementing rate limiting for contract method invocations. - -## Contributing - -### Adding New Features - -When adding new features to the SorobanService: - -1. Update the `sorobanService.ts` file with your new methods -2. Add tests for your new methods in `tests/soroban/sorobanService.test.ts` -3. Update the demo script in `src/services/soroban/demo.ts` to showcase your new features -4. Update this README with documentation for your new features - -### Modifying Existing Features - -When modifying existing features: - -1. Make sure to maintain backward compatibility -2. Update tests to reflect your changes -3. Update the demo script if necessary -4. Update this README if the usage has changed - -### Testing Your Changes - -Always test your changes thoroughly: - -1. Run the unit tests: `npm test -- tests/soroban/sorobanService.test.ts` -2. Run the demo script: `npx ts-node src/services/soroban/demo.ts` -3. Test in a development environment before deploying to production - -## Troubleshooting - -### Common Issues - -1. **Environment Variables Not Set** - - Make sure you have set the required environment variables in your `.env` file - - Check that the variables are being loaded correctly - -2. **Connection Issues** - - Verify that the SOROBAN_RPC_URL is correct and accessible - - Check your network connection - -3. **Contract Method Invocation Failures** - - Verify that the contract ID is correct - - Check that the method name and arguments match the contract's interface - - Look for error messages in the console - -### Getting Help - -If you encounter issues not covered here: - -1. Check the [Soroban documentation](https://soroban.stellar.org/docs) -2. Look for similar issues in the project's issue tracker -3. Ask for help in the project's communication channels \ No newline at end of file diff --git a/src/services/soroban/demo.ts b/src/services/soroban/demo.ts deleted file mode 100644 index c8c95a8..0000000 --- a/src/services/soroban/demo.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { sorobanService } from './sorobanService'; -import dotenv from 'dotenv'; - -// Load environment variables -dotenv.config(); - -/** - * Demo script to demonstrate how to use the SorobanService - * - * To run this script: - * 1. Make sure you have the required environment variables set in your .env file: - * SOROBAN_RPC_URL=https://soroban-testnet.stellar.org - * SOROBAN_SERVER_SECRET=your_secret_key - * - * 2. Run the script: - * npx ts-node src/services/soroban/demo.ts - */ -async function sorobanDemo() { - try { - console.log('Starting SorobanService demo...'); - - // Example 1: Submit a transaction - console.log('\n--- Example 1: Submit a transaction ---'); - // In a real application, you would generate this XDR from a transaction - const transactionXDR = 'AAAA...'; // Replace with a real XDR-encoded transaction - - console.log('Submitting transaction...'); - const transactionHash = await sorobanService.submitTransaction(transactionXDR); - console.log('Transaction submitted successfully!'); - console.log('Transaction hash:', transactionHash); - - // Example 2: Invoke a contract method (e.g., minting an NFT) - console.log('\n--- Example 2: Invoke a contract method (NFT minting) ---'); - const contractId = 'your-contract-id'; // Replace with your actual contract ID - const methodName = 'mint_nft'; - const args = [ - 'user-wallet-address', // Replace with a real wallet address - 'metadata-uri' // Replace with a real metadata URI - ]; - - console.log('Invoking contract method...'); - console.log(`Contract ID: ${contractId}`); - console.log(`Method: ${methodName}`); - console.log(`Args: ${JSON.stringify(args)}`); - - const result = await sorobanService.invokeContractMethod( - contractId, - methodName, - args - ); - - console.log('Contract method invoked successfully!'); - console.log('Result:', result); - - // Example 3: Invoke a contract method for budget management - console.log('\n--- Example 3: Invoke a contract method (budget management) ---'); - const budgetContractId = 'your-budget-contract-id'; // Replace with your actual contract ID - const budgetMethodName = 'allocate_funds'; - const budgetArgs = [ - 'project-id', // Replace with a real project ID - 1000 // amount in stroops - ]; - - console.log('Invoking contract method...'); - console.log(`Contract ID: ${budgetContractId}`); - console.log(`Method: ${budgetMethodName}`); - console.log(`Args: ${JSON.stringify(budgetArgs)}`); - - const budgetResult = await sorobanService.invokeContractMethod( - budgetContractId, - budgetMethodName, - budgetArgs - ); - - console.log('Contract method invoked successfully!'); - console.log('Result:', budgetResult); - - console.log('\nSorobanService demo completed successfully!'); - } catch (error) { - console.error('Error in SorobanService demo:', error); - } -} - -// Run the demo if this script is executed directly -if (require.main === module) { - sorobanDemo() - .then(() => process.exit(0)) - .catch((error) => { - console.error('Unhandled error:', error); - process.exit(1); - }); -} - -export { sorobanDemo }; \ No newline at end of file diff --git a/src/services/soroban/index.ts b/src/services/soroban/index.ts deleted file mode 100644 index 3674a24..0000000 --- a/src/services/soroban/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Soroban Integration - * - * This module provides a service for interacting with Soroban smart contracts. - * It handles transaction submission and contract method invocation. - */ - -export { sorobanService } from './sorobanService'; -export { sorobanDemo } from './demo'; \ No newline at end of file diff --git a/src/services/sorobanService.ts b/src/services/sorobanService.ts deleted file mode 100644 index b9ada66..0000000 --- a/src/services/sorobanService.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Server, Transaction, xdr } from 'soroban-client'; -import { sorobanConfig } from '../config/soroban.config'; - -export class SorobanService { - private server: Server; - private serverSecret: string; - - constructor() { - this.server = new Server(sorobanConfig.rpcUrl); - // Ensure serverSecret is not undefined - if (!sorobanConfig.serverSecret) { - throw new Error('SOROBAN_SERVER_SECRET is required'); - } - this.serverSecret = sorobanConfig.serverSecret; - } - - /** - * Submits a transaction to the Soroban network - * @param transactionXDR - The XDR-encoded transaction - * @returns Promise - The transaction hash - */ - async submitTransaction(transactionXDR: string): Promise { - try { - // Parse the XDR string into a Transaction object - const transaction = new Transaction(transactionXDR, this.serverSecret); - const response = await this.server.sendTransaction(transaction); - return response.hash; - } catch (error: any) { - console.error('Error submitting transaction:', error); - throw new Error(`Failed to submit transaction: ${error.message}`); - } - } - - /** - * Invokes a method on a Soroban smart contract - * @param contractId - The ID of the smart contract - * @param methodName - The name of the method to invoke - * @param args - Array of arguments to pass to the method - * @returns Promise - The result of the contract method invocation - */ - async invokeContractMethod( - contractId: string, - methodName: string, - args: any[] - ): Promise { - try { - // Note: The actual method name may vary depending on the soroban-client version - // This is a placeholder - you'll need to check the actual API documentation - // or inspect the Server object to find the correct method - - // Example implementation - adjust based on actual API - // @ts-ignore - Ignoring type checking until we know the exact API - const result = await this.server.invokeContract( - contractId, - methodName, - args - ); - return result; - } catch (error: any) { - console.error('Error invoking contract method:', error); - throw new Error( - `Failed to invoke contract method ${methodName}: ${error.message}` - ); - } - } -} - -// Export a singleton instance -export const sorobanService = new SorobanService(); \ No newline at end of file diff --git a/src/tests/sorobanService.test.ts b/src/tests/sorobanService.test.ts index 63f8dfc..16fb41a 100644 --- a/src/tests/sorobanService.test.ts +++ b/src/tests/sorobanService.test.ts @@ -1,12 +1,16 @@ -import { sorobanService } from '../services/sorobanService'; +import { sorobanService } from "../modules/soroban/application/services/SorobanService"; // Mock the soroban-client to avoid actual network calls during tests -jest.mock('soroban-client', () => { +jest.mock("soroban-client", () => { return { Server: jest.fn().mockImplementation(() => { return { - sendTransaction: jest.fn().mockResolvedValue({ hash: 'mock-transaction-hash' }), - invokeContract: jest.fn().mockResolvedValue({ result: 'mock-contract-result' }), + sendTransaction: jest + .fn() + .mockResolvedValue({ hash: "mock-transaction-hash" }), + invokeContract: jest + .fn() + .mockResolvedValue({ result: "mock-contract-result" }), }; }), Transaction: jest.fn().mockImplementation(() => { @@ -16,59 +20,65 @@ jest.mock('soroban-client', () => { }; }); -describe('SorobanService', () => { - describe('submitTransaction', () => { - it('should submit a transaction and return the hash', async () => { - const mockTransactionXDR = 'mock-transaction-xdr'; +describe("SorobanService", () => { + describe("submitTransaction", () => { + it("should submit a transaction and return the hash", async () => { + const mockTransactionXDR = "mock-transaction-xdr"; const result = await sorobanService.submitTransaction(mockTransactionXDR); - - expect(result).toBe('mock-transaction-hash'); + + expect(result).toBe("mock-transaction-hash"); }); - - it('should handle errors when submitting a transaction', async () => { + + it("should handle errors when submitting a transaction", async () => { // Mock the sendTransaction method to throw an error - const mockServer = require('soroban-client').Server.mock.results[0].value; - mockServer.sendTransaction.mockRejectedValueOnce(new Error('Transaction failed')); - - const mockTransactionXDR = 'mock-transaction-xdr'; - - await expect(sorobanService.submitTransaction(mockTransactionXDR)) - .rejects - .toThrow('Failed to submit transaction: Transaction failed'); + const mockServer = require("soroban-client").Server.mock.results[0].value; + mockServer.sendTransaction.mockRejectedValueOnce( + new Error("Transaction failed") + ); + + const mockTransactionXDR = "mock-transaction-xdr"; + + await expect( + sorobanService.submitTransaction(mockTransactionXDR) + ).rejects.toThrow("Failed to submit transaction: Transaction failed"); }); }); - - describe('invokeContractMethod', () => { - it('should invoke a contract method and return the result', async () => { - const mockContractId = 'mock-contract-id'; - const mockMethodName = 'mock-method'; - const mockArgs = ['arg1', 'arg2']; - + + describe("invokeContractMethod", () => { + it("should invoke a contract method and return the result", async () => { + const mockContractId = "mock-contract-id"; + const mockMethodName = "mock-method"; + const mockArgs = ["arg1", "arg2"]; + const result = await sorobanService.invokeContractMethod( mockContractId, mockMethodName, mockArgs ); - - expect(result).toEqual({ result: 'mock-contract-result' }); + + expect(result).toEqual({ result: "mock-contract-result" }); }); - - it('should handle errors when invoking a contract method', async () => { + + it("should handle errors when invoking a contract method", async () => { // Mock the invokeContract method to throw an error - const mockServer = require('soroban-client').Server.mock.results[0].value; - mockServer.invokeContract.mockRejectedValueOnce(new Error('Contract invocation failed')); - - const mockContractId = 'mock-contract-id'; - const mockMethodName = 'mock-method'; - const mockArgs = ['arg1', 'arg2']; - - await expect(sorobanService.invokeContractMethod( - mockContractId, - mockMethodName, - mockArgs - )) - .rejects - .toThrow('Failed to invoke contract method mock-method: Contract invocation failed'); + const mockServer = require("soroban-client").Server.mock.results[0].value; + mockServer.invokeContract.mockRejectedValueOnce( + new Error("Contract invocation failed") + ); + + const mockContractId = "mock-contract-id"; + const mockMethodName = "mock-method"; + const mockArgs = ["arg1", "arg2"]; + + await expect( + sorobanService.invokeContractMethod( + mockContractId, + mockMethodName, + mockArgs + ) + ).rejects.toThrow( + "Failed to invoke contract method mock-method: Contract invocation failed" + ); }); }); -}); \ No newline at end of file +}); diff --git a/src/utils/db-monitor.ts b/src/utils/db-monitor.ts index 0debc4e..60af74e 100644 --- a/src/utils/db-monitor.ts +++ b/src/utils/db-monitor.ts @@ -20,7 +20,6 @@ export class DatabaseMonitor { private setupQueryLogging() { if (process.env.ENABLE_QUERY_LOGGING === "true") { - // @ts-expect-error Prisma types don't include query event this.prisma.$on("query", (e: QueryEvent) => { const startTime = performance.now(); const queryId = `${e.query}${Date.now()}`; diff --git a/tests/Photo.test.ts b/tests/Photo.test.ts index b5c7257..fe9d073 100644 --- a/tests/Photo.test.ts +++ b/tests/Photo.test.ts @@ -1,14 +1,14 @@ -import { Photo } from '../src/entities/Photo'; +import { Photo } from "../src/entities/Photo"; -describe('Photo Entity', () => { - describe('constructor', () => { - it('should create a valid photo instance with all properties', () => { +describe("Photo Entity", () => { + describe("constructor", () => { + it("should create a valid photo instance with all properties", () => { const photoProps = { - id: '123e4567-e89b-12d3-a456-426614174000', - url: 'https://example.com/photo.jpg', - userId: '123e4567-e89b-12d3-a456-426614174001', + id: "123e4567-e89b-12d3-a456-426614174000", + url: "https://example.com/photo.jpg", + userId: "123e4567-e89b-12d3-a456-426614174001", uploadedAt: new Date(), - metadata: { size: '1024kb', format: 'jpg' } + metadata: { size: "1024kb", format: "jpg" }, }; const photo = new Photo(photoProps); @@ -20,82 +20,82 @@ describe('Photo Entity', () => { expect(photo.metadata).toEqual(photoProps.metadata); }); - it('should create an empty photo instance when no props are provided', () => { - const photo = new Photo(); - + it("should create an empty photo instance when no props are provided", () => { + const photo = new Photo({}); + expect(photo.id).toBeUndefined(); expect(photo.url).toBeUndefined(); expect(photo.userId).toBeUndefined(); expect(photo.uploadedAt).toBeUndefined(); - expect(photo.metadata).toBeUndefined(); + expect(photo.metadata).toEqual({}); }); }); - describe('validate', () => { - it('should validate a photo with all required fields', () => { + describe("validate", () => { + it("should validate a photo with all required fields", () => { const photo = new Photo({ - url: 'https://example.com/photo.jpg', - userId: '123e4567-e89b-12d3-a456-426614174001' + url: "https://example.com/photo.jpg", + userId: "123e4567-e89b-12d3-a456-426614174001", }); expect(photo.validate()).toBe(true); }); - it('should throw an error when url is missing', () => { + it("should throw an error when url is missing", () => { const photo = new Photo({ - userId: '123e4567-e89b-12d3-a456-426614174001' + userId: "123e4567-e89b-12d3-a456-426614174001", }); - expect(() => photo.validate()).toThrow('Photo URL is required'); + expect(() => photo.validate()).toThrow("Photo URL is required"); }); - it('should throw an error when url is invalid', () => { + it("should throw an error when url is invalid", () => { const photo = new Photo({ - url: 'invalid-url', - userId: '123e4567-e89b-12d3-a456-426614174001' + url: "invalid-url", + userId: "123e4567-e89b-12d3-a456-426614174001", }); - expect(() => photo.validate()).toThrow('Photo URL is invalid'); + expect(() => photo.validate()).toThrow("Photo URL is invalid"); }); - it('should throw an error when userId is missing', () => { + it("should throw an error when userId is missing", () => { const photo = new Photo({ - url: 'https://example.com/photo.jpg' + url: "https://example.com/photo.jpg", }); - expect(() => photo.validate()).toThrow('User ID is required'); + expect(() => photo.validate()).toThrow("User ID is required"); }); }); - describe('updateMetadata', () => { - it('should merge new metadata with existing metadata', () => { + describe("updateMetadata", () => { + it("should merge new metadata with existing metadata", () => { const photo = new Photo({ - url: 'https://example.com/photo.jpg', - userId: '123e4567-e89b-12d3-a456-426614174001', - metadata: { size: '1024kb', format: 'jpg' } + url: "https://example.com/photo.jpg", + userId: "123e4567-e89b-12d3-a456-426614174001", + metadata: { size: "1024kb", format: "jpg" }, }); photo.updateMetadata({ width: 800, height: 600 }); expect(photo.metadata).toEqual({ - size: '1024kb', - format: 'jpg', + size: "1024kb", + format: "jpg", width: 800, - height: 600 + height: 600, }); }); - it('should create metadata object if it was undefined', () => { + it("should create metadata object if it was undefined", () => { const photo = new Photo({ - url: 'https://example.com/photo.jpg', - userId: '123e4567-e89b-12d3-a456-426614174001' + url: "https://example.com/photo.jpg", + userId: "123e4567-e89b-12d3-a456-426614174001", }); photo.updateMetadata({ width: 800, height: 600 }); expect(photo.metadata).toEqual({ width: 800, - height: 600 + height: 600, }); }); }); diff --git a/tests/logging/winston-logger.test.ts b/tests/logging/winston-logger.test.ts index 02f3bd3..f734127 100644 --- a/tests/logging/winston-logger.test.ts +++ b/tests/logging/winston-logger.test.ts @@ -1,8 +1,9 @@ import { Request } from 'express'; -import { LoggerService, createLogger } from '../../src/services/logger.service'; +import { LoggerService, createLogger } from '../../src/modules/shared/application/services/LoggerService'; import { traceIdMiddleware, getTraceId } from '../../src/middlewares/traceId.middleware'; import fs from 'fs'; import path from 'path'; +import { describe, it, expect, beforeEach, jest } from '@jest/globals'; // Mock Winston logger jest.mock('../../src/config/winston.config', () => ({ diff --git a/tests/middlewares/errorHandler.test.ts b/tests/middlewares/errorHandler.test.ts index c390f77..b3f9e31 100644 --- a/tests/middlewares/errorHandler.test.ts +++ b/tests/middlewares/errorHandler.test.ts @@ -19,6 +19,9 @@ describe("Error Handler Middleware", () => { method: "GET", body: {}, query: {}, + url: "/test", + ip: "127.0.0.1", + get: jest.fn().mockReturnValue("test-user-agent"), }; mockResponse = { status: jest.fn().mockReturnThis(), @@ -39,9 +42,8 @@ describe("Error Handler Middleware", () => { expect(mockResponse.status).toHaveBeenCalledWith(400); expect(mockResponse.json).toHaveBeenCalledWith({ - statusCode: 400, + code: "VALIDATION_ERROR", message: "Invalid input", - errorCode: "VALIDATION_ERROR", details: { field: "username" }, }); }); @@ -58,9 +60,8 @@ describe("Error Handler Middleware", () => { expect(mockResponse.status).toHaveBeenCalledWith(401); expect(mockResponse.json).toHaveBeenCalledWith({ - statusCode: 401, + code: "AUTHENTICATION_ERROR", message: "Invalid credentials", - errorCode: "AUTHENTICATION_ERROR", }); }); @@ -76,9 +77,8 @@ describe("Error Handler Middleware", () => { expect(mockResponse.status).toHaveBeenCalledWith(404); expect(mockResponse.json).toHaveBeenCalledWith({ - statusCode: 404, + code: "RESOURCE_NOT_FOUND", message: "User not found", - errorCode: "RESOURCE_NOT_FOUND", }); }); @@ -104,7 +104,15 @@ describe("Error Handler Middleware", () => { const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = "development"; - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + // Mock Winston logger + const mockWinstonLogger = { + error: jest.fn(), + }; + + jest.doMock("../../src/config/winston.config", () => ({ + logger: mockWinstonLogger, + })); + const error = new Error("Test error"); error.stack = "Test stack trace"; @@ -115,15 +123,18 @@ describe("Error Handler Middleware", () => { nextFunction ); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("stack")); - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining("requestBody") - ); - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining("requestQuery") + expect(mockWinstonLogger.error).toHaveBeenCalledWith( + "Unhandled error occurred", + error, + mockRequest, + expect.objectContaining({ + path: "/test", + method: "GET", + errorType: "Error", + }) ); process.env.NODE_ENV = originalEnv; - consoleSpy.mockRestore(); + jest.dontMock("../../src/config/winston.config"); }); }); diff --git a/tests/prisma/testprisma.test.ts b/tests/prisma/testprisma.test.ts index fb056ba..c5b73e9 100644 --- a/tests/prisma/testprisma.test.ts +++ b/tests/prisma/testprisma.test.ts @@ -1,43 +1,44 @@ -import { PrismaClient, users } from '@prisma/client'; +import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); async function main() { - const newUser: users = await prisma.users.create({ + const newUser = await prisma.user.create({ data: { - username: 'john_doe', - email: 'john@example.com', - password_hash: 'hashed_password', - role: 'user' + name: "john_doe", + email: "john@example.com", + password: "hashed_password", + wallet: "test-wallet", + isVerified: false, }, }); - console.log('New user created:', newUser); + console.log("New user created:", newUser); - const allUsers: users[] = await prisma.users.findMany(); - console.log('All users:', allUsers); + const allUsers = await prisma.user.findMany(); + console.log("All users:", allUsers); - const userByEmail: users | null = await prisma.users.findUnique({ + const userByEmail = await prisma.user.findUnique({ where: { - email: 'john@example.com' + email: "john@example.com", }, }); - console.log('User found by email:', userByEmail); + console.log("User found by email:", userByEmail); - const updatedUser: users = await prisma.users.update({ + const updatedUser = await prisma.user.update({ where: { id: newUser.id, }, data: { - email: 'john_updated@example.com' + email: "john_updated@example.com", }, }); - console.log('User updated:', updatedUser); + console.log("User updated:", updatedUser); - const deletedUser: users = await prisma.users.delete({ + const deletedUser = await prisma.user.delete({ where: { id: newUser.id, }, }); - console.log('User deleted:', deletedUser); + console.log("User deleted:", deletedUser); } main() diff --git a/tests/soroban/sorobanService.test.ts b/tests/soroban/sorobanService.test.ts index cfb4e7e..28c2741 100644 --- a/tests/soroban/sorobanService.test.ts +++ b/tests/soroban/sorobanService.test.ts @@ -1,12 +1,16 @@ -import { sorobanService } from '../../src/services/soroban/sorobanService'; +import { sorobanService } from "../../src/modules/soroban/application/services/SorobanService"; // Mock the soroban-client to avoid actual network calls during tests -jest.mock('soroban-client', () => { +jest.mock("soroban-client", () => { return { Server: jest.fn().mockImplementation(() => { return { - sendTransaction: jest.fn().mockResolvedValue({ hash: 'mock-transaction-hash' }), - invokeContract: jest.fn().mockResolvedValue({ result: 'mock-contract-result' }), + sendTransaction: jest + .fn() + .mockResolvedValue({ hash: "mock-transaction-hash" }), + invokeContract: jest + .fn() + .mockResolvedValue({ result: "mock-contract-result" }), }; }), Transaction: jest.fn().mockImplementation(() => { @@ -17,68 +21,74 @@ jest.mock('soroban-client', () => { }); // Mock the config to avoid environment variable issues -jest.mock('../../src/config/soroban.config', () => { +jest.mock("../../src/config/soroban.config", () => { return { sorobanConfig: { - rpcUrl: 'https://mock-soroban-rpc-url', - serverSecret: 'mock-server-secret', + rpcUrl: "https://mock-soroban-rpc-url", + serverSecret: "mock-server-secret", }, }; }); -describe('SorobanService', () => { - describe('submitTransaction', () => { - it('should submit a transaction and return the hash', async () => { - const mockTransactionXDR = 'mock-transaction-xdr'; +describe("SorobanService", () => { + describe("submitTransaction", () => { + it("should submit a transaction and return the hash", async () => { + const mockTransactionXDR = "mock-transaction-xdr"; const result = await sorobanService.submitTransaction(mockTransactionXDR); - - expect(result).toBe('mock-transaction-hash'); + + expect(result).toBe("mock-transaction-hash"); }); - - it('should handle errors when submitting a transaction', async () => { + + it("should handle errors when submitting a transaction", async () => { // Mock the sendTransaction method to throw an error - const mockServer = require('soroban-client').Server.mock.results[0].value; - mockServer.sendTransaction.mockRejectedValueOnce(new Error('Transaction failed')); - - const mockTransactionXDR = 'mock-transaction-xdr'; - - await expect(sorobanService.submitTransaction(mockTransactionXDR)) - .rejects - .toThrow('Failed to submit transaction: Transaction failed'); + const mockServer = require("soroban-client").Server.mock.results[0].value; + mockServer.sendTransaction.mockRejectedValueOnce( + new Error("Transaction failed") + ); + + const mockTransactionXDR = "mock-transaction-xdr"; + + await expect( + sorobanService.submitTransaction(mockTransactionXDR) + ).rejects.toThrow("Failed to submit transaction: Transaction failed"); }); }); - - describe('invokeContractMethod', () => { - it('should invoke a contract method and return the result', async () => { - const mockContractId = 'mock-contract-id'; - const mockMethodName = 'mock-method'; - const mockArgs = ['arg1', 'arg2']; - + + describe("invokeContractMethod", () => { + it("should invoke a contract method and return the result", async () => { + const mockContractId = "mock-contract-id"; + const mockMethodName = "mock-method"; + const mockArgs = ["arg1", "arg2"]; + const result = await sorobanService.invokeContractMethod( mockContractId, mockMethodName, mockArgs ); - - expect(result).toEqual({ result: 'mock-contract-result' }); + + expect(result).toEqual({ result: "mock-contract-result" }); }); - - it('should handle errors when invoking a contract method', async () => { + + it("should handle errors when invoking a contract method", async () => { // Mock the invokeContract method to throw an error - const mockServer = require('soroban-client').Server.mock.results[0].value; - mockServer.invokeContract.mockRejectedValueOnce(new Error('Contract invocation failed')); - - const mockContractId = 'mock-contract-id'; - const mockMethodName = 'mock-method'; - const mockArgs = ['arg1', 'arg2']; - - await expect(sorobanService.invokeContractMethod( - mockContractId, - mockMethodName, - mockArgs - )) - .rejects - .toThrow('Failed to invoke contract method mock-method: Contract invocation failed'); + const mockServer = require("soroban-client").Server.mock.results[0].value; + mockServer.invokeContract.mockRejectedValueOnce( + new Error("Contract invocation failed") + ); + + const mockContractId = "mock-contract-id"; + const mockMethodName = "mock-method"; + const mockArgs = ["arg1", "arg2"]; + + await expect( + sorobanService.invokeContractMethod( + mockContractId, + mockMethodName, + mockArgs + ) + ).rejects.toThrow( + "Failed to invoke contract method mock-method: Contract invocation failed" + ); }); }); -}); \ No newline at end of file +}); diff --git a/tests/sorobanService.test.ts b/tests/sorobanService.test.ts index 2d98150..e468178 100644 --- a/tests/sorobanService.test.ts +++ b/tests/sorobanService.test.ts @@ -1,12 +1,16 @@ -import { sorobanService } from '../src/services/sorobanService'; +import { sorobanService } from "../src/modules/soroban/application/services/SorobanService"; // Mock the soroban-client to avoid actual network calls during tests -jest.mock('soroban-client', () => { +jest.mock("soroban-client", () => { return { Server: jest.fn().mockImplementation(() => { return { - sendTransaction: jest.fn().mockResolvedValue({ hash: 'mock-transaction-hash' }), - invokeContract: jest.fn().mockResolvedValue({ result: 'mock-contract-result' }), + sendTransaction: jest + .fn() + .mockResolvedValue({ hash: "mock-transaction-hash" }), + invokeContract: jest + .fn() + .mockResolvedValue({ result: "mock-contract-result" }), }; }), Transaction: jest.fn().mockImplementation(() => { @@ -17,68 +21,74 @@ jest.mock('soroban-client', () => { }); // Mock the config to avoid environment variable issues -jest.mock('../src/config/soroban.config', () => { +jest.mock("../src/config/soroban.config", () => { return { sorobanConfig: { - rpcUrl: 'https://mock-soroban-rpc-url', - serverSecret: 'mock-server-secret', + rpcUrl: "https://mock-soroban-rpc-url", + serverSecret: "mock-server-secret", }, }; }); -describe('SorobanService', () => { - describe('submitTransaction', () => { - it('should submit a transaction and return the hash', async () => { - const mockTransactionXDR = 'mock-transaction-xdr'; +describe("SorobanService", () => { + describe("submitTransaction", () => { + it("should submit a transaction and return the hash", async () => { + const mockTransactionXDR = "mock-transaction-xdr"; const result = await sorobanService.submitTransaction(mockTransactionXDR); - - expect(result).toBe('mock-transaction-hash'); + + expect(result).toBe("mock-transaction-hash"); }); - - it('should handle errors when submitting a transaction', async () => { + + it("should handle errors when submitting a transaction", async () => { // Mock the sendTransaction method to throw an error - const mockServer = require('soroban-client').Server.mock.results[0].value; - mockServer.sendTransaction.mockRejectedValueOnce(new Error('Transaction failed')); - - const mockTransactionXDR = 'mock-transaction-xdr'; - - await expect(sorobanService.submitTransaction(mockTransactionXDR)) - .rejects - .toThrow('Failed to submit transaction: Transaction failed'); + const mockServer = require("soroban-client").Server.mock.results[0].value; + mockServer.sendTransaction.mockRejectedValueOnce( + new Error("Transaction failed") + ); + + const mockTransactionXDR = "mock-transaction-xdr"; + + await expect( + sorobanService.submitTransaction(mockTransactionXDR) + ).rejects.toThrow("Failed to submit transaction: Transaction failed"); }); }); - - describe('invokeContractMethod', () => { - it('should invoke a contract method and return the result', async () => { - const mockContractId = 'mock-contract-id'; - const mockMethodName = 'mock-method'; - const mockArgs = ['arg1', 'arg2']; - + + describe("invokeContractMethod", () => { + it("should invoke a contract method and return the result", async () => { + const mockContractId = "mock-contract-id"; + const mockMethodName = "mock-method"; + const mockArgs = ["arg1", "arg2"]; + const result = await sorobanService.invokeContractMethod( mockContractId, mockMethodName, mockArgs ); - - expect(result).toEqual({ result: 'mock-contract-result' }); + + expect(result).toEqual({ result: "mock-contract-result" }); }); - - it('should handle errors when invoking a contract method', async () => { + + it("should handle errors when invoking a contract method", async () => { // Mock the invokeContract method to throw an error - const mockServer = require('soroban-client').Server.mock.results[0].value; - mockServer.invokeContract.mockRejectedValueOnce(new Error('Contract invocation failed')); - - const mockContractId = 'mock-contract-id'; - const mockMethodName = 'mock-method'; - const mockArgs = ['arg1', 'arg2']; - - await expect(sorobanService.invokeContractMethod( - mockContractId, - mockMethodName, - mockArgs - )) - .rejects - .toThrow('Failed to invoke contract method mock-method: Contract invocation failed'); + const mockServer = require("soroban-client").Server.mock.results[0].value; + mockServer.invokeContract.mockRejectedValueOnce( + new Error("Contract invocation failed") + ); + + const mockContractId = "mock-contract-id"; + const mockMethodName = "mock-method"; + const mockArgs = ["arg1", "arg2"]; + + await expect( + sorobanService.invokeContractMethod( + mockContractId, + mockMethodName, + mockArgs + ) + ).rejects.toThrow( + "Failed to invoke contract method mock-method: Contract invocation failed" + ); }); }); -}); \ No newline at end of file +}); diff --git a/tests/wallet/WalletAuthIntegration.test.ts b/tests/wallet/WalletAuthIntegration.test.ts index eb92170..5a95fbc 100644 --- a/tests/wallet/WalletAuthIntegration.test.ts +++ b/tests/wallet/WalletAuthIntegration.test.ts @@ -1,9 +1,9 @@ -import AuthService from '../../src/services/AuthService'; -import { WalletService } from '../../src/modules/wallet/services/WalletService'; +import AuthService from "../../src/modules/auth/application/services/AuthService"; +import { WalletService } from "../../src/modules/wallet/services/WalletService"; // Mock the wallet service -jest.mock('../../src/modules/wallet/services/WalletService'); -jest.mock('../../src/config/prisma', () => ({ +jest.mock("../../src/modules/wallet/services/WalletService"); +jest.mock("../../src/config/prisma", () => ({ prisma: { user: { findUnique: jest.fn(), @@ -13,17 +13,18 @@ jest.mock('../../src/config/prisma', () => ({ })); // Mock other dependencies -jest.mock('../../src/modules/user/repositories/PrismaUserRepository'); -jest.mock('../../src/modules/auth/use-cases/send-verification-email.usecase'); -jest.mock('../../src/modules/auth/use-cases/verify-email.usecase'); -jest.mock('../../src/modules/auth/use-cases/resend-verification-email.usecase'); +jest.mock("../../src/modules/user/repositories/PrismaUserRepository"); +jest.mock("../../src/modules/auth/use-cases/send-verification-email.usecase"); +jest.mock("../../src/modules/auth/use-cases/verify-email.usecase"); +jest.mock("../../src/modules/auth/use-cases/resend-verification-email.usecase"); -describe('Wallet Auth Integration', () => { +describe("Wallet Auth Integration", () => { let authService: AuthService; let mockWalletService: jest.Mocked; - const validWalletAddress = 'GCDCSYJ2SPFW4NEJP2RDSINPTMJMUIFNQ5D2DZ52IGI4W2PAJTB5VQ42'; - const invalidWalletAddress = 'invalid-wallet'; + const validWalletAddress = + "GCDCSYJ2SPFW4NEJP2RDSINPTMJMUIFNQ5D2DZ52IGI4W2PAJTB5VQ42"; + const invalidWalletAddress = "invalid-wallet"; beforeEach(() => { // Create mock instances @@ -50,15 +51,23 @@ describe('Wallet Auth Integration', () => { }; // Mock the constructors to return our mock instances - const mockUserRepository = require('../../src/modules/user/repositories/PrismaUserRepository').PrismaUserRepository; - const mockSendEmailUseCase = require('../../src/modules/auth/use-cases/send-verification-email.usecase').SendVerificationEmailUseCase; - const mockVerifyEmailUseCase = require('../../src/modules/auth/use-cases/verify-email.usecase').VerifyEmailUseCase; - const mockResendEmailUseCase = require('../../src/modules/auth/use-cases/resend-verification-email.usecase').ResendVerificationEmailUseCase; + const mockUserRepository = + require("../../src/modules/user/repositories/PrismaUserRepository").PrismaUserRepository; + const mockSendEmailUseCase = + require("../../src/modules/auth/use-cases/send-verification-email.usecase").SendVerificationEmailUseCase; + const mockVerifyEmailUseCase = + require("../../src/modules/auth/use-cases/verify-email.usecase").VerifyEmailUseCase; + const mockResendEmailUseCase = + require("../../src/modules/auth/use-cases/resend-verification-email.usecase").ResendVerificationEmailUseCase; mockUserRepository.mockImplementation(() => mockUserRepositoryInstance); mockSendEmailUseCase.mockImplementation(() => mockSendEmailUseCaseInstance); - mockVerifyEmailUseCase.mockImplementation(() => mockVerifyEmailUseCaseInstance); - mockResendEmailUseCase.mockImplementation(() => mockResendEmailUseCaseInstance); + mockVerifyEmailUseCase.mockImplementation( + () => mockVerifyEmailUseCaseInstance + ); + mockResendEmailUseCase.mockImplementation( + () => mockResendEmailUseCaseInstance + ); mockWalletService = new WalletService() as jest.Mocked; authService = new AuthService(); @@ -69,58 +78,64 @@ describe('Wallet Auth Integration', () => { jest.clearAllMocks(); }); - describe('authenticate', () => { - it('should authenticate user with valid wallet', async () => { - const { prisma } = require('../../src/config/prisma'); - + describe("authenticate", () => { + it("should authenticate user with valid wallet", async () => { + const { prisma } = require("../../src/config/prisma"); + mockWalletService.isWalletValid.mockResolvedValue(true); prisma.user.findUnique.mockResolvedValue({ - id: 'user-123', + id: "user-123", wallet: validWalletAddress, - email: 'test@example.com', + email: "test@example.com", }); const token = await authService.authenticate(validWalletAddress); - expect(mockWalletService.isWalletValid).toHaveBeenCalledWith(validWalletAddress); + expect(mockWalletService.isWalletValid).toHaveBeenCalledWith( + validWalletAddress + ); expect(prisma.user.findUnique).toHaveBeenCalledWith({ where: { wallet: validWalletAddress }, }); expect(token).toBeDefined(); - expect(typeof token).toBe('string'); + expect(typeof token).toBe("string"); }); - it('should reject authentication with invalid wallet', async () => { + it("should reject authentication with invalid wallet", async () => { mockWalletService.isWalletValid.mockResolvedValue(false); - await expect(authService.authenticate(invalidWalletAddress)) - .rejects.toThrow('Invalid wallet address'); + await expect( + authService.authenticate(invalidWalletAddress) + ).rejects.toThrow("Invalid wallet address"); - expect(mockWalletService.isWalletValid).toHaveBeenCalledWith(invalidWalletAddress); + expect(mockWalletService.isWalletValid).toHaveBeenCalledWith( + invalidWalletAddress + ); }); - it('should reject authentication for non-existent user', async () => { - const { prisma } = require('../../src/config/prisma'); - + it("should reject authentication for non-existent user", async () => { + const { prisma } = require("../../src/config/prisma"); + mockWalletService.isWalletValid.mockResolvedValue(true); prisma.user.findUnique.mockResolvedValue(null); - await expect(authService.authenticate(validWalletAddress)) - .rejects.toThrow('User not found'); + await expect( + authService.authenticate(validWalletAddress) + ).rejects.toThrow("User not found"); }); }); - describe('register', () => { + describe("register", () => { const registrationData = { - name: 'John', - lastName: 'Doe', - email: 'john@example.com', - password: 'password123', + name: "John", + lastName: "Doe", + email: "john@example.com", + password: "password123", wallet: validWalletAddress, }; - it('should register user with valid wallet', async () => { - const { prisma } = require('../../src/config/prisma'); + it("should register user with valid wallet", async () => { + const { prisma } = require("../../src/config/prisma"); // Set up wallet service mock mockWalletService.verifyWallet.mockResolvedValue({ @@ -128,7 +143,7 @@ describe('Wallet Auth Integration', () => { isValid: true, accountExists: true, walletAddress: validWalletAddress, - message: 'Wallet verified successfully', + message: "Wallet verified successfully", verifiedAt: new Date(), }); @@ -136,16 +151,17 @@ describe('Wallet Auth Integration', () => { const mockUserRepositoryInstance = (authService as any).userRepository; mockUserRepositoryInstance.findByEmail.mockResolvedValue(null); mockUserRepositoryInstance.create.mockResolvedValue({ - id: 'user-123', - name: 'John', - lastName: 'Doe', - email: 'john@example.com', + id: "user-123", + name: "John", + lastName: "Doe", + email: "john@example.com", wallet: validWalletAddress, isVerified: false, }); // Set up send email use case mock - const mockSendEmailUseCaseInstance = (authService as any).sendVerificationEmailUseCase; + const mockSendEmailUseCaseInstance = (authService as any) + .sendVerificationEmailUseCase; mockSendEmailUseCaseInstance.execute.mockResolvedValue({}); // Set up prisma mock @@ -159,43 +175,50 @@ describe('Wallet Auth Integration', () => { registrationData.wallet ); - expect(mockWalletService.verifyWallet).toHaveBeenCalledWith(validWalletAddress); - expect(result.id).toBe('user-123'); + expect(mockWalletService.verifyWallet).toHaveBeenCalledWith( + validWalletAddress + ); + expect(result.id).toBe("user-123"); expect(result.wallet).toBe(validWalletAddress); expect(result.walletVerified).toBe(true); }); - it('should reject registration with invalid wallet', async () => { + it("should reject registration with invalid wallet", async () => { mockWalletService.verifyWallet.mockResolvedValue({ success: false, isValid: false, accountExists: false, walletAddress: invalidWalletAddress, - message: 'Invalid wallet format', + message: "Invalid wallet format", verifiedAt: new Date(), }); - await expect(authService.register( - registrationData.name, - registrationData.lastName, - registrationData.email, - registrationData.password, + await expect( + authService.register( + registrationData.name, + registrationData.lastName, + registrationData.email, + registrationData.password, + invalidWalletAddress + ) + ).rejects.toThrow("Wallet verification failed: Invalid wallet format"); + + expect(mockWalletService.verifyWallet).toHaveBeenCalledWith( invalidWalletAddress - )).rejects.toThrow('Wallet verification failed: Invalid wallet format'); - - expect(mockWalletService.verifyWallet).toHaveBeenCalledWith(invalidWalletAddress); + ); }); - it('should reject registration with already registered wallet', async () => { - const { prisma } = require('../../src/config/prisma'); - const mockUserRepository = require('../../src/modules/user/repositories/PrismaUserRepository').PrismaUserRepository; + it("should reject registration with already registered wallet", async () => { + const { prisma } = require("../../src/config/prisma"); + const mockUserRepository = + require("../../src/modules/user/repositories/PrismaUserRepository").PrismaUserRepository; mockWalletService.verifyWallet.mockResolvedValue({ success: true, isValid: true, accountExists: true, walletAddress: validWalletAddress, - message: 'Wallet verified successfully', + message: "Wallet verified successfully", verifiedAt: new Date(), }); @@ -206,17 +229,19 @@ describe('Wallet Auth Integration', () => { mockUserRepository.mockImplementation(() => mockUserRepositoryInstance); prisma.user.findUnique.mockResolvedValue({ - id: 'existing-user', + id: "existing-user", wallet: validWalletAddress, }); - await expect(authService.register( - registrationData.name, - registrationData.lastName, - registrationData.email, - registrationData.password, - registrationData.wallet - )).rejects.toThrow('This wallet address is already registered'); + await expect( + authService.register( + registrationData.name, + registrationData.lastName, + registrationData.email, + registrationData.password, + registrationData.wallet + ) + ).rejects.toThrow("This wallet address is already registered"); }); }); });