1
0
mirror of https://github.com/lucko/LuckPerms.git synced 2025-08-22 14:12:48 +02:00

Implement paginated logs for all storage backends

This commit is contained in:
Luck
2024-06-09 16:31:56 +01:00
parent 23575d788a
commit 3b5a936674
16 changed files with 577 additions and 323 deletions

View File

@@ -4,7 +4,11 @@ plugins {
} }
test { test {
useJUnitPlatform {} useJUnitPlatform {
if (!project.hasProperty('dockerTests')) {
excludeTags 'docker'
}
}
} }
jacocoTestReport { jacocoTestReport {
@@ -15,9 +19,14 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.1' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.1' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.1'
testImplementation "org.testcontainers:junit-jupiter:1.19.8"
testImplementation 'org.mockito:mockito-core:5.11.0' testImplementation 'org.mockito:mockito-core:5.11.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0' testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0'
testImplementation 'com.h2database:h2:2.1.214' testImplementation 'com.h2database:h2:2.1.214'
testImplementation 'org.mongodb:mongodb-driver-legacy:4.5.0'
testImplementation 'org.spongepowered:configurate-yaml:3.7.2'
testImplementation 'org.spongepowered:configurate-hocon:3.7.2'
testImplementation 'me.lucko.configurate:configurate-toml:3.7'
api project(':api') api project(':api')
api 'org.checkerframework:checker-qual:3.12.0' api 'org.checkerframework:checker-qual:3.12.0'
@@ -87,7 +96,7 @@ dependencies {
compileOnly 'redis.clients:jedis:4.4.3' compileOnly 'redis.clients:jedis:4.4.3'
compileOnly 'io.nats:jnats:2.16.4' compileOnly 'io.nats:jnats:2.16.4'
compileOnly 'com.rabbitmq:amqp-client:5.12.0' compileOnly 'com.rabbitmq:amqp-client:5.12.0'
api 'org.mongodb:mongodb-driver-legacy:4.5.0' compileOnly 'org.mongodb:mongodb-driver-legacy:4.5.0'
compileOnly 'org.postgresql:postgresql:42.6.0' compileOnly 'org.postgresql:postgresql:42.6.0'
compileOnly 'org.yaml:snakeyaml:1.28' compileOnly 'org.yaml:snakeyaml:1.28'
} }

View File

@@ -42,6 +42,10 @@ public class Log {
return new Builder(); return new Builder();
} }
public static Log of(List<LoggedAction> content) {
return content.isEmpty() ? EMPTY : new Log(content);
}
public static Log empty() { public static Log empty() {
return EMPTY; return EMPTY;
} }

View File

@@ -37,6 +37,10 @@ public class LogPage {
return new LogPage.Builder(); return new LogPage.Builder();
} }
public static LogPage of(List<LoggedAction> content) {
return content.isEmpty() ? EMPTY : new LogPage(content);
}
public static LogPage empty() { public static LogPage empty() {
return EMPTY; return EMPTY;
} }

View File

@@ -62,7 +62,7 @@ public final class ActionFilterMongoBuilder extends FilterMongoBuilder<Action> {
if (value instanceof String | value instanceof UUID) { if (value instanceof String | value instanceof UUID) {
return value; return value;
} else if (value instanceof Action.Target.Type) { } else if (value instanceof Action.Target.Type) {
return LoggedAction.getTypeString((Action.Target.Type) value); return ((Action.Target.Type) value).name();
} else { } else {
throw new IllegalArgumentException("Don't know how to map value with type: " + value.getClass().getName()); throw new IllegalArgumentException("Don't know how to map value with type: " + value.getClass().getName());
} }

View File

@@ -25,6 +25,10 @@
package me.lucko.luckperms.common.filter; package me.lucko.luckperms.common.filter;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
public class PageParameters { public class PageParameters {
private final int pageSize; private final int pageSize;
@@ -43,4 +47,18 @@ public class PageParameters {
return this.pageNumber; return this.pageNumber;
} }
public <T> List<T> paginate(List<T> input) {
int fromIndex = this.pageSize * (this.pageNumber - 1);
if (fromIndex >= input.size()) {
return Collections.emptyList();
}
int toIndex = Math.min(fromIndex + this.pageSize, input.size());
return input.subList(fromIndex, toIndex);
}
public <T> Stream<T> paginate(Stream<T> input) {
return input.skip((long) this.pageSize * (this.pageNumber - 1)).limit(this.pageSize);
}
} }

View File

@@ -76,7 +76,7 @@ public class ConstraintMongoBuilder {
public static <R> FindIterable<R> page(PageParameters pageParameters, FindIterable<R> iterable) { public static <R> FindIterable<R> page(PageParameters pageParameters, FindIterable<R> iterable) {
int pageSize = pageParameters.pageSize(); int pageSize = pageParameters.pageSize();
int pageNumber = pageParameters.pageNumber(); int pageNumber = pageParameters.pageNumber();
return iterable.limit(pageNumber).skip((pageNumber - 1) * pageSize); return iterable.limit(pageSize).skip((pageNumber - 1) * pageSize);
} }
} }

