Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,28 @@ public void setOverwriteOnActive(boolean overwriteOnActive)
}


/**
* Is buy confirmation boolean.
*
* @return the boolean
*/
public boolean isBuyConfirmation()
{
return buyConfirmation;
}


/**
* Sets buy confirmation.
*
* @param buyConfirmation the buy confirmation
*/
public void setBuyConfirmation(boolean buyConfirmation)
{
this.buyConfirmation = buyConfirmation;
}


/**
* Is use bank account boolean.
*
Expand Down Expand Up @@ -486,6 +508,12 @@ public enum GuiAction
@ConfigEntry(path = "overwrite-on-activate")
private boolean overwriteOnActive = false;

@ConfigComment("")
@ConfigComment("Ask the player to confirm before buying a generator, to avoid accidental purchases.")
@ConfigComment("The confirmation is requested via a chat prompt.")
@ConfigEntry(path = "buy-confirmation")
private boolean buyConfirmation = true;

@ConfigComment("")
@ConfigComment("Send a notification message when player unlocks a new generator.")
@ConfigComment("3 messages that will be showed:")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import world.bentobox.bentobox.util.Util;
import world.bentobox.magiccobblestonegenerator.StoneGeneratorAddon;
import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorBundleObject;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorDataObject;
import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorTierObject;
import world.bentobox.magiccobblestonegenerator.managers.StoneGeneratorManager;
import world.bentobox.magiccobblestonegenerator.utils.Constants;
Expand Down Expand Up @@ -124,6 +126,55 @@
}


/**
* This method purchases the given generator for the user, optionally asking for confirmation first.
* <p>
* If the generator cannot be purchased, the relevant message is sent by
* {@link StoneGeneratorManager#canPurchaseGenerator} and the panel is simply rebuilt. When the
* {@code buy-confirmation} setting is enabled, a chat confirmation is requested before the purchase is made, to
* avoid accidental purchases (#109).
*
* @param island Island on which the generator is purchased.
* @param generatorData Data that stores island generators.
* @param generatorTier Generator tier that should be purchased.
*/
protected void purchaseGenerator(Island island, GeneratorDataObject generatorData, GeneratorTierObject generatorTier)
{
if (island == null || !this.manager.canPurchaseGenerator(this.user, island, generatorData, generatorTier))
{
// Cannot purchase. canPurchaseGenerator already sent the reason. Just refresh the panel.
this.build();
return;
}

if (!this.addon.getSettings().isBuyConfirmation())
{
// Confirmation disabled. Purchase directly.
this.manager.purchaseGenerator(this.user, island, generatorData, generatorTier);
this.build();
return;
}

// Ask the player to confirm the purchase.
ConversationUtils.createConfirmation(
confirm ->
{
if (confirm)

Check warning on line 162 in src/main/java/world/bentobox/magiccobblestonegenerator/panels/CommonPanel.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a primitive boolean expression here.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_MagicCobblestoneGenerator&issues=AZ8q6y5GJaAYUuOnjGYU&open=AZ8q6y5GJaAYUuOnjGYU&pullRequest=157
{
this.manager.purchaseGenerator(this.user, island, generatorData, generatorTier);
}

// Rebuild the panel regardless of the answer.
this.build();
},
this.user,
this.user.getTranslation(Constants.CONVERSATIONS + "confirm-generator-purchase",
Constants.GENERATOR, generatorTier.getFriendlyName(),
TextVariables.NUMBER, this.hundredThousandsFormat.format(generatorTier.getGeneratorTierCost())),
null);
}


