Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add database tests #272

Merged
merged 9 commits into from
Sep 4, 2022
Merged
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
6 changes: 2 additions & 4 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
### Checklist

- [ ] Tests have been added in relevant areas
- [ ] Corresponding changes made to the documentation (README.adoc)
- [ ] Corresponding changes made to the documentation (README.adoc) <!-- (if irrelevant check the box too) -->

### Type of change

- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
<!-- Choose one from "Bug fix" / "New Feature" / "Breaking change" / "Internal change" -->

## Description

Expand Down
7 changes: 5 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ lang3-version = "3.12.0"
jetbrains-annotations-version = "23.0.0"
websocket-version = "1.5.3"
junit-version = "5.9.0"
assertj-version = "3.23.1"
assertj-core-version = "3.23.1"
assertj-db-version = "2.0.2"
mockito-version = "4.7.0"
awaitility-version = "4.2.0"
json-unit-version = "2.35.0"
Expand Down Expand Up @@ -54,7 +55,8 @@ flyway-mysql = { group = "org.flywaydb", name = "flyway-mysql", version.ref = "f
junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit-version" }
junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit-version" }
junitEngine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit-version" }
assertj = { group = "org.assertj", name = "assertj-core", version.ref = "assertj-version" }
assertj-core = { group = "org.assertj", name = "assertj-core", version.ref = "assertj-core-version" }
assertj-db = { group = "org.assertj", name = "assertj-db", version.ref = "assertj-db-version" }
mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito-version" }
mockito-inline = { group = "org.mockito", name = "mockito-inline", version.ref = "mockito-version" }
mockito-junit = { group = "org.mockito", name = "mockito-junit-jupiter", version.ref = "mockito-version" }
Expand All @@ -71,6 +73,7 @@ flyway = ["flyway-core", "flyway-mysql"]
junit = ["junit-api", "junit-params"]
mockito = ["mockito-core", "mockito-inline", "mockito-junit"]
jsonUnit = ["json-unit", "json-unit-assertj"]
assertj = ["assertj-core", "assertj-db"]

[plugins]
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow-version" }
Expand Down
2 changes: 1 addition & 1 deletion miner/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies {
testImplementation(libs.bundles.junit)
testRuntimeOnly(libs.junitEngine)

testImplementation(libs.assertj)
testImplementation(libs.bundles.assertj)
testImplementation(libs.bundles.mockito)
testImplementation(libs.awaitility)
testImplementation(libs.unirestMocks)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static java.time.ZoneOffset.UTC;

@RequiredArgsConstructor
@Log4j2
Expand Down Expand Up @@ -54,15 +53,20 @@ public void updateChannelStatusTime(@NotNull String channelId, @NotNull Instant
WHERE `ID` = ?;"""
)){

var timestamp = LocalDateTime.now(UTC);

statement.setObject(1, timestamp);
statement.setTimestamp(1, Timestamp.from(instant));
statement.setString(2, channelId);

statement.executeUpdate();
}
}

@Override
public int addUserPrediction(@NotNull String username, @NotNull String channelId, @NotNull String badge) throws SQLException{
var userId = getOrCreatePredictionUserId(username, channelId);
addUserPrediction(channelId, userId, badge);
return userId;
}

@Override
public void addBalance(@NotNull String channelId, int balance, @Nullable String reason, @NotNull Instant instant) throws SQLException{
try(var conn = getConnection();
Expand All @@ -72,7 +76,7 @@ public void addBalance(@NotNull String channelId, int balance, @Nullable String
)){

statement.setString(1, channelId);
statement.setObject(2, LocalDateTime.ofInstant(instant, UTC));
statement.setTimestamp(2, Timestamp.from(instant));
statement.setInt(3, balance);
statement.setString(4, reason);

Expand All @@ -90,39 +94,22 @@ public void addPrediction(@NotNull String channelId, @NotNull String eventId, @N

statement.setString(1, channelId);
statement.setString(2, eventId);
statement.setObject(3, LocalDateTime.ofInstant(instant, UTC));
statement.setTimestamp(3, Timestamp.from(instant));
statement.setString(4, type);
statement.setString(5, description);

statement.executeUpdate();
}
}

@Override
public void addUserPrediction(@NotNull String username, @NotNull String channelId, @NotNull String badge) throws SQLException{

try(var conn = getConnection();
var predictionStatement = getPredictionStmt(conn)){
conn.setAutoCommit(false);

var userId = getOrCreatePredictionUserId(conn, username, channelId);

predictionStatement.setString(1, channelId);
predictionStatement.setInt(2, userId);
predictionStatement.setString(3, badge);

predictionStatement.executeUpdate();
conn.commit();
}
}

protected int getOrCreatePredictionUserId(@NotNull Connection conn, @NotNull String username, @NotNull String channelId) throws SQLException{
protected int getOrCreatePredictionUserId(@NotNull String username, @NotNull String channelId) throws SQLException{
username = username.toLowerCase(Locale.ROOT);
var lock = getOrCreatePredictionUserIdLocks[hashToIndex(username.hashCode(), getOrCreatePredictionUserIdLocks.length)];
lock.lock();

try(var selectUserStatement = conn.prepareStatement("""
SELECT `ID` FROM `PredictionUser` WHERE `Username`=? AND `ChannelID`=?""")){
try(var conn = getConnection();
var selectUserStatement = conn.prepareStatement("""
SELECT `ID` FROM `PredictionUser` WHERE `Username`=? AND `ChannelID`=?""")){

selectUserStatement.setString(1, username);
selectUserStatement.setString(2, channelId);
Expand Down Expand Up @@ -156,132 +143,71 @@ protected int getOrCreatePredictionUserId(@NotNull Connection conn, @NotNull Str
}
}

protected abstract void addUserPrediction(@NotNull String channelId, int userId, @NotNull String badge) throws SQLException;

private int hashToIndex(int hash, int length){
if(hash == Integer.MIN_VALUE){
return 0;
}
return Math.abs(hash) % length;
}

@NotNull
protected abstract PreparedStatement getPredictionStmt(@NotNull Connection conn) throws SQLException;

@Override
public void cancelPrediction(@NotNull Event event) throws SQLException{
var ended = Optional.ofNullable(event.getEndedAt()).map(ZonedDateTime::toInstant).orElseGet(TimeFactory::now);

try(var conn = getConnection();
var addCanceledPredictionStmt = conn.prepareStatement("""
INSERT INTO `ResolvedPrediction`(`EventID`,`ChannelID`, `Title`,`EventCreated`,`EventEnded`,`Canceled`)
VALUES (?,?,?,?,?,true)""");
var removePredictionsStmt = getDeleteUserPredictionsForChannelStmt(conn)
VALUES (?,?,?,?,?,true)""")
){
conn.setAutoCommit(false);
try{
//Add canceled prediction
addCanceledPredictionStmt.setString(1, event.getId());
addCanceledPredictionStmt.setString(2, event.getChannelId());
addCanceledPredictionStmt.setString(3, event.getTitle());
addCanceledPredictionStmt.setObject(4, event.getCreatedAt().withZoneSameInstant(UTC).toLocalDateTime());
addCanceledPredictionStmt.setObject(5, LocalDateTime.ofInstant(ended, UTC));
addCanceledPredictionStmt.executeUpdate();

//Remove made predictions
removePredictionsStmt.setString(1, event.getChannelId());
removePredictionsStmt.executeUpdate();

conn.commit();
}
catch(SQLException e){
conn.rollback();
throw e;
}
addCanceledPredictionStmt.setString(1, event.getId());
addCanceledPredictionStmt.setString(2, event.getChannelId());
addCanceledPredictionStmt.setString(3, event.getTitle());
addCanceledPredictionStmt.setTimestamp(4, Timestamp.from(event.getCreatedAt().toInstant()));
addCanceledPredictionStmt.setTimestamp(5, Timestamp.from(ended));
addCanceledPredictionStmt.executeUpdate();
}

deleteUserPredictionsForChannel(event.getChannelId());
}

