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:
parent
4b79c561f4
commit
98ea510c49
13 changed files with 628 additions and 332 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue