18 Commits
Author SHA1 Message Date
Michael Burgess 81ff63a38d Build against latest Paper API and local MobArena; fix CI JDK/artifact upload
build / build (push) Successful in 3m31s
2026-08-06 22:40:32 -04:00
Michael Burgess 3e65b02d72 Fix Gitea CI: add JDK setup, drop unsupported cache action
build / build (push) Failing after 2m42s
2026-08-06 20:28:59 -04:00
Michael Burgess f8ba0e4efc Rename ssl config key to flags
build / build (push) Failing after 13s
2026-08-06 20:08:35 -04:00
Michael Burgess 6f74026514 Use raw JDBC parameter string instead of ssl boolean flag
build / build (push) Failing after 14s
2026-08-06 19:45:09 -04:00
Andreas Troelsen eb77193fbd Release 1.0. 2022-07-30 15:58:05 +02:00
Andreas Troelsen 2c65d88c62 Link to releases/actions in README.
After the first release we should be able to link to the `/latest` URL
just fine, so we'll just prep it now. We don't really want to maintain
an "official" test builds channel on Discord, so that's out as well.
2022-07-30 15:58:05 +02:00
Andreas Troelsen 627205d39a Qualify getTables() with catalog and schema.
Without these qualifiers, the tables returned by the metadata object
will include tables from _all_ databases in the database instance, which
is definitely not something we want. Not only does this pose a potential
performance impact, it can also return very false positives, which then
result in an (inaptly named) SQLSyntaxErrorException when the subsequent
query to the potentially non-existent table fails.

To reproduce the issue:

- Create two new, empty databases in a fresh MySQL/MariaDB instance.
- Point the extension at one of them.
- Spin up the server, let the extension initialize the database.
- Stop the server.
- Point the extension at the other database.
- Spin the server back up and watch it crash.

By qualifying the `getTables()` call with the catalog and schema of the
underlying connection object, we limit the search to _just_ that catalog
and schema, which is what we wanted all along. According to the method
documentation, the args are _patterns_, where `%` matches substrings and
`_` matches "any character". So by being hyper-specific, we shouldn't
get any more false positives. In testing on MySQL and MariaDB, only the
`catalog` value is non-null (matches the database name), while `schema`
remains null.
2022-07-30 15:58:05 +02:00
Andreas Troelsen 3f7796772e Upgrade Mockito to 3.12.2.
This fixes an issue with the mock maker not running properly on recent
versions of the JVM, and it also matches the version in the base plugin.
2022-07-30 12:10:50 +02:00
Andreas Troelsen f07df6a556 Add compatibility note in README.
build / build (push) Failing after 2m7s
2022-07-26 17:35:01 +02:00
Andreas Troelsen 9e92094501 Ignore leave events for spectators.
Spectators produce leave events, but they won't have a join event that
matches the leave event they produce. This is because spectators either
joined _as_ spectators (spectating doesn't produce join events), or they
_transitioned_ into the spectator state after _dying_ in the session (in
which case they already produced a death event that "cements" the player
session).

This fixes an edge case problem that occurs when a spectator produces a
leave event while exactly one player is currently in the lobby. In this
case, the leave event triggers a session deletion, since the leave event
logic "thinks" the leave event is triggered by the player in the lobby.
It would be possible to prevent that problem with additional checks in
the leave event logic, but the underlying problem really is that leave
events produced by spectators shouldn't be recorded or reacted to in any
way, shape, or form, as they are not relevant to the stats (leave times
are only really relevant if the leave event happens _during_ a session,
which this change does not affect).
2022-07-25 17:30:19 +02:00
Andreas Troelsen 85df0066f1 Remove player entries on leave/death prior to session start.
While we do want to keep track of join and ready times, we really don't
care about those stats for players who join the lobby _and leave_ prior
to the arena actually starting.

Coincidentally, _not_ storing stats for players that don't actually pick
a class means we don't end up trying to insert a `null` value into the
class name column, which should solve the error thrown when trying to do
just that as a result of the auto-start timer kicking players or players
themselves leaving prior to starting.

Fixes #16
2022-07-03 20:58:42 +02:00
Andreas Troelsen ffc6c89eac Fix references to repo in README.
The project has been migrated to the MobArena org, so let's make sure
the links make sense :)
2022-06-21 20:39:09 +02:00
Andreas Troelsen 4ab74b06cb Upgrade MySQL connector to 8.0.28.
Apparently there's a security issue <= 8.0.27. It doesn't really matter
all that much here since we're in the `provided` scope, but better safe
than sorry.
2022-06-21 20:32:20 +02:00
Andreas Troelsen 2745af3d0d Switch to Spigot API and bump version to 1.17.
This brings the repository up to the same Spigot API version as the core
plugin for essentially the same reasons, plus consistency.
2022-05-27 22:47:04 +02:00
Andreas Troelsen 72bb017834 Fix maven-compiler-plugin configuration.
Apparently the whole `<release>` tag shebang doesn't work on jitpack.
Not sure where it comes from.
2021-12-05 13:57:20 +01:00
Andreas Troelsen 57bf8c6472 Add tests for delete session command.
This is just a single sunshine scenario test because that's all the
command supports right now. Issues #2 and #3 highlight some of the
problems with this command, and when those issues are tackled, tests
should be introduced along with the fixes.
2021-08-06 22:13:14 +02:00
Andreas Troelsen 066a15c2af Add query command tests.
Not a lot of variation in these commands, but the output is a little
difficult to write robust tests for because it's all stringly typed.
These tests will break pretty hard (or require lots of adapting) if we
introduce i18n.

