diff --git a/README.md b/README.md
index 10f5017..125b6d5 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,77 @@
-# Java URL Shortener
+# Shorten
-See full README content in project documentation.
\ No newline at end of file
+A lightweight URL shortener built using plain Java with `com.sun.net.httpserver.HttpServer`, H2 Database, and vanilla HTML/CSS/JS frontend.
+
+## โ
Features
+
+* ๐ **Anonymous URL shortening**: Convert long URLs into short ones instantly.
+* ๐งโ๐ป **User login**: Create an account and log in.
+* ๐ **Custom short URLs**: Logged-in users can create their own alias URLs.
+* โช๏ธ **Redirection**: Short URLs automatically redirect to the long ones.
+
+## ๐ ๏ธ Tech Stack
+
+| Layer | Technology |
+| -------- | ----------------------------------------- |
+| Frontend | HTML, CSS, JavaScript (no framework) |
+| Backend | Java, `com.sun.net.httpserver.HttpServer` |
+| Database | H2 using JDBC |
+| Logging | SLF4J |
+| Testing | JUnit 5, Mockito |
+| CI/CD | GitHub Actions |
+
+```
+
+## ๐ Getting Started
+
+### Prerequisites
+
+* Java 17+
+* Maven
+
+### Run Application
+
+```bash
+mvn clean package
+java -jar target/shorten.jar
+```
+
+Visit `http://localhost:8080` in your browser.
+
+### Database
+
+* H2 in-memory mode used.
+* JDBC with prepared statements.
+
+### GitHub Actions
+
+* CI configured in `.github/workflows/ci.yml` to run tests on every PR and merge to `main`.
+
+## ๐ API Endpoints
+
+| Endpoint | Method | Description |
+| --------------- | ------ | ------------------------------ |
+| `/shorten` | POST | Shortens a given long URL |
+| `/s/{shortUrl}` | GET | Redirects to original long URL |
+| `/register` | POST | Register a new user |
+| `/login` | POST | Log in existing user |
+
+## ๐งช Testing
+
+* All core components are tested with JUnit 5
+* Mocked database and auth dependencies with Mockito
+
+## ๐ Project Management
+
+* [ ] GitHub **Issues** created for each task
+* [ ] Separate **branches** for every feature
+* [ ] Pull Requests with self-review
+* [ ] CI runs on every PR and merge
+
+## ๐ License
+
+This project is licensed under the MIT License.
+
+---
+
+Built with โค๏ธ using pure Java.
diff --git a/pom.xml b/pom.xml
index b88700e..7c571a1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,6 +49,12 @@
5.12.0
test
+
+ org.projectlombok
+ lombok
+ 1.18.38
+ provided
+
diff --git a/src/main/java/urlshortener/Server.java b/src/main/java/urlshortener/Server.java
index f405286..59cca9c 100644
--- a/src/main/java/urlshortener/Server.java
+++ b/src/main/java/urlshortener/Server.java
@@ -5,9 +5,11 @@
import java.net.InetSocketAddress;
import java.sql.*;
+import lombok.extern.slf4j.Slf4j;
import urlshortener.config.AppConfig;
import urlshortener.service.*;
+@Slf4j
public class Server {
private static Connection conn;
@@ -29,7 +31,7 @@ public static void main(String[] args) throws Exception {
server.setExecutor(null);
server.start();
- System.out.println("Server running on http://localhost:8080/");
+ log.info("Server running on http://localhost:8080/");
}
public static Connection getConn() {
diff --git a/src/main/java/urlshortener/service/LoginHandler.java b/src/main/java/urlshortener/service/LoginHandler.java
index 6599d60..fd5033e 100644
--- a/src/main/java/urlshortener/service/LoginHandler.java
+++ b/src/main/java/urlshortener/service/LoginHandler.java
@@ -2,6 +2,7 @@
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
+import lombok.extern.slf4j.Slf4j;
import urlshortener.utils.HelperMethods;
import java.io.File;
@@ -15,6 +16,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
+@Slf4j
public class LoginHandler implements HttpHandler {
private static Connection conn;
@@ -52,16 +54,16 @@ public void handle(HttpExchange exchange) throws IOException {
ResultSet rs = ps.executeQuery();
if (rs.next()) {
- System.out.println("Login successful for user: " + username);
+ log.info("Login successful for user: {}", username);
String encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8.toString());
exchange.getResponseHeaders().set("Location", "/index?username=" + encodedUsername);
exchange.sendResponseHeaders(302, -1);
} else {
- System.out.println("Invalid credentials for user: " + username);
+ log.info("Invalid credentials for user: {}", username);
HelperMethods.respond(exchange, 401, "Invalid credentials");
}
} catch (SQLException e) {
- System.err.println("Error querying user: " + e.getMessage());
+ log.error("Error querying user: {}", e.getMessage());
HelperMethods.respond(exchange, 500, "Database error");
}
}
diff --git a/src/main/java/urlshortener/service/RegisterHandler.java b/src/main/java/urlshortener/service/RegisterHandler.java
index d45ed20..654e51e 100644
--- a/src/main/java/urlshortener/service/RegisterHandler.java
+++ b/src/main/java/urlshortener/service/RegisterHandler.java
@@ -2,6 +2,7 @@
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
+import lombok.extern.slf4j.Slf4j;
import urlshortener.utils.HelperMethods;
import java.io.File;
@@ -12,6 +13,7 @@
import java.sql.PreparedStatement;
import java.sql.SQLException;
+@Slf4j
public class RegisterHandler implements HttpHandler {
private static Connection conn;
@@ -48,14 +50,15 @@ public void handle(HttpExchange exchange) throws IOException {
ps.setString(2, password);
int rowsAffected = ps.executeUpdate();
if (rowsAffected > 0) {
- System.out.println("User registered successfully: " + username);
+ log.info("User registered successfully: {}", username);
exchange.getResponseHeaders().set("Location", "/index");
exchange.sendResponseHeaders(302, -1);
} else {
+ log.error("Failed to register user {}", username);
HelperMethods.respond(exchange, 500, "Failed to register user");
}
} catch (SQLException e) {
- System.err.println("Error inserting user: " + e.getMessage());
+ log.error("Error inserting user: {}", e.getMessage());
HelperMethods.respond(exchange, 400, "User already exists");
}
}
diff --git a/src/test/java/urlshortener/ServerTest.java b/src/test/java/urlshortener/ServerTest.java
new file mode 100644
index 0000000..2d54720
--- /dev/null
+++ b/src/test/java/urlshortener/ServerTest.java
@@ -0,0 +1,55 @@
+package urlshortener;
+
+import org.junit.jupiter.api.*;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class ServerTest {
+
+ private static Connection conn;
+
+ @BeforeAll
+ static void setupDatabase() throws Exception {
+ // Use real in-memory H2 database for integration-style test
+ conn = DriverManager.getConnection("jdbc:h2:mem:testdb", "sa", "");
+ try (Statement stmt = conn.createStatement()) {
+ stmt.execute("CREATE TABLE users (username VARCHAR(255) PRIMARY KEY, password VARCHAR(255))");
+ }
+ }
+
+ @Test
+ void testUsersTableCreatedSuccessfully() throws Exception {
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery("SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'USERS'");
+ assertTrue(rs.next(), "Users table should exist");
+ }
+
+ @Test
+ void testMockedConnectionTableCreation() throws Exception {
+ // Mocks
+ Connection mockConn = mock(Connection.class);
+ Statement mockStmt = mock(Statement.class);
+
+ when(mockConn.createStatement()).thenReturn(mockStmt);
+ when(mockStmt.execute(anyString())).thenReturn(true);
+
+ // Run the DB setup logic
+ mockConn.createStatement().execute("CREATE TABLE users (username VARCHAR(255) PRIMARY KEY, password VARCHAR(255))");
+
+ // Verify SQL execution
+ verify(mockConn).createStatement();
+ verify(mockStmt).execute("CREATE TABLE users (username VARCHAR(255) PRIMARY KEY, password VARCHAR(255))");
+ }
+
+ @Test
+ void testGetConnectionReturnsStaticConnection() throws Exception {
+ Server.main(new String[]{}); // Start the server once
+ assertNotNull(Server.getConn(), "Connection should not be null");
+ }
+}
diff --git a/src/test/java/urlshortener/config/AppConfig.java b/src/test/java/urlshortener/config/AppConfig.java
new file mode 100644
index 0000000..2235950
--- /dev/null
+++ b/src/test/java/urlshortener/config/AppConfig.java
@@ -0,0 +1,30 @@
+package urlshortener.config;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AppConfigTest {
+
+ @Test
+ void testAppConfigPropertiesLoading() {
+ try (InputStream in = AppConfig.class.getClassLoader().getResourceAsStream("application.properties")) {
+ assertNotNull(in, "application.properties file should exist");
+
+ Properties props = new Properties();
+ props.load(in);
+
+ // Verify individual properties
+ assertEquals("8080", props.getProperty("server.port"), "server.port should match");
+ assertEquals("http://localhost:8080", props.getProperty("app.baseUrl"), "app.baseUrl should match");
+ assertEquals("jdbc:h2:./shorten-db", props.getProperty("database.url"), "database.url should match");
+ assertEquals("sa", props.getProperty("database.username"), "database.username should match");
+ assertEquals("", props.getProperty("database.password"), "database.password should match");
+ } catch (Exception e) {
+ fail("Failed to load application.properties: " + e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/urlshortener/utils/HashGeneratorTest.java b/src/test/java/urlshortener/utils/HashGeneratorTest.java
new file mode 100644
index 0000000..68d5318
--- /dev/null
+++ b/src/test/java/urlshortener/utils/HashGeneratorTest.java
@@ -0,0 +1,22 @@
+package urlshortener.utils;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class HashGeneratorTest {
+
+ @Test
+ void testGenerateShortCode() {
+ // Generate a short code
+ String shortCode = HashGenerator.generateShortCode();
+
+ // Assert that the short code is not null
+ assertNotNull(shortCode, "Short code should not be null");
+
+ // Assert that the short code is not empty
+ assertFalse(shortCode.isEmpty(), "Short code should not be empty");
+
+ // Assert that the short code has the expected length
+ assertEquals(6, shortCode.length(), "Short code should be 6 characters long");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/urlshortener/utils/HelperMethodsTest.java b/src/test/java/urlshortener/utils/HelperMethodsTest.java
new file mode 100644
index 0000000..c613e50
--- /dev/null
+++ b/src/test/java/urlshortener/utils/HelperMethodsTest.java
@@ -0,0 +1,51 @@
+package urlshortener.utils;
+
+import com.sun.net.httpserver.HttpExchange;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class HelperMethodsTest {
+
+ @Test
+ void testReadRequestBody() throws IOException {
+ // Mock HttpExchange
+ HttpExchange exchange = mock(HttpExchange.class);
+
+ // Mock InputStream with sample request body
+ String requestBody = "testRequestBody";
+ InputStream inputStream = new ByteArrayInputStream(requestBody.getBytes(StandardCharsets.UTF_8));
+ when(exchange.getRequestBody()).thenReturn(inputStream);
+
+ // Call the method
+ String result = HelperMethods.readRequestBody(exchange);
+
+ // Verify the result
+ assertEquals(requestBody, result, "The request body should match the expected value");
+ }
+
+ @Test
+ void testRespond() throws IOException {
+ // Mock HttpExchange
+ HttpExchange exchange = mock(HttpExchange.class);
+
+ // Mock OutputStream
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ when(exchange.getResponseBody()).thenReturn(outputStream);
+
+ // Call the method
+ String response = "testResponse";
+ HelperMethods.respond(exchange, 200, response);
+
+ // Verify the response headers
+ verify(exchange).sendResponseHeaders(200, response.getBytes().length);
+
+ // Verify the response body
+ assertEquals(response, outputStream.toString(), "The response body should match the expected value");
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
new file mode 100644
index 0000000..ef265ef
--- /dev/null
+++ b/src/test/resources/application.properties
@@ -0,0 +1,8 @@
+# Server configuration
+server.port=8080
+app.baseUrl=http://localhost:8080
+
+# Database configuration
+database.url=jdbc:h2:./shorten-db
+database.username=sa
+database.password=
\ No newline at end of file