diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java index 03b0ace8e..3c0f70f5e 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java @@ -403,6 +403,24 @@ public int compare(VoteSite v1, VoteSite v2) { return voteSites; } + /** + * Gets configured vote-site section keys without filtering by Enabled or + * validating the site's settings. + * + * @return the configured vote-site section keys + */ + public ArrayList getRawVoteSiteNames() { + ArrayList siteNames = new ArrayList<>(); + + if (!getData().isConfigurationSection("VoteSites")) { + return siteNames; + } + + siteNames = ArrayUtils.convert(getData().getConfigurationSection("VoteSites").getKeys(false)); + siteNames.removeIf(siteName -> !getData().isConfigurationSection("VoteSites." + siteName)); + return siteNames; + } + /** * Gets the names of vote sites. * diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/VotiferEvent.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/VotiferEvent.java index 6bd11cb98..d961140e7 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/VotiferEvent.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/listeners/VotiferEvent.java @@ -92,7 +92,8 @@ public void run() { } String voteSiteNameStr = plugin.getVoteSiteManager().getVoteSiteName(false, voteSite, matchSite); - boolean createSite = !plugin.getVoteSiteManager().hasVoteSite(voteSiteNameStr); + boolean createSite = !plugin.getVoteSiteManager().hasVoteSite(voteSiteNameStr) + && !plugin.getVoteSiteManager().hasConfiguredVoteSite(voteSiteNameStr); String serviceSite = voteSite; diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java index 109c089ec..9592e034a 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java @@ -82,6 +82,60 @@ public String normalizeVoteSiteKey(String name) { return name.replaceAll("[\\.\\s]+", "_"); } + /** + * Resolves identifiers against every configured vote-site section, including + * disabled sites. + * + * @param identifiers vote-site keys, service sites, or display names + * @return the configured vote-site key, or null when no configuration matches + */ + private String getConfiguredVoteSiteName(String... identifiers) { + if (identifiers == null) { + return null; + } + + ArrayList configuredSites = plugin.getConfigVoteSites().getRawVoteSiteNames(); + if (configuredSites == null || configuredSites.isEmpty()) { + return null; + } + + for (String identifier : identifiers) { + if (identifier == null || identifier.isEmpty()) { + continue; + } + + String normalizedIdentifier = normalizeVoteSiteKey(identifier); + for (String siteName : configuredSites) { + if (siteName == null) { + continue; + } + + String serviceSite = plugin.getConfigVoteSites().getServiceSite(siteName); + String displayName = plugin.getConfigVoteSites().getDisplayName(siteName); + if (siteName.equalsIgnoreCase(identifier) || siteName.equalsIgnoreCase(normalizedIdentifier) + || (serviceSite != null && !serviceSite.isEmpty() + && serviceSite.equalsIgnoreCase(identifier)) + || (displayName != null && !displayName.isEmpty() + && displayName.equalsIgnoreCase(identifier))) { + return siteName; + } + } + } + + return null; + } + + /** + * Checks whether an identifier belongs to any configured vote site, including + * a disabled site. + * + * @param identifiers vote-site identifiers + * @return true when a configured vote-site section matches + */ + public boolean hasConfiguredVoteSite(String... identifiers) { + return getConfiguredVoteSiteName(identifiers) != null; + } + /** * Attempts to map a URL, display name, or key to the configured vote site key. * @@ -91,6 +145,10 @@ public String normalizeVoteSiteKey(String name) { * found */ public String getVoteSiteName(boolean checkEnabled, String... urls) { + if (urls == null) { + return null; + } + for (String url : urls) { if (url == null) { return null; @@ -119,6 +177,13 @@ public String getVoteSiteName(boolean checkEnabled, String... urls) { } } + if (!checkEnabled) { + String configuredSiteName = getConfiguredVoteSiteName(urls); + if (configuredSiteName != null) { + return configuredSiteName; + } + } + for (String url : urls) { return url; } @@ -147,7 +212,8 @@ public VoteSite getVoteSite(String site, boolean checkEnabled) { } } - if (plugin.getConfigFile().isAutoCreateVoteSites() && !hasVoteSite(siteName)) { + if (plugin.getConfigFile().isAutoCreateVoteSites() && !hasVoteSite(siteName) + && !hasConfiguredVoteSite(siteName)) { if (!ServiceSiteValidator.isValid(siteName)) { plugin.getLogger().warning("Unable to auto-create vote site with unsupported name '" + ServiceSiteValidator.sanitizeForLog(siteName) + "'"); @@ -214,6 +280,9 @@ public String getVoteSiteServiceSite(String name) { */ public boolean hasVoteSite(String site) { String siteName = getVoteSiteName(false, site); + if (siteName == null) { + return false; + } for (VoteSite voteSite : getVoteSites()) { if (voteSite.getKey().equalsIgnoreCase(siteName)) { diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java new file mode 100644 index 000000000..a3f6ce3bb --- /dev/null +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/listeners/VotiferEventDisabledVoteSiteTest.java @@ -0,0 +1,139 @@ +package com.bencodez.votingplugin.tests.listeners; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; +import java.util.logging.Logger; + +import org.bukkit.Server; +import org.bukkit.plugin.PluginManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.bencodez.votingplugin.VotingPluginMain; +import com.bencodez.votingplugin.config.ConfigVoteSites; +import com.bencodez.votingplugin.events.PlayerVoteEvent; +import com.bencodez.votingplugin.listeners.VotiferEvent; +import com.bencodez.votingplugin.votesites.VoteSiteManager; +import com.vexsoftware.votifier.model.Vote; + +/** + * Tests Votifier ingestion when a received service belongs to a disabled + * configured vote site. + */ +public class VotiferEventDisabledVoteSiteTest { + + private static final String SERVICE_SITE = "disabled.example.com"; + + private VotingPluginMain plugin; + + private ConfigVoteSites configVoteSites; + + private VoteSiteManager voteSiteManager; + + private ScheduledExecutorService voteTimer; + + private PluginManager pluginManager; + + private VotiferEvent listener; + + @BeforeEach + public void setUp() { + plugin = mock(VotingPluginMain.class, RETURNS_DEEP_STUBS); + configVoteSites = mock(ConfigVoteSites.class); + voteSiteManager = mock(VoteSiteManager.class); + voteTimer = mock(ScheduledExecutorService.class); + + Server server = mock(Server.class); + pluginManager = mock(PluginManager.class); + + when(plugin.getLogger()).thenReturn(Logger.getLogger("VotiferEventDisabledVoteSiteTest")); + when(plugin.getConfigVoteSites()).thenReturn(configVoteSites); + when(plugin.getVoteSiteManager()).thenReturn(voteSiteManager); + when(plugin.getVoteTimer()).thenReturn(voteTimer); + when(plugin.getServer()).thenReturn(server); + when(server.getPluginManager()).thenReturn(pluginManager); + + when(plugin.getOptions().getBedrockPlayerPrefix()).thenReturn("."); + when(plugin.getBungeeSettings().isUseBungeecoord()).thenReturn(false); + when(plugin.getConfigFile().isAdvancedServiceSiteHandling()).thenReturn(false); + when(plugin.getTimeChecker().isActiveProcessing()).thenReturn(false); + + // Execute submitted vote work immediately so assertions do not need a real + // executor thread. + doAnswer(invocation -> { + Runnable task = invocation.getArgument(0); + task.run(); + return CompletableFuture.completedFuture(null); + }).when(voteTimer).submit(any(Runnable.class)); + + listener = new VotiferEvent(plugin); + } + + /** + * Creates a mocked NuVotifier event. + * + * @param serviceSite service site supplied by NuVotifier + * @return the event + */ + private com.vexsoftware.votifier.model.VotifierEvent createVoteEvent(String serviceSite) { + Vote vote = mock(Vote.class); + when(vote.getServiceName()).thenReturn(serviceSite); + when(vote.getAddress()).thenReturn("127.0.0.1"); + when(vote.getUsername()).thenReturn("Steve"); + + com.vexsoftware.votifier.model.VotifierEvent event = + mock(com.vexsoftware.votifier.model.VotifierEvent.class); + when(event.getVote()).thenReturn(vote); + return event; + } + + @Test + public void testDisabledConfiguredSiteIsNotGeneratedByVotifierPath() { + when(plugin.getConfigFile().isAutoCreateVoteSites()).thenReturn(true); + + when(voteSiteManager.getVoteSiteName(false, SERVICE_SITE, "")).thenReturn("DisabledSite"); + when(voteSiteManager.hasVoteSite("DisabledSite")).thenReturn(false); + when(voteSiteManager.hasConfiguredVoteSite("DisabledSite")).thenReturn(true); + + when(voteSiteManager.getVoteSiteName(true, SERVICE_SITE, "")).thenReturn(SERVICE_SITE); + when(voteSiteManager.getVoteSite(SERVICE_SITE, true)).thenReturn(null); + + listener.onVotiferEvent(createVoteEvent(SERVICE_SITE)); + + verify(configVoteSites, never()).tryGenerateVoteSite(anyString()); + + // Proves the submitted task continued through vote resolution rather than + // passing only because processing stopped before the generation decision. + verify(pluginManager).callEvent(any(PlayerVoteEvent.class)); + } + + @Test + public void testUnknownSiteStillAttemptsGenerationByVotifierPath() { + when(plugin.getConfigFile().isAutoCreateVoteSites()).thenReturn(true); + + when(voteSiteManager.getVoteSiteName(false, SERVICE_SITE, "")).thenReturn(SERVICE_SITE); + when(voteSiteManager.hasVoteSite(SERVICE_SITE)).thenReturn(false); + when(voteSiteManager.hasConfiguredVoteSite(SERVICE_SITE)).thenReturn(false); + + // Return false to avoid depending on reload behavior. This test only needs to + // prove that a genuinely unknown site still attempts generation. + when(configVoteSites.tryGenerateVoteSite(SERVICE_SITE)).thenReturn(false); + + when(voteSiteManager.getVoteSiteName(true, SERVICE_SITE, "")).thenReturn(SERVICE_SITE); + when(voteSiteManager.getVoteSite(SERVICE_SITE, true)).thenReturn(null); + + listener.onVotiferEvent(createVoteEvent(SERVICE_SITE)); + + verify(configVoteSites).tryGenerateVoteSite(SERVICE_SITE); + verify(pluginManager).callEvent(any(PlayerVoteEvent.class)); + } +} diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/ConfigVoteSitesRawNamesTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/ConfigVoteSitesRawNamesTest.java new file mode 100644 index 000000000..0742a2720 --- /dev/null +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/ConfigVoteSitesRawNamesTest.java @@ -0,0 +1,82 @@ +package com.bencodez.votingplugin.tests.votesite; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import java.util.ArrayList; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Test; + +import com.bencodez.votingplugin.config.ConfigVoteSites; + +/** + * Tests the unfiltered configured vote-site section lookup. + */ +public class ConfigVoteSitesRawNamesTest { + + /** + * Creates a partially real ConfigVoteSites mock whose data comes from the + * supplied in-memory YAML configuration. + * + * @param data in-memory configuration data + * @return the configuration object + */ + private ConfigVoteSites configWithData(YamlConfiguration data) { + ConfigVoteSites config = mock(ConfigVoteSites.class, CALLS_REAL_METHODS); + doReturn(data).when(config).getData(); + return config; + } + + @Test + public void testRawNamesIncludesEnabledAndDisabledSections() { + YamlConfiguration data = new YamlConfiguration(); + + data.createSection("VoteSites.EnabledSite"); + data.set("VoteSites.EnabledSite.Enabled", true); + data.set("VoteSites.EnabledSite.ServiceSite", "enabled.example.com"); + + data.createSection("VoteSites.DisabledSite"); + data.set("VoteSites.DisabledSite.Enabled", false); + data.set("VoteSites.DisabledSite.ServiceSite", "disabled.example.com"); + + ArrayList names = configWithData(data).getRawVoteSiteNames(); + + assertEquals(2, names.size()); + assertTrue(names.contains("EnabledSite")); + assertTrue(names.contains("DisabledSite"), + "Disabled sections must remain visible to existence checks"); + } + + @Test + public void testRawNamesIgnoresMalformedScalarChildren() { + YamlConfiguration data = new YamlConfiguration(); + + data.createSection("VoteSites.RealSite"); + data.set("VoteSites.RealSite.Enabled", false); + data.set("VoteSites.MalformedSite", "this is a scalar and not a vote-site section"); + + ArrayList names = configWithData(data).getRawVoteSiteNames(); + + assertEquals(1, names.size()); + assertEquals("RealSite", names.get(0)); + } + + @Test + public void testRawNamesReturnsEmptyWhenVoteSitesSectionIsMissing() { + YamlConfiguration data = new YamlConfiguration(); + + assertTrue(configWithData(data).getRawVoteSiteNames().isEmpty()); + } + + @Test + public void testRawNamesReturnsEmptyWhenVoteSitesValueIsMalformed() { + YamlConfiguration data = new YamlConfiguration(); + data.set("VoteSites", "not a configuration section"); + + assertTrue(configWithData(data).getRawVoteSiteNames().isEmpty()); + } +} diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerDisabledVoteSiteTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerDisabledVoteSiteTest.java new file mode 100644 index 000000000..f7ecceb66 --- /dev/null +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerDisabledVoteSiteTest.java @@ -0,0 +1,191 @@ +package com.bencodez.votingplugin.tests.votesite; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +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.Arrays; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.bencodez.simpleapi.time.ParsedDuration; +import com.bencodez.votingplugin.VotingPluginMain; +import com.bencodez.votingplugin.config.Config; +import com.bencodez.votingplugin.config.ConfigVoteSites; +import com.bencodez.votingplugin.data.ServerData; +import com.bencodez.votingplugin.votesites.VoteSite; +import com.bencodez.votingplugin.votesites.VoteSiteManager; + +/** + * Regression tests for configured-but-disabled vote sites. + */ +public class VoteSiteManagerDisabledVoteSiteTest { + + private VotingPluginMain plugin; + + private ConfigVoteSites voteSitesConfig; + + private Config configFile; + + private VoteSiteManager manager; + + @BeforeEach + public void setUp() { + plugin = mock(VotingPluginMain.class); + voteSitesConfig = mock(ConfigVoteSites.class); + configFile = mock(Config.class); + + when(plugin.getConfigVoteSites()).thenReturn(voteSitesConfig); + when(plugin.getConfigFile()).thenReturn(configFile); + when(plugin.getLogger()).thenReturn(Logger.getLogger("VoteSiteManagerDisabledVoteSiteTest")); + + when(voteSitesConfig.getVoteURL(anyString())).thenReturn("example.com"); + when(voteSitesConfig.getServiceSite(anyString())).thenReturn("ServiceSite"); + when(voteSitesConfig.getVoteDelay(anyString())).thenReturn(ParsedDuration.parse("12h", TimeUnit.HOURS)); + when(voteSitesConfig.getVoteSiteEnabled(anyString())).thenReturn(true); + when(voteSitesConfig.getPriority(anyString())).thenReturn(1); + when(voteSitesConfig.getDisplayName(anyString())).thenReturn("DisplayName"); + when(voteSitesConfig.getItem(anyString())).thenReturn(null); + when(voteSitesConfig.getVoteSiteResetVoteDelayDaily(anyString())).thenReturn(false); + when(voteSitesConfig.getVoteSiteGiveOffline(anyString())).thenReturn(false); + when(voteSitesConfig.getWaitUntilVoteDelay(anyString())).thenReturn(false); + when(voteSitesConfig.getVoteDelayDailyHour(anyString())).thenReturn(0); + when(voteSitesConfig.getVoteSiteHidden(anyString())).thenReturn(false); + when(voteSitesConfig.getVoteSiteIgnoreCanVote(anyString())).thenReturn(false); + when(voteSitesConfig.getPermissionToView(anyString())).thenReturn(""); + + ServerData serverData = mock(ServerData.class); + when(serverData.getServiceSites()).thenReturn(new ArrayList(Arrays.asList("ServiceSite"))); + when(plugin.getServerData()).thenReturn(serverData); + + manager = new VoteSiteManager(plugin); + } + + /** + * Configures one disabled vote-site section that is intentionally absent from + * the manager's loaded vote-site list. + * + * @param key the configured vote-site key + * @param serviceSite the configured service site + * @param displayName the configured display name + */ + private void configureDisabledVoteSite(String key, String serviceSite, String displayName) { + when(voteSitesConfig.getRawVoteSiteNames()) + .thenReturn(new ArrayList(Arrays.asList(key))); + when(voteSitesConfig.getVoteSiteEnabled(key)).thenReturn(false); + when(voteSitesConfig.getServiceSite(key)).thenReturn(serviceSite); + when(voteSitesConfig.getDisplayName(key)).thenReturn(displayName); + + manager.setVoteSites(Collections.synchronizedList(new ArrayList())); + } + + @Test + public void testDisabledConfiguredVoteSiteMatchesEverySupportedIdentifier() { + configureDisabledVoteSite("site_key", "disabled.example.com", "Disabled Voting Site"); + + assertEquals("site_key", manager.getVoteSiteName(false, "SITE_KEY"), + "Configured keys should be matched case-insensitively"); + assertEquals("site_key", manager.getVoteSiteName(false, "DISABLED.EXAMPLE.COM"), + "Disabled sites should still match their ServiceSite"); + assertEquals("site_key", manager.getVoteSiteName(false, "DISABLED VOTING SITE"), + "Disabled sites should still match their display name"); + assertEquals("site_key", manager.getVoteSiteName(false, "site.key"), + "Generated-key normalization should match dots to underscores"); + } + + @Test + public void testDisabledConfiguredVoteSiteDoesNotResolveForEnabledOnlyLookup() { + configureDisabledVoteSite("DisabledSite", "disabled.example.com", "Disabled Voting Site"); + + assertEquals("disabled.example.com", manager.getVoteSiteName(true, "disabled.example.com"), + "Enabled-only lookup must not return a disabled configured site"); + assertEquals("Disabled Voting Site", manager.getVoteSiteName(true, "Disabled Voting Site"), + "Enabled-only display-name lookup must not return a disabled site"); + } + + @Test + public void testDisabledConfiguredVoteSiteIsConfiguredButNotLoaded() { + configureDisabledVoteSite("site_key", "disabled.example.com", "Disabled Voting Site"); + + assertTrue(manager.hasConfiguredVoteSite("site_key")); + assertTrue(manager.hasConfiguredVoteSite("SITE_KEY")); + assertTrue(manager.hasConfiguredVoteSite("disabled.example.com")); + assertTrue(manager.hasConfiguredVoteSite("Disabled Voting Site")); + assertTrue(manager.hasConfiguredVoteSite("site.key")); + + assertFalse(manager.hasVoteSite("disabled.example.com"), + "hasVoteSite must continue to describe the loaded vote-site list"); + assertFalse(manager.hasVoteSite("site_key"), + "A configured disabled site must not appear loaded"); + } + + @Test + public void testDisabledConfiguredVoteSiteIsNeverAutoCreated() { + when(configFile.isAutoCreateVoteSites()).thenReturn(true); + configureDisabledVoteSite("DisabledSite", "disabled.example.com", "Disabled Voting Site"); + + assertNull(manager.getVoteSite("disabled.example.com", false), + "A configured disabled site should remain unavailable"); + assertNull(manager.getVoteSite("disabled.example.com", true), + "Enabled-only lookup should return no disabled VoteSite"); + + verify(voteSitesConfig, never()).tryGenerateVoteSite(anyString()); + } + + @Test + public void testConfiguredVoteSiteCanMatchSecondIdentifier() { + configureDisabledVoteSite("DisabledSite", "disabled.example.com", "Disabled Voting Site"); + + assertEquals("DisabledSite", + manager.getVoteSiteName(false, "unmatched-service.example.net", "disabled.example.com"), + "The advanced ServiceSite fallback must be checked"); + } + + @Test + public void testEmptyConfiguredAliasesDoNotMatchEmptyInput() { + configureDisabledVoteSite("DisabledSite", "", ""); + + assertFalse(manager.hasConfiguredVoteSite(""), + "Empty ServiceSite and display-name values must not match"); + assertEquals("", manager.getVoteSiteName(false, ""), + "Empty input should retain the existing fallback behavior"); + } + + @Test + public void testNullConfiguredSiteInputIsSafe() { + when(configFile.isAutoCreateVoteSites()).thenReturn(true); + configureDisabledVoteSite("DisabledSite", "disabled.example.com", "Disabled Voting Site"); + + assertNull(manager.getVoteSiteName(false, (String) null)); + assertFalse(manager.hasConfiguredVoteSite((String) null)); + assertFalse(manager.hasConfiguredVoteSite((String[]) null)); + assertFalse(manager.hasVoteSite(null)); + + verify(voteSitesConfig, never()).tryGenerateVoteSite(anyString()); + } + + @Test + public void testUnknownSiteStillAutoCreatesWhenDisabledSitesAreConfigured() { + when(configFile.isAutoCreateVoteSites()).thenReturn(true); + when(voteSitesConfig.tryGenerateVoteSite("new.example.com")).thenReturn(true); + configureDisabledVoteSite("DisabledSite", "disabled.example.com", "Disabled Voting Site"); + + VoteSite generated = manager.getVoteSite("new.example.com", false); + + assertNotNull(generated, "An unrelated unknown site should still be auto-created"); + assertEquals("new_example_com", generated.getKey()); + verify(voteSitesConfig).tryGenerateVoteSite("new.example.com"); + } +}