Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions docs/e2e-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Which Guilds feature is tested where, and what is deliberately left out. See `e2e-testing.md` for
how to run any of it.

141 tests across 12 spec files, plus 3 preflight tests that run before every suite and fail the run
162 tests across 14 spec files, plus 3 preflight tests that run before every suite and fail the run
if Guilds or any of its hard dependencies did not load.

## Preflight — `plugins/guilds-preflight.spec.ts`
Expand All @@ -17,12 +17,12 @@ meaningful.
| The economy and permission providers Vault needs are registered | Vault's `Economy` and `Permission` services resolve |
| Guilds commands are reachable and its language is English | `/guild` answers, and does so in English |

## A. Guild lifecycle — `tests/lifecycle.spec.ts` (19)
## A. Guild lifecycle — `tests/lifecycle.spec.ts` (21)

| Requirement | Test |
| --- | --- |
| Successful guild creation | creates a guild, charges for it, and shows it in the guild list |
| Creation confirmation and cancellation | cancelling the prompt leaves no guild and no charge |
| Creation confirmation and cancellation | cancelling the prompt leaves no guild and no charge / refuses the confirmation when the player joined a guild in the meantime / refuses the confirmation when another guild took the name in the meantime |
| Invalid guild names and prefixes | rejects a guild name that breaks the configured requirements / rejects a name too long to serve as its own prefix / rejects an explicit prefix that breaks the prefix requirements |
| Duplicate guild names | rejects a guild name another guild already holds |
| Insufficient funds | rejects creation when the player cannot afford it |
Expand All @@ -32,7 +32,7 @@ meaningful.
| Status and information commands | toggles the guild between public and private / changes the guild prefix / sets, shows and clears the guild MOTD / reports guild membership through the guild list and rejects guild-scoped commands elsewhere |
| Name blacklist | rejects a guild name that is a substring of a blacklisted word / accepts a guild name that contains a blacklisted word |

## B. Membership management — `tests/membership.spec.ts` (19)
## B. Membership management — `tests/membership.spec.ts` (21)

| Requirement | Test |
| --- | --- |
Expand All @@ -49,9 +49,10 @@ meaningful.
| Promoting and demoting | promotion and demotion change what a member may do / promotion is refused when there is no higher role to move into / a member without the promote role is stopped before the command runs |
| Transferring ownership | ownership can be transferred, and the old master loses the master role |
| Guild capacity | a full guild refuses another member |
| Finding a guild by name | a guild is found when its name is typed in a different case |
| Permissions after leaving or being removed | a player who leaves loses the permissions their guild granted them / a player who is kicked loses the permissions their guild granted them |

## C. Roles and permissions — `tests/roles.spec.ts` (8), `tests/admin.spec.ts` (7)
## C. Roles and permissions — `tests/roles.spec.ts` (8), `tests/admin.spec.ts` (8)

| Requirement | Test |
| --- | --- |
Expand All @@ -64,6 +65,7 @@ meaningful.
| Tier permissions after upgrade | upgrading charges the tier price and moves the guild onto the new tier / **an upgrade leaves the master with the permissions the new tier grants** |
| A player in no guild | a player in no guild is told so rather than refused by role |
| Admin commands that name a guild and a second argument | an admin command that names a guild and a second argument reaches its handler / an admin command that names a guild which does not exist says so / an admin command that names a guild and a second argument is refused for an unknown player |
| Admin commands that name a guild | an admin command finds its guild when the name is typed in a different case |
| Admin commands that act on a player | an admin command can put a player into a guild / an admin command can hand a player upgrade tickets / an admin command can hand a guild to one of its members |
| Admin commands that act on an arena | an admin command can teleport to an arena spawn point |
| The player-facing bank commands | the player-facing bank commands work |
Expand Down Expand Up @@ -128,15 +130,19 @@ meaningful.
| Alliance chat between guilds | `chat.spec.ts`, ally chat reaches an allied guild and not the third party |
| Cleanup on deletion | a deleted guild drops out of its allies |

## H. Invite codes — `tests/codes.spec.ts` (12, of which 5 are one `describe.serial` chain)
## H. Invite codes — `tests/codes.spec.ts` (18, of which 5 are one `describe.serial` chain)

