test: +14 тестов + DIP интерфейсы

- SocialCommandTest: 5 тестов (hug, noMention, cooldown, self, bot)
- CustomRoleManagerTest: 8 тестов
- RadioManagerTest: 8 тестов (set/get URL, start/stop, volume)
- VoiceButtonHandlerTest: smoke test
- DIP: WarningRepository, ActiveMuteRepository, ModerationCaseRepository
  теперь интерфейсы + Sql* реализации
Все: 438 tests, 0 failures (BlackjackServiceTest flaky)
This commit is contained in:
loki5512344 2026-07-04 20:25:25 +02:00
parent 4b79c561f4
commit 98ea510c49
13 changed files with 628 additions and 332 deletions

View file

@ -7,6 +7,9 @@ import creator.loki.database.Database;
import creator.loki.feature.config.repository.GuildConfigRepository;
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
import creator.loki.feature.moderation.repository.ModerationCaseRepository;
import creator.loki.feature.moderation.repository.SqlActiveMuteRepository;
import creator.loki.feature.moderation.repository.SqlModerationCaseRepository;
import creator.loki.feature.moderation.repository.SqlWarningRepository;
import creator.loki.feature.moderation.repository.WarningRepository;
public class ModerationModule extends AbstractModule {
@ -17,19 +20,19 @@ public class ModerationModule extends AbstractModule {
@Provides
@Singleton
WarningRepository provideWarningRepository(Database database) {
return new WarningRepository(database);
return new SqlWarningRepository(database);
}
@Provides
@Singleton
ModerationCaseRepository provideModerationCaseRepository(Database database) {
return new ModerationCaseRepository(database);
return new SqlModerationCaseRepository(database);
}
@Provides
@Singleton
ActiveMuteRepository provideActiveMuteRepository(Database database) {
return new ActiveMuteRepository(database);
return new SqlActiveMuteRepository(database);
}
@Provides

View file

@ -1,104 +1,16 @@
package creator.loki.feature.moderation.repository;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import creator.loki.database.Database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public interface ActiveMuteRepository {
void upsert(String guildId, String userId, long endsAtMs, long caseId);
/** Активные муты (для снятия роли по таймеру и восстановления после рестарта). */
public class ActiveMuteRepository {
private static final Logger logger = LoggerFactory.getLogger(ActiveMuteRepository.class);
private final Database database;
void delete(String guildId, String userId);
public ActiveMuteRepository(Database database) {
this.database = database;
}
Optional<ActiveMuteEntry> find(String guildId, String userId);
/**
* @param endsAtMs {@code 0} - бессрочный мут (таймер не ставится)
*/
public void upsert(String guildId, String userId, long endsAtMs, long caseId) {
String sql =
"""
INSERT INTO active_mutes (guild_id, user_id, ends_at_ms, case_id)
VALUES (?, ?, ?, ?)
ON CONFLICT(guild_id, user_id) DO UPDATE SET ends_at_ms = excluded.ends_at_ms, case_id = excluded.case_id
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.setLong(3, endsAtMs);
stmt.setLong(4, caseId);
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Ошибка upsert active_mutes guild={} user={}", guildId, userId, e);
}
}
List<ActiveMuteEntry> listTimedAfter(long nowMs);
public void delete(String guildId, String userId) {
String sql = "DELETE FROM active_mutes WHERE guild_id = ? AND user_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Ошибка delete active_mutes", e);
}
}
public Optional<ActiveMuteEntry> find(String guildId, String userId) {
String sql =
"SELECT guild_id, user_id, ends_at_ms, case_id FROM active_mutes WHERE guild_id = ? AND user_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(
new ActiveMuteEntry(
rs.getString("guild_id"),
rs.getString("user_id"),
rs.getLong("ends_at_ms"),
rs.getLong("case_id")));
}
}
} catch (SQLException e) {
logger.error("Ошибка find active_mutes", e);
}
return Optional.empty();
}
/** Муты с таймером, которые ещё должны истечь после {@code nowMs}. */
public List<ActiveMuteEntry> listTimedAfter(long nowMs) {
String sql =
"""
SELECT guild_id, user_id, ends_at_ms, case_id FROM active_mutes
WHERE ends_at_ms > ? AND ends_at_ms > 0
""";
List<ActiveMuteEntry> list = new ArrayList<>();
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setLong(1, nowMs);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
list.add(
new ActiveMuteEntry(
rs.getString("guild_id"),
rs.getString("user_id"),
rs.getLong("ends_at_ms"),
rs.getLong("case_id")));
}
}
} catch (SQLException e) {
logger.error("Ошибка list active_mutes", e);
}
return list;
}
public record ActiveMuteEntry(String guildId, String userId, long endsAtMs, long caseId) {}
record ActiveMuteEntry(String guildId, String userId, long endsAtMs, long caseId) {}
}

View file

@ -1,110 +1,20 @@
package creator.loki.feature.moderation.repository;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Optional;
import creator.loki.database.Database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Кейсы модерации (мут и т.п.) для команды {@code !case}. */
public class ModerationCaseRepository {
private static final Logger logger = LoggerFactory.getLogger(ModerationCaseRepository.class);
private final Database database;
public ModerationCaseRepository(Database database) {
this.database = database;
}
/**
* @param durationSec длительность в секундах или {@code null} (без срока / N/A)
* @param untilMs время окончания мута (epoch ms) или {@code null}
*/
public long insert(
public interface ModerationCaseRepository {
long insert(
String guildId,
String action,
String targetId,
String moderatorId,
String reason,
Long durationSec,
Long untilMs) {
String sql =
"""
INSERT INTO moderation_cases (guild_id, action, target_id, moderator_id,
reason, created_at, duration_sec, until_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""";
long now = System.currentTimeMillis();
try (Connection conn = database.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, guildId);
stmt.setString(2, action);
stmt.setString(3, targetId);
stmt.setString(4, moderatorId);
stmt.setString(5, reason);
stmt.setLong(6, now);
if (durationSec == null) {
stmt.setNull(7, java.sql.Types.INTEGER);
} else {
stmt.setLong(7, durationSec);
}
if (untilMs == null) {
stmt.setNull(8, java.sql.Types.BIGINT);
} else {
stmt.setLong(8, untilMs);
}
stmt.executeUpdate();
try (ResultSet keys = stmt.getGeneratedKeys()) {
if (keys.next()) {
return keys.getLong(1);
}
}
} catch (SQLException e) {
logger.error("Ошибка записи moderation_cases guild={}", guildId, e);
}
return -1;
}
Long untilMs);
public Optional<CaseEntry> findById(String guildId, long caseId) {
String sql =
"""
SELECT id, guild_id, action, target_id, moderator_id, reason, created_at, duration_sec, until_ms
FROM moderation_cases WHERE id = ? AND guild_id = ?
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setLong(1, caseId);
stmt.setString(2, guildId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(map(rs));
}
}
} catch (SQLException e) {
logger.error("Ошибка чтения case id={}", caseId, e);
}
return Optional.empty();
}
Optional<CaseEntry> findById(String guildId, long caseId);
private static CaseEntry map(ResultSet rs) throws SQLException {
Long duration = rs.getObject("duration_sec") == null ? null : rs.getLong("duration_sec");
Long until = rs.getObject("until_ms") == null ? null : rs.getLong("until_ms");
return new CaseEntry(
rs.getLong("id"),
rs.getString("guild_id"),
rs.getString("action"),
rs.getString("target_id"),
rs.getString("moderator_id"),
rs.getString("reason"),
rs.getLong("created_at"),
duration,
until);
}
public record CaseEntry(
record CaseEntry(
long id,
String guildId,
String action,

View file

@ -0,0 +1,101 @@
package creator.loki.feature.moderation.repository;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import creator.loki.database.Database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SqlActiveMuteRepository implements ActiveMuteRepository {
private static final Logger logger = LoggerFactory.getLogger(SqlActiveMuteRepository.class);
private final Database database;
public SqlActiveMuteRepository(Database database) {
this.database = database;
}
@Override
public void upsert(String guildId, String userId, long endsAtMs, long caseId) {
String sql =
"""
INSERT INTO active_mutes (guild_id, user_id, ends_at_ms, case_id)
VALUES (?, ?, ?, ?)
ON CONFLICT(guild_id, user_id) DO UPDATE SET ends_at_ms = excluded.ends_at_ms, case_id = excluded.case_id
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.setLong(3, endsAtMs);
stmt.setLong(4, caseId);
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Ошибка upsert active_mutes guild={} user={}", guildId, userId, e);
}
}
@Override
public void delete(String guildId, String userId) {
String sql = "DELETE FROM active_mutes WHERE guild_id = ? AND user_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Ошибка delete active_mutes", e);
}
}
@Override
public Optional<ActiveMuteEntry> find(String guildId, String userId) {
String sql =
"SELECT guild_id, user_id, ends_at_ms, case_id FROM active_mutes WHERE guild_id = ? AND user_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(
new ActiveMuteEntry(
rs.getString("guild_id"),
rs.getString("user_id"),
rs.getLong("ends_at_ms"),
rs.getLong("case_id")));
}
}
} catch (SQLException e) {
logger.error("Ошибка find active_mutes", e);
}
return Optional.empty();
}
@Override
public List<ActiveMuteEntry> listTimedAfter(long nowMs) {
String sql =
"""
SELECT guild_id, user_id, ends_at_ms, case_id FROM active_mutes
WHERE ends_at_ms > ? AND ends_at_ms > 0
""";
List<ActiveMuteEntry> list = new ArrayList<>();
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setLong(1, nowMs);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
list.add(
new ActiveMuteEntry(
rs.getString("guild_id"),
rs.getString("user_id"),
rs.getLong("ends_at_ms"),
rs.getLong("case_id")));
}
}
} catch (SQLException e) {
logger.error("Ошибка list active_mutes", e);
}
return list;
}
}

View file

@ -0,0 +1,103 @@
package creator.loki.feature.moderation.repository;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Optional;
import creator.loki.database.Database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SqlModerationCaseRepository implements ModerationCaseRepository {
private static final Logger logger = LoggerFactory.getLogger(SqlModerationCaseRepository.class);
private final Database database;
public SqlModerationCaseRepository(Database database) {
this.database = database;
}
@Override
public long insert(
String guildId,
String action,
String targetId,
String moderatorId,
String reason,
Long durationSec,
Long untilMs) {
String sql =
"""
INSERT INTO moderation_cases (guild_id, action, target_id, moderator_id,
reason, created_at, duration_sec, until_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""";
long now = System.currentTimeMillis();
try (Connection conn = database.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, guildId);
stmt.setString(2, action);
stmt.setString(3, targetId);
stmt.setString(4, moderatorId);
stmt.setString(5, reason);
stmt.setLong(6, now);
if (durationSec == null) {
stmt.setNull(7, java.sql.Types.INTEGER);
} else {
stmt.setLong(7, durationSec);
}
if (untilMs == null) {
stmt.setNull(8, java.sql.Types.BIGINT);
} else {
stmt.setLong(8, untilMs);
}
stmt.executeUpdate();
try (ResultSet keys = stmt.getGeneratedKeys()) {
if (keys.next()) {
return keys.getLong(1);
}
}
} catch (SQLException e) {
logger.error("Ошибка записи moderation_cases guild={}", guildId, e);
}
return -1;
}
@Override
public Optional<CaseEntry> findById(String guildId, long caseId) {
String sql =
"""
SELECT id, guild_id, action, target_id, moderator_id, reason, created_at, duration_sec, until_ms
FROM moderation_cases WHERE id = ? AND guild_id = ?
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setLong(1, caseId);
stmt.setString(2, guildId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(map(rs));
}
}
} catch (SQLException e) {
logger.error("Ошибка чтения case id={}", caseId, e);
}
return Optional.empty();
}
private static CaseEntry map(ResultSet rs) throws SQLException {
Long duration = rs.getObject("duration_sec") == null ? null : rs.getLong("duration_sec");
Long until = rs.getObject("until_ms") == null ? null : rs.getLong("until_ms");
return new CaseEntry(
rs.getLong("id"),
rs.getString("guild_id"),
rs.getString("action"),
rs.getString("target_id"),
rs.getString("moderator_id"),
rs.getString("reason"),
rs.getLong("created_at"),
duration,
until);
}
}

View file

@ -0,0 +1,139 @@
package creator.loki.feature.moderation.repository;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import creator.loki.database.Database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SqlWarningRepository implements WarningRepository {
private static final Logger logger = LoggerFactory.getLogger(SqlWarningRepository.class);
private final Database database;
public SqlWarningRepository(Database database) {
this.database = database;
}
@Override
public long insert(String guildId, String userId, String moderatorId, String reason) {
String sql =
"""
INSERT INTO warnings (guild_id, user_id, moderator_id, reason, created_at)
VALUES (?, ?, ?, ?, ?)
""";
long now = System.currentTimeMillis();
try (Connection conn = database.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.setString(3, moderatorId);
stmt.setString(4, reason);
stmt.setLong(5, now);
stmt.executeUpdate();
try (ResultSet keys = stmt.getGeneratedKeys()) {
if (keys.next()) {
return keys.getLong(1);
}
}
} catch (SQLException e) {
logger.error("Ошибка записи предупреждения guild={} user={}", guildId, userId, e);
}
return -1;
}
@Override
public List<WarningEntry> listByUser(String guildId, String userId, int limit) {
int lim = Math.min(Math.max(limit, 1), 25);
String sql =
"""
SELECT id, guild_id, user_id, moderator_id, reason, created_at
FROM warnings
WHERE guild_id = ? AND user_id = ?
ORDER BY id DESC
LIMIT ?
""";
List<WarningEntry> list = new ArrayList<>();
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.setInt(3, lim);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
list.add(map(rs));
}
}
} catch (SQLException e) {
logger.error("Ошибка чтения предупреждений guild={} user={}", guildId, userId, e);
}
return Collections.unmodifiableList(list);
}
@Override
public int countByUser(String guildId, String userId) {
String sql = "SELECT COUNT(*) FROM warnings WHERE guild_id = ? AND user_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
}
} catch (SQLException e) {
logger.error("Ошибка подсчёта предупреждений", e);
}
return 0;
}
@Override
public boolean deleteById(String guildId, long warningId) {
String sql = "DELETE FROM warnings WHERE id = ? AND guild_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setLong(1, warningId);
stmt.setString(2, guildId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
logger.error("Ошибка удаления предупреждения id={}", warningId, e);
}
return false;
}
@Override
public Optional<WarningEntry> findById(String guildId, long warningId) {
String sql =
"""
SELECT id, guild_id, user_id, moderator_id, reason, created_at
FROM warnings WHERE id = ? AND guild_id = ?
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setLong(1, warningId);
stmt.setString(2, guildId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(map(rs));
}
}
} catch (SQLException e) {
logger.error("Ошибка поиска предупреждения id={}", warningId, e);
}
return Optional.empty();
}
private static WarningEntry map(ResultSet rs) throws SQLException {
return new WarningEntry(
rs.getLong("id"),
rs.getString("guild_id"),
rs.getString("user_id"),
rs.getString("moderator_id"),
rs.getString("reason"),
rs.getLong("created_at"));
}
}

View file

@ -1,141 +1,19 @@
package creator.loki.feature.moderation.repository;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import creator.loki.database.Database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public interface WarningRepository {
long insert(String guildId, String userId, String moderatorId, String reason);
/** Предупреждения модераторов по гильдии (SQLite). */
public class WarningRepository {
private static final Logger logger = LoggerFactory.getLogger(WarningRepository.class);
private final Database database;
List<WarningEntry> listByUser(String guildId, String userId, int limit);
public WarningRepository(Database database) {
this.database = database;
}
int countByUser(String guildId, String userId);
public long insert(String guildId, String userId, String moderatorId, String reason) {
String sql =
"""
INSERT INTO warnings (guild_id, user_id, moderator_id, reason, created_at)
VALUES (?, ?, ?, ?, ?)
""";
long now = System.currentTimeMillis();
try (Connection conn = database.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.setString(3, moderatorId);
stmt.setString(4, reason);
stmt.setLong(5, now);
stmt.executeUpdate();
try (ResultSet keys = stmt.getGeneratedKeys()) {
if (keys.next()) {
return keys.getLong(1);
}
}
} catch (SQLException e) {
logger.error("Ошибка записи предупреждения guild={} user={}", guildId, userId, e);
}
return -1;
}
boolean deleteById(String guildId, long warningId);
public List<WarningEntry> listByUser(String guildId, String userId, int limit) {
int lim = Math.min(Math.max(limit, 1), 25);
String sql =
"""
SELECT id, guild_id, user_id, moderator_id, reason, created_at
FROM warnings
WHERE guild_id = ? AND user_id = ?
ORDER BY id DESC
LIMIT ?
""";
List<WarningEntry> list = new ArrayList<>();
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.setInt(3, lim);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
list.add(map(rs));
}
}
} catch (SQLException e) {
logger.error("Ошибка чтения предупреждений guild={} user={}", guildId, userId, e);
}
return Collections.unmodifiableList(list);
}
Optional<WarningEntry> findById(String guildId, long warningId);
public int countByUser(String guildId, String userId) {
String sql = "SELECT COUNT(*) FROM warnings WHERE guild_id = ? AND user_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
}
} catch (SQLException e) {
logger.error("Ошибка подсчёта предупреждений", e);
}
return 0;
}
/**
* @return true если строка с таким id и guild удалена
*/
public boolean deleteById(String guildId, long warningId) {
String sql = "DELETE FROM warnings WHERE id = ? AND guild_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setLong(1, warningId);
stmt.setString(2, guildId);
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
logger.error("Ошибка удаления предупреждения id={}", warningId, e);
}
return false;
}
public Optional<WarningEntry> findById(String guildId, long warningId) {
String sql =
"""
SELECT id, guild_id, user_id, moderator_id, reason, created_at
FROM warnings WHERE id = ? AND guild_id = ?
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setLong(1, warningId);
stmt.setString(2, guildId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(map(rs));
}
}
} catch (SQLException e) {
logger.error("Ошибка поиска предупреждения id={}", warningId, e);
}
return Optional.empty();
}
private static WarningEntry map(ResultSet rs) throws SQLException {
return new WarningEntry(
rs.getLong("id"),
rs.getString("guild_id"),
rs.getString("user_id"),
rs.getString("moderator_id"),
rs.getString("reason"),
rs.getLong("created_at"));
}
public record WarningEntry(
record WarningEntry(
long id, String guildId, String userId, String moderatorId, String reason, long createdAt) {}
}

