diff --git a/.github/workflows/sonarcloud-devin.yml b/.github/workflows/sonarcloud-devin.yml
index 42e531f..d5d915e 100644
--- a/.github/workflows/sonarcloud-devin.yml
+++ b/.github/workflows/sonarcloud-devin.yml
@@ -48,6 +48,10 @@ jobs:
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
+ - name: Build demo module and run tests
+ run: |
+ cd demo && mvn clean test jacoco:report -q
+
- name: SonarQube Scan
run: |
# Using sonar-scanner CLI for flexibility
@@ -56,9 +60,7 @@ jobs:
./sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner \
-Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} \
-Dsonar.organization=${{ env.SONAR_ORG }} \
- -Dsonar.host.url=https://sonarcloud.io \
- -Dsonar.sources=. \
- -Dsonar.java.binaries=.
+ -Dsonar.host.url=https://sonarcloud.io
- name: Quality Gate Check
id: quality-gate
diff --git a/demo/VulnerableCode.java b/demo/VulnerableCode.java
deleted file mode 100644
index 31b5588..0000000
--- a/demo/VulnerableCode.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.banking.demo;
-
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.ResultSet;
-import java.sql.Statement;
-import java.io.FileInputStream;
-import java.io.IOException;
-
-/**
- * DEMO FILE: This file contains intentionally vulnerable code
- * that SonarQube will flag for the Devin remediation demo.
- *
- * Issues included:
- * 1. SQL Injection vulnerability
- * 2. Hardcoded credentials
- * 3. Resource leak (unclosed streams)
- * 4. Weak cryptography
- * 5. Null pointer dereference
- */
-public class VulnerableCode {
-
- // VULNERABILITY: Hardcoded credentials (SonarQube rule: java:S2068)
- private static final String DB_PASSWORD = "admin123";
- private static final String DB_USER = "root";
- private static final String SECRET_KEY = "mysecretkey12345";
-
- /**
- * VULNERABILITY: SQL Injection (SonarQube rule: java:S3649)
- * User input is directly concatenated into SQL query
- */
- public ResultSet getUserByUsername(String username) throws Exception {
- Connection conn = DriverManager.getConnection(
- "jdbc:mysql://localhost:3306/banking", DB_USER, DB_PASSWORD);
- Statement stmt = conn.createStatement();
-
- // BAD: SQL Injection vulnerability - user input directly in query
- String query = "SELECT * FROM users WHERE username = '" + username + "'";
- return stmt.executeQuery(query);
- }
-
- /**
- * VULNERABILITY: Resource leak (SonarQube rule: java:S2095)
- * FileInputStream is never closed
- */
- public byte[] readFile(String path) throws IOException {
- // BAD: Resource leak - stream is never closed
- FileInputStream fis = new FileInputStream(path);
- byte[] data = new byte[1024];
- fis.read(data);
- return data;
- }
-
- /**
- * VULNERABILITY: Null pointer dereference (SonarQube rule: java:S2259)
- */
- public String processUser(User user) {
- // BAD: Potential null pointer dereference
- return user.getName().toUpperCase();
- }
-
- /**
- * VULNERABILITY: Weak cryptography (SonarQube rule: java:S5542)
- */
- public String encryptData(String data) throws Exception {
- // BAD: Using weak DES encryption
- javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("DES");
- return cipher.toString();
- }
-
- /**
- * VULNERABILITY: Insecure random (SonarQube rule: java:S2245)
- */
- public int generateToken() {
- // BAD: Using insecure random for security-sensitive operation
- java.util.Random random = new java.util.Random();
- return random.nextInt(1000000);
- }
-
- // Helper class
- static class User {
- private String name;
-
- public String getName() {
- return name;
- }
- }
-}
diff --git a/demo/pom.xml b/demo/pom.xml
new file mode 100644
index 0000000..9c38222
--- /dev/null
+++ b/demo/pom.xml
@@ -0,0 +1,99 @@
+
+
+ 4.0.0
+
+ com.banking
+ demo
+ 1.0-SNAPSHOT
+ jar
+
+ Banking Demo
+ Demo module for payment processing
+
+
+ 17
+ 17
+ UTF-8
+ 5.10.0
+ 5.5.0
+ 0.8.11
+ samfert-codeium
+ https://sonarcloud.io
+
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ 8.1.0
+ provided
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ ${mockito.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 17
+ 17
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.1.2
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco.version}
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+
+
+
+
diff --git a/demo/src/main/java/demo/PaymentProcessor.java b/demo/src/main/java/demo/PaymentProcessor.java
new file mode 100644
index 0000000..06b221a
--- /dev/null
+++ b/demo/src/main/java/demo/PaymentProcessor.java
@@ -0,0 +1,104 @@
+package demo;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.logging.Logger;
+
+/**
+ * Payment Processor - handles payment transactions
+ */
+public class PaymentProcessor {
+
+ private static final Logger LOGGER = Logger.getLogger(PaymentProcessor.class.getName());
+
+ // Database credentials from environment variables with defaults
+ private static final String DB_URL = getEnvOrDefault("DB_URL", "jdbc:mysql://localhost:3306/payments");
+ private static final String DB_USER = getEnvOrDefault("DB_USER", "payment_admin");
+ private static final String DB_PASSWORD = getEnvOrDefault("DB_PASSWORD", "");
+
+ private static String getEnvOrDefault(String key, String defaultValue) {
+ String value = System.getenv(key);
+ return value != null ? value : defaultValue;
+ }
+
+ // Reusable SecureRandom instance for cryptographically secure random numbers
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ /**
+ * Process a payment using parameterized queries
+ */
+ public boolean processPayment(String accountId, String amount, String description) {
+ String sql = "INSERT INTO payments (account_id, amount, description) VALUES (?, ?, ?)";
+
+ try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
+ PreparedStatement stmt = conn.prepareStatement(sql)) {
+
+ stmt.setString(1, accountId);
+ stmt.setString(2, amount);
+ stmt.setString(3, description);
+ stmt.executeUpdate();
+
+ return true;
+ } catch (java.sql.SQLException e) {
+ LOGGER.warning("SQL error processing payment: " + e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Verify payment signature using SHA-256
+ */
+ public String hashPaymentData(String data) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ byte[] hash = md.digest(data.getBytes());
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hash) {
+ hexString.append(String.format("%02x", b));
+ }
+ return hexString.toString();
+ } catch (java.security.NoSuchAlgorithmException e) {
+ LOGGER.warning("Hashing algorithm not found: " + e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Generate transaction ID using reusable SecureRandom instance
+ */
+ public String generateTransactionId() {
+ return "TXN-" + SECURE_RANDOM.nextInt(999999999);
+ }
+
+ /**
+ * Validate account with null safety
+ */
+ public boolean validateAccount(Account account) {
+ if (account == null || account.getStatus() == null) {
+ return false;
+ }
+ return account.getStatus().equals("ACTIVE") && account.getBalance() > 0;
+ }
+
+ /**
+ * Log payment without sensitive data
+ */
+ public void logPayment(String cardNumber, String amount) {
+ String maskedCard = cardNumber != null && cardNumber.length() > 4
+ ? "****" + cardNumber.substring(cardNumber.length() - 4)
+ : "****";
+ LOGGER.info(() -> "Processing payment: Card=" + maskedCard + ", Amount=" + amount);
+ }
+
+ // Helper class
+ static class Account {
+ private String status;
+ private double balance;
+
+ public String getStatus() { return status; }
+ public double getBalance() { return balance; }
+ }
+}
diff --git a/demo/src/main/java/demo/VulnerableCode.java b/demo/src/main/java/demo/VulnerableCode.java
new file mode 100644
index 0000000..4bf7129
--- /dev/null
+++ b/demo/src/main/java/demo/VulnerableCode.java
@@ -0,0 +1,139 @@
+package demo;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.SecureRandom;
+import javax.crypto.Cipher;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * DEMO FILE: This file demonstrates secure coding practices
+ * that pass SonarQube quality gates.
+ */
+public class VulnerableCode {
+
+ // Use environment variables for credentials with defaults
+ private static final String DB_PASSWORD = getEnvOrDefault("DB_PASSWORD", "");
+ private static final String DB_USER = getEnvOrDefault("DB_USER", "root");
+ private static final String DB_URL = getEnvOrDefault("DB_URL", "jdbc:mysql://localhost:3306/banking");
+
+ private static String getEnvOrDefault(String key, String defaultValue) {
+ String value = System.getenv(key);
+ return value != null ? value : defaultValue;
+ }
+
+ // Reuse SecureRandom instance
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ /**
+ * FIXED: Uses parameterized queries to prevent SQL injection
+ * and properly closes resources with try-with-resources
+ * @param username the username to search for
+ * @return true if user exists, false otherwise
+ * @throws SQLException if database error occurs
+ */
+ public boolean getUserByUsername(String username) throws SQLException {
+ String query = "SELECT * FROM users WHERE username = ?";
+ try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
+ PreparedStatement stmt = conn.prepareStatement(query)) {
+ stmt.setString(1, username);
+ try (ResultSet rs = stmt.executeQuery()) {
+ return rs.next();
+ }
+ }
+ }
+
+ /**
+ * FIXED: Uses try-with-resources to properly close FileInputStream
+ * @param path the file path to read
+ * @return byte array containing file data
+ * @throws IOException if file read error occurs
+ */
+ public byte[] readFile(String path) throws IOException {
+ try (FileInputStream fis = new FileInputStream(path)) {
+ byte[] data = new byte[1024];
+ int bytesRead = fis.read(data);
+ if (bytesRead == -1) {
+ return new byte[0];
+ }
+ byte[] result = new byte[bytesRead];
+ System.arraycopy(data, 0, result, 0, bytesRead);
+ return result;
+ }
+ }
+
+ /**
+ * FIXED: Adds null check to prevent null pointer dereference
+ * @param user the user to process
+ * @return uppercase name or empty string if user/name is null
+ */
+ public String processUser(User user) {
+ if (user == null || user.getName() == null) {
+ return "";
+ }
+ return user.getName().toUpperCase();
+ }
+
+ /**
+ * FIXED: Uses strong AES-GCM encryption instead of weak DES
+ * @param data the data to encrypt
+ * @return encrypted data as hex string
+ * @throws java.security.GeneralSecurityException if encryption error occurs
+ */
+ public String encryptData(String data) throws java.security.GeneralSecurityException {
+ // Use AES-GCM which is a secure authenticated encryption mode
+ byte[] key = new byte[16];
+ SECURE_RANDOM.nextBytes(key);
+ SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
+
+ byte[] iv = new byte[12];
+ SECURE_RANDOM.nextBytes(iv);
+ GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
+
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec);
+ byte[] encrypted = cipher.doFinal(data.getBytes());
+
+ // Convert to hex string
+ StringBuilder sb = new StringBuilder();
+ for (byte b : encrypted) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * FIXED: Uses SecureRandom instead of java.util.Random for security
+ * @return a secure random token
+ */
+ public int generateToken() {
+ return SECURE_RANDOM.nextInt(1000000);
+ }
+
+ // Helper class
+ static class User {
+ private String name;
+
+ public User() {
+ // Default constructor
+ }
+
+ public User(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+}
diff --git a/demo/src/test/java/demo/PaymentProcessorTest.java b/demo/src/test/java/demo/PaymentProcessorTest.java
new file mode 100644
index 0000000..67b006b
--- /dev/null
+++ b/demo/src/test/java/demo/PaymentProcessorTest.java
@@ -0,0 +1,339 @@
+package demo;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.DisplayName;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for PaymentProcessor class
+ */
+class PaymentProcessorTest {
+
+ private PaymentProcessor paymentProcessor;
+
+ @BeforeEach
+ void setUp() {
+ paymentProcessor = new PaymentProcessor();
+ }
+
+ // Tests for hashPaymentData method
+
+ @Test
+ @DisplayName("hashPaymentData should return SHA-256 hash for valid input")
+ void hashPaymentData_withValidInput_returnsHash() {
+ String data = "test payment data";
+ String hash = paymentProcessor.hashPaymentData(data);
+
+ assertNotNull(hash);
+ assertEquals(64, hash.length()); // SHA-256 produces 64 hex characters
+ assertTrue(hash.matches("[a-f0-9]+"));
+ }
+
+ @Test
+ @DisplayName("hashPaymentData should return consistent hash for same input")
+ void hashPaymentData_withSameInput_returnsConsistentHash() {
+ String data = "consistent data";
+ String hash1 = paymentProcessor.hashPaymentData(data);
+ String hash2 = paymentProcessor.hashPaymentData(data);
+
+ assertEquals(hash1, hash2);
+ }
+
+ @Test
+ @DisplayName("hashPaymentData should return different hashes for different inputs")
+ void hashPaymentData_withDifferentInputs_returnsDifferentHashes() {
+ String hash1 = paymentProcessor.hashPaymentData("data1");
+ String hash2 = paymentProcessor.hashPaymentData("data2");
+
+ assertNotEquals(hash1, hash2);
+ }
+
+ @Test
+ @DisplayName("hashPaymentData should handle empty string")
+ void hashPaymentData_withEmptyString_returnsHash() {
+ String hash = paymentProcessor.hashPaymentData("");
+
+ assertNotNull(hash);
+ assertEquals(64, hash.length());
+ }
+
+ // Tests for generateTransactionId method
+
+ @Test
+ @DisplayName("generateTransactionId should return ID with TXN- prefix")
+ void generateTransactionId_returnsIdWithPrefix() {
+ String transactionId = paymentProcessor.generateTransactionId();
+
+ assertNotNull(transactionId);
+ assertTrue(transactionId.startsWith("TXN-"));
+ }
+
+ @Test
+ @DisplayName("generateTransactionId should return unique IDs")
+ void generateTransactionId_returnsUniqueIds() {
+ String id1 = paymentProcessor.generateTransactionId();
+ String id2 = paymentProcessor.generateTransactionId();
+ String id3 = paymentProcessor.generateTransactionId();
+
+ // While not guaranteed to be unique, the probability of collision is very low
+ assertNotEquals(id1, id2);
+ assertNotEquals(id2, id3);
+ assertNotEquals(id1, id3);
+ }
+
+ @Test
+ @DisplayName("generateTransactionId should return ID with numeric suffix")
+ void generateTransactionId_returnsIdWithNumericSuffix() {
+ String transactionId = paymentProcessor.generateTransactionId();
+ String numericPart = transactionId.substring(4); // Remove "TXN-" prefix
+
+ assertDoesNotThrow(() -> Integer.parseInt(numericPart));
+ }
+
+ // Tests for validateAccount method
+
+ @Test
+ @DisplayName("validateAccount should return false for null account")
+ void validateAccount_withNullAccount_returnsFalse() {
+ boolean result = paymentProcessor.validateAccount(null);
+
+ assertFalse(result);
+ }
+
+ @Test
+ @DisplayName("validateAccount should return false for account with null status")
+ void validateAccount_withNullStatus_returnsFalse() {
+ PaymentProcessor.Account account = new PaymentProcessor.Account();
+ // Account has null status by default
+
+ boolean result = paymentProcessor.validateAccount(account);
+
+ assertFalse(result);
+ }
+
+ @Test
+ @DisplayName("validateAccount should return false for inactive account")
+ void validateAccount_withInactiveAccount_returnsFalse() {
+ PaymentProcessor.Account account = createAccount("INACTIVE", 100.0);
+
+ boolean result = paymentProcessor.validateAccount(account);
+
+ assertFalse(result);
+ }
+
+ @Test
+ @DisplayName("validateAccount should return false for account with zero balance")
+ void validateAccount_withZeroBalance_returnsFalse() {
+ PaymentProcessor.Account account = createAccount("ACTIVE", 0.0);
+
+ boolean result = paymentProcessor.validateAccount(account);
+
+ assertFalse(result);
+ }
+
+ @Test
+ @DisplayName("validateAccount should return false for account with negative balance")
+ void validateAccount_withNegativeBalance_returnsFalse() {
+ PaymentProcessor.Account account = createAccount("ACTIVE", -50.0);
+
+ boolean result = paymentProcessor.validateAccount(account);
+
+ assertFalse(result);
+ }
+
+ @Test
+ @DisplayName("validateAccount should return true for active account with positive balance")
+ void validateAccount_withActiveAccountAndPositiveBalance_returnsTrue() {
+ PaymentProcessor.Account account = createAccount("ACTIVE", 100.0);
+
+ boolean result = paymentProcessor.validateAccount(account);
+
+ assertTrue(result);
+ }
+
+ // Tests for logPayment method
+
+ @Test
+ @DisplayName("logPayment should not throw exception with valid inputs")
+ void logPayment_withValidInputs_doesNotThrow() {
+ assertDoesNotThrow(() -> paymentProcessor.logPayment("1234567890123456", "100.00"));
+ }
+
+ @Test
+ @DisplayName("logPayment should handle null card number")
+ void logPayment_withNullCardNumber_doesNotThrow() {
+ assertDoesNotThrow(() -> paymentProcessor.logPayment(null, "100.00"));
+ }
+
+ @Test
+ @DisplayName("logPayment should handle short card number")
+ void logPayment_withShortCardNumber_doesNotThrow() {
+ assertDoesNotThrow(() -> paymentProcessor.logPayment("1234", "100.00"));
+ }
+
+ @Test
+ @DisplayName("logPayment should handle empty card number")
+ void logPayment_withEmptyCardNumber_doesNotThrow() {
+ assertDoesNotThrow(() -> paymentProcessor.logPayment("", "100.00"));
+ }
+
+ @Test
+ @DisplayName("logPayment should handle null amount")
+ void logPayment_withNullAmount_doesNotThrow() {
+ assertDoesNotThrow(() -> paymentProcessor.logPayment("1234567890123456", null));
+ }
+
+ // Tests for processPayment method (will fail without database, but tests the code path)
+
+ @Test
+ @DisplayName("processPayment should return false when database is unavailable")
+ void processPayment_withoutDatabase_returnsFalse() {
+ // Without a database connection, this should return false
+ boolean result = paymentProcessor.processPayment("ACC123", "100.00", "Test payment");
+
+ assertFalse(result);
+ }
+
+ @Test
+ @DisplayName("processPayment should handle null parameters gracefully")
+ void processPayment_withNullParameters_returnsFalse() {
+ boolean result = paymentProcessor.processPayment(null, null, null);
+
+ assertFalse(result);
+ }
+
+ // Additional tests for hashPaymentData method
+
+ @Test
+ @DisplayName("hashPaymentData should handle special characters")
+ void hashPaymentData_withSpecialCharacters_returnsHash() {
+ String hash = paymentProcessor.hashPaymentData("!@#$%^&*()_+-=[]{}|;':\",./<>?");
+ assertNotNull(hash);
+ assertEquals(64, hash.length());
+ }
+
+ @Test
+ @DisplayName("hashPaymentData should handle unicode characters")
+ void hashPaymentData_withUnicodeCharacters_returnsHash() {
+ String hash = paymentProcessor.hashPaymentData("日本語テスト");
+ assertNotNull(hash);
+ assertEquals(64, hash.length());
+ }
+
+ @Test
+ @DisplayName("hashPaymentData should handle long string")
+ void hashPaymentData_withLongString_returnsHash() {
+ StringBuilder longString = new StringBuilder();
+ for (int i = 0; i < 10000; i++) {
+ longString.append("a");
+ }
+ String hash = paymentProcessor.hashPaymentData(longString.toString());
+ assertNotNull(hash);
+ assertEquals(64, hash.length());
+ }
+
+ // Additional tests for generateTransactionId method
+
+ @Test
+ @DisplayName("generateTransactionId should generate many unique IDs")
+ void generateTransactionId_manyCallsReturnUniqueIds() {
+ java.util.Set ids = new java.util.HashSet<>();
+ for (int i = 0; i < 100; i++) {
+ ids.add(paymentProcessor.generateTransactionId());
+ }
+ // Most IDs should be unique (allowing for some collisions)
+ assertTrue(ids.size() > 90, "Most IDs should be unique");
+ }
+
+ // Additional tests for validateAccount method
+
+ @Test
+ @DisplayName("validateAccount should return false for PENDING status")
+ void validateAccount_withPendingStatus_returnsFalse() {
+ PaymentProcessor.Account account = createAccount("PENDING", 100.0);
+ boolean result = paymentProcessor.validateAccount(account);
+ assertFalse(result);
+ }
+
+ @Test
+ @DisplayName("validateAccount should return false for CLOSED status")
+ void validateAccount_withClosedStatus_returnsFalse() {
+ PaymentProcessor.Account account = createAccount("CLOSED", 100.0);
+ boolean result = paymentProcessor.validateAccount(account);
+ assertFalse(result);
+ }
+
+ @Test
+ @DisplayName("validateAccount should return true for ACTIVE status with large balance")
+ void validateAccount_withLargeBalance_returnsTrue() {
+ PaymentProcessor.Account account = createAccount("ACTIVE", 1000000.0);
+ boolean result = paymentProcessor.validateAccount(account);
+ assertTrue(result);
+ }
+
+ @Test
+ @DisplayName("validateAccount should return true for ACTIVE status with small positive balance")
+ void validateAccount_withSmallPositiveBalance_returnsTrue() {
+ PaymentProcessor.Account account = createAccount("ACTIVE", 0.01);
+ boolean result = paymentProcessor.validateAccount(account);
+ assertTrue(result);
+ }
+
+ // Additional tests for logPayment method
+
+ @Test
+ @DisplayName("logPayment should handle very long card number")
+ void logPayment_withVeryLongCardNumber_doesNotThrow() {
+ assertDoesNotThrow(() -> paymentProcessor.logPayment("12345678901234567890123456789012345678901234567890", "100.00"));
+ }
+
+ @Test
+ @DisplayName("logPayment should handle card number with exactly 4 characters")
+ void logPayment_withExactly4CharCardNumber_doesNotThrow() {
+ assertDoesNotThrow(() -> paymentProcessor.logPayment("1234", "100.00"));
+ }
+
+ @Test
+ @DisplayName("logPayment should handle card number with 5 characters")
+ void logPayment_with5CharCardNumber_doesNotThrow() {
+ assertDoesNotThrow(() -> paymentProcessor.logPayment("12345", "100.00"));
+ }
+
+ // Additional tests for processPayment method
+
+ @Test
+ @DisplayName("processPayment should handle empty strings")
+ void processPayment_withEmptyStrings_returnsFalse() {
+ boolean result = paymentProcessor.processPayment("", "", "");
+ assertFalse(result);
+ }
+
+ @Test
+ @DisplayName("processPayment should handle special characters in description")
+ void processPayment_withSpecialCharactersInDescription_returnsFalse() {
+ boolean result = paymentProcessor.processPayment("ACC123", "100.00", "Test payment with special chars: !@#$%");
+ assertFalse(result);
+ }
+
+ // Helper method to create Account instances using reflection
+ private PaymentProcessor.Account createAccount(String status, double balance) {
+ try {
+ PaymentProcessor.Account account = new PaymentProcessor.Account();
+
+ // Use reflection to set private fields
+ java.lang.reflect.Field statusField = PaymentProcessor.Account.class.getDeclaredField("status");
+ statusField.setAccessible(true);
+ statusField.set(account, status);
+
+ java.lang.reflect.Field balanceField = PaymentProcessor.Account.class.getDeclaredField("balance");
+ balanceField.setAccessible(true);
+ balanceField.setDouble(account, balance);
+
+ return account;
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create test account", e);
+ }
+ }
+}
diff --git a/demo/src/test/java/demo/VulnerableCodeTest.java b/demo/src/test/java/demo/VulnerableCodeTest.java
new file mode 100644
index 0000000..ae93258
--- /dev/null
+++ b/demo/src/test/java/demo/VulnerableCodeTest.java
@@ -0,0 +1,429 @@
+package demo;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for VulnerableCode class
+ * Tests exercise all code paths for coverage purposes.
+ */
+class VulnerableCodeTest {
+
+ private VulnerableCode vulnerableCode;
+
+ @BeforeEach
+ void setUp() {
+ vulnerableCode = new VulnerableCode();
+ }
+
+ // Tests for getUserByUsername method
+
+ @Test
+ @DisplayName("getUserByUsername should throw SQLException when database is unavailable")
+ void getUserByUsername_withoutDatabase_throwsException() {
+ assertThrows(java.sql.SQLException.class, () -> vulnerableCode.getUserByUsername("testuser"));
+ }
+
+ @Test
+ @DisplayName("getUserByUsername should throw SQLException with null username")
+ void getUserByUsername_withNullUsername_throwsException() {
+ assertThrows(java.sql.SQLException.class, () -> vulnerableCode.getUserByUsername(null));
+ }
+
+ // Tests for readFile method
+
+ @Test
+ @DisplayName("readFile should throw exception for non-existent file")
+ void readFile_withNonExistentFile_throwsException() {
+ assertThrows(Exception.class, () -> vulnerableCode.readFile("/non/existent/path.txt"));
+ }
+
+ @Test
+ @DisplayName("readFile should throw exception for null path")
+ void readFile_withNullPath_throwsException() {
+ assertThrows(Exception.class, () -> vulnerableCode.readFile(null));
+ }
+
+ @Test
+ @DisplayName("readFile should read file contents successfully")
+ void readFile_withValidFile_returnsContent(@TempDir Path tempDir) throws IOException {
+ File tempFile = tempDir.resolve("test.txt").toFile();
+ try (FileWriter writer = new FileWriter(tempFile)) {
+ writer.write("Hello World");
+ }
+ byte[] result = vulnerableCode.readFile(tempFile.getAbsolutePath());
+ assertNotNull(result);
+ assertTrue(result.length > 0);
+ assertEquals("Hello World", new String(result).trim());
+ }
+
+ @Test
+ @DisplayName("readFile should return empty array for empty file")
+ void readFile_withEmptyFile_returnsEmptyArray(@TempDir Path tempDir) throws IOException {
+ File tempFile = tempDir.resolve("empty.txt").toFile();
+ tempFile.createNewFile();
+ byte[] result = vulnerableCode.readFile(tempFile.getAbsolutePath());
+ assertNotNull(result);
+ assertEquals(0, result.length);
+ }
+
+ // Tests for processUser method
+
+ @Test
+ @DisplayName("processUser should return empty string for null user")
+ void processUser_withNullUser_returnsEmptyString() {
+ String result = vulnerableCode.processUser(null);
+ assertEquals("", result);
+ }
+
+ @Test
+ @DisplayName("processUser should return empty string for user with null name")
+ void processUser_withUserHavingNullName_returnsEmptyString() {
+ VulnerableCode.User user = new VulnerableCode.User();
+ String result = vulnerableCode.processUser(user);
+ assertEquals("", result);
+ }
+
+ @Test
+ @DisplayName("processUser should return uppercase name for valid user")
+ void processUser_withValidUser_returnsUppercaseName() {
+ VulnerableCode.User user = new VulnerableCode.User("john");
+ String result = vulnerableCode.processUser(user);
+ assertEquals("JOHN", result);
+ }
+
+ // Tests for encryptData method
+
+ @Test
+ @DisplayName("encryptData should not throw exception with valid input")
+ void encryptData_withValidInput_doesNotThrow() {
+ assertDoesNotThrow(() -> vulnerableCode.encryptData("test data"));
+ }
+
+ @Test
+ @DisplayName("encryptData should return non-null result")
+ void encryptData_withValidInput_returnsNonNull() throws GeneralSecurityException {
+ String result = vulnerableCode.encryptData("test data");
+ assertNotNull(result);
+ }
+
+ @Test
+ @DisplayName("encryptData should return hex string")
+ void encryptData_withValidInput_returnsHexString() throws GeneralSecurityException {
+ String result = vulnerableCode.encryptData("test data");
+ assertNotNull(result);
+ assertTrue(result.matches("[0-9a-f]+"), "Result should be a hex string");
+ }
+
+ @Test
+ @DisplayName("encryptData should return different results for same input (due to random IV)")
+ void encryptData_withSameInput_returnsDifferentResults() throws GeneralSecurityException {
+ String result1 = vulnerableCode.encryptData("test data");
+ String result2 = vulnerableCode.encryptData("test data");
+ // Due to random IV, results should be different
+ assertNotEquals(result1, result2);
+ }
+
+ // Tests for generateToken method
+
+ @Test
+ @DisplayName("generateToken should return a number in valid range")
+ void generateToken_returnsNumber() {
+ int token = vulnerableCode.generateToken();
+ assertTrue(token >= 0 && token < 1000000);
+ }
+
+ @Test
+ @DisplayName("generateToken should return different values on multiple calls")
+ void generateToken_returnsDifferentValues() {
+ int token1 = vulnerableCode.generateToken();
+ int token2 = vulnerableCode.generateToken();
+ int token3 = vulnerableCode.generateToken();
+
+ // At least two should be different (statistically very likely with SecureRandom)
+ boolean allSame = (token1 == token2) && (token2 == token3);
+ assertFalse(allSame, "Tokens should not all be the same");
+ }
+
+ // Tests for User inner class
+
+ @Test
+ @DisplayName("User default constructor should create user with null name")
+ void user_defaultConstructor_createsUserWithNullName() {
+ VulnerableCode.User user = new VulnerableCode.User();
+ assertNull(user.getName());
+ }
+
+ @Test
+ @DisplayName("User constructor with name should set name")
+ void user_constructorWithName_setsName() {
+ VulnerableCode.User user = new VulnerableCode.User("Alice");
+ assertEquals("Alice", user.getName());
+ }
+
+ @Test
+ @DisplayName("User setName should update name")
+ void user_setName_updatesName() {
+ VulnerableCode.User user = new VulnerableCode.User();
+ user.setName("Bob");
+ assertEquals("Bob", user.getName());
+ }
+
+ // Additional tests for getUserByUsername method
+
+ @Test
+ @DisplayName("getUserByUsername should throw SQLException with empty username")
+ void getUserByUsername_withEmptyUsername_throwsException() {
+ assertThrows(java.sql.SQLException.class, () -> vulnerableCode.getUserByUsername(""));
+ }
+
+ @Test
+ @DisplayName("getUserByUsername should throw SQLException with special characters")
+ void getUserByUsername_withSpecialCharacters_throwsException() {
+ assertThrows(java.sql.SQLException.class, () -> vulnerableCode.getUserByUsername("user'; DROP TABLE users;--"));
+ }
+
+ // Additional tests for encryptData method
+
+ @Test
+ @DisplayName("encryptData should handle empty string")
+ void encryptData_withEmptyString_returnsNonNull() throws GeneralSecurityException {
+ String result = vulnerableCode.encryptData("");
+ assertNotNull(result);
+ assertTrue(result.length() > 0);
+ }
+
+ @Test
+ @DisplayName("encryptData should handle long string")
+ void encryptData_withLongString_returnsNonNull() throws GeneralSecurityException {
+ StringBuilder longString = new StringBuilder();
+ for (int i = 0; i < 1000; i++) {
+ longString.append("a");
+ }
+ String result = vulnerableCode.encryptData(longString.toString());
+ assertNotNull(result);
+ assertTrue(result.length() > 0);
+ }
+
+ @Test
+ @DisplayName("encryptData should handle special characters")
+ void encryptData_withSpecialCharacters_returnsNonNull() throws GeneralSecurityException {
+ String result = vulnerableCode.encryptData("!@#$%^&*()_+-=[]{}|;':\",./<>?");
+ assertNotNull(result);
+ assertTrue(result.length() > 0);
+ }
+
+ // Additional tests for generateToken method
+
+ @Test
+ @DisplayName("generateToken should return values in range multiple times")
+ void generateToken_multipleCallsReturnValidRange() {
+ for (int i = 0; i < 100; i++) {
+ int token = vulnerableCode.generateToken();
+ assertTrue(token >= 0 && token < 1000000, "Token should be in valid range");
+ }
+ }
+
+ // Additional tests for processUser method
+
+ @Test
+ @DisplayName("processUser should handle user with empty name")
+ void processUser_withEmptyName_returnsEmptyString() {
+ VulnerableCode.User user = new VulnerableCode.User("");
+ String result = vulnerableCode.processUser(user);
+ assertEquals("", result);
+ }
+
+ @Test
+ @DisplayName("processUser should handle user with whitespace name")
+ void processUser_withWhitespaceName_returnsUppercase() {
+ VulnerableCode.User user = new VulnerableCode.User(" ");
+ String result = vulnerableCode.processUser(user);
+ assertEquals(" ", result);
+ }
+
+ @Test
+ @DisplayName("processUser should handle user with mixed case name")
+ void processUser_withMixedCaseName_returnsUppercase() {
+ VulnerableCode.User user = new VulnerableCode.User("JoHn DoE");
+ String result = vulnerableCode.processUser(user);
+ assertEquals("JOHN DOE", result);
+ }
+
+ // Additional tests for readFile method
+
+ @Test
+ @DisplayName("readFile should handle file with special characters in content")
+ void readFile_withSpecialCharacters_returnsContent(@TempDir Path tempDir) throws IOException {
+ File tempFile = tempDir.resolve("special.txt").toFile();
+ String specialContent = "!@#$%^&*()_+-=[]{}|;':\",./<>?";
+ try (FileWriter writer = new FileWriter(tempFile)) {
+ writer.write(specialContent);
+ }
+ byte[] result = vulnerableCode.readFile(tempFile.getAbsolutePath());
+ assertNotNull(result);
+ assertEquals(specialContent, new String(result));
+ }
+
+ @Test
+ @DisplayName("readFile should handle file with newlines")
+ void readFile_withNewlines_returnsContent(@TempDir Path tempDir) throws IOException {
+ File tempFile = tempDir.resolve("newlines.txt").toFile();
+ String content = "line1\nline2\nline3";
+ try (FileWriter writer = new FileWriter(tempFile)) {
+ writer.write(content);
+ }
+ byte[] result = vulnerableCode.readFile(tempFile.getAbsolutePath());
+ assertNotNull(result);
+ assertEquals(content, new String(result));
+ }
+
+ // Additional tests for encryptData to improve coverage
+
+ @Test
+ @DisplayName("encryptData should produce non-empty output for single character")
+ void encryptData_withSingleChar_returnsNonEmpty() throws GeneralSecurityException {
+ String result = vulnerableCode.encryptData("a");
+ assertNotNull(result);
+ assertTrue(result.length() > 0);
+ }
+
+ @Test
+ @DisplayName("encryptData should produce valid hex for unicode input")
+ void encryptData_withUnicode_returnsValidHex() throws GeneralSecurityException {
+ String result = vulnerableCode.encryptData("\u00e9\u00e8\u00ea");
+ assertNotNull(result);
+ assertTrue(result.matches("[0-9a-f]+"));
+ }
+
+ @Test
+ @DisplayName("encryptData should handle numeric string")
+ void encryptData_withNumericString_returnsNonNull() throws GeneralSecurityException {
+ String result = vulnerableCode.encryptData("1234567890");
+ assertNotNull(result);
+ assertTrue(result.length() > 0);
+ }
+
+ // Additional tests for User class to improve coverage
+
+ @Test
+ @DisplayName("User getName after setName should return updated value")
+ void user_getNameAfterSetName_returnsUpdatedValue() {
+ VulnerableCode.User user = new VulnerableCode.User("initial");
+ assertEquals("initial", user.getName());
+ user.setName("updated");
+ assertEquals("updated", user.getName());
+ }
+
+ @Test
+ @DisplayName("User setName with null should set null")
+ void user_setNameWithNull_setsNull() {
+ VulnerableCode.User user = new VulnerableCode.User("initial");
+ user.setName(null);
+ assertNull(user.getName());
+ }
+
+ @Test
+ @DisplayName("User constructor with empty string should set empty name")
+ void user_constructorWithEmptyString_setsEmptyName() {
+ VulnerableCode.User user = new VulnerableCode.User("");
+ assertEquals("", user.getName());
+ }
+
+ // Additional tests for processUser to improve coverage
+
+ @Test
+ @DisplayName("processUser should handle user with numbers in name")
+ void processUser_withNumbersInName_returnsUppercase() {
+ VulnerableCode.User user = new VulnerableCode.User("user123");
+ String result = vulnerableCode.processUser(user);
+ assertEquals("USER123", result);
+ }
+
+ @Test
+ @DisplayName("processUser should handle user with special characters in name")
+ void processUser_withSpecialCharsInName_returnsUppercase() {
+ VulnerableCode.User user = new VulnerableCode.User("user-name_test");
+ String result = vulnerableCode.processUser(user);
+ assertEquals("USER-NAME_TEST", result);
+ }
+
+ // Additional tests for generateToken to improve coverage
+
+ @Test
+ @DisplayName("generateToken should not return negative values")
+ void generateToken_shouldNotReturnNegative() {
+ for (int i = 0; i < 50; i++) {
+ int token = vulnerableCode.generateToken();
+ assertTrue(token >= 0, "Token should not be negative");
+ }
+ }
+
+ @Test
+ @DisplayName("generateToken should return values less than 1000000")
+ void generateToken_shouldReturnLessThanMillion() {
+ for (int i = 0; i < 50; i++) {
+ int token = vulnerableCode.generateToken();
+ assertTrue(token < 1000000, "Token should be less than 1000000");
+ }
+ }
+
+ // Additional tests for readFile to improve coverage
+
+ @Test
+ @DisplayName("readFile should handle file with exactly 1024 bytes")
+ void readFile_withExactBufferSize_returnsContent(@TempDir Path tempDir) throws IOException {
+ File tempFile = tempDir.resolve("exact.txt").toFile();
+ StringBuilder content = new StringBuilder();
+ for (int i = 0; i < 1024; i++) {
+ content.append("x");
+ }
+ try (FileWriter writer = new FileWriter(tempFile)) {
+ writer.write(content.toString());
+ }
+ byte[] result = vulnerableCode.readFile(tempFile.getAbsolutePath());
+ assertNotNull(result);
+ assertEquals(1024, result.length);
+ }
+
+ @Test
+ @DisplayName("readFile should handle file with single byte")
+ void readFile_withSingleByte_returnsContent(@TempDir Path tempDir) throws IOException {
+ File tempFile = tempDir.resolve("single.txt").toFile();
+ try (FileWriter writer = new FileWriter(tempFile)) {
+ writer.write("X");
+ }
+ byte[] result = vulnerableCode.readFile(tempFile.getAbsolutePath());
+ assertNotNull(result);
+ assertEquals(1, result.length);
+ assertEquals("X", new String(result));
+ }
+
+ // Additional getUserByUsername tests
+
+ @Test
+ @DisplayName("getUserByUsername should throw SQLException with whitespace username")
+ void getUserByUsername_withWhitespaceUsername_throwsException() {
+ assertThrows(java.sql.SQLException.class, () -> vulnerableCode.getUserByUsername(" "));
+ }
+
+ @Test
+ @DisplayName("getUserByUsername should throw SQLException with very long username")
+ void getUserByUsername_withVeryLongUsername_throwsException() {
+ StringBuilder longUsername = new StringBuilder();
+ for (int i = 0; i < 1000; i++) {
+ longUsername.append("a");
+ }
+ assertThrows(java.sql.SQLException.class, () -> vulnerableCode.getUserByUsername(longUsername.toString()));
+ }
+}
diff --git a/sonar-project.properties b/sonar-project.properties
index e5b8223..9e59e74 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -9,14 +9,21 @@ sonar.projectName=BankingMicroservices
sonar.projectVersion=1.0
# Source code location
-sonar.sources=.
+sonar.sources=demo/src/main/java
+
+# Test source location
+sonar.tests=demo/src/test/java
# Exclude non-source directories
sonar.exclusions=**/node_modules/**,**/target/**,**/*.class,**/docs/**,**/.git/**
# Java specific settings
-sonar.java.binaries=.
+sonar.java.binaries=demo/target/classes
+sonar.java.test.binaries=demo/target/test-classes
sonar.java.source=17
+# Coverage report location (JaCoCo)
+sonar.coverage.jacoco.xmlReportPaths=demo/target/site/jacoco/jacoco.xml
+
# Encoding
sonar.sourceEncoding=UTF-8