| Requirement | Test |
| --- | --- |
| Creating invite codes | a code can be created, listed and deleted |
| Viewing and listing codes | a code can be created, listed and deleted / *and it is listed for the guild master* |
| Redeeming a code | a code can be redeemed and puts the player in the guild |
| Redeeming a code more than once | a code with two uses can be redeemed twice |
| Redeeming a code grants what the guild grants | a player who joins by code gets the permissions their guild grants them |
| The join cooldown applies to a code | a code cannot be redeemed while the join cooldown is running |
| Deleting codes | a code can be created, listed and deleted |
| Invalid codes | an invalid code is refused |
| Use counts that could never be redeemed | a code with no uses is refused and nothing is stored / a code with a negative number of uses is refused and nothing is stored / a refused code does not use up the guild allowance of codes |
| Reusing codes where restricted | a code runs out of uses and then says so |
| Permission restrictions | code information is restricted by role / *a member can be added and cannot manage codes* |
| Full lifecycle | *one code through its whole life* — a `describe.serial` chain: create → listed → redeem → deleted, asserting membership at each step rather than the command's reply |
Expand Down Expand Up @@ -188,6 +194,10 @@ the server answering in another language for the rest of the run. ACF's `usePerI
false)` is what makes that safe, and one player switching language leaves everyone else in English
asserts it rather than assuming it.

## L. Startup and the guild data — `tests/startup-loading.spec.ts` (4)

## Runtime smoke coverage — `tests/smoke.spec.ts` (7)

## Fixture self-check — `tests/bank.spec.ts`

`STARTING_BALANCE is what the fixtures actually fund` is not a Guilds test. It checks that the
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/me/glaremasters/guilds/guild/Guild.java
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,13 @@
* @param code the code being added
* @param uses the amount of uses it has
* @param creator the creator of the code
* @throws IllegalArgumentException if the code would have no uses left to begin with
*/
public void addCode(String code, int uses, Player creator) {
if (uses < GuildCode.MIN_USES) {
throw new IllegalArgumentException(
"An invite code needs at least " + GuildCode.MIN_USES + " use, but got " + uses);
}
codes.add(new GuildCode(code, uses, creator.getUniqueId(), new ArrayList<>()));
}

Expand Down Expand Up @@ -435,7 +440,7 @@
* @param key the message to send
* @param replacements any args we need to handle
*/
public void sendMessage(CommandManager manager, Messages key, String... replacements) {

Check warning on line 443 in src/main/java/me/glaremasters/guilds/guild/Guild.java

View workflow job for this annotation

GitHub Actions / Build

[rawtypes] found raw type: CommandManager

Check warning on line 443 in src/main/java/me/glaremasters/guilds/guild/Guild.java

View workflow job for this annotation

GitHub Actions / Plugwright 1.21.8

[rawtypes] found raw type: CommandManager
getOnlineMembers().forEach(m -> {
final Player player = Bukkit.getPlayer(m.getUuid());
if (player != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import me.glaremasters.guilds.Guilds
import me.glaremasters.guilds.configuration.sections.CodeSettings
import me.glaremasters.guilds.exceptions.ExpectationNotMet
import me.glaremasters.guilds.guild.Guild
import me.glaremasters.guilds.guild.GuildCode
import me.glaremasters.guilds.guild.GuildHandler
import me.glaremasters.guilds.messages.Messages
import me.glaremasters.guilds.utils.Constants
Expand All @@ -38,6 +39,10 @@ internal class CommandCode : BaseCommand() {
@Syntax("%uses")
@CommandPermission(Constants.CODE_PERM + "create")
fun create(player: Player, @Conditions("perm:perm=CREATE_CODE") guild: Guild, @Default("1") uses: Int) {
if (uses < GuildCode.MIN_USES) {
throw ExpectationNotMet(Messages.CODES__INVALID_USES)
}

if (guild.getActiveCheck(settingsManager.getProperty(CodeSettings.ACTIVE_CODE_AMOUNT))) {
throw ExpectationNotMet(Messages.CODES__MAX)
}
Expand Down
10 changes: 10 additions & 0 deletions src/main/kotlin/me/glaremasters/guilds/guild/GuildCode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ class GuildCode(
val redeemers: MutableList<UUID>
) {

companion object {
/**
* The smallest number of uses a code can be created with.
*
* A code that has been redeemed down to zero is a normal, spent code, so the rule belongs at
* creation: a count that could never be redeemed is refused before the code is stored.
*/
const val MIN_USES = 1
}

/**
* Adds a player to the list of redeemers and reduces the number of uses by 1.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ enum class Messages : MessageKeyProvider {
CODES__OUT,
CODES__INFO,
CODES__MAX,
CODES__INVALID_USES,
CODES__GUILD_MESSAGE,

VAULTS__MAXED,
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/languages/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ codes:
out: "&cThat invite code is all out of uses! Please try another one!"
info: "&aInvite Code Information:\n&aID: &6{code}\n&aUses: &6{amount}\n&aCreator: &6{creator}\n&aRedeemers: &6{redeemers}"
max: "&cYou already have too many active invite codes in your guild!"
invalid-uses: "&cThe number of uses must be at least 1!"
vaults:
maxed: "&cYou can't open this vault because you already have the max amount of vaults for your Guild Tier!"
blacklisted: "&cYou can't put that item in your Guild Vault because it is blacklisted!"
Expand Down
2 changes: 2 additions & 0 deletions src/test/e2e/plugins/support/expected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ export const MSG = {
codeJoined: (guild: string) => `You've successfully joined ${guild} using an invite code!`,
codeOut: 'That invite code is all out of uses!',
codeMax: 'You already have too many active invite codes in your guild!',
/** codes.invalid-uses — what a code creation with no uses to offer is answered with. */
codeInvalidUses: 'The number of uses must be at least 1!',

// motd
motdNotSet: 'Your guild currently does not have a MOTD set',
Expand Down
63 changes: 63 additions & 0 deletions src/test/e2e/tests/codes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,69 @@ test('a guild cannot hold more active codes than the configured limit', async ({
await commandWithout(player, guilds, '/guild code create', MSG.codeMax, MSG.codeCreated);
});

test('a code with no uses is refused and nothing is stored', async ({ player, guilds }) => {
await guilds.createGuild(player);

await commandWithout(player, guilds, '/guild code create 0', MSG.codeInvalidUses, MSG.codeCreated);

// Nothing was stored, so the guild has no codes at all.
await commandWithout(player, guilds, '/guild code list', MSG.codeEmpty, MSG.codeListHeader);
});

test('a code with a negative number of uses is refused and nothing is stored', async ({ player, guilds }) => {
await guilds.createGuild(player);

await commandWithout(player, guilds, '/guild code create -1', MSG.codeInvalidUses, MSG.codeCreated);

// Nothing was stored, so the guild has no codes at all.
await commandWithout(player, guilds, '/guild code list', MSG.codeEmpty, MSG.codeListHeader);
});

test('a refused code does not use up the guild allowance of codes', async ({ player, guilds }) => {
await guilds.createGuild(player);

await commandWithout(player, guilds, '/guild code create 0', MSG.codeInvalidUses, MSG.codeCreated);
await commandWithout(player, guilds, '/guild code create -1', MSG.codeInvalidUses, MSG.codeCreated);

// codes.amount is two in the staged config, and neither refusal took a slot.
for (let index = 0; index < 2; index++) {
const since = player.getMessageBufferIndex();
await guilds.run(player, '/guild code create');
await expectReceived(player, MSG.codeCreated, since);
}

await commandWithout(player, guilds, '/guild code create', MSG.codeMax, MSG.codeCreated);
});

test('a code with two uses can be redeemed twice', async ({ player, createPlayer, guilds }) => {
const guild = await guilds.createGuild(player);
const created = player.getMessageBufferIndex();
await guilds.run(player, '/guild code create 2');
await expectReceived(player, MSG.codeCreated, created);
const code = parseCode(player.messageBuffer.slice(created).join('\n'))!;

const first = await createPlayer({ username: guilds.playerName() });
const joining = first.getMessageBufferIndex();
await guilds.run(first, `/guild code redeem ${code}`);
await expectReceived(first, MSG.codeJoined(guild), joining);

// Tier 1 allows two members, so the first joiner has to make room for the second.
await guilds.run(player, `/guild kick ${first.username}`);
await expect(player).toHaveReceivedMessage(MSG.kickSuccess(first.username));

const second = await createPlayer({ username: guilds.playerName() });
const secondJoining = second.getMessageBufferIndex();
await guilds.run(second, `/guild code redeem ${code}`);
await expectReceived(second, MSG.codeJoined(guild), secondJoining);

// Both uses are spent now, so the code is refused.
await guilds.run(player, `/guild kick ${second.username}`);
await expect(player).toHaveReceivedMessage(MSG.kickSuccess(second.username));

const third = await createPlayer({ username: guilds.playerName() });
await commandWithout(third, guilds, `/guild code redeem ${code}`, MSG.codeOut, MSG.codeJoined(guild));
});

test('code information is restricted by role', async ({ player, createPlayer, guilds }) => {
const guild = await guilds.createGuild(player);
const created = player.getMessageBufferIndex();
Expand Down
Loading
Loading