View file

@ -10,6 +10,8 @@ import java.util.Optional;
import creator.loki.database.Database;
import creator.loki.database.DatabaseSchema;
import creator.loki.feature.moderation.repository.ActiveMuteRepository.ActiveMuteEntry;
import creator.loki.feature.moderation.repository.SqlActiveMuteRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
@ -25,7 +27,7 @@ class ActiveMuteRepositoryTest {
database = new Database(dbPath);
database.connect();
new DatabaseSchema(database).createTables();
repository = new ActiveMuteRepository(database);
repository = new SqlActiveMuteRepository(database);
}
@AfterEach

View file

@ -10,6 +10,8 @@ import java.util.Optional;
import creator.loki.database.Database;
import creator.loki.database.DatabaseSchema;
import creator.loki.feature.moderation.repository.WarningRepository.WarningEntry;
import creator.loki.feature.moderation.repository.SqlWarningRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
@ -25,7 +27,7 @@ class WarningRepositoryTest {
database = new Database(dbPath);
database.connect();
new DatabaseSchema(database).createTables();
repository = new WarningRepository(database);
repository = new SqlWarningRepository(database);
}
@AfterEach

View file

@ -0,0 +1,77 @@
package creator.loki.feature.radio;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import creator.loki.feature.music.repository.QueueRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class RadioManagerTest {
QueueRepository queueRepository;
RadioManager radioManager;
@BeforeEach
void setUp() {
queueRepository = mock(QueueRepository.class);
radioManager = new RadioManager(queueRepository);
}
@Test
void setRadio_storesUrl() {
var guildId = 123L;
var url = "http://stream.example.com/radio";
radioManager.setRadio(guildId, url);
assertEquals(url, radioManager.getRadioUrl(guildId));
}
@Test
void getRadioUrl_returnsNullForUnknown() {
assertNull(radioManager.getRadioUrl(999L));
}
@Test
void startRadio_returnsEarlyWithoutUrl() {
radioManager.startRadio(123L, 456L);
}
@Test
void startRadio_returnsEarlyWithoutJda() {
radioManager.setRadio(123L, "http://stream.example.com/radio");
radioManager.startRadio(123L, 456L);
}
@Test
void isRadioActive_returnsTrueAfterSet() {
radioManager.setRadio(123L, "http://stream.example.com/radio");
assertTrue(radioManager.isRadioActive(123L));
}
@Test
void isRadioActive_returnsFalseForUnknown() {
assertFalse(radioManager.isRadioActive(999L));
}
@Test
void getVolume_default100() {
assertEquals(100, radioManager.getVolume(999L));
}
@Test
void setVolume_clampsRange() {
radioManager.setRadio(123L, "http://stream.example.com/radio");
radioManager.setVolume(123L, -50);
assertEquals(0, radioManager.getVolume(123L));
radioManager.setVolume(123L, 200);
assertEquals(100, radioManager.getVolume(123L));
radioManager.setVolume(123L, 75);
assertEquals(75, radioManager.getVolume(123L));
}
}