View File

@@ -25,12 +25,10 @@
package me.lucko.luckperms.common.filter.mongo; package me.lucko.luckperms.common.filter.mongo;
import com.mongodb.client.FindIterable;
import com.mongodb.client.model.Filters; import com.mongodb.client.model.Filters;
import me.lucko.luckperms.common.filter.Filter; import me.lucko.luckperms.common.filter.Filter;
import me.lucko.luckperms.common.filter.FilterField; import me.lucko.luckperms.common.filter.FilterField;
import me.lucko.luckperms.common.filter.FilterList; import me.lucko.luckperms.common.filter.FilterList;
import me.lucko.luckperms.common.filter.PageParameters;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import java.util.List; import java.util.List;
@@ -64,10 +62,4 @@ public abstract class FilterMongoBuilder<T> extends ConstraintMongoBuilder {
return make(filters.operator(), filters); return make(filters.operator(), filters);
} }
public static <R> FindIterable<R> page(PageParameters pageParameters, FindIterable<R> iterable) {
int pageSize = pageParameters.pageSize();
int pageNumber = pageParameters.pageNumber();
return iterable.limit(pageNumber).skip((pageNumber - 1) * pageSize);
}
} }

View File

@@ -177,7 +177,7 @@ public abstract class AbstractConfigurateStorage implements StorageImplementatio
@Override @Override
public LogPage getLogPage(FilterList<Action> filters, PageParameters page) throws Exception { public LogPage getLogPage(FilterList<Action> filters, PageParameters page) throws Exception {
throw new UnsupportedOperationException(); return this.actionLogger.getLogPage(filters, page);
} }
@Override @Override

View File

