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