@Override
public void resolvePrediction(@NotNull Event event, @NotNull String outcome, @NotNull String badge, double returnRatioForWin) throws SQLException{
var ended = Optional.ofNullable(event.getEndedAt()).map(ZonedDateTime::toInstant).orElseGet(TimeFactory::now);

resolveUserPredictions(returnRatioForWin, event.getChannelId(), badge);

try(var conn = getConnection();
var getOpenPredictionStmt = conn.prepareStatement("""
SELECT * FROM `UserPrediction` WHERE `ChannelID`=?""");
var updatePredictionUserStmt = getUpdatePredictionUserStmt(conn);
var addResolvedPredictionStmt = conn.prepareStatement("""
INSERT INTO `ResolvedPrediction`(`EventID`,`ChannelID`, `Title`,`EventCreated`,`EventEnded`,`Canceled`,`Outcome`,`Badge`,`ReturnRatioForWin`)
VALUES (?,?,?,?,?,false,?,?,?)""");
var removePredictionsStmt = getDeleteUserPredictionsForChannelStmt(conn)
VALUES (?,?,?,?,?,false,?,?,?)""")
){
conn.setAutoCommit(false);

try{
//Get user predictions, determine win/lose and update
double returnOnInvestment = returnRatioForWin - 1;
getOpenPredictionStmt.setString(1, event.getChannelId());
try(var result = getOpenPredictionStmt.executeQuery()){
while(result.next()){
var userPrediction = Converters.convertUserPrediction(result);
if(badge.equals(userPrediction.getBadge())){
updatePredictionUserStmt.setInt(1, 1);
updatePredictionUserStmt.setDouble(2, returnOnInvestment);
}
else{
updatePredictionUserStmt.setInt(1, 0);
updatePredictionUserStmt.setDouble(2, -1);
}
updatePredictionUserStmt.setInt(3, userPrediction.getUserId());
updatePredictionUserStmt.setString(4, userPrediction.getChannelId());
updatePredictionUserStmt.addBatch();
}
updatePredictionUserStmt.executeBatch();
}

//Add the resolved prediction
addResolvedPredictionStmt.setString(1, event.getId());
addResolvedPredictionStmt.setString(2, event.getChannelId());
addResolvedPredictionStmt.setString(3, event.getTitle());
addResolvedPredictionStmt.setObject(4, event.getCreatedAt().withZoneSameInstant(UTC).toLocalDateTime());
addResolvedPredictionStmt.setObject(5, LocalDateTime.ofInstant(ended, UTC));
addResolvedPredictionStmt.setString(6, outcome);
addResolvedPredictionStmt.setString(7, badge);
addResolvedPredictionStmt.setDouble(8, returnRatioForWin);
addResolvedPredictionStmt.executeUpdate();

//Remove Predictions
removePredictionsStmt.setString(1, event.getChannelId());
removePredictionsStmt.executeUpdate();

conn.commit();
}
catch(SQLException e){
conn.rollback();
throw e;
}
addResolvedPredictionStmt.setString(1, event.getId());
addResolvedPredictionStmt.setString(2, event.getChannelId());
addResolvedPredictionStmt.setString(3, event.getTitle());
addResolvedPredictionStmt.setTimestamp(4, Timestamp.from(event.getCreatedAt().toInstant()));
addResolvedPredictionStmt.setTimestamp(5, Timestamp.from(ended));
addResolvedPredictionStmt.setString(6, outcome);
addResolvedPredictionStmt.setString(7, badge);
addResolvedPredictionStmt.setDouble(8, returnRatioForWin);
addResolvedPredictionStmt.executeUpdate();
}

deleteUserPredictionsForChannel(event.getChannelId());
}

@NotNull
protected abstract PreparedStatement getUpdatePredictionUserStmt(@NotNull Connection conn) throws SQLException;
protected abstract void resolveUserPredictions(double returnRatioForWin, @NotNull String channelId, @NotNull String badge) throws SQLException;

@Override
public void deleteUserPredictions() throws SQLException{
public void deleteAllUserPredictions() throws SQLException{
log.debug("Removing all user predictions.");
try(var conn = getConnection();
var statement = conn.prepareStatement("""
DELETE FROM `UserPrediction`"""
)){
var statement = conn.prepareStatement("DELETE FROM `UserPrediction`")){
statement.executeUpdate();
}
}

@NotNull
private PreparedStatement getDeleteUserPredictionsForChannelStmt(@NotNull Connection conn) throws SQLException{
return conn.prepareStatement("""
DELETE FROM `UserPrediction`
WHERE `ChannelID`=?"""
);
}

@Override
public void deleteUserPredictionsForChannel(@NotNull String channelId) throws SQLException{
log.debug("Removing user predictions for channelId '{}'.", channelId);
Expand All @@ -292,6 +218,28 @@ public void deleteUserPredictionsForChannel(@NotNull String channelId) throws SQ
}
}

@Override
@NotNull
public Optional<String> getStreamerIdFromName(@NotNull String channelName) throws SQLException{
log.debug("Getting streamerId from channel {}", channelName);
try(var conn = getConnection();
var statement = conn.prepareStatement("""
SELECT `ID`
FROM `Channel`
WHERE `Username`=?"""
)){
statement.setString(1, channelName);

try(var result = statement.executeQuery()){
if(result.next()){
return Optional.ofNullable(result.getString("ID"));
}
}

return Optional.empty();
}
}

@Override
@NotNull
public Collection<OutcomeStatistic> getOutcomeStatisticsForChannel(@NotNull String channelId, int minBetsPlacedByUser) throws SQLException{
Expand All @@ -308,7 +256,7 @@ public Collection<OutcomeStatistic> getOutcomeStatisticsForChannel(@NotNull Stri
INNER JOIN `PredictionUser` AS pu
ON up.`UserID`=pu.`ID` AND up.`ChannelID` = pu.`ChannelID`
WHERE up.`ChannelID`=?
AND `PredictionCnt`>?
AND `PredictionCnt`>=?
GROUP BY `Badge`"""
)){
statement.setString(1, channelId);
Expand All @@ -330,30 +278,16 @@ public void close(){
dataSource.close();
}

@Override
@NotNull
public Optional<String> getStreamerIdFromName(@NotNull String channelName) throws SQLException{
log.debug("Getting streamerId from channel {}", channelName);
try(var conn = getConnection();
var statement = conn.prepareStatement("""
SELECT `ID`
FROM `Channel`
WHERE `Username`=?"""
)){
statement.setString(1, channelName);

try(var result = statement.executeQuery()){
if(result.next()){
return Optional.ofNullable(result.getString("ID"));
}
}

return Optional.empty();
}
protected Connection getConnection() throws SQLException{
return dataSource.getConnection();
}

@NotNull
protected Connection getConnection() throws SQLException{
return dataSource.getConnection();
private PreparedStatement getDeleteUserPredictionsForChannelStmt(@NotNull Connection conn) throws SQLException{
return conn.prepareStatement("""
DELETE FROM `UserPrediction`
WHERE `ChannelID`=?"""
);
}
}
Loading