Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/339Lr3BJ)
### How the tests work (and Docker requirement)

This project ships with an end‑to‑end CLI integration test suite that uses Testcontainers to spin up a temporary MySQL database.
Expand Down
Empty file modified mvnw
100644 → 100755
Empty file.
174 changes: 137 additions & 37 deletions src/main/java/com/example/Main.java
Original file line number Diff line number Diff line change
@@ -1,62 +1,162 @@
package com.example;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Arrays;
import com.example.db.SimpleDriverManagerDataSource;
import com.example.repo.*;

import javax.sql.DataSource;
import java.io.*;
import java.util.*;

public class Main {

static void main(String[] args) {
public static void main(String[] args) {
if (isDevMode(args)) {
DevDatabaseInitializer.start();
}
new Main().run();

try {
new Main().run(args);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public void run() {
// Resolve DB settings with precedence: System properties -> Environment variables
public void run(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out), true);

String jdbcUrl = resolveConfig("APP_JDBC_URL", "APP_JDBC_URL");
String dbUser = resolveConfig("APP_DB_USER", "APP_DB_USER");
String dbPass = resolveConfig("APP_DB_PASS", "APP_DB_PASS");

if (jdbcUrl == null || dbUser == null || dbPass == null) {
throw new IllegalStateException(
"Missing DB configuration. Provide APP_JDBC_URL, APP_DB_USER, APP_DB_PASS " +
"as system properties (-Dkey=value) or environment variables.");
throw new IllegalStateException("Missing database configuration");
}

try (Connection connection = DriverManager.getConnection(jdbcUrl, dbUser, dbPass)) {
} catch (SQLException e) {
throw new RuntimeException(e);
DataSource ds = new SimpleDriverManagerDataSource(jdbcUrl, dbUser, dbPass);
AccountRepository accounts = new JdbcAccountRepository(ds);
MoonMissionRepository missions = new JdbcMoonMissionRepository(ds);

while (true) {
out.print("Username:");
out.flush();
String username = in.readLine();
if (username == null) return;
if ("0".equals(username.trim())) return;

out.print("Password:");
out.flush();
String password = in.readLine();
if (password == null) return;

if (accounts.authenticate(username, password).isPresent()) {
break;
}

out.println("Invalid username or password");
out.println("0) Exit");
}

while (true) {
out.println("1) List moon missions");
out.println("2) Get a moon mission by mission_id");
out.println("3) Count missions for a given year");
out.println("4) Create an account");
out.println("5) Update an account password");
out.println("6) Delete an account");
out.println("0) Exit");

String choice = in.readLine();
if (choice == null) return;

switch (choice.trim()) {
case "1" -> {
for (String name : missions.listSpacecraftNames()) {
out.println(name);
}
}
case "2" -> {
out.print("mission_id:");
out.flush();
long id = Long.parseLong(in.readLine());
missions.getMissionAsMap(id)
.ifPresentOrElse(
m -> out.println(format(m)),
() -> out.println("not found")
);
}
case "3" -> {
out.print("year:");
out.flush();
int year = Integer.parseInt(in.readLine());
int count = missions.countByYear(year);
out.println(year + ": " + count);
}
case "4" -> {
out.print("first name:");
out.flush();
String first = in.readLine();

out.print("last name:");
out.flush();
String last = in.readLine();

out.print("ssn:");
out.flush();
String ssn = in.readLine();

out.print("password:");
out.flush();
String pw = in.readLine();

long id = accounts.createAccount(first, last, ssn, pw);
out.println("account created " + id);
}
case "5" -> {
out.print("user_id:");
out.flush();
long id = Long.parseLong(in.readLine());

out.print("new password:");
out.flush();
String pw = in.readLine();

accounts.updatePassword(id, pw);
out.println("password updated");
}
case "6" -> {
out.print("user_id:");
out.flush();
long id = Long.parseLong(in.readLine());

accounts.deleteAccount(id);
out.println("account deleted");
}
case "0" -> {
return;
}
default -> out.println("invalid option");
}
Comment on lines +78 to +139
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard readLine() + numeric parsing to avoid hard-crashing on malformed input.
Long.parseLong(in.readLine()) and Integer.parseInt(in.readLine()) will throw on null/blank/non-numeric input; consider re-prompting or printing “invalid option” / “invalid input”.

🤖 Prompt for AI Agents
In src/main/java/com/example/Main.java around lines 78-139, the code calls
in.readLine() and passes the result straight to Long.parseLong/Integer.parseInt
(and assumes non-null strings for other fields), which will throw on
null/blank/non-numeric input; change these handlers to validate inputs: check
readLine() != null and trim() is not empty, and wrap numeric parsing in
try-catch(NumberFormatException) to handle malformed numbers; for invalid or
null inputs print a clear "invalid input" (or re-prompt if you prefer) and
skip/return to the menu instead of letting the exception propagate; also
validate required string fields (first, last, ssn, password) for null/blank and
handle them the same way.

}
//Todo: Starting point for your code
}

/**
* Determines if the application is running in development mode based on system properties,
* environment variables, or command-line arguments.
*
* @param args an array of command-line arguments
* @return {@code true} if the application is in development mode; {@code false} otherwise
*/
private static String format(Map<String, Object> row) {
return row.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + "=" + e.getValue())
.reduce((a, b) -> a + " " + b)
.orElse("");
}

private static boolean isDevMode(String[] args) {
if (Boolean.getBoolean("devMode")) //Add VM option -DdevMode=true
return true;
if ("true".equalsIgnoreCase(System.getenv("DEV_MODE"))) //Environment variable DEV_MODE=true
return true;
return Arrays.asList(args).contains("--dev"); //Argument --dev
if (Boolean.getBoolean("devMode")) return true;
if ("true".equalsIgnoreCase(System.getenv("DEV_MODE"))) return true;
return args != null && Arrays.asList(args).contains("--dev");
}

/**
* Reads configuration with precedence: Java system property first, then environment variable.
* Returns trimmed value or null if neither source provides a non-empty value.
*/
private static String resolveConfig(String propertyKey, String envKey) {
String v = System.getProperty(propertyKey);
if (v == null || v.trim().isEmpty()) {
v = System.getenv(envKey);
}
return (v == null || v.trim().isEmpty()) ? null : v.trim();
private static String resolveConfig(String prop, String env) {
String v = System.getProperty(prop);
if (v == null || v.isBlank()) v = System.getenv(env);
return (v == null || v.isBlank()) ? null : v.trim();
}
}
39 changes: 39 additions & 0 deletions src/main/java/com/example/db/SimpleDriverManagerDataSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.db;

import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.logging.Logger;

public class SimpleDriverManagerDataSource implements DataSource {

private final String url;
private final String user;
private final String pass;

public SimpleDriverManagerDataSource(String url, String user, String pass) {
this.url = url;
this.user = user;
this.pass = pass;
}

@Override
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, user, pass);
}

@Override
public Connection getConnection(String username, String password) throws SQLException {
return DriverManager.getConnection(url, username, password);
}

@Override public PrintWriter getLogWriter() { throw new UnsupportedOperationException(); }
@Override public void setLogWriter(PrintWriter out) { throw new UnsupportedOperationException(); }
@Override public void setLoginTimeout(int seconds) { throw new UnsupportedOperationException(); }
@Override public int getLoginTimeout() { return 0; }
@Override public Logger getParentLogger() { throw new UnsupportedOperationException(); }
@Override public <T> T unwrap(Class<T> iface) { throw new UnsupportedOperationException(); }
@Override public boolean isWrapperFor(Class<?> iface) { return false; }
}
10 changes: 10 additions & 0 deletions src/main/java/com/example/repo/AccountRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.repo;

import java.util.Optional;

public interface AccountRepository {
Optional<Long> authenticate(String name, String password);
long createAccount(String firstName, String lastName, String ssn, String password);
void updatePassword(long userId, String newPassword);
void deleteAccount(long userId);
}
Comment on lines +5 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Repository boundary is clean, but password handling is plaintext end-to-end.
If this isn’t strictly “assignment-only”, consider hashing (e.g., BCrypt) and changing authenticate to verify hashes rather than matching password = ?.

🤖 Prompt for AI Agents
In src/main/java/com/example/repo/AccountRepository.java around lines 5–10, the
repository currently handles plaintext passwords; update the implementation so
passwords are hashed (e.g., BCrypt) before persisting and only the hash is
stored. Keep the public signatures (accept raw password on
createAccount/updatePassword and authenticate) but change the persistence logic:
on createAccount/updatePassword, hash the provided password with BCrypt and save
the hash; on authenticate, load the stored hash for the given username and
verify the raw password with BCrypt.checkpw, returning the user id only on
successful verification. Also ensure DB column length suits the hash, avoid
logging raw passwords, and handle/propagate hashing-related exceptions
appropriately.

103 changes: 103 additions & 0 deletions src/main/java/com/example/repo/JdbcAccountRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.example.repo;

import javax.sql.DataSource;
import java.sql.*;
import java.util.Optional;

public class JdbcAccountRepository implements AccountRepository {

private final DataSource ds;

public JdbcAccountRepository(DataSource ds) {
this.ds = ds;
}

@Override
public Optional<Long> authenticate(String name, String password) {
String sql = "SELECT user_id FROM account WHERE name = ? AND password = ?";
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, name);
ps.setString(2, password);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) return Optional.of(rs.getLong(1));
return Optional.empty();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

@Override
public long createAccount(String firstName, String lastName, String ssn, String password) {
String name = makeUsername(firstName, lastName);
String sql = "INSERT INTO account (name, first_name, last_name, ssn, password) VALUES (?, ?, ?, ?, ?)";

try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, name);
ps.setString(2, firstName);
ps.setString(3, lastName);
ps.setString(4, ssn);
ps.setString(5, password);
ps.executeUpdate();

try (ResultSet keys = ps.getGeneratedKeys()) {
if (keys.next()) return keys.getLong(1);
}

try (PreparedStatement ps2 = c.prepareStatement("SELECT user_id FROM account WHERE name = ?")) {
ps2.setString(1, name);
try (ResultSet rs = ps2.executeQuery()) {
if (rs.next()) return rs.getLong(1);
}
}

throw new IllegalStateException("Could not get new user id");
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
Comment on lines +31 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

createAccount() fallback lookup by name can return the wrong user if usernames collide.
If account.name isn’t guaranteed UNIQUE, SELECT user_id ... WHERE name=? is unsafe—especially with the XXX padding strategy. Consider either (a) enforcing uniqueness at DB level, or (b) removing the fallback and failing if no generated key is returned.

-            try (PreparedStatement ps2 = c.prepareStatement("SELECT user_id FROM account WHERE name = ?")) {
-                ps2.setString(1, name);
-                try (ResultSet rs = ps2.executeQuery()) {
-                    if (rs.next()) return rs.getLong(1);
-                }
-            }
-
-            throw new IllegalStateException("Could not get new user id");
+            throw new IllegalStateException("Could not get generated user id");
🤖 Prompt for AI Agents
In src/main/java/com/example/repo/JdbcAccountRepository.java around lines 31 to
60, the fallback SELECT by name can return the wrong user when usernames
collide; remove the unsafe fallback and fail when no generated key is returned
(or alternatively enforce a UNIQUE constraint on account.name at the database
level and ensure makeUsername produces unique values). Update the method to rely
on getGeneratedKeys only and throw a clear exception if it is absent, or if you
choose DB-side uniqueness, add the unique constraint/migration and handle the
SQLIntegrityConstraintViolationException to surface a meaningful
duplicate-username error.


@Override
public void updatePassword(long userId, String newPassword) {
String sql = "UPDATE account SET password = ? WHERE user_id = ?";
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, newPassword);
ps.setLong(2, userId);
ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

@Override
public void deleteAccount(long userId) {
String sql = "DELETE FROM account WHERE user_id = ?";
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setLong(1, userId);
ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

private static String makeUsername(String first, String last) {
String f = first == null ? "" : first.trim();
String l = last == null ? "" : last.trim();
return cap(take3(f)) + cap(take3(l));
}

private static String take3(String s) {
if (s.isEmpty()) return "XXX";
return s.length() <= 3 ? s : s.substring(0, 3);
}

private static String cap(String s) {
if (s.isEmpty()) return s;
String low = s.toLowerCase();
return Character.toUpperCase(low.charAt(0)) + low.substring(1);
}
}
Loading
Loading