// ---------------------------------------------------------------------
// Section: Private methods
// ---------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -604,15 +604,7 @@ else if (isUnlocked)
case "VIEW" -> {
GeneratorViewPanel.openPanel(this, generatorTier);
}
case "BUY" -> {
if (this.island != null && this.manager.canPurchaseGenerator(user, this.island, this.generatorData, generatorTier))
{
this.manager.purchaseGenerator(this.user, this.island, this.generatorData, generatorTier);
}

// Build whole gui.
this.build();
}
case "BUY" -> this.purchaseGenerator(this.island, this.generatorData, generatorTier);
case "ACTIVATE" -> {
if (this.island != null && this.manager.canActivateGenerator(user, this.island, this.generatorData, generatorTier))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -800,15 +800,7 @@ else if (isUnlocked)
case "VIEW" -> {
GeneratorViewPanel.openPanel(this, this.generatorTier);
}
case "BUY" -> {
if (this.island != null && this.manager.canPurchaseGenerator(user, this.island, this.generatorData, this.generatorTier))
{
this.manager.purchaseGenerator(this.user, this.island, this.generatorData, this.generatorTier);
}

// Build whole gui.
this.build();
}
case "BUY" -> this.purchaseGenerator(this.island, this.generatorData, this.generatorTier);
case "ACTIVATE" -> {
if (this.island != null && this.manager.canActivateGenerator(user, this.island, this.generatorData, this.generatorTier))
{
Expand Down Expand Up @@ -1139,15 +1131,7 @@ private PanelItem createInfoButton(ItemTemplateRecord template, TemplatedPanel.I
case "VIEW" -> {
GeneratorViewPanel.openPanel(this, this.generatorTier);
}
case "BUY" -> {
if (this.island != null && this.manager.canPurchaseGenerator(user, this.island, this.generatorData, this.generatorTier))
{
this.manager.purchaseGenerator(this.user, this.island, this.generatorData, this.generatorTier);
}

// Build whole gui.
this.build();
}
case "BUY" -> this.purchaseGenerator(this.island, this.generatorData, this.generatorTier);
case "ACTIVATE" -> {
if (this.island != null && this.manager.canActivateGenerator(user, this.island, this.generatorData, this.generatorTier))
{
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,8 @@ stone-generator:
write-permissions: "<yellow>Please enter the required permissions, one per line in chat, and 'quit' on a line by itself to finish.</yellow>"
# Message that appears after successful permission updating.
permissions-changed: "<green>Success, generator permissions were updated.</green>"
# Message that asks the player to confirm a generator purchase before money is taken.
confirm-generator-purchase: "<yellow>Please confirm that you want to buy </yellow>[generator]<yellow> for [number].</yellow>"
# Message that appears after importing library data into database.
confirm-data-replacement: "<yellow>Please confirm that you want to replace your current generators with new one.</yellow>"
# Message that appears after successful data importing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package world.bentobox.magiccobblestonegenerator.panels;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;

Check warning on line 7 in src/test/java/world/bentobox/magiccobblestonegenerator/panels/CommonPanelPurchaseTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import 'org.mockito.Mockito.mock'.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_MagicCobblestoneGenerator&issues=AZ8q6y7cJaAYUuOnjGYV&open=AZ8q6y7cJaAYUuOnjGYV&pullRequest=157
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;

import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.magiccobblestonegenerator.CommonTestSetup;
import world.bentobox.magiccobblestonegenerator.StoneGeneratorAddon;
import world.bentobox.magiccobblestonegenerator.config.Settings;
import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorDataObject;
import world.bentobox.magiccobblestonegenerator.database.objects.GeneratorTierObject;
import world.bentobox.magiccobblestonegenerator.managers.StoneGeneratorManager;

/**
* Tests for the shared purchase-with-confirmation helper in {@link CommonPanel} (#109).
*/
class CommonPanelPurchaseTest extends CommonTestSetup {

@Mock
private StoneGeneratorAddon addon;
@Mock
private StoneGeneratorManager manager;
@Mock
private User panelUser;
@Mock
private GeneratorDataObject generatorData;
@Mock
private GeneratorTierObject generatorTier;

private Settings settings;
private TestPanel panel;

/**
* Minimal concrete CommonPanel that counts build() calls.
*/
private static class TestPanel extends CommonPanel {
int builds = 0;

TestPanel(StoneGeneratorAddon addon, User user, org.bukkit.World world) {
super(addon, user, world);
}

@Override
protected void build() {
this.builds++;
}

void purchase(Island island, GeneratorDataObject data, GeneratorTierObject tier) {
this.purchaseGenerator(island, data, tier);
}
}

@Override
@BeforeEach
public void setUp() throws Exception {
super.setUp();

settings = new Settings();
when(addon.getSettings()).thenReturn(settings);
when(addon.getAddonManager()).thenReturn(manager);

when(panelUser.getLocale()).thenReturn(Locale.ENGLISH);
when(panelUser.getTranslation(anyString()))
.thenAnswer((Answer<String>) inv -> inv.getArgument(0, String.class));
when(panelUser.getTranslationOrNothing(anyString()))
.thenAnswer((Answer<String>) inv -> inv.getArgument(0, String.class));

when(generatorTier.getFriendlyName()).thenReturn("Basic Tier");

panel = new TestPanel(addon, panelUser, world);
}

@Test
void testSettingsBuyConfirmationDefaultsToTrue() {
assertTrue(new Settings().isBuyConfirmation());
}

@Test
void testCannotPurchaseDoesNotBuy() {
when(manager.canPurchaseGenerator(any(), any(), any(), any())).thenReturn(false);

panel.purchase(island, generatorData, generatorTier);

verify(manager, never()).purchaseGenerator(any(), any(), any(), any());
assertEquals(1, panel.builds);
}

@Test
void testConfirmationDisabledPurchasesDirectly() {
settings.setBuyConfirmation(false);
when(manager.canPurchaseGenerator(any(), any(), any(), any())).thenReturn(true);

try (MockedStatic<ConversationUtils> cu = Mockito.mockStatic(ConversationUtils.class)) {
panel.purchase(island, generatorData, generatorTier);

verify(manager).purchaseGenerator(panelUser, island, generatorData, generatorTier);
cu.verifyNoInteractions();
}
assertEquals(1, panel.builds);
}

@Test
void testConfirmationEnabledBuysOnlyAfterConfirm() {
settings.setBuyConfirmation(true);
when(manager.canPurchaseGenerator(any(), any(), any(), any())).thenReturn(true);

List<Consumer<Boolean>> captured = new ArrayList<>();
try (MockedStatic<ConversationUtils> cu = Mockito.mockStatic(ConversationUtils.class)) {
cu.when(() -> ConversationUtils.createConfirmation(any(), any(), any(), any()))
.thenAnswer(inv -> {
captured.add(inv.getArgument(0));
return null;
});

panel.purchase(island, generatorData, generatorTier);

// Nothing purchased until the player confirms.
verify(manager, never()).purchaseGenerator(any(), any(), any(), any());
assertEquals(1, captured.size());

// Player confirms.
captured.get(0).accept(true);
verify(manager).purchaseGenerator(panelUser, island, generatorData, generatorTier);
}
}

@Test
void testConfirmationEnabledDeclineDoesNotBuy() {
settings.setBuyConfirmation(true);
when(manager.canPurchaseGenerator(any(), any(), any(), any())).thenReturn(true);

List<Consumer<Boolean>> captured = new ArrayList<>();
try (MockedStatic<ConversationUtils> cu = Mockito.mockStatic(ConversationUtils.class)) {
cu.when(() -> ConversationUtils.createConfirmation(any(), any(), any(), any()))
.thenAnswer(inv -> {
captured.add(inv.getArgument(0));
return null;
});

panel.purchase(island, generatorData, generatorTier);
// Player declines.
captured.get(0).accept(false);

verify(manager, never()).purchaseGenerator(any(), any(), any(), any());
}
}
}
Loading