title: Software Design —
Overview
CS320 - Software Design and Engineering Enhancement
Capstone Submission: Contact Management System
Author: Keith Pottratz Course: CS320 Date: January 2026 Artifact: Contact Management System Repository: CS320
1. Artifact Description
What is it?
The artifact is a Contact Management System written in Java. It is a software application that allows users to store, retrieve, update, and delete contact information (contact ID, first name, last name, phone number, and address). The system demonstrates core software engineering principles including data validation, object-oriented design, and comprehensive unit testing.
When was it created?
The original artifact was created in December 2024 as part of my CS320 coursework. The original implementation consisted of four files:
Contact.java- A domain model class with field validationContactService.java- A service class managing contacts using a HashMapContactTest.java- Unit tests for the Contact classContactServiceTest.java- Unit tests for the ContactService class
The enhancements were implemented in January 2026 as part of my Computer Science Capstone portfolio project, transforming the basic implementation into a professionally architected, secure, and production-ready system.
2. Justification for Inclusion in ePortfolio
Why did you select this item?
I selected the Contact Management System for my ePortfolio because it represents an ideal candidate for demonstrating growth in Software Design and Engineering. The original artifact was functional but lacked the architectural sophistication expected in professional software development. This created an opportunity to showcase my ability to:
- Recognize architectural limitations in existing code
- Apply industry-standard design patterns to improve code quality
- Implement security best practices to protect against common vulnerabilities
- Refactor code while maintaining full functionality (all tests passing)
- Document technical decisions with clear rationale
What specific components showcase your skills and abilities?
Design Patterns and Architecture
| Component | Skills Demonstrated |
|———–|———————|
| Repository Pattern (IContactRepository, InMemoryContactRepository) | Abstraction, interface design, separation of concerns |
| Service Layer with Dependency Injection (IContactService, ContactServiceImpl) | SOLID principles, loose coupling, testability |
| Builder Pattern (ContactBuilder) | Fluent API design, object creation patterns |
| Custom Exception Hierarchy (5 exception classes) | Error handling strategy, API design |
Security Implementation
| Component | Skills Demonstrated | |———–|———————| | Thread Safety (ConcurrentHashMap) | Concurrent programming, race condition prevention | | Input Sanitization (ContactValidator) | XSS prevention, SQL injection prevention, defensive programming | | Resource Limits (MAX_CONTACTS) | DoS prevention, resource management | | Audit Logging (SLF4J/Logback) | Security monitoring, compliance, observability |
Testing and Quality
| Component | Skills Demonstrated | |———–|———————| | 70 Unit Tests | Comprehensive test coverage | | Mockito Integration | Mock-based isolated testing, behavior verification | | Security Tests | Thread safety verification, input validation testing |
How was the artifact improved?
The artifact was transformed through seven major enhancements:
| # | Enhancement | Before | After |
|---|---|---|---|
| 1 | Exception Handling | Generic IllegalArgumentException |
Custom hierarchy with 5 specific exception types |
| 2 | Data Access | Direct HashMap in service class | Repository pattern with interface abstraction |
| 3 | Service Architecture | Tightly coupled service | Dependency injection with interface-based design |
| 4 | Object Creation | Constructor only | Builder pattern with fluent interface |
| 5 | Logging | None | SLF4J/Logback with audit trail |
| 6 | Testing | Basic JUnit tests (14 tests) | Mockito mocking + comprehensive coverage (70 tests) |
| 7 | Security | No security considerations | Thread safety, input sanitization, DoS prevention |
Quantitative Improvements:
- Test count: 14 -> 70 (400% increase)
- Source files: 4 -> 14 (250% increase)
- Security vulnerabilities addressed: 9 critical issues mitigated
- Design patterns applied: 3 (Repository, Builder, Dependency Injection)
3. Course Outcomes Assessment
Did you meet the course outcomes you planned to meet?
Yes. The enhancements successfully demonstrate progress toward all five course outcomes, with particular strength in Outcomes 3, 4, and 5.
Course Outcome 1: Collaborative Environments
“Employ strategies for building collaborative environments that enable diverse audiences to support organizational decision making in the field of computer science”
Met through:
- Interface-based design allows different team members to work on implementations independently
- Repository pattern separates data access concerns, enabling parallel development
- Dependency injection allows components to be developed and tested in isolation
- Clear package structure (
exception/,repository/,service/,validation/) organizes code for team navigation
Evidence:
src/main/java/com/example/contact/
├── repository/
│ ├── IContactRepository.java <- Interface defines contract
│ └── InMemoryContactRepository.java <- Team A implements this
├── service/
│ ├── IContactService.java <- Interface defines contract
│ └── ContactServiceImpl.java <- Team B implements this
Course Outcome 2: Professional Communication
“Design, develop, and deliver professional-quality oral, written, and visual communications that are coherent, technically sound, and appropriately adapted to specific audiences and contexts”
Met through:
- Comprehensive JavaDoc documentation on all public interfaces and methods
- Clear exception messages that communicate specific validation failures
- Audit logging that creates readable operational records
Evidence:
/**
* Service interface for Contact business operations.
* Defines the contract for contact management operations,
* allowing for different service implementations.
*/
public interface IContactService {
/**
* Adds a new contact to the system.
* @param contact the contact to add
* @throws DuplicateContactException if a contact with the same ID already exists
* @throws ContactValidationException if the contact is null or invalid
*/
void addContact(Contact contact);
Course Outcome 3: Design and Evaluation
“Design and evaluate computing solutions that solve a given problem using algorithmic principles and computer science practices and standards appropriate to its solution, while managing the trade-offs involved in design choices”
Met through:
- Repository Pattern: Chose abstraction over simplicity, trading initial development time for long-term flexibility and testability
- Builder Pattern: Chose readability over constructor simplicity, making object creation self-documenting
- Custom Exceptions: Chose specificity over generic exceptions, enabling precise error handling at the cost of additional classes
- ConcurrentHashMap: Chose thread safety over raw performance, accepting slight overhead for correctness
Trade-off Analysis: | Decision | Trade-off | Rationale | |———-|———–|———–| | Interface-based design | More files/complexity | Enables mocking, future implementations | | ConcurrentHashMap | ~10% slower than HashMap | Prevents data corruption in concurrent access | | Input validation | Performance overhead | Prevents security vulnerabilities |
Course Outcome 4: Industry Techniques and Tools
“Demonstrate an ability to use well-founded and innovative techniques, skills, and tools in computing practices for the purpose of implementing computer solutions that deliver value and accomplish industry-specific goals”
Met through:
- Maven for build management and dependency management
- JUnit 5 for modern unit testing
- Mockito for mock-based isolated testing
- SLF4J/Logback for production-grade logging
- Design Patterns (Repository, Builder, Dependency Injection)
- SOLID Principles throughout the architecture
Course Outcome 5: Security Mindset
“Develop a security mindset that anticipates adversarial exploits in software architecture and designs to expose potential vulnerabilities, mitigate design flaws, and ensure privacy and enhanced security of data and resources”
Met through identification and mitigation of 9 security vulnerabilities:
| Vulnerability | OWASP Category | Mitigation |
|---|---|---|
| Thread Safety Issues | A04: Insecure Design | ConcurrentHashMap |
| No Input Sanitization | A03: Injection | ContactValidator with XSS/SQL patterns |
| No Authentication | A01: Broken Access Control | Foundation for future implementation |
| No Audit Logging | A09: Security Logging Failures | SLF4J audit logger |
| Information Disclosure | A05: Security Misconfiguration | Secure error messages |
| No Data Protection | A02: Cryptographic Failures | Foundation for encryption |
| Direct Object Reference | A01: Broken Access Control | Validation layer |
| Null Pointer Vulnerability | - | Null parameter validation |
| Denial of Service | A04: Insecure Design | MAX_CONTACTS = 10000 limit |
Security Code Example:
// XSS Prevention Pattern
private static final Pattern XSS_PATTERN = Pattern.compile(
".*(<script|javascript:|onerror=|onclick=|onload=).*",
Pattern.CASE_INSENSITIVE
);
// Audit Logging
auditLogger.warn("Security: XSS pattern detected in {} - input rejected", fieldName);
Updates to Outcome-Coverage Plans
My original plan focused primarily on Outcomes 3 and 4. Through the enhancement process, I discovered that security (Outcome 5) deserved equal emphasis. The addition of the security enhancements (thread safety, input sanitization, resource limits) significantly strengthened the portfolio piece and better demonstrates professional-level thinking.
4. Reflection on the Enhancement Process
What did you learn?
Technical Learning
-
Design Patterns Have Real Value - Before this project, I understood design patterns theoretically. Implementing the Repository pattern showed me how abstraction enables testability – I could mock the repository and test service logic in complete isolation. The Builder pattern made my test code more readable and maintainable.
-
Security Requires Proactive Thinking - The original code had no security considerations. Learning to think like an attacker – “What if someone passes
<script>in the name field?” – fundamentally changed how I approach code. I now consider security at design time, not as an afterthought. -
Thread Safety is Non-Negotiable for Shared State - Understanding why HashMap can corrupt under concurrent access, and how ConcurrentHashMap prevents this, taught me that correct behavior under concurrency must be designed in, not patched later.
-
Testing Strategy Matters - Writing 70 tests taught me that different types of tests serve different purposes:
- Unit tests with mocks verify behavior in isolation
- Integration tests verify components work together
- Security tests verify protection against attacks
Professional Learning
-
Documentation is Part of the Code - Writing JavaDoc forced me to think about my API from the user’s perspective. If I couldn’t clearly explain what a method does, that was a sign the method needed refactoring.
-
Refactoring Requires Discipline - Making changes while keeping all tests passing required careful, incremental work. I learned to make one change, run tests, commit, then make the next change.
-
Trade-offs Are Everywhere - Every design decision involves trade-offs. Choosing an interface adds a file but enables flexibility. Choosing validation adds overhead but prevents attacks. Professional engineering means making these trade-offs consciously.
What challenges did you face?
Challenge 1: Maintaining Backward Compatibility
Problem: The original tests expected IllegalArgumentException, but my new code throws ContactValidationException.
Solution: I updated the tests to expect the new exception types while ensuring ContactValidationException extends RuntimeException, maintaining behavioral compatibility for code that catches generic exceptions.
Learning: API evolution requires careful consideration of existing consumers.
Challenge 2: Input Validation vs. Usability
Problem: My initial SQL injection pattern rejected names like “O’Brien” because it contained an apostrophe.
Solution: I refined the regex pattern to detect actual SQL injection (' OR '1'='1) while allowing legitimate apostrophes in names.
Learning: Security measures must be precise – overly aggressive validation creates usability problems.
Challenge 3: Test Isolation with Dependencies
Problem: Testing ContactServiceImpl was difficult because it depended on the repository.
Solution: Used Mockito to create mock repositories, allowing me to test service logic independently and verify specific method calls.
Learning: Dependency injection isn’t just about flexibility – it’s essential for testability.
Challenge 4: Concurrent Testing
Problem: How do you test that code is thread-safe?
Solution: Created tests that spawn multiple threads performing concurrent operations, then verify no exceptions occurred and all data is consistent.
Learning: Thread safety testing requires creative approaches since race conditions are non-deterministic.
5. Conclusion
The enhancement of the Contact Management System demonstrates substantial growth in software design and engineering competency. The transformation from a basic, tightly-coupled implementation to a professionally architected, secure, and well-tested system showcases:
- Architectural thinking through design patterns and SOLID principles
- Security mindset through vulnerability identification and mitigation
- Professional practices through comprehensive testing and documentation
- Technical proficiency with industry-standard tools and frameworks
The 70 passing tests, the secure input validation, the audit logging, and the clean separation of concerns all represent not just code changes, but a fundamental shift in how I approach software development – thinking about maintainability, security, and collaboration from the start.
This artifact demonstrates that I can take existing code, identify its limitations, and systematically improve it using industry best practices while maintaining full functionality. This is precisely the kind of work professional software engineers do daily.
6. Project Structure
CS320/
├── pom.xml # Maven build configuration
├── src/
│ ├── main/
│ │ ├── java/com/example/contact/
│ │ │ ├── Contact.java # Domain entity with validation
│ │ │ ├── ContactBuilder.java # Builder pattern implementation
│ │ │ ├── ContactService.java # Legacy service (deprecated)
│ │ │ ├── exception/
│ │ │ │ ├── ContactException.java # Base exception class
│ │ │ │ ├── ContactNotFoundException.java # Entity not found
│ │ │ │ ├── ContactValidationException.java # Validation failures
│ │ │ │ ├── DuplicateContactException.java # Duplicate ID handling
│ │ │ │ └── ResourceLimitException.java # Resource limit exceeded
│ │ │ ├── repository/
│ │ │ │ ├── IContactRepository.java # Repository interface
│ │ │ │ └── InMemoryContactRepository.java # Thread-safe implementation
│ │ │ ├── service/
│ │ │ │ ├── IContactService.java # Service interface
│ │ │ │ └── ContactServiceImpl.java # Business logic implementation
│ │ │ └── validation/
│ │ │ └── ContactValidator.java # Security validation
│ │ └── resources/
│ │ └── logback.xml # Logging configuration
│ └── test/java/com/example/contact/
│ ├── ContactTest.java # Domain entity tests
│ ├── ContactBuilderTest.java # Builder pattern tests
│ ├── ContactServiceTest.java # Service integration tests
│ ├── ContactServiceMockTest.java # Mocked dependency tests
│ ├── ContactValidatorTest.java # Validation logic tests
│ └── SecurityTest.java # Security feature tests
7. Technology Stack
| Component | Technology | Version |
|---|---|---|
| Language | Java | 11 |
| Build Tool | Maven | 3.x |
| Testing Framework | JUnit | 5.10.0 |
| Mocking Framework | Mockito | 5.8.0 |
| Logging API | SLF4J | 2.0.9 |
| Logging Implementation | Logback | 1.4.14 |
8. Source Code
8.1 Build Configuration (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>contact-service</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Contact Service</name>
<description>Contact Management Service</description>
<properties>
<maven.compiler.release>11</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<!-- Logback Implementation -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M9</version>
</plugin>
</plugins>
</build>
</project>
8.2 Logging Configuration (logback.xml)
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Console Appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- File Appender for Audit Logging -->
<appender name="AUDIT_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/audit.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/audit.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Application File Appender -->
<appender name="APP_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Audit Logger -->
<logger name="AUDIT" level="INFO" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="AUDIT_FILE"/>
</logger>
<!-- Application Loggers -->
<logger name="com.example.contact" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="APP_FILE"/>
</logger>
<!-- Root Logger -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
8.3 Domain Layer
Contact.java
Domain entity with field validation. The contactId is immutable once set in the constructor.
/*
* Keith Pottratz
* CS320
* Java Contact
* December 8, 2024
* Updated: January 2026 - Added custom exception handling
*/
package com.example.contact;
import com.example.contact.exception.ContactValidationException;
public class Contact {
private final String contactId;
private String firstName;
private String lastName;
private String phone;
private String address;
public Contact(String contactId, String firstName, String lastName, String phone, String address) {
if (contactId == null || contactId.length() > 10) {
throw new ContactValidationException("contactId",
"Invalid contact ID: must be non-null and max 10 characters");
}
if (firstName == null || firstName.length() > 10) {
throw new ContactValidationException("firstName",
"Invalid first name: must be non-null and max 10 characters");
}
if (lastName == null || lastName.length() > 10) {
throw new ContactValidationException("lastName",
"Invalid last name: must be non-null and max 10 characters");
}
if (phone == null || phone.length() != 10 || !phone.matches("\\d+")) {
throw new ContactValidationException("phone",
"Invalid phone number: must be exactly 10 digits");
}
if (address == null || address.length() > 30) {
throw new ContactValidationException("address",
"Invalid address: must be non-null and max 30 characters");
}
this.contactId = contactId;
this.firstName = firstName;
this.lastName = lastName;
this.phone = phone;
this.address = address;
}
public String getContactId() { return contactId; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) {
if (firstName == null || firstName.length() > 10) {
throw new ContactValidationException("firstName",
"Invalid first name: must be non-null and max 10 characters");
}
this.firstName = firstName;
}
public String getLastName() { return lastName; }
public void setLastName(String lastName) {
if (lastName == null || lastName.length() > 10) {
throw new ContactValidationException("lastName",
"Invalid last name: must be non-null and max 10 characters");
}
this.lastName = lastName;
}
public String getPhone() { return phone; }
public void setPhone(String phone) {
if (phone == null || phone.length() != 10 || !phone.matches("\\d+")) {
throw new ContactValidationException("phone",
"Invalid phone number: must be exactly 10 digits");
}
this.phone = phone;
}
public String getAddress() { return address; }
public void setAddress(String address) {
if (address == null || address.length() > 30) {
throw new ContactValidationException("address",
"Invalid address: must be non-null and max 30 characters");
}
this.address = address;
}
}
ContactBuilder.java
Builder pattern implementation providing a fluent interface for constructing Contact objects.
/*
* Keith Pottratz
* CS320
* Contact Builder
* January 2026
*/
package com.example.contact;
import com.example.contact.exception.ContactValidationException;
public class ContactBuilder {
private String contactId;
private String firstName;
private String lastName;
private String phone;
private String address;
public ContactBuilder() { }
public ContactBuilder(Contact contact) {
if (contact != null) {
this.contactId = contact.getContactId();
this.firstName = contact.getFirstName();
this.lastName = contact.getLastName();
this.phone = contact.getPhone();
this.address = contact.getAddress();
}
}
public ContactBuilder withContactId(String contactId) { this.contactId = contactId; return this; }
public ContactBuilder withFirstName(String firstName) { this.firstName = firstName; return this; }
public ContactBuilder withLastName(String lastName) { this.lastName = lastName; return this; }
public ContactBuilder withPhone(String phone) { this.phone = phone; return this; }
public ContactBuilder withAddress(String address) { this.address = address; return this; }
public Contact build() {
return new Contact(contactId, firstName, lastName, phone, address);
}
public boolean isValid() {
if (contactId == null || contactId.length() > 10) {
throw new ContactValidationException("contactId",
"Invalid contact ID: must be non-null and max 10 characters");
}
if (firstName == null || firstName.length() > 10) {
throw new ContactValidationException("firstName",
"Invalid first name: must be non-null and max 10 characters");
}
if (lastName == null || lastName.length() > 10) {
throw new ContactValidationException("lastName",
"Invalid last name: must be non-null and max 10 characters");
}
if (phone == null || phone.length() != 10 || !phone.matches("\\d+")) {
throw new ContactValidationException("phone",
"Invalid phone number: must be exactly 10 digits");
}
if (address == null || address.length() > 30) {
throw new ContactValidationException("address",
"Invalid address: must be non-null and max 30 characters");
}
return true;
}
}
ContactService.java (Legacy - Deprecated)
Original service implementation retained for comparison with the enhanced version.
/*
* Keith Pottratz
* CS320
* Java Contact
* December 8, 2024
* Updated: January 2026 - Added exception handling for contact operations
*/
package com.example.contact;
import java.util.HashMap;
import java.util.Map;
public class ContactService {
private final Map<String, Contact> contacts = new HashMap<>();
public void addContact(Contact contact) {
if (contacts.containsKey(contact.getContactId())) {
throw new IllegalArgumentException("Contact ID already exists");
}
contacts.put(contact.getContactId(), contact);
}
public void deleteContact(String contactId) {
if (!contacts.containsKey(contactId)) {
throw new IllegalArgumentException("Contact ID not found");
}
contacts.remove(contactId);
}
public void updateContact(String contactId, String firstName, String lastName,
String phone, String address) {
Contact contact = contacts.get(contactId);
if (contact == null) {
throw new IllegalArgumentException("Contact ID not found");
}
if (firstName != null && !firstName.isBlank()) { contact.setFirstName(firstName); }
if (lastName != null && !lastName.isBlank()) { contact.setLastName(lastName); }
if (phone != null && !phone.isBlank()) { contact.setPhone(phone); }
if (address != null && !address.isBlank()) { contact.setAddress(address); }
}
public Contact getContact(String contactId) {
return contacts.get(contactId);
}
}
8.4 Exception Hierarchy
ContactException.java (Base Exception)
/*
* Keith Pottratz
* January 2026
*/
package com.example.contact.exception;
public class ContactException extends RuntimeException {
public ContactException(String message) {
super(message);
}
public ContactException(String message, Throwable cause) {
super(message, cause);
}
}
ContactValidationException.java
/*
* Keith Pottratz
* January 2026
*/
package com.example.contact.exception;
public class ContactValidationException extends ContactException {
private final String fieldName;
public ContactValidationException(String message) {
super(message);
this.fieldName = null;
}
public ContactValidationException(String fieldName, String message) {
super(message);
this.fieldName = fieldName;
}
public ContactValidationException(String message, Throwable cause) {
super(message, cause);
this.fieldName = null;
}
public String getFieldName() {
return fieldName;
}
}
ContactNotFoundException.java
/*
* Keith Pottratz
* January 2026
*/
package com.example.contact.exception;
public class ContactNotFoundException extends ContactException {
public ContactNotFoundException(String contactId) {
super("Contact not found with ID: " + contactId);
}
public ContactNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
DuplicateContactException.java
/*
* Keith Pottratz
* January 2026
*/
package com.example.contact.exception;
public class DuplicateContactException extends ContactException {
public DuplicateContactException(String contactId) {
super("Contact already exists with ID: " + contactId);
}
public DuplicateContactException(String message, Throwable cause) {
super(message, cause);
}
}
ResourceLimitException.java
/*
* Keith Pottratz
* January 2026
*/
package com.example.contact.exception;
public class ResourceLimitException extends ContactException {
public ResourceLimitException(String message) {
super(message);
}
public ResourceLimitException(String message, Throwable cause) {
super(message, cause);
}
}
Exception Hierarchy Diagram:
RuntimeException
└── ContactException (base)
├── ContactValidationException (invalid input data)
├── ContactNotFoundException (contact ID not found)
├── DuplicateContactException (duplicate contact ID)
└── ResourceLimitException (resource limit exceeded)
8.5 Repository Layer
IContactRepository.java (Interface)
/*
* Keith Pottratz
* CS320
* Contact Repository Interface
* January 2026
*/
package com.example.contact.repository;
import com.example.contact.Contact;
import java.util.List;
import java.util.Optional;
public interface IContactRepository {
void save(Contact contact);
Optional<Contact> findById(String contactId);
boolean existsById(String contactId);
boolean deleteById(String contactId);
List<Contact> findAll();
int count();
}
InMemoryContactRepository.java (Implementation)
/*
* Keith Pottratz
* CS320
* In-Memory Contact Repository Implementation
* January 2026
*/
package com.example.contact.repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.example.contact.Contact;
import com.example.contact.exception.ResourceLimitException;
public class InMemoryContactRepository implements IContactRepository {
private static final Logger logger = LoggerFactory.getLogger(InMemoryContactRepository.class);
private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT");
public static final int MAX_CONTACTS = 10000;
private final Map<String, Contact> contacts = new ConcurrentHashMap<>();
@Override
public void save(Contact contact) {
if (contact == null) {
logger.warn("Attempted to save null contact");
throw new IllegalArgumentException("Contact cannot be null");
}
if (!contacts.containsKey(contact.getContactId()) && contacts.size() >= MAX_CONTACTS) {
auditLogger.warn("Security: Maximum contact limit ({}) reached - save rejected for ID: {}",
MAX_CONTACTS, contact.getContactId());
throw new ResourceLimitException("Maximum contact limit reached: " + MAX_CONTACTS);
}
Contact previous = contacts.put(contact.getContactId(), contact);
if (previous == null) {
auditLogger.info("Contact created: ID={}, Name={} {}",
contact.getContactId(), contact.getFirstName(), contact.getLastName());
} else {
auditLogger.info("Contact updated: ID={}, Name={} {}",
contact.getContactId(), contact.getFirstName(), contact.getLastName());
}
}
@Override
public Optional<Contact> findById(String contactId) {
if (contactId == null) { return Optional.empty(); }
return Optional.ofNullable(contacts.get(contactId));
}
@Override
public boolean existsById(String contactId) {
if (contactId == null) { return false; }
return contacts.containsKey(contactId);
}
@Override
public boolean deleteById(String contactId) {
if (contactId == null) { return false; }
Contact removed = contacts.remove(contactId);
if (removed != null) {
auditLogger.info("Contact deleted: ID={}, Name={} {}",
contactId, removed.getFirstName(), removed.getLastName());
return true;
}
return false;
}
@Override
public List<Contact> findAll() {
return new ArrayList<>(contacts.values());
}
@Override
public int count() {
return contacts.size();
}
public int getMaxContacts() { return MAX_CONTACTS; }
public void clear() {
int count = contacts.size();
contacts.clear();
auditLogger.info("Repository cleared: {} contacts removed", count);
}
}
8.6 Service Layer
IContactService.java (Interface)
/*
* Keith Pottratz
* CS320
* Contact Service Interface
* January 2026
*/
package com.example.contact.service;
import java.util.List;
import com.example.contact.Contact;
import com.example.contact.exception.ContactNotFoundException;
import com.example.contact.exception.ContactValidationException;
import com.example.contact.exception.DuplicateContactException;
public interface IContactService {
void addContact(Contact contact);
void deleteContact(String contactId);
void updateContact(String contactId, String firstName, String lastName,
String phone, String address);
Contact getContact(String contactId);
List<Contact> getAllContacts();
}
ContactServiceImpl.java (Implementation)
/*
* Keith Pottratz
* CS320
* Contact Service Implementation
* January 2026
*/
package com.example.contact.service;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.example.contact.Contact;
import com.example.contact.exception.ContactNotFoundException;
import com.example.contact.exception.ContactValidationException;
import com.example.contact.exception.DuplicateContactException;
import com.example.contact.repository.IContactRepository;
import com.example.contact.validation.ContactValidator;
public class ContactServiceImpl implements IContactService {
private static final Logger logger = LoggerFactory.getLogger(ContactServiceImpl.class);
private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT");
private final IContactRepository repository;
private final ContactValidator validator;
public ContactServiceImpl(IContactRepository repository) {
this(repository, new ContactValidator());
}
public ContactServiceImpl(IContactRepository repository, ContactValidator validator) {
if (repository == null) {
throw new IllegalArgumentException("Repository cannot be null");
}
this.repository = repository;
this.validator = validator != null ? validator : new ContactValidator();
logger.info("ContactServiceImpl initialized");
}
@Override
public void addContact(Contact contact) {
if (contact == null) {
auditLogger.warn("Security: Attempted to add null contact");
throw new ContactValidationException("Contact cannot be null");
}
try {
validator.validate(contact);
} catch (ContactValidationException e) {
auditLogger.warn("Security: Contact validation failed for ID {}: {}",
contact.getContactId(), e.getMessage());
throw e;
}
if (repository.existsById(contact.getContactId())) {
auditLogger.warn("Duplicate contact ID attempted: {}", contact.getContactId());
throw new DuplicateContactException(contact.getContactId());
}
repository.save(contact);
logger.info("Contact added successfully: ID={}", contact.getContactId());
}
@Override
public void deleteContact(String contactId) {
if (contactId == null || contactId.isBlank()) {
auditLogger.warn("Security: Attempted to delete contact with null/blank ID");
throw new ContactValidationException("Contact ID cannot be null or blank");
}
if (!repository.existsById(contactId)) {
throw new ContactNotFoundException(contactId);
}
repository.deleteById(contactId);
logger.info("Contact deleted: ID={}", contactId);
}
@Override
public void updateContact(String contactId, String firstName, String lastName,
String phone, String address) {
if (contactId == null || contactId.isBlank()) {
throw new ContactValidationException("Contact ID cannot be null or blank");
}
Contact contact = repository.findById(contactId)
.orElseThrow(() -> new ContactNotFoundException(contactId));
boolean updated = false;
if (firstName != null && !firstName.isBlank()) {
validator.validateName(firstName, "firstName");
contact.setFirstName(firstName);
updated = true;
}
if (lastName != null && !lastName.isBlank()) {
validator.validateName(lastName, "lastName");
contact.setLastName(lastName);
updated = true;
}
if (phone != null && !phone.isBlank()) {
validator.validatePhone(phone);
contact.setPhone(phone);
updated = true;
}
if (address != null && !address.isBlank()) {
validator.validateAddress(address);
contact.setAddress(address);
updated = true;
}
if (updated) {
repository.save(contact);
logger.info("Contact updated: ID={}", contactId);
}
}
@Override
public Contact getContact(String contactId) {
if (contactId == null || contactId.isBlank()) { return null; }
return repository.findById(contactId).orElse(null);
}
@Override
public List<Contact> getAllContacts() {
return repository.findAll();
}
}
8.7 Validation Layer
ContactValidator.java
/*
* Keith Pottratz
* CS320
* Contact Validator
* January 2026
*/
package com.example.contact.validation;
import com.example.contact.Contact;
import com.example.contact.exception.ContactValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Pattern;
public class ContactValidator {
private static final Logger logger = LoggerFactory.getLogger(ContactValidator.class);
private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT");
// XSS attack pattern detection
private static final Pattern XSS_PATTERN = Pattern.compile(
".*(<script|javascript:|onerror=|onclick=|onload=|onmouseover=|<iframe|<object|<embed).*",
Pattern.CASE_INSENSITIVE
);
// SQL injection pattern detection (allows legitimate apostrophes like O'Brien)
private static final Pattern SQL_INJECTION_PATTERN = Pattern.compile(
".*(;\\s*--|--\\s*$|'\\s*(or|and)\\s+'|\"\\s*(or|and)\\s*\"?\\d|union\\s+select|drop\\s+table).*",
Pattern.CASE_INSENSITIVE
);
// Valid name characters: letters, spaces, hyphens, apostrophes
private static final Pattern VALID_NAME_PATTERN = Pattern.compile("^[a-zA-Z\\s\\-']+$");
// Control character detection
private static final Pattern CONTROL_CHAR_PATTERN =
Pattern.compile(".*[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F].*");
public void validate(Contact contact) {
if (contact == null) {
throw new ContactValidationException("Contact cannot be null");
}
validateContactId(contact.getContactId());
validateName(contact.getFirstName(), "firstName");
validateName(contact.getLastName(), "lastName");
validatePhone(contact.getPhone());
validateAddress(contact.getAddress());
}
public void validateContactId(String contactId) {
if (contactId == null || contactId.isBlank()) {
throw new ContactValidationException("contactId", "Contact ID cannot be null or blank");
}
checkForMaliciousContent(contactId, "contactId");
checkForControlCharacters(contactId, "contactId");
}
public void validateName(String name, String fieldName) {
if (name == null || name.isBlank()) {
throw new ContactValidationException(fieldName, fieldName + " cannot be null or blank");
}
checkForMaliciousContent(name, fieldName);
checkForControlCharacters(name, fieldName);
if (!VALID_NAME_PATTERN.matcher(name).matches()) {
auditLogger.warn("Security: Invalid characters in {} - value contained non-alphabetic characters",
fieldName);
throw new ContactValidationException(fieldName,
fieldName + " contains invalid characters (only letters, spaces, hyphens, and apostrophes allowed)");
}
}
public void validatePhone(String phone) {
if (phone == null || phone.isBlank()) {
throw new ContactValidationException("phone", "Phone cannot be null or blank");
}
if (!phone.matches("\\d+")) {
auditLogger.warn("Security: Invalid phone number format attempted");
throw new ContactValidationException("phone", "Phone must contain only digits");
}
}
public void validateAddress(String address) {
if (address == null || address.isBlank()) {
throw new ContactValidationException("address", "Address cannot be null or blank");
}
checkForMaliciousContent(address, "address");
checkForControlCharacters(address, "address");
}
private void checkForMaliciousContent(String value, String fieldName) {
if (XSS_PATTERN.matcher(value).matches()) {
auditLogger.warn("Security: XSS pattern detected in {} - input rejected", fieldName);
throw new ContactValidationException(fieldName,
fieldName + " contains potentially unsafe content");
}
if (SQL_INJECTION_PATTERN.matcher(value).matches()) {
auditLogger.warn("Security: SQL injection pattern detected in {} - input rejected", fieldName);
throw new ContactValidationException(fieldName,
fieldName + " contains potentially unsafe content");
}
}
private void checkForControlCharacters(String value, String fieldName) {
if (CONTROL_CHAR_PATTERN.matcher(value).matches()) {
auditLogger.warn("Security: Control characters detected in {} - input rejected", fieldName);
throw new ContactValidationException(fieldName,
fieldName + " contains invalid control characters");
}
}
}
8.8 Test Suite
ContactTest.java (6 tests)
Tests the Contact domain entity constructor validation and field constraints.
/*
* Keith Pottratz
* CS320
* December 8, 2024
* Updated: January 2026 - Updated to use custom exceptions
*/
package com.example.contact;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
import com.example.contact.exception.ContactValidationException;
public class ContactTest {
@Test
public void testContactCreationValid() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
assertEquals("12345", contact.getContactId());
assertEquals("John", contact.getFirstName());
assertEquals("Doe", contact.getLastName());
assertEquals("1234567890", contact.getPhone());
assertEquals("123 Main St", contact.getAddress());
}
@Test
public void testInvalidContactId() {
assertThrows(ContactValidationException.class,
() -> new Contact(null, "John", "Doe", "1234567890", "123 Main St"));
assertThrows(ContactValidationException.class,
() -> new Contact("12345678901", "John", "Doe", "1234567890", "123 Main St"));
}
@Test
public void testInvalidFirstName() {
assertThrows(ContactValidationException.class,
() -> new Contact("12345", null, "Doe", "1234567890", "123 Main St"));
assertThrows(ContactValidationException.class,
() -> new Contact("12345", "ThisNameIsTooLong", "Doe", "1234567890", "123 Main St"));
}
@Test
public void testInvalidLastName() {
assertThrows(ContactValidationException.class,
() -> new Contact("12345", "John", null, "1234567890", "123 Main St"));
assertThrows(ContactValidationException.class,
() -> new Contact("12345", "John", "ThisNameIsTooLong", "1234567890", "123 Main St"));
}
@Test
public void testInvalidPhone() {
assertThrows(ContactValidationException.class,
() -> new Contact("12345", "John", "Doe", null, "123 Main St"));
assertThrows(ContactValidationException.class,
() -> new Contact("12345", "John", "Doe", "123", "123 Main St"));
assertThrows(ContactValidationException.class,
() -> new Contact("12345", "John", "Doe", "12345678901", "123 Main St"));
}
@Test
public void testInvalidAddress() {
assertThrows(ContactValidationException.class,
() -> new Contact("12345", "John", "Doe", "1234567890", null));
assertThrows(ContactValidationException.class,
() -> new Contact("12345", "John", "Doe", "1234567890",
"This address is way too long to be valid and should throw an error."));
}
}
ContactBuilderTest.java (9 tests)
Tests the Builder pattern implementation including method chaining, validation, and copy construction.
/*
* Keith Pottratz
* CS320
* Contact Builder Test
* January 2026
*/
package com.example.contact;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import com.example.contact.exception.ContactValidationException;
public class ContactBuilderTest {
@Test
public void testBuildValidContact() {
Contact contact = new ContactBuilder()
.withContactId("12345")
.withFirstName("John")
.withLastName("Doe")
.withPhone("1234567890")
.withAddress("123 Main St")
.build();
assertEquals("12345", contact.getContactId());
assertEquals("John", contact.getFirstName());
}
@Test
public void testBuildFromExistingContact() {
Contact original = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
Contact modified = new ContactBuilder(original)
.withContactId("67890")
.withFirstName("Jane")
.build();
assertEquals("67890", modified.getContactId());
assertEquals("Jane", modified.getFirstName());
assertEquals("Doe", modified.getLastName());
}
@Test
public void testBuilderMethodChaining() {
ContactBuilder builder = new ContactBuilder();
assertSame(builder, builder.withContactId("12345"));
assertSame(builder, builder.withFirstName("John"));
assertSame(builder, builder.withLastName("Doe"));
assertSame(builder, builder.withPhone("1234567890"));
assertSame(builder, builder.withAddress("123 Main St"));
}
@Test
public void testBuildWithInvalidContactId() {
ContactBuilder builder = new ContactBuilder()
.withContactId(null).withFirstName("John").withLastName("Doe")
.withPhone("1234567890").withAddress("123 Main St");
assertThrows(ContactValidationException.class, builder::build);
}
@Test
public void testBuildWithInvalidFirstName() {
ContactBuilder builder = new ContactBuilder()
.withContactId("12345").withFirstName("ThisNameIsTooLong").withLastName("Doe")
.withPhone("1234567890").withAddress("123 Main St");
assertThrows(ContactValidationException.class, builder::build);
}
@Test
public void testBuildWithInvalidPhone() {
ContactBuilder builder = new ContactBuilder()
.withContactId("12345").withFirstName("John").withLastName("Doe")
.withPhone("123").withAddress("123 Main St");
assertThrows(ContactValidationException.class, builder::build);
}
@Test
public void testIsValidWithValidData() {
ContactBuilder builder = new ContactBuilder()
.withContactId("12345").withFirstName("John").withLastName("Doe")
.withPhone("1234567890").withAddress("123 Main St");
assertTrue(builder.isValid());
}
@Test
public void testIsValidWithInvalidData() {
ContactBuilder builder = new ContactBuilder()
.withContactId("12345").withFirstName(null).withLastName("Doe")
.withPhone("1234567890").withAddress("123 Main St");
assertThrows(ContactValidationException.class, builder::isValid);
}
@Test
public void testBuilderFromNullContact() {
ContactBuilder builder = new ContactBuilder(null);
assertThrows(ContactValidationException.class, builder::build);
}
}
ContactServiceTest.java (8 tests)
Integration tests for the service layer using a real InMemoryContactRepository.
/*
* Keith Pottratz
* CS320
* November 15, 2024
* Updated: January 2026 - Updated to use new architecture with dependency injection
*/
package com.example.contact;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.example.contact.exception.ContactNotFoundException;
import com.example.contact.exception.ContactValidationException;
import com.example.contact.exception.DuplicateContactException;
import com.example.contact.repository.IContactRepository;
import com.example.contact.repository.InMemoryContactRepository;
import com.example.contact.service.ContactServiceImpl;
import com.example.contact.service.IContactService;
public class ContactServiceTest {
private IContactService service;
private IContactRepository repository;
@BeforeEach
public void setUp() {
repository = new InMemoryContactRepository();
service = new ContactServiceImpl(repository);
}
@Test
public void testAddContact() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
service.addContact(contact);
assertEquals(contact, service.getContact("12345"));
}
@Test
public void testDeleteContact() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
service.addContact(contact);
service.deleteContact("12345");
assertNull(service.getContact("12345"));
}
@Test
public void testUpdateContact() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
service.addContact(contact);
service.updateContact("12345", "Jane", null, "0987654321", null);
assertEquals("Jane", contact.getFirstName());
assertEquals("0987654321", contact.getPhone());
assertEquals("Doe", contact.getLastName());
assertEquals("123 Main St", contact.getAddress());
}
@Test
public void testAddDuplicateContact() {
Contact contact1 = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
Contact contact2 = new Contact("12345", "Jane", "Smith", "0987654321", "456 Oak St");
service.addContact(contact1);
assertThrows(DuplicateContactException.class, () -> service.addContact(contact2));
}
@Test
public void testDeleteNonexistentContact() {
assertThrows(ContactNotFoundException.class, () -> service.deleteContact("99999"));
}
@Test
public void testUpdateNonexistentContact() {
assertThrows(ContactNotFoundException.class, () ->
service.updateContact("99999", "Jane", "Doe", "1234567890", "456 Oak St"));
}
@Test
public void testAddNullContact() {
assertThrows(ContactValidationException.class, () -> service.addContact(null));
}
@Test
public void testGetAllContacts() {
Contact contact1 = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
Contact contact2 = new Contact("67890", "Jane", "Smith", "0987654321", "456 Oak St");
service.addContact(contact1);
service.addContact(contact2);
assertEquals(2, service.getAllContacts().size());
}
}
ContactServiceMockTest.java (16 tests)
Isolated unit tests using Mockito to mock the repository dependency.
/*
* Keith Pottratz
* CS320
* Contact Service Mock Test
* January 2026
*/
package com.example.contact;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.mockito.ArgumentMatchers.any;
import org.mockito.Mock;
import static org.mockito.Mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import com.example.contact.exception.ContactNotFoundException;
import com.example.contact.exception.ContactValidationException;
import com.example.contact.exception.DuplicateContactException;
import com.example.contact.repository.IContactRepository;
import com.example.contact.service.ContactServiceImpl;
import com.example.contact.service.IContactService;
import com.example.contact.validation.ContactValidator;
@ExtendWith(MockitoExtension.class)
public class ContactServiceMockTest {
@Mock
private IContactRepository mockRepository;
private IContactService service;
private ContactValidator validator;
@BeforeEach
public void setUp() {
validator = new ContactValidator();
service = new ContactServiceImpl(mockRepository, validator);
}
@Test
public void testAddContact_Success() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
when(mockRepository.existsById("12345")).thenReturn(false);
service.addContact(contact);
verify(mockRepository).existsById("12345");
verify(mockRepository).save(contact);
}
@Test
public void testAddContact_DuplicateThrowsException() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
when(mockRepository.existsById("12345")).thenReturn(true);
assertThrows(DuplicateContactException.class, () -> service.addContact(contact));
verify(mockRepository, never()).save(any());
}
@Test
public void testAddContact_NullThrowsException() {
assertThrows(ContactValidationException.class, () -> service.addContact(null));
verify(mockRepository, never()).save(any());
}
@Test
public void testDeleteContact_Success() {
when(mockRepository.existsById("12345")).thenReturn(true);
when(mockRepository.deleteById("12345")).thenReturn(true);
service.deleteContact("12345");
verify(mockRepository).deleteById("12345");
}
@Test
public void testDeleteContact_NotFoundThrowsException() {
when(mockRepository.existsById("99999")).thenReturn(false);
assertThrows(ContactNotFoundException.class, () -> service.deleteContact("99999"));
verify(mockRepository, never()).deleteById(any());
}
@Test
public void testDeleteContact_NullIdThrowsException() {
assertThrows(ContactValidationException.class, () -> service.deleteContact(null));
verify(mockRepository, never()).deleteById(any());
}
@Test
public void testDeleteContact_BlankIdThrowsException() {
assertThrows(ContactValidationException.class, () -> service.deleteContact(" "));
verify(mockRepository, never()).deleteById(any());
}
@Test
public void testUpdateContact_Success() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
when(mockRepository.findById("12345")).thenReturn(Optional.of(contact));
service.updateContact("12345", "Jane", null, "0987654321", null);
assertEquals("Jane", contact.getFirstName());
assertEquals("0987654321", contact.getPhone());
verify(mockRepository).save(contact);
}
@Test
public void testUpdateContact_NotFoundThrowsException() {
when(mockRepository.findById("99999")).thenReturn(Optional.empty());
assertThrows(ContactNotFoundException.class, () ->
service.updateContact("99999", "Jane", null, null, null));
verify(mockRepository, never()).save(any());
}
@Test
public void testUpdateContact_NullIdThrowsException() {
assertThrows(ContactValidationException.class, () ->
service.updateContact(null, "Jane", null, null, null));
verify(mockRepository, never()).findById(any());
}
@Test
public void testGetContact_Success() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
when(mockRepository.findById("12345")).thenReturn(Optional.of(contact));
Contact result = service.getContact("12345");
assertNotNull(result);
assertEquals("12345", result.getContactId());
}
@Test
public void testGetContact_NotFound() {
when(mockRepository.findById("99999")).thenReturn(Optional.empty());
assertNull(service.getContact("99999"));
}
@Test
public void testGetContact_NullIdReturnsNull() {
assertNull(service.getContact(null));
verify(mockRepository, never()).findById(any());
}
@Test
public void testGetAllContacts_Success() {
Contact contact1 = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
Contact contact2 = new Contact("67890", "Jane", "Smith", "0987654321", "456 Oak St");
when(mockRepository.findAll()).thenReturn(Arrays.asList(contact1, contact2));
List<Contact> results = service.getAllContacts();
assertEquals(2, results.size());
}
@Test
public void testGetAllContacts_EmptyList() {
when(mockRepository.findAll()).thenReturn(Arrays.asList());
assertTrue(service.getAllContacts().isEmpty());
}
@Test
public void testServiceRejectsNullRepository() {
assertThrows(IllegalArgumentException.class, () -> new ContactServiceImpl(null));
}
@Test
public void testUpdateContact_NoFieldsProvided() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
when(mockRepository.findById("12345")).thenReturn(Optional.of(contact));
service.updateContact("12345", null, null, null, null);
assertEquals("John", contact.getFirstName());
verify(mockRepository, never()).save(any());
}
}
ContactValidatorTest.java (18 tests)
Tests input validation and security features including XSS, SQL injection, and control character detection.
/*
* Keith Pottratz
* CS320
* Contact Validator Test
* January 2026
*/
package com.example.contact;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.example.contact.exception.ContactValidationException;
import com.example.contact.validation.ContactValidator;
public class ContactValidatorTest {
private ContactValidator validator;
@BeforeEach
public void setUp() {
validator = new ContactValidator();
}
// Contact Validation
@Test
public void testValidateValidContact() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
assertDoesNotThrow(() -> validator.validate(contact));
}
@Test
public void testValidateNullContact() {
assertThrows(ContactValidationException.class, () -> validator.validate(null));
}
// Name Validation
@Test
public void testValidateName_Valid() {
assertDoesNotThrow(() -> validator.validateName("John", "firstName"));
assertDoesNotThrow(() -> validator.validateName("Mary-Jane", "firstName"));
assertDoesNotThrow(() -> validator.validateName("O'Brien", "lastName"));
assertDoesNotThrow(() -> validator.validateName("Jean Pierre", "firstName"));
}
@Test
public void testValidateName_Null() {
assertThrows(ContactValidationException.class, () -> validator.validateName(null, "firstName"));
}
@Test
public void testValidateName_Blank() {
assertThrows(ContactValidationException.class, () -> validator.validateName(" ", "firstName"));
}
@Test
public void testValidateName_InvalidCharacters() {
assertThrows(ContactValidationException.class, () -> validator.validateName("John123", "firstName"));
assertThrows(ContactValidationException.class, () -> validator.validateName("John@Doe", "firstName"));
assertThrows(ContactValidationException.class, () -> validator.validateName("John<script>", "firstName"));
}
// XSS Prevention
@Test
public void testValidateAddress_XSSPrevention() {
assertThrows(ContactValidationException.class, () ->
validator.validateAddress("<script>alert('xss')</script>"));
assertThrows(ContactValidationException.class, () ->
validator.validateAddress("javascript:alert('xss')"));
assertThrows(ContactValidationException.class, () ->
validator.validateAddress("<img onerror=alert('xss')>"));
assertThrows(ContactValidationException.class, () ->
validator.validateAddress("<div onclick=alert('xss')>"));
}
@Test
public void testValidateContactId_XSSPrevention() {
assertThrows(ContactValidationException.class, () -> validator.validateContactId("<script>"));
}
// SQL Injection Prevention
@Test
public void testValidateAddress_SQLInjectionPrevention() {
assertThrows(ContactValidationException.class, () ->
validator.validateAddress("123; DROP TABLE contacts;--"));
assertThrows(ContactValidationException.class, () ->
validator.validateAddress("' OR '1'='1"));
assertThrows(ContactValidationException.class, () ->
validator.validateAddress("1 UNION SELECT * FROM users"));
}
// Control Character Tests
@Test
public void testValidateAddress_ControlCharacters() {
assertThrows(ContactValidationException.class, () ->
validator.validateAddress("123 Main\0 St"));
assertThrows(ContactValidationException.class, () ->
validator.validateAddress("123 Main\u0007 St"));
}
// Phone Validation
@Test
public void testValidatePhone_Valid() {
assertDoesNotThrow(() -> validator.validatePhone("1234567890"));
}
@Test
public void testValidatePhone_Null() {
assertThrows(ContactValidationException.class, () -> validator.validatePhone(null));
}
@Test
public void testValidatePhone_Blank() {
assertThrows(ContactValidationException.class, () -> validator.validatePhone(" "));
}
@Test
public void testValidatePhone_NonDigits() {
assertThrows(ContactValidationException.class, () -> validator.validatePhone("123-456-7890"));
assertThrows(ContactValidationException.class, () -> validator.validatePhone("(123)4567890"));
}
// Address Validation
@Test
public void testValidateAddress_Valid() {
assertDoesNotThrow(() -> validator.validateAddress("123 Main St"));
assertDoesNotThrow(() -> validator.validateAddress("456 Oak Ave, Apt 7"));
}
@Test
public void testValidateAddress_Null() {
assertThrows(ContactValidationException.class, () -> validator.validateAddress(null));
}
@Test
public void testValidateAddress_Blank() {
assertThrows(ContactValidationException.class, () -> validator.validateAddress(" "));
}
// Contact ID Validation
@Test
public void testValidateContactId_Valid() {
assertDoesNotThrow(() -> validator.validateContactId("12345"));
assertDoesNotThrow(() -> validator.validateContactId("ABC123"));
}
@Test
public void testValidateContactId_Null() {
assertThrows(ContactValidationException.class, () -> validator.validateContactId(null));
}
@Test
public void testValidateContactId_Blank() {
assertThrows(ContactValidationException.class, () -> validator.validateContactId(" "));
}
}
SecurityTest.java (13 tests)
Tests security features including thread safety, resource limits, and input sanitization.
/*
* Keith Pottratz
* CS320
* Security Tests
* January 2026
*/
package com.example.contact;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.example.contact.exception.ContactValidationException;
import com.example.contact.repository.InMemoryContactRepository;
import com.example.contact.service.ContactServiceImpl;
import com.example.contact.service.IContactService;
public class SecurityTest {
private InMemoryContactRepository repository;
private IContactService service;
@BeforeEach
public void setUp() {
repository = new InMemoryContactRepository();
service = new ContactServiceImpl(repository);
}
// Thread Safety Tests
@Test
public void testConcurrentAddContacts() throws InterruptedException {
int numThreads = 10;
int contactsPerThread = 100;
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
CountDownLatch latch = new CountDownLatch(numThreads);
AtomicInteger successCount = new AtomicInteger(0);
for (int t = 0; t < numThreads; t++) {
final int threadId = t;
executor.submit(() -> {
try {
for (int i = 0; i < contactsPerThread; i++) {
String id = String.format("%d-%d", threadId, i);
if (id.length() <= 10) {
Contact contact = new Contact(id, "First", "Last",
"1234567890", "123 Main St");
service.addContact(contact);
successCount.incrementAndGet();
}
}
} catch (Exception e) {
// Some may fail due to validation
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
assertEquals(successCount.get(), service.getAllContacts().size());
}
@Test
public void testConcurrentReadWrite() throws InterruptedException {
for (int i = 0; i < 100; i++) {
String id = String.format("%05d", i);
Contact contact = new Contact(id, "First", "Last", "1234567890", "123 Main St");
service.addContact(contact);
}
int numThreads = 10;
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
CountDownLatch latch = new CountDownLatch(numThreads);
List<Exception> exceptions = new ArrayList<>();
for (int t = 0; t < numThreads; t++) {
executor.submit(() -> {
try {
for (int i = 0; i < 50; i++) {
if (i % 2 == 0) {
service.getContact(String.format("%05d", i % 100));
} else {
try {
service.updateContact(String.format("%05d", i % 100),
"Updated", null, null, null);
} catch (ContactValidationException e) { }
}
}
} catch (Exception e) {
synchronized (exceptions) { exceptions.add(e); }
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
assertTrue(exceptions.isEmpty(), "Concurrent operations should not throw exceptions");
}
// Resource Limit Tests
@Test
public void testMaxContactsLimit() {
assertEquals(10000, InMemoryContactRepository.MAX_CONTACTS);
}
@Test
public void testRepositoryClear() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
repository.save(contact);
assertEquals(1, repository.count());
repository.clear();
assertEquals(0, repository.count());
}
// Input Sanitization Tests
@Test
public void testServiceRejectsXSSInName() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
service.addContact(contact);
assertThrows(ContactValidationException.class, () ->
service.updateContact("12345", "<script>alert('xss')</script>", null, null, null));
}
@Test
public void testServiceRejectsInvalidNameCharacters() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
service.addContact(contact);
assertThrows(ContactValidationException.class, () ->
service.updateContact("12345", "John123", null, null, null));
}
@Test
public void testServiceRejectsXSSInAddress() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
service.addContact(contact);
assertThrows(ContactValidationException.class, () ->
service.updateContact("12345", null, null, null, "<script>alert('xss')</script>"));
}
// Null/Blank Input Tests
@Test
public void testDeleteWithBlankId() {
assertThrows(ContactValidationException.class, () -> service.deleteContact(""));
}
@Test
public void testUpdateWithBlankId() {
assertThrows(ContactValidationException.class, () ->
service.updateContact("", "Jane", null, null, null));
}
// Audit Logging Verification
@Test
public void testOperationsGenerateLogs() {
Contact contact = new Contact("12345", "John", "Doe", "1234567890", "123 Main St");
service.addContact(contact);
assertNotNull(service.getContact("12345"));
service.updateContact("12345", "Jane", null, null, null);
assertEquals("Jane", service.getContact("12345").getFirstName());
service.deleteContact("12345");
assertNull(service.getContact("12345"));
}
}
9. Appendix: Enhancement Summary
| Metric | Before | After |
|---|---|---|
| Source Files | 4 | 14 |
| Test Files | 2 | 6 |
| Total Tests | 14 | 70 |
| Design Patterns | 0 | 3 |
| Security Features | 0 | 5 |
| Logging | None | Full audit trail |
| Thread Safety | No | Yes |
| Build Tool | None | Maven |
All 70 tests pass. BUILD SUCCESS.