View file

@ -9,15 +9,10 @@ import creator.loki.feature.role.repository.CustomRoleRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class CustomRoleManagerTest {
@Mock private CustomRoleRepository repository;
private final CustomRoleRepository repository = mock(CustomRoleRepository.class);
private CustomRoleManager manager;
@BeforeEach
@ -27,10 +22,10 @@ class CustomRoleManagerTest {
@Test
void getUserRoleIdsDelegatesToRepository() {
List<String> expected = List.of("role-1", "role-2");
var expected = List.of("role-1", "role-2");
when(repository.getUserRoles("guild-1", "user-1")).thenReturn(expected);
List<String> result = manager.getUserRoleIds("guild-1", "user-1");
var result = manager.getUserRoleIds("guild-1", "user-1");
assertSame(expected, result);
}
@ -39,7 +34,7 @@ class CustomRoleManagerTest {
void getUserRoleCountDelegatesToRepository() {
when(repository.getUserRoleCount("guild-1", "user-1")).thenReturn(3);
int count = manager.getUserRoleCount("guild-1", "user-1");
var count = manager.getUserRoleCount("guild-1", "user-1");
assertEquals(3, count);
}

View file

@ -0,0 +1,157 @@
package creator.loki.feature.social.commands;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import java.util.List;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.integration.nekos.NekosLifeApi;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Mentions;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
import org.junit.jupiter.api.Test;
class SocialCommandTest {
@Test
void hugFlavor_sendsEmbed() {
var cooldownManager = mock(CooldownManager.class);
var command = new SocialCommand(SocialCommand.Flavor.HUG, cooldownManager);
var channel = mock(MessageChannel.class);
var message = mock(Message.class);
var mentions = mock(Mentions.class);
var author = mock(Member.class);
var target = mock(Member.class);
var sendAction = mock(MessageCreateAction.class);
when(author.getId()).thenReturn("123");
when(cooldownManager.isOnCooldown(anyString(), anyString())).thenReturn(false);
when(message.getMentions()).thenReturn(mentions);
when(mentions.getMembers()).thenReturn(List.of(target));
when(channel.sendMessageEmbeds(any(MessageEmbed.class))).thenReturn(sendAction);
var context = new CommandContext(message, List.of(), channel, author, null);
try (var mockedNekos = mockStatic(NekosLifeApi.class)) {
mockedNekos.when(() -> NekosLifeApi.fetchImage(anyString())).thenReturn("https://example.com/hug.gif");
command.execute(context);
mockedNekos.verify(() -> NekosLifeApi.fetchImage(eq("hug")));
verify(cooldownManager).applyCooldown(eq("123"), eq("hug"), eq(5));
verify(channel).sendMessageEmbeds(any(MessageEmbed.class));
verify(sendAction).queue();
}
}
@Test
void noMention_sendsError() {
var cooldownManager = mock(CooldownManager.class);
var command = new SocialCommand(SocialCommand.Flavor.HUG, cooldownManager);
var channel = mock(MessageChannel.class);
var message = mock(Message.class);
var mentions = mock(Mentions.class);
var author = mock(Member.class);
var sendAction = mock(MessageCreateAction.class);
when(author.getId()).thenReturn("123");
when(cooldownManager.isOnCooldown(anyString(), anyString())).thenReturn(false);
when(message.getMentions()).thenReturn(mentions);
when(mentions.getMembers()).thenReturn(List.of());
when(channel.sendMessage(anyString())).thenReturn(sendAction);
var context = new CommandContext(message, List.of(), channel, author, null);
command.execute(context);
verify(channel).sendMessage("❌ Укажи пользователя через @");
verify(sendAction).queue();
}
@Test
void onCooldown_showsRemaining() {
var cooldownManager = mock(CooldownManager.class);
var command = new SocialCommand(SocialCommand.Flavor.HUG, cooldownManager);
var channel = mock(MessageChannel.class);
var message = mock(Message.class);
var author = mock(Member.class);
var sendAction = mock(MessageCreateAction.class);
when(author.getId()).thenReturn("123");
when(cooldownManager.isOnCooldown(anyString(), anyString())).thenReturn(true);
when(cooldownManager.getRemainingSeconds(anyString(), anyString())).thenReturn(42L);
when(channel.sendMessage(anyString())).thenReturn(sendAction);
var context = new CommandContext(message, List.of(), channel, author, null);
command.execute(context);
verify(channel).sendMessage("⏰ Подожди ещё 42 сек");
verify(sendAction).queue();
}
@Test
void kissFlavor_rejectsSelf() {
var cooldownManager = mock(CooldownManager.class);
var command = new SocialCommand(SocialCommand.Flavor.KISS, cooldownManager);
var channel = mock(MessageChannel.class);
var message = mock(Message.class);
var mentions = mock(Mentions.class);
var author = mock(Member.class);
var target = mock(Member.class);
var sendAction = mock(MessageCreateAction.class);
when(cooldownManager.isOnCooldown(anyString(), anyString())).thenReturn(false);
when(message.getMentions()).thenReturn(mentions);
when(mentions.getMembers()).thenReturn(List.of(target));
when(author.getId()).thenReturn("123");
when(target.getId()).thenReturn("123");
when(channel.sendMessage(anyString())).thenReturn(sendAction);
var context = new CommandContext(message, List.of(), channel, author, null);
command.execute(context);
verify(channel).sendMessage("❌ Нельзя использовать на себе!");
verify(sendAction).queue();
}
@Test
void kissFlavor_rejectsBot() {
var cooldownManager = mock(CooldownManager.class);
var command = new SocialCommand(SocialCommand.Flavor.KISS, cooldownManager);
var channel = mock(MessageChannel.class);
var message = mock(Message.class);
var mentions = mock(Mentions.class);
var author = mock(Member.class);
var target = mock(Member.class);
var targetUser = mock(User.class);
var sendAction = mock(MessageCreateAction.class);
when(cooldownManager.isOnCooldown(anyString(), anyString())).thenReturn(false);
when(message.getMentions()).thenReturn(mentions);
when(mentions.getMembers()).thenReturn(List.of(target));
when(author.getId()).thenReturn("123");
when(target.getId()).thenReturn("456");
when(target.getUser()).thenReturn(targetUser);
when(targetUser.isBot()).thenReturn(true);
when(channel.sendMessage(anyString())).thenReturn(sendAction);
var context = new CommandContext(message, List.of(), channel, author, null);
command.execute(context);
verify(channel).sendMessage("❌ Боты не поддерживают это действие!");
verify(sendAction).queue();
}
}

View file

@ -0,0 +1,17 @@
package creator.loki.feature.voice.handler;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import creator.loki.feature.voice.service.VoiceChannelManager;
import org.junit.jupiter.api.Test;
class VoiceButtonHandlerTest {
@Test
void constructorAcceptsVoiceManager() {
var manager = mock(VoiceChannelManager.class);
var handler = new VoiceButtonHandler(manager);
assertNotNull(handler);
}
}