The tests don't cover tab completion. That will have to come later.
2021-08-06 22:13:14 +02:00
Andreas Troelsen f96c69bb42 Add import/export command tests.
These are pretty much whitebox tests, so they don't really provide any
improvements to the architecture, but they do add a level of confidence
in terms of refactoring the somewhat unorthodox import/export structure.
2021-08-06 22:13:14 +02:00
20 changed files with 646 additions and 44 deletions
+11 -7
View File
@@ -15,19 +15,23 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Cache dependencies
uses: actions/cache@v2
- name: Set up JDK
uses: actions/setup-java@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
distribution: temurin
java-version: '25'
- name: Checkout MobArena
run: git clone --depth 1 https://git.tss3.us/skywalker3200/MobArena.git ../MobArena
- name: Publish MobArena to local Maven repo
run: cd ../MobArena && ./gradlew publishToMavenLocal --no-daemon
- name: Build, test, package
run: ./mvnw -B package --file pom.xml
- name: Upload artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: MobArenaStats.jar
path: target/MobArenaStats.jar
+9 -3
View File
@@ -1,14 +1,20 @@
# MobArenaStats [![Build Status](https://github.com/garbagemule/MobArenaStats/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/garbagemule/MobArenaStats/actions/workflows/build.yml)
# MobArenaStats [![Build Status](https://github.com/mobarena/MobArenaStats/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/mobarena/MobArenaStats/actions/workflows/build.yml)
MobArenaStats is a _plugin extension_ for [MobArena](https://github.com/garbagemule/MobArena).
The extension collects stats from MobArena sessions into persistent storage such as MySQL, MariaDB, and SQLite databases.
It hooks into MobArena's command handler to provide commands for querying and managing the stats.
**Note:** This extension requires MobArena 0.107 or later to work.
## Getting Started
Download a copy of the latest MobArenaStats build and place it in your server's `plugins` folder.
You can grab a build from the _Artifacts_ section of the latest run of the [build workflow](https://github.com/garbagemule/MobArenaStats/actions/workflows/build.yml) in GitHub Actions, or you can join the MobArena Discord server and grab one from the `#test-builds` channel.
Download a copy of MobArenaStats from one of these release channels:
- Stable (recommended): [Latest release](https://github.com/mobarena/MobArenaStats/releases/latest)
- Dev (experimental): [Latest build](https://github.com/mobarena/MobArenaStats/actions/workflows/build.yml)
Place the jar-file in your server's `plugins` folder like you would any other plugin.
Start your server and let the plugin load.
It will generate a default `config.yml` file and set up a default SQLite data store.
+14 -19
View File
@@ -6,7 +6,7 @@
<groupId>org.mobarena</groupId>
<artifactId>mobarena-stats</artifactId>
<version>1.0-SNAPSHOT</version>
<version>1.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -34,19 +34,19 @@
<version>2.2.1</version>
</dependency>
<!-- https://hub.spigotmc.org/nexus/content/groups/public/org/bukkit/bukkit/ -->
<!-- https://repo.papermc.io/repository/maven-public/io/papermc/paper/paper-api/ -->
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
<version>1.13-R0.1-SNAPSHOT</version>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>26.2.build.84-stable</version>
<scope>provided</scope>
</dependency>
<!-- https://jitpack.io/#garbagemule/MobArena -->
<!-- Built and installed locally from F:\skywalker3200\MobArena via `./gradlew publishToMavenLocal` -->
<dependency>
<groupId>com.github.garbagemule</groupId>
<groupId>com.garbagemule</groupId>
<artifactId>MobArena</artifactId>
<version>b99713f1</version>
<version>0.109.1-tss3</version>
<scope>provided</scope>
</dependency>
@@ -54,7 +54,7 @@
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
<version>8.0.28</version>
<scope>provided</scope>
</dependency>
@@ -86,7 +86,7 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>3.11.2</version>
<version>3.12.2</version>
<scope>test</scope>
</dependency>
@@ -133,13 +133,8 @@
<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/groups/public/</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
@@ -157,7 +152,7 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
<release>25</release>
</configuration>
</plugin>
@@ -170,7 +165,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<version>3.6.2</version>
<executions>
<execution>
<phase>package</phase>
+61
View File
@@ -0,0 +1,61 @@
#----------------------------------------------------------------------
# The store is where all the collected data is kept. Different store
# types are supported:
#
# - sqlite: stores stats in an SQLite database
# - mysql: stores stats in a MySQL/MariaDB database
# - csv: stores stats in local CSV files
#
# Stores may require configuration of additional properties, such as
# file paths or database credentials.
#----------------------------------------------------------------------
store:
#--------------------------------------------------------------------
# Which type of store to use.
#
# Changing this value will _not_ result in an automatic conversion
# of existing store data. To transfer data to a different store,
# make an export of the current store first, then change type and
# import the exported data.
#
type: mysql
#--------------------------------------------------------------------
#--------------------------------------------------------------------
# SQLite store properties
#
# - filename: name of the database file, relative to plugin folder
#
#filename: stats.db
#--------------------------------------------------------------------
#--------------------------------------------------------------------
# MySQL/MariaDB store properties
#
# - host: where the database instance is hosted
# - port: database port number
# - database: name of the database (must exist!)
# - username: username of a valid database user
# - password: password of a valid database user
# - flags: extra JDBC connection parameters, appended to the URL as-is
# (e.g. 'verifyServerCertificate=false&sslmode=required').
# Leave blank/omit to append no extra parameters.
#
host: localhost
port: 3306
database: mobarena_stats
username: mobarena
password: 'change-me'
flags: 'verifyServerCertificate=false&sslmode=required'
#--------------------------------------------------------------------
#--------------------------------------------------------------------
# CSV store properties
#
# - folder: where to store data files, relative to plugin folder
# - separator: symbol to separate fields and values with
#
#folder: data
#separator: ';'
#--------------------------------------------------------------------
@@ -47,6 +47,11 @@ public class Session {
public void playerLeave(Arena arena, Player player) {
UUID playerId = player.getUniqueId();
if (sessionStats.startTime == null) {
playerStats.remove(playerId);
return;
}
PlayerSessionStats stats = playerStats.get(playerId);
if (stats == null) {
return;
@@ -63,6 +68,11 @@ public class Session {
public void playerDeath(Arena arena, Player player) {
UUID playerId = player.getUniqueId();
if (sessionStats.startTime == null) {
playerStats.remove(playerId);
return;
}
PlayerSessionStats stats = playerStats.get(playerId);
if (stats == null) {
return;
@@ -88,6 +88,10 @@ public class SessionListener implements Listener {
Arena arena = event.getArena();
Player player = event.getPlayer();
if (arena.inSpec(player)) {
return;
}
Session session = sessionStore.getByArena(arena);
if (session == null) {
log.warning("Unexpected leave event for non-existent session of arena " + arena.getSlug());
@@ -70,8 +70,10 @@ class SchemaMigrator {
// (available via the underlying JDBC connection object) to find
// out if the schema migrations table exists.
Connection connection = handle.getConnection();
String catalog = connection.getCatalog();
String schema = connection.getSchema();
DatabaseMetaData meta = connection.getMetaData();
try (ResultSet tables = meta.getTables(null, null, "schema_migrations", null)) {
try (ResultSet tables = meta.getTables(catalog, schema, "schema_migrations", null)) {
while (tables.next()) {
String name = tables.getString("TABLE_NAME");
if (name.equals("schema_migrations")) {
@@ -29,11 +29,11 @@ public class MariadbStatsStore {
String host = config.getString("host", "localhost");
int port = config.getInt("port", 3306);
String database = config.getString("database", "mobarena_stats");
boolean ssl = config.getBoolean("ssl", false);
String flags = config.getString("flags", "");
String params = "useSSL=" + ssl;
String url = "jdbc:mariadb://" + host + ":" + port + "/" + database;
return "jdbc:mariadb://" + host + ":" + port + "/" + database + "?" + params;
return flags.isEmpty() ? url : url + "?" + flags;
}
}
@@ -27,11 +27,11 @@ public class MysqlStatsStore {
String host = config.getString("host", "localhost");
int port = config.getInt("port", 3306);
String database = config.getString("database", "mobarena_stats");
boolean ssl = config.getBoolean("ssl", false);
String flags = config.getString("flags", "");
String params = "useSSL=" + ssl;
String url = "jdbc:mysql://" + host + ":" + port + "/" + database;
return "jdbc:mysql://" + host + ":" + port + "/" + database + "?" + params;
return flags.isEmpty() ? url : url + "?" + flags;
}
}
+3 -2
View File
@@ -38,14 +38,15 @@ store:
# - database: name of the database (must exist!)
# - username: username of a valid database user
# - password: password of a valid database user
# - ssl: whether to use SSL for database connections
# - flags: extra JDBC connection parameters, appended to the URL as-is
# (e.g. 'verifyServerCertificate=false&sslmode=required')
#
#host: localhost
#port: 3306
#database: ''
#username: ''
#password: ''
#ssl: false
#flags: ''
#--------------------------------------------------------------------
#--------------------------------------------------------------------
@@ -0,0 +1,61 @@
package org.mobarena.stats.command;
import com.garbagemule.MobArena.Messenger;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.command.CommandSender;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mobarena.stats.MobArenaStats;
import org.mobarena.stats.store.ArenaStats;
import org.mobarena.stats.store.StatsStore;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ArenaStatsCommandTest {
MobArenaStats plugin;
ArenaStatsCommand subject;
@BeforeEach
void setup() {
plugin = mock(MobArenaStats.class);
subject = new ArenaStatsCommand(plugin);
}
@Test
void noArgumentsReturnsFalse() {
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {};
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(false));
}
@Test
void success() {
String slug = "castle";
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {slug};
Messenger messenger = mock(Messenger.class);
StatsStore store = mock(StatsStore.class);
ArenaStats stats = new ArenaStats(1, 2, 3, 4, 5, 6, 7);
when(am.getGlobalMessenger()).thenReturn(messenger);
when(plugin.getStatsStore()).thenReturn(store);
when(plugin.getAsyncExecutor()).thenReturn(Runnable::run);
when(store.getArenaStats(slug)).thenReturn(stats);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(true));
verify(messenger).tell(eq(sender), contains(slug));
}
}
@@ -0,0 +1,50 @@
package org.mobarena.stats.command;
import com.garbagemule.MobArena.Messenger;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.command.CommandSender;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mobarena.stats.MobArenaStats;
import org.mobarena.stats.store.StatsStore;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.UUID;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DeleteSessionStatsCommandTest {
MobArenaStats plugin;
DeleteSessionStatsCommand subject;
@BeforeEach
void setup() {
plugin = mock(MobArenaStats.class);
subject = new DeleteSessionStatsCommand(plugin);
}
@Test
void success() {
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {id};
Messenger messenger = mock(Messenger.class);
StatsStore store = mock(StatsStore.class);
when(am.getGlobalMessenger()).thenReturn(messenger);
when(plugin.getStatsStore()).thenReturn(store);
when(plugin.getAsyncExecutor()).thenReturn(Runnable::run);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(true));
verify(store).delete(UUID.fromString(id));
verify(messenger).tell(eq(sender), contains("deleted"));
}
}
@@ -0,0 +1,79 @@
package org.mobarena.stats.command;
import com.garbagemule.MobArena.Messenger;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mobarena.stats.MobArenaStats;
import org.mobarena.stats.store.StatsStore;
import org.mobarena.stats.store.StatsStoreRegistry;
import org.mockito.ArgumentMatchers;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ExportCommandTest {
MobArenaStats plugin;
ExportCommand subject;
@BeforeEach
void setup() {
plugin = mock(MobArenaStats.class);
subject = new ExportCommand(plugin);
}
@Test
void failure() throws Exception {
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {};
Messenger messenger = mock(Messenger.class);
StatsStore source = mock(StatsStore.class);
StatsStore target = mock(StatsStore.class);
StatsStoreRegistry registry = mock(StatsStoreRegistry.class);
when(am.getGlobalMessenger()).thenReturn(messenger);
when(plugin.getStatsStore()).thenReturn(source);
when(plugin.getStatsStoreRegistry()).thenReturn(registry);
when(plugin.getAsyncExecutor()).thenReturn(Runnable::run);
when(registry.create(ArgumentMatchers.any(ConfigurationSection.class))).thenReturn(target);
doThrow(IOException.class).when(source).export(target);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(true));
verify(messenger).tell(eq(sender), contains("Exporting stats"));
verify(messenger).tell(eq(sender), contains("failed"));
}
@Test
void success() throws Exception {
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {};
Messenger messenger = mock(Messenger.class);
StatsStore source = mock(StatsStore.class);
StatsStore target = mock(StatsStore.class);
StatsStoreRegistry registry = mock(StatsStoreRegistry.class);
when(am.getGlobalMessenger()).thenReturn(messenger);
when(plugin.getStatsStore()).thenReturn(source);
when(plugin.getStatsStoreRegistry()).thenReturn(registry);
when(plugin.getAsyncExecutor()).thenReturn(Runnable::run);
when(registry.create(ArgumentMatchers.any(ConfigurationSection.class))).thenReturn(target);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(true));
verify(messenger).tell(eq(sender), contains("Exporting stats"));
verify(messenger).tell(eq(sender), contains("complete"));
}
}
@@ -0,0 +1,49 @@
package org.mobarena.stats.command;
import com.garbagemule.MobArena.Messenger;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.command.CommandSender;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mobarena.stats.MobArenaStats;
import org.mobarena.stats.store.GlobalStats;
import org.mobarena.stats.store.StatsStore;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class GlobalStatsCommandTest {
MobArenaStats plugin;
GlobalStatsCommand subject;
@BeforeEach
void setup() {
plugin = mock(MobArenaStats.class);
subject = new GlobalStatsCommand(plugin);
}
@Test
void success() {
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {};
Messenger messenger = mock(Messenger.class);
StatsStore store = mock(StatsStore.class);
GlobalStats stats = new GlobalStats(1, 2, 3, 4);
when(am.getGlobalMessenger()).thenReturn(messenger);
when(plugin.getStatsStore()).thenReturn(store);
when(plugin.getAsyncExecutor()).thenReturn(Runnable::run);
when(store.getGlobalStats()).thenReturn(stats);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(true));
verify(messenger).tell(eq(sender), contains("Global stats"));
}
}
@@ -0,0 +1,147 @@
package org.mobarena.stats.command;
import com.garbagemule.MobArena.Messenger;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mobarena.stats.MobArenaStats;
import org.mobarena.stats.store.StatsStore;
import org.mobarena.stats.store.StatsStoreRegistry;
import org.mockito.ArgumentMatchers;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ImportCommandTest {
@TempDir
File data;
MobArenaStats plugin;
ImportCommand subject;
@BeforeEach
void setup() {
plugin = mock(MobArenaStats.class);
subject = new ImportCommand(plugin);
}
@Test
void noArgumentsReturnsFalse() {
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {};
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(false));
}
@Test
void nonExistentSourceFileNoImport() {
String filename = "stats.export-321.db";
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {filename};
when(plugin.getDataFolder()).thenReturn(data);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(false));
verify(sender).sendMessage(contains("not found"));
}
@Test
void wrongFilenamePrefixNoImport() throws Exception {
String filename = "stats.db";
Files.createFile(data.toPath().resolve(filename));
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {filename};
when(plugin.getDataFolder()).thenReturn(data);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(true));
verify(sender).sendMessage(contains("filename must start with"));
}
@Test
void wrongFilenameSuffixNoImport() throws Exception {
String filename = "stats.export-123.sql";
Files.createFile(data.toPath().resolve(filename));
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {filename};
when(plugin.getDataFolder()).thenReturn(data);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(true));
verify(sender).sendMessage(contains("filename must end with"));
}
@Test
void failure() throws Exception {
String filename = "stats.export-123.db";
Files.createFile(data.toPath().resolve(filename));
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {filename};
Messenger messenger = mock(Messenger.class);
StatsStore source = mock(StatsStore.class);
StatsStore target = mock(StatsStore.class);
StatsStoreRegistry registry = mock(StatsStoreRegistry.class);
when(am.getGlobalMessenger()).thenReturn(messenger);
when(plugin.getDataFolder()).thenReturn(data);
when(plugin.getStatsStore()).thenReturn(target);
when(plugin.getStatsStoreRegistry()).thenReturn(registry);
when(plugin.getAsyncExecutor()).thenReturn(Runnable::run);
when(registry.create(ArgumentMatchers.any(ConfigurationSection.class))).thenReturn(source);
doThrow(IOException.class).when(source).export(target);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(true));
verify(messenger).tell(eq(sender), contains("Importing stats"));
verify(messenger).tell(eq(sender), contains("failed"));
}
@Test
void success() throws Exception {
String filename = "stats.export-123.db";
Files.createFile(data.toPath().resolve(filename));
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {filename};
Messenger messenger = mock(Messenger.class);
StatsStore source = mock(StatsStore.class);
StatsStore target = mock(StatsStore.class);
StatsStoreRegistry registry = mock(StatsStoreRegistry.class);
when(am.getGlobalMessenger()).thenReturn(messenger);
when(plugin.getDataFolder()).thenReturn(data);
when(plugin.getStatsStore()).thenReturn(target);
when(plugin.getStatsStoreRegistry()).thenReturn(registry);
when(plugin.getAsyncExecutor()).thenReturn(Runnable::run);
when(registry.create(ArgumentMatchers.any(ConfigurationSection.class))).thenReturn(source);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(true));
verify(messenger).tell(eq(sender), contains("Importing stats"));
verify(source).export(target);
verify(messenger).tell(eq(sender), contains("complete"));
}
}
@@ -0,0 +1,83 @@
package org.mobarena.stats.command;
import com.garbagemule.MobArena.Messenger;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mobarena.stats.MobArenaStats;
import org.mobarena.stats.store.PlayerStats;
import org.mobarena.stats.store.StatsStore;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class PlayerStatsCommandTest {
MobArenaStats plugin;
PlayerStatsCommand subject;
@BeforeEach
void setup() {
plugin = mock(MobArenaStats.class);
subject = new PlayerStatsCommand(plugin);
}
@Test
void noArgumentsReturnsFalseForConsole() {
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {};
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(false));
}
@Test
void noArgumentsUsesSenderNameForPlayers() {
String name = "alice";
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(Player.class);
String[] args = {};
Messenger messenger = mock(Messenger.class);
StatsStore store = mock(StatsStore.class);
PlayerStats stats = new PlayerStats(1, 2, 3, 4);
when(sender.getName()).thenReturn(name);
when(am.getGlobalMessenger()).thenReturn(messenger);
when(plugin.getStatsStore()).thenReturn(store);
when(plugin.getAsyncExecutor()).thenReturn(Runnable::run);
when(store.getPlayerStats(name)).thenReturn(stats);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(true));
verify(messenger).tell(eq(sender), contains(name));
}
@Test
void success() {
String name = "garbagemule";
ArenaMaster am = mock(ArenaMaster.class);
CommandSender sender = mock(CommandSender.class);
String[] args = {name};
Messenger messenger = mock(Messenger.class);
StatsStore store = mock(StatsStore.class);
PlayerStats stats = new PlayerStats(1, 2, 3, 4);
when(am.getGlobalMessenger()).thenReturn(messenger);
when(plugin.getStatsStore()).thenReturn(store);
when(plugin.getAsyncExecutor()).thenReturn(Runnable::run);
when(store.getPlayerStats(name)).thenReturn(stats);
boolean result = subject.execute(am, sender, args);
assertThat(result, equalTo(true));
verify(messenger).tell(eq(sender), contains(name));
}
}
@@ -110,10 +110,23 @@ class SessionListenerTest {
verify(session).playerReady(player, className);
}
@Test
void ignoresPlayerLeaveForSpectators() {
Player player = mock(Player.class);
Arena arena = mock(Arena.class);
when(arena.inSpec(player)).thenReturn(true);
ArenaPlayerLeaveEvent event = new ArenaPlayerLeaveEvent(player, arena);
subject.on(event);
verifyNoInteractions(sessionStore);
}
@Test
void logsWarningIfPlayerLeavesInNonExistentSession() {
Player player = mock(Player.class);
Arena arena = mock(Arena.class);
when(arena.inSpec(player)).thenReturn(false);
when(sessionStore.getByArena(arena)).thenReturn(null);
ArenaPlayerLeaveEvent event = new ArenaPlayerLeaveEvent(player, arena);
@@ -127,6 +140,7 @@ class SessionListenerTest {
Player player = mock(Player.class);
Arena arena = mock(Arena.class);
Session session = mock(Session.class);
when(arena.inSpec(player)).thenReturn(false);
when(arena.isRunning()).thenReturn(false);
when(arena.getPlayersInLobby()).thenReturn(Collections.singleton(player));
when(sessionStore.getByArena(arena)).thenReturn(session);
@@ -142,6 +156,7 @@ class SessionListenerTest {
Player player = mock(Player.class);
Arena arena = mock(Arena.class);
Session session = mock(Session.class);
when(arena.inSpec(player)).thenReturn(false);
when(arena.isRunning()).thenReturn(false);
when(arena.getPlayersInLobby()).thenReturn(Collections.singleton(player));
when(sessionStore.getByArena(arena)).thenReturn(session);
@@ -158,6 +173,7 @@ class SessionListenerTest {
Player other = mock(Player.class);
Arena arena = mock(Arena.class);
Session session = mock(Session.class);
when(arena.inSpec(player)).thenReturn(false);
when(arena.isRunning()).thenReturn(false);
when(arena.getPlayersInLobby()).thenReturn(new HashSet<>(Arrays.asList(player, other)));
when(sessionStore.getByArena(arena)).thenReturn(session);
@@ -173,6 +189,7 @@ class SessionListenerTest {
Player player = mock(Player.class);
Arena arena = mock(Arena.class);
Session session = mock(Session.class);
when(arena.inSpec(player)).thenReturn(false);
when(arena.isRunning()).thenReturn(true);
when(sessionStore.getByArena(arena)).thenReturn(session);
ArenaPlayerLeaveEvent event = new ArenaPlayerLeaveEvent(player, arena);
@@ -95,6 +95,19 @@ class SessionTest {
assertThat(actual.className, equalTo(className));
}
@Test
void removeFromSessionOnLeaveBeforeStart() {
UUID playerId = UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe");
Player player = Mocks.player(playerId, "garbagemule");
Arena arena = mock(Arena.class);
subject.playerJoin(player);
subject.playerLeave(arena, player);
PlayerSessionStats actual = subject.getPlayerStats(playerId);
assertThat(actual, nullValue());
}
@Test
void setLeaveTimeOnLeave() {
UUID playerId = UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe");
@@ -103,6 +116,7 @@ class SessionTest {
ArenaPlayer ap = mock(ArenaPlayer.class);
when(arena.getArenaPlayer(player)).thenReturn(ap);
subject.playerJoin(player);
subject.start();
subject.playerLeave(arena, player);
@@ -118,6 +132,7 @@ class SessionTest {
ArenaPlayer ap = mock(ArenaPlayer.class);
when(arena.getArenaPlayer(player)).thenReturn(ap);
subject.playerJoin(player);
subject.start();
subject.playerLeave(arena, player);
@@ -133,6 +148,7 @@ class SessionTest {
ArenaPlayer ap = mock(ArenaPlayer.class);
when(arena.getArenaPlayer(player)).thenReturn(ap);
subject.playerJoin(player);
subject.start();
subject.getPlayerStats(playerId).conclusion = PlayerConclusion.VICTORY;
subject.playerLeave(arena, player);
@@ -141,6 +157,19 @@ class SessionTest {
assertThat(actual.conclusion, not(equalTo(PlayerConclusion.RETREAT)));
}
@Test
void removeFromSessionOnDeathBeforeStart() {
UUID playerId = UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe");
Player player = Mocks.player(playerId, "garbagemule");
Arena arena = mock(Arena.class);
subject.playerJoin(player);
subject.playerDeath(arena, player);
PlayerSessionStats actual = subject.getPlayerStats(playerId);
assertThat(actual, nullValue());
}
@Test
void setDeathTimeOnDeath() {
UUID playerId = UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe");
@@ -149,6 +178,7 @@ class SessionTest {
ArenaPlayer ap = mock(ArenaPlayer.class);
when(arena.getArenaPlayer(player)).thenReturn(ap);
subject.playerJoin(player);
subject.start();
subject.playerDeath(arena, player);
@@ -164,6 +194,7 @@ class SessionTest {
ArenaPlayer ap = mock(ArenaPlayer.class);
when(arena.getArenaPlayer(player)).thenReturn(ap);
subject.playerJoin(player);
subject.start();
subject.playerDeath(arena, player);
@@ -179,6 +210,7 @@ class SessionTest {
ArenaPlayer ap = mock(ArenaPlayer.class);
when(arena.getArenaPlayer(player)).thenReturn(ap);
subject.playerJoin(player);
subject.start();
subject.getPlayerStats(playerId).conclusion = PlayerConclusion.VICTORY;
subject.playerDeath(arena, player);
@@ -246,6 +278,7 @@ class SessionTest {
when(arena.getArenaPlayer(corpse)).thenReturn(ap);
subject.playerJoin(corpse);
subject.playerJoin(survivor);
subject.start();
subject.playerDeath(arena, corpse);
subject.complete();
@@ -16,7 +16,7 @@ class MariadbStatsStoreTest {
String result = MariadbStatsStore.getUrl(config);
String expected = "jdbc:mariadb://localhost:3306/mobarena_stats?useSSL=false";
String expected = "jdbc:mariadb://localhost:3306/mobarena_stats";
assertThat(result, equalTo(expected));
}
@@ -26,11 +26,11 @@ class MariadbStatsStoreTest {
config.set("host", "stats.example.com");
config.set("port", 1337);
config.set("database", "mastats");
config.set("ssl", true);
config.set("flags", "verifyServerCertificate=false&sslmode=required");
String result = MariadbStatsStore.getUrl(config);
String expected = "jdbc:mariadb://stats.example.com:1337/mastats?useSSL=true";
String expected = "jdbc:mariadb://stats.example.com:1337/mastats?verifyServerCertificate=false&sslmode=required";
assertThat(result, equalTo(expected));
}
@@ -16,7 +16,7 @@ class MysqlStatsStoreTest {
String result = MysqlStatsStore.getUrl(config);
String expected = "jdbc:mysql://localhost:3306/mobarena_stats?useSSL=false";
String expected = "jdbc:mysql://localhost:3306/mobarena_stats";
assertThat(result, equalTo(expected));
}
@@ -26,11 +26,11 @@ class MysqlStatsStoreTest {
config.set("host", "stats.example.com");
config.set("port", 1337);
config.set("database", "mastats");
config.set("ssl", true);
config.set("flags", "verifyServerCertificate=false&sslmode=required");
String result = MysqlStatsStore.getUrl(config);
String expected = "jdbc:mysql://stats.example.com:1337/mastats?useSSL=true";
String expected = "jdbc:mysql://stats.example.com:1337/mastats?verifyServerCertificate=false&sslmode=required";
assertThat(result, equalTo(expected));
}