@@ -25,12 +25,14 @@
package me.lucko.luckperms.common.storage.implementation.file; package me.lucko.luckperms.common.storage.implementation.file;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import me.lucko.luckperms.common.actionlog.ActionJsonSerializer; import me.lucko.luckperms.common.actionlog.ActionJsonSerializer;
import me.lucko.luckperms.common.actionlog.Log; import me.lucko.luckperms.common.actionlog.Log;
import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LogPage;
import me.lucko.luckperms.common.actionlog.LoggedAction;
import me.lucko.luckperms.common.cache.BufferedRequest; import me.lucko.luckperms.common.cache.BufferedRequest;
import me.lucko.luckperms.common.filter.FilterList; import me.lucko.luckperms.common.filter.FilterList;
import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.filter.PageParameters;
@@ -45,11 +47,14 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class FileActionLogger { public class FileActionLogger {
@@ -133,36 +138,37 @@ public class FileActionLogger {
} }
} }
public Log getLog() throws IOException { private Stream<LoggedAction> getRawLog() throws IOException {
// if there is log content waiting to be written, flush immediately before trying to read // if there is log content waiting to be written, flush immediately before trying to read
if (this.saveBuffer.isEnqueued()) { if (this.saveBuffer.isEnqueued()) {
this.saveBuffer.requestDirectly(); this.saveBuffer.requestDirectly();
} }
if (!Files.exists(this.contentFile)) { if (!Files.exists(this.contentFile)) {
return Log.empty(); return Stream.empty();
} }
Log.Builder log = Log.builder(); Stream.Builder<LoggedAction> builder = Stream.builder();
try (BufferedReader reader = Files.newBufferedReader(this.contentFile, StandardCharsets.UTF_8)) { try (BufferedReader reader = Files.newBufferedReader(this.contentFile, StandardCharsets.UTF_8)) {
String line; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
try { try {
JsonElement parsed = GsonProvider.parser().parse(line); JsonElement parsed = GsonProvider.parser().parse(line);
log.add(ActionJsonSerializer.deserialize(parsed)); builder.add(ActionJsonSerializer.deserialize(parsed));
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
} }
return builder.build();
}
return log.build(); public Log getLog() throws IOException {
return Log.of(getRawLog().collect(Collectors.toList()));
} }
public LogPage getLogPage(FilterList<Action> filters, PageParameters page) throws IOException { public LogPage getLogPage(FilterList<Action> filters, PageParameters page) throws IOException {
return LogPage.of(page.paginate(getRawLog().filter(filters::evaluate).sorted(Comparator.reverseOrder())).collect(Collectors.toList()));
} }
private final class SaveBuffer extends BufferedRequest<Void> { private final class SaveBuffer extends BufferedRequest<Void> {

View File

@@ -31,11 +31,13 @@ import com.mongodb.MongoClientOptions;
import com.mongodb.MongoClientURI; import com.mongodb.MongoClientURI;
import com.mongodb.MongoCredential; import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress; import com.mongodb.ServerAddress;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters; import com.mongodb.client.model.Filters;
import com.mongodb.client.model.ReplaceOptions; import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.Sorts;
import me.lucko.luckperms.common.actionlog.Log; import me.lucko.luckperms.common.actionlog.Log;
import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LogPage;
import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.actionlog.LoggedAction;
@@ -70,6 +72,7 @@ import net.luckperms.api.node.Node;
import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.NodeBuilder;
import org.bson.Document; import org.bson.Document;
import org.bson.UuidRepresentation; import org.bson.UuidRepresentation;
import org.bson.conversions.Bson;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
@@ -193,7 +196,7 @@ public class MongoStorage implements StorageImplementation {
public LogPage getLogPage(FilterList<Action> filters, PageParameters page) throws Exception { public LogPage getLogPage(FilterList<Action> filters, PageParameters page) throws Exception {
LogPage.Builder log = LogPage.builder(); LogPage.Builder log = LogPage.builder();
MongoCollection<Document> c = this.database.getCollection(this.prefix + "action"); MongoCollection<Document> c = this.database.getCollection(this.prefix + "action");
try (MongoCursor<Document> cursor = FilterMongoBuilder.page(page, c.find(ActionFilterMongoBuilder.INSTANCE.make(filters))).iterator()) { try (MongoCursor<Document> cursor = ConstraintMongoBuilder.page(page, c.find(ActionFilterMongoBuilder.INSTANCE.make(filters)).sort(Sorts.descending("timestamp"))).iterator()) {
while (cursor.hasNext()) { while (cursor.hasNext()) {
log.add(actionFromDoc(cursor.next())); log.add(actionFromDoc(cursor.next()));
} }

View File

@@ -268,7 +268,7 @@ public class SqlStorage implements StorageImplementation {
ActionFilterSqlBuilder sqlBuilder = new ActionFilterSqlBuilder(); ActionFilterSqlBuilder sqlBuilder = new ActionFilterSqlBuilder();
sqlBuilder.builder().append(ACTION_SELECT_ALL); sqlBuilder.builder().append(ACTION_SELECT_ALL);
sqlBuilder.visit(filter); sqlBuilder.visit(filter);
sqlBuilder.builder().append(" ORDER BY id DESC"); sqlBuilder.builder().append(" ORDER BY time DESC");
sqlBuilder.visit(page); sqlBuilder.visit(page);
try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor)) { try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor)) {

View File

@@ -25,6 +25,8 @@
package me.lucko.luckperms.common.storage.misc; package me.lucko.luckperms.common.storage.misc;
import com.google.common.collect.ImmutableMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@@ -54,6 +56,10 @@ public class StorageCredentials {
this.properties = properties; this.properties = properties;
} }
public StorageCredentials(String address, String database, String username, String password) {
this(address, database, username, password, 10, 10, 1800000, 0, 5000, ImmutableMap.of());
}
public String getAddress() { public String getAddress() {
return Objects.requireNonNull(this.address, "address"); return Objects.requireNonNull(this.address, "address");
} }

View File

@@ -0,0 +1,355 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.storage;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import me.lucko.luckperms.common.actionlog.filter.ActionFilters;
import me.lucko.luckperms.common.actionlog.Log;
import me.lucko.luckperms.common.actionlog.LogPage;
import me.lucko.luckperms.common.actionlog.LoggedAction;
import me.lucko.luckperms.common.filter.PageParameters;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.config.LuckPermsConfiguration;
import me.lucko.luckperms.common.event.EventDispatcher;
import me.lucko.luckperms.common.model.Group;
import me.lucko.luckperms.common.model.PrimaryGroupHolder;
import me.lucko.luckperms.common.model.User;
import me.lucko.luckperms.common.model.manager.group.GroupManager;
import me.lucko.luckperms.common.model.manager.group.StandardGroupManager;
import me.lucko.luckperms.common.model.manager.user.StandardUserManager;
import me.lucko.luckperms.common.model.manager.user.UserManager;
import me.lucko.luckperms.common.node.types.Inheritance;
import me.lucko.luckperms.common.node.types.Permission;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap;
import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter;
import me.lucko.luckperms.common.storage.implementation.StorageImplementation;
import net.luckperms.api.actionlog.Action;
import net.luckperms.api.model.PlayerSaveResult;
import net.luckperms.api.model.PlayerSaveResult.Outcome;
import net.luckperms.api.model.data.DataType;
import net.luckperms.api.node.Node;
import net.luckperms.api.node.types.InheritanceNode;
import net.luckperms.api.node.types.PermissionNode;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Instant;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.AdditionalAnswers.answer;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public abstract class AbstractStorageTest {
@Mock protected LuckPermsPlugin plugin;
@Mock protected LuckPermsBootstrap bootstrap;
@Mock protected LuckPermsConfiguration configuration;
private StorageImplementation storage;
@BeforeEach
public final void setupMocksAndStorage() throws Exception {
lenient().when(this.plugin.getBootstrap()).thenReturn(this.bootstrap);
lenient().when(this.plugin.getConfiguration()).thenReturn(this.configuration);
lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class));
lenient().when(this.bootstrap.getScheduler()).thenReturn(mock(SchedulerAdapter.class));
lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION)).thenReturn(PrimaryGroupHolder.AllParentsByWeight::new);
lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION_METHOD)).thenReturn("parents-by-weight");
lenient().when(this.bootstrap.getResourceStream(anyString()))
.then(answer((String path) -> AbstractStorageTest.class.getClassLoader().getResourceAsStream(path)));
lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class));
this.storage = makeStorage(this.plugin);
this.storage.init();
}
protected abstract StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception;
protected void cleanupResources() {
// do nothing
}
@AfterEach
public final void shutdownStorage() {
this.storage.shutdown();
cleanupResources();
}
@Test
public void testActionLog() throws Exception {
LoggedAction action = LoggedAction.build()
.source(UUID.randomUUID())
.sourceName("Test Source")
.targetType(Action.Target.Type.USER)
.target(UUID.randomUUID())
.targetName("Test Target")
.description("hello 123 hello 123")
.build();
this.storage.logAction(action);
Log log = this.storage.getLog();
assertEquals(1, log.getContent().size());
assertEquals(action, log.getContent().first());
}
@Test
public void testActionLogPage() throws Exception {
UUID sourceUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
Instant baseTime = Instant.now();
Function<Integer, LoggedAction> mockAction = i -> LoggedAction.build()
.source(i % 2 == 0 ? sourceUuid : UUID.randomUUID())
.sourceName("Test Source")
.targetType(Action.Target.Type.USER)
.target(targetUuid)
.targetName("Test Target")
.description("hello " + i)
.timestamp(baseTime.plusSeconds(i))
.build();
for (int i = 0; i < 100; i++) {
this.storage.logAction(mockAction.apply(-i));
}
for (int i = 0; i < 100; i++) {
this.storage.logAction(mockAction.apply(i));
}
for (int i = 100; i < 200; i++) {
this.storage.logAction(mockAction.apply(-i));
}
for (int i = 0; i < 10; i++) {
this.storage.logAction(LoggedAction.build()
.source(UUID.randomUUID())
.sourceName("Test Source")
.targetType(Action.Target.Type.GROUP)
.targetName(i % 2 == 0 ? "test_group" : "dummy")
.description("group test " + i)
.build());
}
for (int i = 0; i < 10; i++) {
this.storage.logAction(LoggedAction.build()
.source(UUID.randomUUID())
.sourceName("Test Source")
.targetType(Action.Target.Type.TRACK)
.targetName(i % 2 == 0 ? "test_track" : "dummy")
.description("track test " + i)
.build());
}
LogPage page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(5, 1));
assertEquals(ImmutableList.of(
mockAction.apply(98),
mockAction.apply(96),
mockAction.apply(94),
mockAction.apply(92),
mockAction.apply(90)
), page.getContent());
page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(5, 3));
assertEquals(ImmutableList.of(
mockAction.apply(78),
mockAction.apply(76),
mockAction.apply(74),
mockAction.apply(72),
mockAction.apply(70)
), page.getContent());
page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(500, 1));
assertEquals(150, page.getContent().size());
page = this.storage.getLogPage(ActionFilters.all(), new PageParameters(500, 1));
assertEquals(320, page.getContent().size());
page = this.storage.getLogPage(ActionFilters.user(targetUuid), new PageParameters(500, 1));
assertEquals(300, page.getContent().size());
page = this.storage.getLogPage(ActionFilters.group("test_group"), new PageParameters(10, 1));
assertEquals(5, page.getContent().size());
page = this.storage.getLogPage(ActionFilters.track("test_track"), new PageParameters(10, 1));
assertEquals(5, page.getContent().size());
page = this.storage.getLogPage(ActionFilters.search("hello"), new PageParameters(500, 1));
assertEquals(300, page.getContent().size());
}
@Test
public void testSavePlayerData() throws Exception {
UUID uniqueId = UUID.randomUUID();
// clean insert
PlayerSaveResult r1 = this.storage.savePlayerData(uniqueId, "Player1");
assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT), r1.getOutcomes());
assertNull(r1.getOtherUniqueIds());
assertNull(r1.getPreviousUsername());
// no change expected
PlayerSaveResult r2 = this.storage.savePlayerData(uniqueId, "Player1");
assertEquals(ImmutableSet.of(Outcome.NO_CHANGE), r2.getOutcomes());
assertNull(r2.getOtherUniqueIds());
assertNull(r2.getPreviousUsername());
// changed username
PlayerSaveResult r3 = this.storage.savePlayerData(uniqueId, "Player2");
assertEquals(ImmutableSet.of(Outcome.USERNAME_UPDATED), r3.getOutcomes());
assertNull(r3.getOtherUniqueIds());
assertTrue("Player1".equalsIgnoreCase(r3.getPreviousUsername()));
// changed uuid
UUID newUniqueId = UUID.randomUUID();
PlayerSaveResult r4 = this.storage.savePlayerData(newUniqueId, "Player2");
assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT, Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME), r4.getOutcomes());
assertNotNull(r4.getOtherUniqueIds());
assertEquals(ImmutableSet.of(uniqueId), r4.getOtherUniqueIds());
assertNull(r2.getPreviousUsername());
}
@Test
public void testGetPlayerUniqueIdAndName() throws Exception {
UUID uniqueId = UUID.randomUUID();
String username = "Player1";
this.storage.savePlayerData(uniqueId, username);
assertEquals(uniqueId, this.storage.getPlayerUniqueId("Player1"));
assertTrue(username.equalsIgnoreCase(this.storage.getPlayerName(uniqueId)));
}
@Test
public void testGetPlayerUniqueIdAndNameNull() throws Exception {
assertNull(this.storage.getPlayerUniqueId("Player1"));
assertNull(this.storage.getPlayerName(UUID.randomUUID()));
}
@Test
public void testSaveAndLoadGroup() throws Exception {
StandardGroupManager groupManager = new StandardGroupManager(this.plugin);
//noinspection unchecked,rawtypes
lenient().when(this.plugin.getGroupManager()).thenReturn((GroupManager) groupManager);
Group group = this.storage.createAndLoadGroup("test");
group.normalData().add(Permission.builder()
.permission("test.1")
.withContext("server", "test")
.build()
);
group.normalData().add(Permission.builder()
.permission("test.2")
.withContext("world", "test")
.build()
);
group.normalData().add(Permission.builder()
.permission("test.3")
.expiry(1, TimeUnit.HOURS)
.withContext("server", "test")
.withContext("world", "test")
.withContext("hello", "test")
.build()
);
Set<Node> nodes = group.normalData().asSet();
assertEquals(3, nodes.size());
this.storage.saveGroup(group);
groupManager.unload("test");
Group loaded = this.storage.loadGroup("test").orElse(null);
assertNotNull(loaded);
assertNotSame(group, loaded);
assertEquals(nodes, loaded.normalData().asSet());
}
@Test
public void testSaveAndDeleteUser() throws Exception {
StandardUserManager userManager = new StandardUserManager(this.plugin);
//noinspection unchecked,rawtypes
when(this.plugin.getUserManager()).thenReturn((UserManager) userManager);
UUID exampleUniqueId = UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5");
String exampleUsername = "Notch";
PermissionNode examplePermission = Permission.builder()
.permission("test.1")
.withContext("server", "test")
.build();
InheritanceNode defaultGroupNode = Inheritance.builder(GroupManager.DEFAULT_GROUP_NAME).build();
// create a default user, assert that is doesn't appear in unique users list
this.storage.savePlayerData(exampleUniqueId, exampleUsername);
assertFalse(this.storage.getUniqueUsers().contains(exampleUniqueId));
// give the user a node, assert that it does appear in unique users list
User user = this.storage.loadUser(exampleUniqueId, exampleUsername);
user.setNode(DataType.NORMAL, examplePermission, true);
this.storage.saveUser(user);
assertTrue(this.storage.getUniqueUsers().contains(exampleUniqueId));
// clear all nodes (reset to default) and assert that it does not appear in unique users list
user.clearNodes(DataType.NORMAL, null, true);
this.storage.saveUser(user);
assertFalse(this.storage.getUniqueUsers().contains(exampleUniqueId));
assertEquals(ImmutableSet.of(defaultGroupNode), user.normalData().asSet());
// give it a node again, assert that it shows as a unique user
user.setNode(DataType.NORMAL, examplePermission, true);
this.storage.saveUser(user);
assertTrue(this.storage.getUniqueUsers().contains(exampleUniqueId));
assertEquals(ImmutableSet.of(defaultGroupNode, examplePermission), user.normalData().asSet());
// reload user data from the db and assert that it is unchanged
user = this.storage.loadUser(exampleUniqueId, exampleUsername);
assertEquals(ImmutableSet.of(defaultGroupNode, examplePermission), user.normalData().asSet());
}
}

