Skip to content

Commit

Permalink
Add rank unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
MSWS committed Sep 9, 2024
1 parent d6ef4c6 commit 8ede047
Show file tree
Hide file tree
Showing 29 changed files with 536 additions and 82 deletions.
15 changes: 8 additions & 7 deletions src/GangsAPI/Permissions/IGangRank.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ namespace GangsAPI.Permissions;
/// (i.e. rank 0 (owner) is higher than rank 1 (admin), which is higher than rank 2 (member))
/// This means rank 0 **must** be the owner rank, and all ranks must be non-negative.
/// </summary>
public interface IGangRank {
public interface IGangRank : IComparable<IGangRank> {
[Flags]
public enum Permissions {
NONE = 0,

/// <summary>
/// The member may invite others to the gang.
/// </summary>
Expand Down Expand Up @@ -59,18 +61,17 @@ public enum Permissions {
MANAGE_PERKS = 1 << 7,

/// <summary>
/// The member may manage the ranks of the gang, regardless
/// The member may manage the ranks (names + perms) of the gang, regardless
/// of rank system, the member will not be able to manage
/// their own rank.
/// their own rank, or create other ranks.
/// The member may not grant any permissions that they themselves do not have.
/// </summary>
MANAGE_RANKS = 1 << 8,

/// <summary>
/// The member may create new ranks for the gang.
/// All ranks created must not have a rank higher than
/// the member's current rank. Depending on the rank system,
/// these ranks may also be required to have a rank lower
/// than the member's current rank.
/// the member's current rank.
/// </summary>
CREATE_RANKS = 1 << 9,

Expand All @@ -96,7 +97,7 @@ public enum Permissions {
/// <summary>
/// The member may manage invites to the gang.
/// </summary>
MANAGE_INVITES = 1 << 13,
MANAGE_INVITES = 1 << 13 | INVITE_OTHERS,
}

string Name { get; }
Expand Down
79 changes: 68 additions & 11 deletions src/GangsAPI/Services/IRankManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using GangsAPI.Data.Gang;
using GangsAPI.Permissions;
using GangsAPI.Services.Player;

namespace GangsAPI.Services;

Expand Down Expand Up @@ -56,13 +57,45 @@ public interface IRankManager : IPluginBehavior {
/// Deletes a rank from a specific gang.
/// If an error occured, this will return false.
/// If the rank is the owner rank, this will return false.
/// All players that had the rank originally will be demoted
/// to their immediate lower rank.
/// If force is false and any members have this rank, this will return false.
/// If force is true, the the manager is required to ensure that
/// the <see cref="IPlayerManager"/> is aware of the rank deletion.
/// This may require demoting all players with the rank to a lower rank,
/// or removing those players from the gang entirely (if a lower rank does not exist).
/// </summary>
/// <param name="gang"></param>
/// <param name="rank"></param>
/// <param name="force"></param>
/// <returns></returns>
Task<bool> DeleteRank(int gang, int rank);
Task<bool> DeleteRank(int gang, int rank, DeleteStrat strat);

public enum DeleteStrat {
/// <summary>
/// Cancel the deletion of the rank if any
/// players currently have the rank.
/// </summary>
CANCEL,

/// <summary>
/// Attempt to demote all players with the rank,
/// if a lower rank does not exist, cancel the deletion.
/// </summary>
DEMOTE_FAIL,

/// <summary>
/// Attempt to demote all players with the rank,
/// if a lower rank does not exist, kick the player from the gang.
/// </summary>
DEMOTE_KICK
}

/// <summary>
/// Deletes all ranks of a specific gang,
/// this should only be used when deleting a gang.
/// </summary>
/// <param name="gang"></param>
/// <returns></returns>
Task<bool> DeleteAllRanks(int gang);

/// <summary>
/// Updates the name and permissions of a rank.
Expand All @@ -73,20 +106,42 @@ public interface IRankManager : IPluginBehavior {
/// <param name="rank"></param>
/// <returns>
/// True if the rank was updated successfully, false otherwise.
/// (Rank did not exist, rank is the owner rank, etc.)
/// </returns>
Task<bool> UpdateRank(int gang, IGangRank rank) => AddRank(gang, rank);
Task<bool> UpdateRank(int gang, IGangRank rank);

/// <summary>
/// Populates a gang with default ranks. At minimum this must
/// include the owner rank.
/// </summary>
/// <returns></returns>
async Task<IEnumerable<IGangRank>> AssignDefaultRanks(int gang) {
var owner = await CreateRank(gang, "Owner", 0, IGangRank.Permissions.OWNER);
if (owner == null)
throw new InvalidOperationException("Failed to create owner rank.");
return new[] { owner };
var memberPerms = IGangRank.Permissions.BANK_DEPOSIT
| IGangRank.Permissions.VIEW_MEMBERS;
var officerPerms = memberPerms | IGangRank.Permissions.INVITE_OTHERS
| IGangRank.Permissions.PURCHASE_PERKS
| IGangRank.Permissions.BANK_WITHDRAW | IGangRank.Permissions.KICK_OTHERS;
var managerPerms = officerPerms | IGangRank.Permissions.MANAGE_PERKS
| IGangRank.Permissions.MANAGE_RANKS
| IGangRank.Permissions.MANAGE_INVITES
| IGangRank.Permissions.PROMOTE_OTHERS
| IGangRank.Permissions.DEMOTE_OTHERS;
var coOwnerPerms = managerPerms | IGangRank.Permissions.CREATE_RANKS;

var defaultRanks = new[] {
await CreateRank(gang, "Owner", 0, IGangRank.Permissions.OWNER)
?? throw new InvalidOperationException("Failed to create owner rank."),
await CreateRank(gang, "Co-Owner", 10, coOwnerPerms)
?? throw new InvalidOperationException("Failed to create co-owner rank."),
await CreateRank(gang, "Manager", 30, managerPerms)
?? throw new InvalidOperationException("Failed to create manager rank."),
await CreateRank(gang, "Officer", 50, officerPerms)
?? throw new InvalidOperationException("Failed to create officer rank."),
await CreateRank(gang, "Member", 100, memberPerms)
?? throw new InvalidOperationException("Failed to create member rank.")
};
if (defaultRanks.Any(r => r == null))
throw new InvalidOperationException("Failed to create default ranks.");
return defaultRanks;
}

#region Aliases
Expand All @@ -95,14 +150,16 @@ async Task<IEnumerable<IGangRank>> AssignDefaultRanks(int gang) {

Task<IGangRank?> GetRank(IGang gang, int rank) => GetRank(gang.GangId, rank);

Task<bool> DeleteRank(int gang, IGangRank rank)
=> DeleteRank(gang, rank.Rank);
Task<bool> DeleteRank(int gang, IGangRank rank, DeleteStrat strat)
=> DeleteRank(gang, rank.Rank, strat);

Task<IEnumerable<IGangRank>> GetRanks(IGang gang) => GetRanks(gang.GangId);

Task<IEnumerable<IGangRank>> AssignDefaultRanks(IGang gang)
=> AssignDefaultRanks(gang.GangId);

Task<bool> DeleteAllRanks(IGang gang) => DeleteAllRanks(gang.GangId);

#endregion

/// <summary>
Expand Down
17 changes: 13 additions & 4 deletions src/GangsImpl/AbstractDB/AbstractDBGangManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
using CounterStrikeSharp.API.Core;
using Dapper;
using GangsAPI.Data.Gang;
using GangsAPI.Services;
using GangsAPI.Services.Gang;
using GangsAPI.Services.Player;

namespace GenericDB;

public abstract class AbstractDBGangManager(IPlayerManager playerMgr,
string connectionString, string table = "gang_gangs", bool testing = false)
: IGangManager {
IRankManager rankMgr, string connectionString, string table = "gang_gangs",
bool testing = false) : IGangManager {
protected DbConnection Connection = null!;
protected DbTransaction? Transaction;

Expand Down Expand Up @@ -60,10 +61,13 @@ public async Task<bool> UpdateGang(IGang gang) {
public async Task<bool> DeleteGang(int id) {
var members = await playerMgr.GetMembers(id);
foreach (var member in members) {
member.GangId = null;
member.GangId = null;
member.GangRank = null;
await playerMgr.UpdatePlayer(member);
}

await rankMgr.DeleteAllRanks(id);

var query = $"DELETE FROM {table} WHERE GangId = @id";
return await Connection.ExecuteAsync(query, new { id }, Transaction) > 0;
}
Expand All @@ -79,6 +83,7 @@ public async Task<bool> DeleteGang(int id) {
if (player.GangId != null)
throw new InvalidOperationException(
$"Attempted to create a gang for {owner} who is already in gang {player.GangId}");

var query = $"INSERT INTO {table} (Name) VALUES (@name)";
var result =
await Connection.ExecuteAsync(query, new { name }, Transaction);
Expand All @@ -88,7 +93,11 @@ public async Task<bool> DeleteGang(int id) {
}

var id = await GetLastId();
player.GangId = id;

await rankMgr.AssignDefaultRanks(id);

player.GangId = id;
player.GangRank = 0;
await playerMgr.UpdatePlayer(player);
var gang = new DBGang(id, name);
return gang.Clone() as IGang;
Expand Down
4 changes: 4 additions & 0 deletions src/GangsImpl/AbstractDB/AbstractDBPlayerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ public async Task<IEnumerable<IGangPlayer>> GetMembers(int gangId) {
}

public async Task<bool> UpdatePlayer(IGangPlayer player) {
if (player.GangId == null != (player.GangRank == null))
throw new InvalidOperationException(
"Player must have both GangId and GangRank set or neither set");

var query =
$"UPDATE {table} SET Name = @Name, GangId = @GangId, GangRank = @GangRank WHERE Steam = @Steam";
return await Connection.ExecuteAsync(query, player, Transaction) == 1;
Expand Down
144 changes: 144 additions & 0 deletions src/GangsImpl/AbstractDB/AbstractDBRankManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using System.Data.Common;
using CounterStrikeSharp.API.Core;
using Dapper;
using GangsAPI.Data.Gang;
using GangsAPI.Permissions;
using GangsAPI.Services;
using GangsAPI.Services.Gang;
using GangsAPI.Services.Player;

namespace GenericDB;

public abstract class AbstractDBRankManager(IPlayerManager playerMgr,
string connectionString, string table = "gang_ranks", bool testing = false)
: IRankManager {
protected DbConnection Connection = null!;
protected DbTransaction? Transaction;

public void Start(BasePlugin? plugin, bool hotReload) {
Connection = CreateDbConnection(connectionString);

try {
Connection.Open();

if (testing) Transaction = Connection.BeginTransaction();

var command = Connection.CreateCommand();

command.Transaction = Transaction;
command.CommandText = CreateTableQuery(table, testing);

command.ExecuteNonQuery();
} catch (Exception e) {
Transaction?.Rollback();
throw new InvalidOperationException("Failed initializing the database",
e);
}
}


public void Dispose() {
Transaction?.Dispose();
Connection.Dispose();
}

abstract protected DbConnection CreateDbConnection(string connectionString);

virtual protected string CreateTableQuery(string tableName, bool inTesting) {
return inTesting ?
$"CREATE TEMPORARY TABLE IF NOT EXISTS {tableName} (GangId INT NOT NULL, Rank INT NOT NULL, Name VARCHAR(255) NOT NULL, Permissions INT NOT NULL, PRIMARY KEY (GangId, Rank))" :
$"CREATE TABLE IF NOT EXISTS {tableName} (GangId INT NOT NULL, Rank INT NOT NULL, Name VARCHAR(255) NOT NULL, Permissions INT NOT NULL, PRIMARY KEY (GangId, Rank))";
}

public async Task<Dictionary<int, IEnumerable<IGangRank>>> GetAllRanks() {
var ranks = await Connection.QueryAsync<DBRank>($"SELECT * FROM {table}");
return ranks.GroupBy(r => r.GangId)
.ToDictionary(g => g.Key, g => g.AsEnumerable<IGangRank>());
}

public async Task<IEnumerable<IGangRank>> GetRanks(int gang) {
return await Connection.QueryAsync<DBRank>(
$"SELECT * FROM {table} WHERE GangId = @gang", new { gang }, Transaction);
}

public async Task<IGangRank?> GetRank(int gang, int rank) {
return await Connection.QueryFirstOrDefaultAsync<DBRank>(
$"SELECT * FROM {table} WHERE GangId = @gang AND Rank = @rank",
new { gang, rank }, Transaction);
}

public async Task<bool> AddRank(int gang, IGangRank rank) {
if (rank.Rank < 0) return false;
if (await GetRank(gang, rank.Rank) != null) return false;

var query =
$"INSERT INTO {table} (GangId, Rank, Name, Permissions) VALUES (@GangId, @Rank, @Name, @Perms)";
return await Connection.ExecuteAsync(query,
new { GangId = gang, rank.Rank, rank.Name, rank.Perms }, Transaction)
== 1;
}

public async Task<IGangRank?> CreateRank(int gang, string name, int rank,
IGangRank.Permissions permissions) {
var rankObj = new DBRank {
GangId = gang, Rank = rank, Name = name, Perms = permissions
};

var success = await AddRank(gang, rankObj);
return success ? rankObj : null;
}

public async Task<bool> DeleteRank(int gang, int rank,
IRankManager.DeleteStrat strat) {
if (rank <= 0) return false;
// Check if any players have this rank

if (strat == IRankManager.DeleteStrat.CANCEL) {
var prePlayerCheck = await playerMgr.GetMembers(gang);
if (prePlayerCheck.Any(p => p.GangRank == rank)) return false;
}

var lowerRank = await Connection.QueryFirstOrDefaultAsync<DBRank>(
$"SELECT * FROM {table} WHERE GangId = @GangId AND Rank > @Rank ORDER BY Rank ASC LIMIT 1",
new { GangId = gang, rank }, Transaction);

var members = (await playerMgr.GetMembers(gang))
.Where(p => p.GangRank == rank)
.ToList();

if (strat == IRankManager.DeleteStrat.DEMOTE_FAIL && lowerRank == null
&& members.Count != 0)
return false;

foreach (var player in members) {
player.GangId = lowerRank?.GangId ?? null;
player.GangRank = lowerRank?.Rank ?? null;
await playerMgr.UpdatePlayer(player);
}

var query = $"DELETE FROM {table} WHERE GangId = @GangId AND Rank = @Rank";

return await Connection.ExecuteAsync(query, new { GangId = gang, rank },
Transaction) == 1;
}

public async Task<bool> DeleteAllRanks(int gang) {
var query = $"DELETE FROM {table} WHERE GangId = @GangId";
return await Connection.ExecuteAsync(query, new { gang }, Transaction) > 0;
}

public async Task<bool> UpdateRank(int gang, IGangRank rank) {
switch (rank.Rank) {
case < 0:
case > 0 when rank.Perms.HasFlag(IGangRank.Permissions.OWNER):
return false;
default: {
// Update name and permissions
var query =
$"UPDATE {table} SET Name = @Name, Permissions = @Perms WHERE GangId = @GangId AND Rank = @Rank";
return await Connection.ExecuteAsync(query,
new { rank.Name, rank.Perms, gang, rank.Rank }, Transaction) == 1;
}
}
}
}
14 changes: 14 additions & 0 deletions src/GangsImpl/AbstractDB/DBRank.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using GangsAPI.Permissions;

namespace GenericDB;

public class DBRank : IGangRank {
public int GangId { get; init; }
public string Name { get; init; }

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / test (8.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / test (8.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / test (9.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / test (9.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / build (9.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 7 in src/GangsImpl/AbstractDB/DBRank.cs

View workflow job for this annotation

GitHub Actions / build (9.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public int Rank { get; init; }
public IGangRank.Permissions Perms { get; init; }

public int CompareTo(IGangRank? other) {
return other == null ? 1 : Rank.CompareTo(other.Rank);
}
}
Loading

0 comments on commit 8ede047

Please sign in to comment.