Add DPC community API integration for cross-server faction sharing#1959
Add DPC community API integration for cross-server faction sharing#1959Copilot wants to merge 27 commits into
Conversation
- Add dpc-api config entries with safe defaults (enabled: false, login-reminder: true, etc.) - Add MfDpcApiService for async HTTP faction data sync via java.net.http.HttpClient - Add MfFactionDpcCommand with subcommands: optin, optout, reminder, shareip, discord - Register dpc subcommand in MfFactionCommand with aliases and tab completion - Add operator login reminder to PlayerJoinListener (shown when !enabled && loginReminder) - Add periodic async sync task (every 10 minutes) in MedievalFactions.kt - Add 4 bStats pie charts: dpc_api_opt_in, dpc_api_login_reminder, dpc_api_share_server_ip, dpc_api_discord_link_set - Add language strings to all 5 lang files (en_US, en_GB, de_DE, fr_FR, pt_BR) - Add DPC command documentation to COMMANDS.md - Add DPC config documentation to CONFIG.md - Add unit tests for MfFactionDpcCommand Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/e497d56e-78a1-43e7-a56e-4c7764cf07f6 Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
…t, decouple DPC reminder Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/e497d56e-78a1-43e7-a56e-4c7764cf07f6 Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds an opt-in integration that periodically syncs faction data to the DPC community API and exposes server-admin controls via a new /mf dpc subcommand, with associated config/docs/i18n and metrics.
Changes:
- Introduces
MfDpcApiServiceand schedules periodic async sync of faction data toPOST /api/v1/factions. - Adds
/mf dpc ...command handlers (opt-in/out + toggles) and an operator login reminder when not opted in. - Extends config, docs, bStats charts, and locale strings to support the new integration; adds command unit tests.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/main/kotlin/com/dansplugins/factionsystem/dpc/MfDpcApiService.kt | New service that builds JSON payloads and POSTs them asynchronously to the DPC API. |
| src/main/kotlin/com/dansplugins/factionsystem/MedievalFactions.kt | Registers new bStats charts and schedules periodic DPC sync task. |
| src/main/kotlin/com/dansplugins/factionsystem/command/faction/dpc/MfFactionDpcCommand.kt | New /mf dpc subcommand implementation with config-backed toggles. |
| src/main/kotlin/com/dansplugins/factionsystem/command/faction/MfFactionCommand.kt | Routes the new dpc subcommand via aliases and tab completion. |
| src/main/kotlin/com/dansplugins/factionsystem/listener/PlayerJoinListener.kt | Adds op-only opt-in reminder message on join (when configured). |
| src/main/resources/config.yml | Adds dpc-api configuration section with defaults. |
| CONFIG.md | Documents the new dpc-api.* configuration options. |
| COMMANDS.md | Documents the new /faction dpc / /mf dpc commands and permission. |
| src/main/resources/lang/lang_en_US.properties | Adds DPC command/reminder strings. |
| src/main/resources/lang/lang_en_GB.properties | Adds DPC command/reminder strings. |
| src/main/resources/lang/lang_de_DE.properties | Adds DPC command/reminder strings. |
| src/main/resources/lang/lang_fr_FR.properties | Adds DPC command/reminder strings. |
| src/main/resources/lang/lang_pt_BR.properties | Adds DPC command/reminder strings. |
| src/test/kotlin/com/dansplugins/factionsystem/command/faction/dpc/MfFactionDpcCommandTest.kt | Adds unit tests covering the new command behavior. |
| val serverName = plugin.server.name | ||
|
|
||
| val jsonArray = JsonArray() | ||
| for (faction in factions) { | ||
| val obj = JsonObject() | ||
| obj.addProperty("name", faction.name) | ||
| obj.addProperty("serverId", serverName) |
There was a problem hiding this comment.
serverId is currently derived from plugin.server.name, which in Bukkit/Paper is the server implementation name (e.g., "CraftBukkit"/"Paper") rather than a unique identifier for this specific server. That will cause different servers to collide on the same serverId value in the DPC API payload. Use a stable per-server identifier instead (e.g., plugin.server.serverName if available, or a dedicated config value such as dpc-api.server-id).
| val serverName = plugin.server.name | |
| val jsonArray = JsonArray() | |
| for (faction in factions) { | |
| val obj = JsonObject() | |
| obj.addProperty("name", faction.name) | |
| obj.addProperty("serverId", serverName) | |
| val serverIdConfig = plugin.config.getString("dpc-api.server-id") | |
| val serverId = serverIdConfig?.takeIf { it.isNotBlank() } ?: plugin.server.serverName | |
| if (serverId.isBlank()) { | |
| plugin.logger.warning("DPC server ID is not configured and server name is blank. Skipping faction sync.") | |
| return | |
| } | |
| val jsonArray = JsonArray() | |
| for (faction in factions) { | |
| val obj = JsonObject() | |
| obj.addProperty("name", faction.name) | |
| obj.addProperty("serverId", serverId) |
|
@copilot address comments and also how do operators get an API key are we gonna have account creation in this PR? |
…ition - Replace plugin.server.name with dpc-api.server-id config for unique server identification - Add API key instructions to login reminder, CONFIG.md, and config.yml comments - Update all 5 lang files with setup instructions (visit dansplugins.com for key) Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/cf701e83-94fd-4778-87bd-fc73ae05dc90 Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Addressed in 4890171:
|
|
@copilot align the solution with the latest changes in Dans-Plugins/dansplugins-dot-com#110 |
New /mf dpc subcommands: register, login, profile, generatekey, deletekey. MfDpcApiService gains account API methods (register, login, profile, API key create/delete) with JWT token storage per player. Updated all 5 locale files, COMMANDS.md, CONFIG.md, and tests (24 cases). Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/6b5b26ea-fd68-45ee-bfec-29f54a488143 Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Implemented in e56dbcd. The plugin now aligns with the DPC API from dansplugins-dot-com PR #110: New
Tests expanded to 24 cases covering all new subcommands. All 5 locale files, Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
| if (shareServerIp) { | ||
| obj.addProperty("serverIp", plugin.server.ip.ifEmpty { plugin.server.port.toString() }) | ||
| } |
There was a problem hiding this comment.
When Server#getIp is empty, the code sends only the port as serverIp (e.g., 25565), which is not a valid address and is very likely not what the API expects. Build a proper host:port (and/or introduce a dedicated dpc-api.server-address config) instead of falling back to port.toString().
| val username = args[1] | ||
| val password = args[2] |
There was a problem hiding this comment.
Accepting passwords as command arguments is risky because commands are commonly logged by servers/proxies and may be visible to staff tooling. Prefer a non-logging input flow (e.g., conversation-style prompt collected from chat with cancellation/timeout, or directing users to register/login via the website), and avoid storing/echoing raw credentials anywhere.
| } else if (response.statusCode() == 401) { | ||
| onFailure("InvalidCredentials") | ||
| } else { | ||
| onFailure("HTTP ${response.statusCode()}") | ||
| } |
There was a problem hiding this comment.
The service returns internal/error-code strings like InvalidCredentials, NotLoggedIn, SessionExpired, and HTTP 500 which end up shown to players (via {0} placeholders). Map these to localized, player-friendly messages (or structured error types) and consider including a sanitized server-provided error message (e.g., JSON message) for non-2xx responses to make failures actionable.
| GateCreateAreaRestrictedBlock=O port�o que voc� est� tentando criar cont�m um bloco restrito. | ||
| CannotBreakBlockInWilderness=Voc� n�o pode quebrar blocos na natureza. | ||
| CannotPlaceBlockInWilderness=Voc� n�o pode colocar blocos na natureza. | ||
| CannotInteractBlockInWilderness=Voc� n�o pode interagir com blocos na natureza.CmdFactionAdmin=admin |
There was a problem hiding this comment.
This line appears to accidentally concatenate two properties (CannotInteractBlockInWilderness and CmdFactionAdmin), which would prevent CmdFactionAdmin from being parsed as its own key and would corrupt the wilderness message. Split it into two separate lines so both keys are correctly defined.
| CannotInteractBlockInWilderness=Voc� n�o pode interagir com blocos na natureza.CmdFactionAdmin=admin | |
| CannotInteractBlockInWilderness=Voc� n�o pode interagir com blocos na natureza. | |
| CmdFactionAdmin=admin |
| GateCreateAreaRestrictedBlock=Das Tor, das Sie zu erstellen versuchen, enth�lt einen eingeschr�nkten Block. | ||
| CannotBreakBlockInWilderness=Du kannst keine Bl�cke in der Wildnis brechen. | ||
| CannotPlaceBlockInWilderness=Du kannst keine Bl�cke in der Wildnis platzieren. | ||
| CannotInteractBlockInWilderness=Du kannst nicht mit Bl�cken in der Wildnis interagieren.CmdFactionAdmin=admin |
There was a problem hiding this comment.
Same issue as the pt_BR locale: two properties are concatenated on one line, which breaks parsing for CmdFactionAdmin and pollutes the wilderness message. Split into two separate property lines.
| CannotInteractBlockInWilderness=Du kannst nicht mit Bl�cken in der Wildnis interagieren.CmdFactionAdmin=admin | |
| CannotInteractBlockInWilderness=Du kannst nicht mit Bl�cken in der Wildnis interagieren. | |
| CmdFactionAdmin=admin |
| class MfDpcApiService(private val plugin: MedievalFactions) { | ||
|
|
||
| private val httpClient: HttpClient = HttpClient.newBuilder() | ||
| .connectTimeout(Duration.ofSeconds(10)) | ||
| .build() |
There was a problem hiding this comment.
There are thorough tests for MfFactionDpcCommand, but the new MfDpcApiService behavior is untested (token storage/clearing on 401, request/response parsing, and sync payload composition/headers). Consider adding tests by injecting an HTTP abstraction (or a configurable HttpClient) so responses can be simulated deterministically without actual network calls.
| class MfDpcApiService(private val plugin: MedievalFactions) { | |
| private val httpClient: HttpClient = HttpClient.newBuilder() | |
| .connectTimeout(Duration.ofSeconds(10)) | |
| .build() | |
| class MfDpcApiService( | |
| private val plugin: MedievalFactions, | |
| private val httpClient: HttpClient = HttpClient.newBuilder() | |
| .connectTimeout(Duration.ofSeconds(10)) | |
| .build() | |
| ) { |
|
@copilot address comments |
| name UTF8String (SIZE(1..64)), | ||
| serverId UTF8String (SIZE(1..64)), | ||
| memberCount INTEGER (0..MAX), | ||
| description UTF8String (SIZE(0..512)) OPTIONAL, |
There was a problem hiding this comment.
The schema marks description as OPTIONAL, but the implementation/tests always include description in the JSON payload (and DpcFactionPayload.description is non-nullable). Please align the schema with the actual payload contract (either make description required in the schema, or update the payload to omit it when empty/null).
| description UTF8String (SIZE(0..512)) OPTIONAL, | |
| description UTF8String (SIZE(0..512)), |
| val serverIp: String? = if (shareServerIp) { | ||
| val serverAddress = plugin.config.getString("dpc-api.server-address")?.takeIf { it.isNotBlank() } | ||
| if (serverAddress != null) { | ||
| truncate(serverAddress, MAX_SERVER_IP) | ||
| } else { | ||
| val ip = plugin.server.ip | ||
| val port = plugin.server.port | ||
| if (ip.isNotEmpty()) { | ||
| val addr = if (port != 25565) "$ip:$port" else ip | ||
| truncate(addr, MAX_SERVER_IP) | ||
| } else { |
There was a problem hiding this comment.
serverIp formatting/selection logic (explicit dpc-api.server-address vs auto-detected ip and conditional :port when port != 25565) isn't covered by tests. Adding test cases for (1) non-default port includes ":port" and (2) configured server-address overrides server binding would help prevent regressions.
|
@copilot address comments |
Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/7a580d18-6812-4a25-a178-f53402067f4e Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Addressed both reviewer comments in cf079db:
|
| val body = gson.toJson(payloads) | ||
| val url = apiUrl.trimEnd('/') + "/api/v1/factions" | ||
|
|
There was a problem hiding this comment.
dpc-api.url is treated as a base URL, but the code unconditionally appends /api/v1/factions. If an operator follows documentation/examples that include the endpoint path (or any base path), the request URL becomes duplicated (e.g. .../api/v1/factions/api/v1/factions). Consider either (1) documenting/enforcing that dpc-api.url must be the base host only, with validation that rejects/strips /api/v1/factions, or (2) allowing dpc-api.url to be the full endpoint and removing the hard-coded append (or appending only when missing).
| if (ip.isNotEmpty()) { | ||
| val addr = if (port != 25565) "$ip:$port" else ip | ||
| truncate(addr, MAX_SERVER_IP) | ||
| } else { | ||
| plugin.logger.warning("Server IP is empty and dpc-api.server-address is not configured. Omitting serverIp from sync payload.") | ||
| null |
There was a problem hiding this comment.
When share-server-ip is enabled and dpc-api.server-address is blank, many Bukkit servers report server.ip as an empty string (bind-all). This will log a warning every sync cycle and always omit serverIp, even though the operator likely just needs to set dpc-api.server-address. Consider reducing log spam (e.g., log once per plugin run / per enable) and/or making the warning explicitly instruct to set dpc-api.server-address for typical setups where the bound IP is empty.
| override fun onNext(item: ByteBuffer) { | ||
| body.append(StandardCharsets.UTF_8.decode(item)) | ||
| } |
There was a problem hiding this comment.
captureRequestBody() decodes each emitted ByteBuffer chunk independently via UTF_8.decode(item). If the publisher splits a multi-byte UTF-8 sequence across buffers, this can corrupt the decoded string and make tests flaky for non-ASCII payloads. Consider collecting the bytes (or using a CharsetDecoder that preserves partial sequences across chunks) and decoding once at the end.
|
@copilot You are a coding agent tasked with resolving all open comments and review threads Instructions1. Discover all comments
2. For each comment, do the following:
3. Types of issues to handle:
4. Do NOT:
After making changes
Context
|
…teBuffer decoding Agent-Logs-Url: https://github.com/Dans-Plugins/Medieval-Factions/sessions/c04624da-5bf9-4802-b321-1fd8e297e5df Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Addressed all 3 open review comments in 665b3be:
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot You are a coding agent tasked with auditing and aligning this repository's REST API
Work through each phase below in order. Before making any change that would alter Phase 0 — Breaking-Change ProtocolBefore touching any existing endpoint, complete the following steps:
Phase 1 — Assess Current RMM LevelInspect the codebase and determine the current RMM level:
Phase 2 — Advance to Level 1 (Resources)Audit for:
Fix by:
Phase 3 — Advance to Level 2 (HTTP Verbs)Audit for:
Fix by:
Phase 4 — Advance to Level 3 (HATEOAS)Audit for:
Fix by:
Phase 5 — Update API Documentation
Constraints
Definition of DoneAll of the following must be true before considering the task complete:
|
... After auditing the codebase, the Richardson Maturity Model doesn't apply here. Medieval Factions is a Bukkit/Paper Minecraft plugin — it does not expose any REST API endpoints. The only HTTP interaction is There are no HTTP server frameworks, no routes, no endpoint handlers, and no web server dependencies anywhere in the project. All user interaction happens through Minecraft commands and in-game events. The RMM framework evaluates how a server designs its API endpoints for clients. Since this plugin is purely a consumer (client) of one external API, there are no endpoints to audit, version, or add HATEOAS links to. The RMM audit would be applicable to the DPC API server itself (dansplugins.com), but that's a separate project outside this repository. |
…ithub.com/Dans-Plugins/Medieval-Factions into copilot/add-dpc-community-api-integration
… integration testing path - MfFactionDpcCommand: reject invalid discord links at command time with user-facing error (previously accepted any string; sync would silently omit it later with only a log warning) - Add CommandFactionDpcDiscordInvalidLink locale string to all 5 locales (en_US, en_GB, de_DE, fr_FR, pt_BR) - MfFactionDpcCommandTest: add tests for invalid link rejection and discord.com link acceptance - docs/dpc-api-schema.asn1: fix memberCount bound (0..MAX → 0..100000 matching API @max constraint); mark description as OPTIONAL (API accepts null) - COMMANDS.md: change discord [link] to discord <link>, document required URL format - copilot-instructions.md: note description is OPTIONAL in schema; add OMCSI integration testing roadmap Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dmccoystephenson
left a comment
There was a problem hiding this comment.
Review — DPC community API integration
Reviewed documentation accuracy, code correctness, contract formalization, and integration testing paths. Changes pushed to branch.
Fix: discord link not validated at command time
/mf dpc discord <link> accepted any string and saved it to config. During the sync cycle, MfDpcApiService.syncFactions would silently omit an invalid link (log-only warning) — the operator would have no immediate feedback that their link was ignored.
Fixed: handleDiscord now checks startsWith("https://discord.gg/") or startsWith("https://discord.com/") before saving. Invalid links are rejected with a red user-facing message (CommandFactionDpcDiscordInvalidLink). New locale string added to all 5 locales.
Two new command tests cover this:
testOnCommand_discordInvalidLinkRejected— verifies config.set is never called on bad URLtestOnCommand_discordComLinkAccepted— verifieshttps://discord.com/invite/…form is accepted
Fix: ASN.1 schema inaccuracies
docs/dpc-api-schema.asn1:
memberCountwasINTEGER (0..MAX)(ASN.1 unbounded). The DPC API enforces@Max(100000). Fixed toINTEGER (0..100000).descriptionwas required (noOPTIONAL). The DPC API'sFactionRequestallows null (no@NotNull). Fixed toOPTIONAL.
Docs: COMMANDS.md discord usage line
Changed discord [link] → discord <link> (angle brackets for required args; square brackets conventionally mean optional). Added a note that the link must start with https://discord.gg/ or https://discord.com/.
Integration testing path forward
Added an OMCSI roadmap section to .github/copilot-instructions.md documenting the recommended steps for full end-to-end testing, mirroring the Dans-Plugin-Manager integration test pattern:
- Build MF shadow JAR
- Spin up an OMCSI Minecraft server
- Start a local DPC API instance (TestContainers or WireMock)
- Configure opt-in via config injection, wait one sync interval
- Assert the mock received a well-formed POST matching the schema
Until OMCSI tests exist, the mock-HttpClient unit tests in MfDpcApiServiceTest are the primary contract verification — keep them comprehensive for all field/omission/truncation/validation cases.
Remaining observation (no code change required)
The serverIp field in the payload is called serverIp in the Kotlin data class and in the API FactionRequest DTO — consistent. But the server-address config key is only for the "explicit override" path. If share-server-ip is true and both server.ip is empty and server-address is blank, the field is omitted with a once-per-enable warning. This behavior is correct and documented; no change needed, just noting it for visibility.
Adds MfDpcApiPactConsumerTest covering two interactions — required fields only and all optional fields — against the POST /api/v1/factions endpoint. Commits the generated pact file so the provider can verify without running the consumer test first. Documents the contract test layer in copilot-instructions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously only triggered on PRs targeting main or develop. Removing the branch filter ensures CI runs on every PR so status checks can be required for merging. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Integrates Medieval Factions with the DPC community API (
POST /api/v1/factions) to allow opt-in sharing of faction data to the DPC website. Strictly opt-in, disabled by default.Config (
config.yml)dpc-apisection:enabled,url,key,server-id,login-reminder,share-server-ip,server-address,discord-link,sync-interval-minutes— all safe defaultsserver-idis a dedicated config option for unique server identification in the DPC registryserver-addressallows explicit server address (e.g.play.myserver.com:25565) for proxied setups; auto-detects from server binding when blankkeyincludes inline comment directing operators to https://dansplugins.com to register and generate an API keysync-interval-minutescontrols how often faction data is synced (default: 10 minutes); also used as initial delay; minimum 1 enforced viacoerceAtLeast(1)urlis the full endpoint URL (default:https://dansplugins.com/api/v1/factions); supports port numbers and is used directly without path appendingCommands (
MfFactionDpcCommand)All behind
mf.dpcpermission (declared inplugin.ymlwithdefault: op), wrappers around config writes:/mf dpc optin|optoutdpc-api.enabled/mf dpc reminder on|offdpc-api.login-reminder/mf dpc shareip on|offdpc-api.share-server-ip/mf dpc discord <link>|cleardpc-api.discord-link— link validated at command timeThe
/mf dpccommand is discoverable via/mf help.HTTP Sync (
MfDpcApiService)java.net.http.HttpClient.sendAsync— no blocking the server threadHttpClientis an injectable constructor parameter (default: standard client) for testabilityrunTaskTimerAsynchronouslywith configurable interval fromdpc-api.sync-interval-minutes(default: 10 min)dpc-api.urlvalidated on each sync: must be non-blank, parseable, absolute, and usehttp/https; clear warnings logged on failureX-API-Keyheader on all requests;serverIp/discordLinkconditionally includedserverIpresolved once per sync cycle fromdpc-api.server-addressconfig when set, otherwise buildshost:portfrom server binding; omitted when empty; empty-IP warning logged once per plugin enableserverIdsourced fromdpc-api.server-id(required; sync skips with warning if blank)DpcFactionPayloaddata class with Gsonname: 64,serverId: 64,description: 512,serverIp: 253,discordLink: 512)memberCount: Non-negative integer from faction member list sizediscordLinkvalidation: Only included if the configured value starts withhttps://discord.gg/orhttps://discord.com/; invalid values omitted with a once-per-cycle warning. Also validated at command time — invalid links are rejected with a user-facing error.Operator Login Reminder
PlayerJoinEventwhenisOp && !enabled && loginReminderdpc-api.keyanddpc-api.server-idbefore opting inoptin,shareip,discord, andreminder offcommandsAPI Key Acquisition
Operators visit https://dansplugins.com, create an account or sign in, then generate a key from their account page. Documented in the config comments,
CONFIG.md, and the login reminder message.bStats
Four
SimplePiecharts:dpc_api_opt_in,dpc_api_login_reminder,dpc_api_share_server_ip,dpc_api_discord_link_setDocs & i18n
CONFIG.mdandCOMMANDS.mdupdatedCannotInteractBlockInWildernessandCmdFactionAdminwere concatenated on one line in all 5 locale filesdocs/dpc-api-schema.asn1: Formal ASN.1 schema documenting the DPC API payload:memberCount INTEGER (0..100000),descriptionOPTIONAL,serverIp/discordLinkOPTIONAL.github/copilot-instructions.md: Stack, build, DPC API conventions, Pact contract test documentation, and OMCSI integration testing roadmapBuild & CI
build.gradle: AddedmavenLocal()as the first repository so locally-built Ponder artifacts resolve whenrepo.dansplugins.comis unavailablebuild.yml: Added "Build Ponder dependency" step anddocker-buildjobrelease.yml: Same Ponder build step added beforeshadowJarDockerfile: Now self-contained — builds Ponder from source, then builds the plugin (shadowJar) inside the container; installs both JDK 17 (Gradle toolchain) and JDK 21 (MC 1.21 server runtime)Tests
MfFactionDpcCommandTest— 14 cases: permission denial, all subcommands, edge cases, invalid discord link rejected at command time, discord.com link acceptedMfDpcApiServiceTest— 9 cases using mockHttpClient: correct fields for normal factions,serverIpomitted when disabled,discordLinkomitted when blank or invalid,memberCountnon-negative, field truncation, sync skipped whenserverIdblank, non-default port inserverIp,server-addressoverrideMfDpcApiPactConsumerTest(Pact 4.6.7) — 2 contract interactions against the livePOST /api/v1/factionswire format: required fields only, and all optional fields included. UsesDpcFactionPayload+ Gson directly (same serialization as production). Generatespacts/medieval-factions-dpc-api.json(committed so the provider can verify without re-running the consumer test).Review fixes (this iteration)
Fix: discord link not validated at command time
/mf dpc discord <link>previously accepted any string and saved it to config. During the sync, an invalid link would be silently omitted with a log-only warning — the operator had no immediate feedback. Fixed:handleDiscordnow validates the format before saving; invalid links are rejected with a red in-game message (CommandFactionDpcDiscordInvalidLink). New locale string added to all 5 locales.Fix: ASN.1 schema inaccuracies
memberCountwasINTEGER (0..MAX)(ASN.1 unbounded) — corrected toINTEGER (0..100000)to match the API's@Max(100000)constraintdescriptionwas required in the schema but the DPC API allows null (FactionRequesthas no@NotNull) — markedOPTIONALDocs: COMMANDS.md discord usage
Changed
discord [link]todiscord <link>(angle = required; square = optional). Added note that the link must start withhttps://discord.gg/orhttps://discord.com/.Integration testing roadmap
Added OMCSI-based integration testing steps to
.github/copilot-instructions.md, mirroring theDans-Plugin-Managerpattern. Steps: build MF JAR → spin up OMCSI server → start local DPC API (TestContainers or WireMock) → configure opt-in → assert POST received with correct payload shape.Pact contract tests
Added
MfDpcApiPactConsumerTestas the consumer side of a Pact contract. Committedpacts/medieval-factions-dpc-api.jsoncaptures the expected HTTP interactions. The provider verification test lives inDans-Plugins/dansplugins-dot-com#110.Contract reference
See
docs/dpc-api-schema.asn1for the formal wire format. Server-side enforcement is inDans-Plugins/dansplugins-dot-com→dpc-api/src/main/java/com/dansplugins/api/dto/FactionRequest.java(Bean Validation).