View File

@@ -0,0 +1,116 @@
package me.lucko.luckperms.common.storage;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.storage.implementation.StorageImplementation;
import me.lucko.luckperms.common.storage.implementation.file.CombinedConfigurateStorage;
import me.lucko.luckperms.common.storage.implementation.file.SeparatedConfigurateStorage;
import me.lucko.luckperms.common.storage.implementation.file.loader.HoconLoader;
import me.lucko.luckperms.common.storage.implementation.file.loader.JsonLoader;
import me.lucko.luckperms.common.storage.implementation.file.loader.TomlLoader;
import me.lucko.luckperms.common.storage.implementation.file.loader.YamlLoader;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import static org.mockito.Mockito.lenient;
public class ConfigurateStorageTest {
@Nested
class SeparatedYaml extends AbstractStorageTest {
@TempDir
private Path directory;
@Override
protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception {
lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory);
return new SeparatedConfigurateStorage(plugin, "YAML", new YamlLoader(), ".yml", "yaml-storage");
}
}
@Nested
class SeparatedJson extends AbstractStorageTest {
@TempDir
private Path directory;
@Override
protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception {
lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory);
return new SeparatedConfigurateStorage(plugin, "JSON", new JsonLoader(), ".json", "json-storage");
}
}
@Nested
class SeparatedHocon extends AbstractStorageTest {
@TempDir
private Path directory;
@Override
protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception {
lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory);
return new SeparatedConfigurateStorage(plugin, "HOCON", new HoconLoader(), ".conf", "hocon-storage");
}
}
@Nested
class SeparatedToml extends AbstractStorageTest {
@TempDir
private Path directory;
@Override
protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception {
lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory);
return new SeparatedConfigurateStorage(plugin, "TOML", new TomlLoader(), ".toml", "toml-storage");
}
}
@Nested
class CombinedYaml extends AbstractStorageTest {
@TempDir
private Path directory;
@Override
protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception {
lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory);
return new CombinedConfigurateStorage(plugin, "YAML", new YamlLoader(), ".yml", "yaml-storage");
}
}
@Nested
class CombinedJson extends AbstractStorageTest {
@TempDir
private Path directory;
@Override
protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception {
lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory);
return new CombinedConfigurateStorage(plugin, "JSON", new JsonLoader(), ".json", "json-storage");
}
}
@Nested
class CombinedHocon extends AbstractStorageTest {
@TempDir
private Path directory;
@Override
protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception {
lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory);
return new CombinedConfigurateStorage(plugin, "HOCON", new HoconLoader(), ".conf", "hocon-storage");
}
}
@Nested
class CombinedToml extends AbstractStorageTest {
@TempDir
private Path directory;
@Override
protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception {
lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory);
return new CombinedConfigurateStorage(plugin, "TOML", new TomlLoader(), ".toml", "toml-storage");
}
}
}

View File

@@ -0,0 +1,36 @@
package me.lucko.luckperms.common.storage;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.storage.implementation.StorageImplementation;
import me.lucko.luckperms.common.storage.implementation.mongodb.MongoStorage;
import me.lucko.luckperms.common.storage.misc.StorageCredentials;
import org.junit.jupiter.api.Tag;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
//@Tag("docker")
public class MongoStorageTest extends AbstractStorageTest {
private final GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("mongo"))
.withExposedPorts(27017);
@Override
protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception {
this.container.start();
String host = this.container.getHost();
Integer port = this.container.getFirstMappedPort();
StorageCredentials credentials = new StorageCredentials(
host + ":" + port,
"minecraft",
"",
""
);
return new MongoStorage(plugin, credentials, "", "");
}
@Override
protected void cleanupResources() {
this.container.stop();
}
}

View File

@@ -25,316 +25,22 @@
package me.lucko.luckperms.common.storage; package me.lucko.luckperms.common.storage;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import me.lucko.luckperms.common.actionlog.filter.ActionFilters;
import me.lucko.luckperms.common.actionlog.Log;
import me.lucko.luckperms.common.actionlog.LogPage;
import me.lucko.luckperms.common.actionlog.LoggedAction;
import me.lucko.luckperms.common.filter.PageParameters;
import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.config.LuckPermsConfiguration;
import me.lucko.luckperms.common.event.EventDispatcher;
import me.lucko.luckperms.common.model.Group;
import me.lucko.luckperms.common.model.PrimaryGroupHolder;
import me.lucko.luckperms.common.model.User;
import me.lucko.luckperms.common.model.manager.group.GroupManager;
import me.lucko.luckperms.common.model.manager.group.StandardGroupManager;
import me.lucko.luckperms.common.model.manager.user.StandardUserManager;
import me.lucko.luckperms.common.model.manager.user.UserManager;
import me.lucko.luckperms.common.node.types.Inheritance;
import me.lucko.luckperms.common.node.types.Permission;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; import me.lucko.luckperms.common.storage.implementation.StorageImplementation;
import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter;
import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage; import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage;
import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory;
import me.lucko.luckperms.common.storage.implementation.sql.connection.file.NonClosableConnection; import me.lucko.luckperms.common.storage.implementation.sql.connection.file.NonClosableConnection;
import net.luckperms.api.actionlog.Action;
import net.luckperms.api.model.PlayerSaveResult;
import net.luckperms.api.model.PlayerSaveResult.Outcome;
import net.luckperms.api.model.data.DataType;
import net.luckperms.api.node.Node;
import net.luckperms.api.node.types.InheritanceNode;
import net.luckperms.api.node.types.PermissionNode;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function; import java.util.function.Function;
import static org.junit.jupiter.api.Assertions.assertEquals; public class SqlStorageTest extends AbstractStorageTest {
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.AdditionalAnswers.answer;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @Override
public class SqlStorageTest { protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception {
return new SqlStorage(plugin, new TestH2ConnectionFactory(), "luckperms_");
@Mock private LuckPermsPlugin plugin;
@Mock private LuckPermsBootstrap bootstrap;
@Mock private LuckPermsConfiguration configuration;
private SqlStorage storage;
@BeforeEach
public void setupMocksAndDatabase() throws Exception {
lenient().when(this.plugin.getBootstrap()).thenReturn(this.bootstrap);
lenient().when(this.plugin.getConfiguration()).thenReturn(this.configuration);
lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class));
lenient().when(this.bootstrap.getScheduler()).thenReturn(mock(SchedulerAdapter.class));
lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION)).thenReturn(PrimaryGroupHolder.AllParentsByWeight::new);
lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION_METHOD)).thenReturn("parents-by-weight");
lenient().when(this.bootstrap.getResourceStream(anyString()))
.then(answer((String path) -> SqlStorageTest.class.getClassLoader().getResourceAsStream(path)));
lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class));
this.storage = new SqlStorage(this.plugin, new TestH2ConnectionFactory(), "luckperms_");
this.storage.init();
}
@AfterEach
public void shutdownDatabase() {
this.storage.shutdown();
}
@Test
public void testActionLog() throws Exception {
LoggedAction action = LoggedAction.build()
.source(UUID.randomUUID())
.sourceName("Test Source")
.targetType(Action.Target.Type.USER)
.target(UUID.randomUUID())
.targetName("Test Target")
.description("hello 123 hello 123")
.build();
this.storage.logAction(action);
Log log = this.storage.getLog();
assertEquals(1, log.getContent().size());
assertEquals(action, log.getContent().first());
}
@Test
public void testActionLogPage() throws Exception {
UUID sourceUuid = UUID.randomUUID();
UUID targetUuid = UUID.randomUUID();
Function<Integer, LoggedAction> mockAction = i -> LoggedAction.build()
.source(i % 2 == 0 ? sourceUuid : UUID.randomUUID())
.sourceName("Test Source")
.targetType(Action.Target.Type.USER)
.target(targetUuid)
.targetName("Test Target")
.description("hello " + i)
.build();
for (int i = 0; i < 100; i++) {
this.storage.logAction(mockAction.apply(i));
}
for (int i = 0; i < 10; i++) {
this.storage.logAction(LoggedAction.build()
.source(UUID.randomUUID())
.sourceName("Test Source")
.targetType(Action.Target.Type.GROUP)
.targetName(i % 2 == 0 ? "test_group" : "dummy")
.description("group test " + i)
.build());
}
for (int i = 0; i < 10; i++) {
this.storage.logAction(LoggedAction.build()
.source(UUID.randomUUID())
.sourceName("Test Source")
.targetType(Action.Target.Type.TRACK)
.targetName(i % 2 == 0 ? "test_track" : "dummy")
.description("track test " + i)
.build());
}
LogPage page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(5, 1));
assertEquals(ImmutableList.of(
mockAction.apply(98),
mockAction.apply(96),
mockAction.apply(94),
mockAction.apply(92),
mockAction.apply(90)
), page.getContent());
page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(5, 3));
assertEquals(ImmutableList.of(
mockAction.apply(78),
mockAction.apply(76),
mockAction.apply(74),
mockAction.apply(72),
mockAction.apply(70)
), page.getContent());
page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(200, 1));
assertEquals(50, page.getContent().size());
page = this.storage.getLogPage(ActionFilters.all(), new PageParameters(200, 1));
assertEquals(120, page.getContent().size());
page = this.storage.getLogPage(ActionFilters.user(targetUuid), new PageParameters(200, 1));
assertEquals(100, page.getContent().size());
page = this.storage.getLogPage(ActionFilters.group("test_group"), new PageParameters(10, 1));
assertEquals(5, page.getContent().size());
page = this.storage.getLogPage(ActionFilters.track("test_track"), new PageParameters(10, 1));
assertEquals(5, page.getContent().size());
page = this.storage.getLogPage(ActionFilters.search("hello"), new PageParameters(200, 1));
assertEquals(100, page.getContent().size());
}
@Test
public void testSavePlayerData() throws Exception {
UUID uniqueId = UUID.randomUUID();
// clean insert
PlayerSaveResult r1 = this.storage.savePlayerData(uniqueId, "Player1");
assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT), r1.getOutcomes());
assertNull(r1.getOtherUniqueIds());
assertNull(r1.getPreviousUsername());
// no change expected
PlayerSaveResult r2 = this.storage.savePlayerData(uniqueId, "Player1");
assertEquals(ImmutableSet.of(Outcome.NO_CHANGE), r2.getOutcomes());
assertNull(r2.getOtherUniqueIds());
assertNull(r2.getPreviousUsername());
// changed username
PlayerSaveResult r3 = this.storage.savePlayerData(uniqueId, "Player2");
assertEquals(ImmutableSet.of(Outcome.USERNAME_UPDATED), r3.getOutcomes());
assertNull(r3.getOtherUniqueIds());
assertTrue("Player1".equalsIgnoreCase(r3.getPreviousUsername()));
// changed uuid
UUID newUniqueId = UUID.randomUUID();
PlayerSaveResult r4 = this.storage.savePlayerData(newUniqueId, "Player2");
assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT, Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME), r4.getOutcomes());
assertNotNull(r4.getOtherUniqueIds());
assertEquals(ImmutableSet.of(uniqueId), r4.getOtherUniqueIds());
assertNull(r2.getPreviousUsername());
}
@Test
public void testGetPlayerUniqueIdAndName() throws Exception {
UUID uniqueId = UUID.randomUUID();
String username = "Player1";
this.storage.savePlayerData(uniqueId, username);
assertEquals(uniqueId, this.storage.getPlayerUniqueId("Player1"));
assertTrue(username.equalsIgnoreCase(this.storage.getPlayerName(uniqueId)));
}
@Test
public void testGetPlayerUniqueIdAndNameNull() throws Exception {
assertNull(this.storage.getPlayerUniqueId("Player1"));
assertNull(this.storage.getPlayerName(UUID.randomUUID()));
}
@Test
public void testSaveAndLoadGroup() throws Exception {
StandardGroupManager groupManager = new StandardGroupManager(this.plugin);
//noinspection unchecked,rawtypes
lenient().when(this.plugin.getGroupManager()).thenReturn((GroupManager) groupManager);
Group group = this.storage.createAndLoadGroup("test");
group.normalData().add(Permission.builder()
.permission("test.1")
.withContext("server", "test")
.build()
);
group.normalData().add(Permission.builder()
.permission("test.2")
.withContext("world", "test")
.build()
);
group.normalData().add(Permission.builder()
.permission("test.3")
.expiry(1, TimeUnit.HOURS)
.withContext("server", "test")
.withContext("world", "test")
.withContext("hello", "test")
.build()
);
Set<Node> nodes = group.normalData().asSet();
assertEquals(3, nodes.size());
this.storage.saveGroup(group);
groupManager.unload("test");
Group loaded = this.storage.loadGroup("test").orElse(null);
assertNotNull(loaded);
assertNotSame(group, loaded);
assertEquals(nodes, loaded.normalData().asSet());
}
@Test
public void testSaveAndDeleteUser() throws SQLException {
StandardUserManager userManager = new StandardUserManager(this.plugin);
//noinspection unchecked,rawtypes
when(this.plugin.getUserManager()).thenReturn((UserManager) userManager);
UUID exampleUniqueId = UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5");
String exampleUsername = "Notch";
PermissionNode examplePermission = Permission.builder()
.permission("test.1")
.withContext("server", "test")
.build();
InheritanceNode defaultGroupNode = Inheritance.builder(GroupManager.DEFAULT_GROUP_NAME).build();
// create a default user, assert that is doesn't appear in unique users list
this.storage.savePlayerData(exampleUniqueId, exampleUsername);
assertFalse(this.storage.getUniqueUsers().contains(exampleUniqueId));
// give the user a node, assert that it does appear in unique users list
User user = this.storage.loadUser(exampleUniqueId, exampleUsername);
user.setNode(DataType.NORMAL, examplePermission, true);
this.storage.saveUser(user);
assertTrue(this.storage.getUniqueUsers().contains(exampleUniqueId));
// clear all nodes (reset to default) and assert that it does not appear in unique users list
user.clearNodes(DataType.NORMAL, null, true);
this.storage.saveUser(user);
assertFalse(this.storage.getUniqueUsers().contains(exampleUniqueId));
assertEquals(ImmutableSet.of(defaultGroupNode), user.normalData().asSet());
// give it a node again, assert that it shows as a unique user
user.setNode(DataType.NORMAL, examplePermission, true);
this.storage.saveUser(user);
assertTrue(this.storage.getUniqueUsers().contains(exampleUniqueId));
assertEquals(ImmutableSet.of(defaultGroupNode, examplePermission), user.normalData().asSet());
// reload user data from the db and assert that it is unchanged
user = this.storage.loadUser(exampleUniqueId, exampleUsername);
assertEquals(ImmutableSet.of(defaultGroupNode, examplePermission), user.normalData().asSet());
} }
private static class TestH2ConnectionFactory implements ConnectionFactory { private static class TestH2ConnectionFactory implements ConnectionFactory {
@@ -379,5 +85,4 @@ public class SqlStorageTest {
this.connection.shutdown(); this.connection.shutdown();
} }
} }
} }