From 058a949e05d8a0642737d875c983b6e8fc62ede5 Mon Sep 17 00:00:00 2001 From: "minekube-ai-engineer[bot]" Date: Sun, 13 Sep 2026 23:36:43 +0000 Subject: [PATCH] fix(connect): report token mismatch, not org switching, for org-owned endpoint auth failures --- .../watch/WatchAuthFailureMessage.java | 28 +++++++--- .../watch/WatchAuthFailureMessageTest.java | 52 +++++++++++++++---- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/com/minekube/connect/watch/WatchAuthFailureMessage.java b/core/src/main/java/com/minekube/connect/watch/WatchAuthFailureMessage.java index b3f648e7c..8254d3a27 100644 --- a/core/src/main/java/com/minekube/connect/watch/WatchAuthFailureMessage.java +++ b/core/src/main/java/com/minekube/connect/watch/WatchAuthFailureMessage.java @@ -4,21 +4,35 @@ final class WatchAuthFailureMessage { private static final String TOKEN_ENDPOINT_MISMATCH = "CONNECT_AUTH_TOKEN_ENDPOINT_MISMATCH:"; private static final String ENDPOINT_ORG_OWNED = "CONNECT_AUTH_ENDPOINT_ORG_OWNED:"; + /** + * Both auth codes mean the same user-facing condition: the presented token does not match the token + * stored for this endpoint name. The Watch service reports {@code CONNECT_AUTH_ENDPOINT_ORG_OWNED} for + * ANY non-matching token when the endpoint name has an organization parent, so the wording must never + * send the user to fix their organization selection instead of their token. + */ + private static final String TOKEN_MISMATCH_GUIDANCE = + "WatchService rejected the endpoint token for this endpoint name: " + + "it does not match the token currently stored for this endpoint. " + + "If you reset the token in the Minekube dashboard, the previous token is invalidated; " + + "put the new token byte-for-byte into the connector token file " + + "(token.json in the connector data directory, or the CONNECT_TOKEN environment variable) " + + "rather than into config.yml, which only holds the endpoint name, then restart the connector. " + + "If you do not own this endpoint name, choose a different endpoint name."; + + private static final String ORG_OWNED_CLAUSE = + " This endpoint name belongs to an organization, so only a token created for it inside that " + + "organization is accepted; a different token cannot take the name over."; + private WatchAuthFailureMessage() { } static String format(String responseBody) { String message = responseBody == null ? "" : responseBody.trim(); if (message.startsWith(TOKEN_ENDPOINT_MISMATCH)) { - return "WatchService rejected the endpoint token for this endpoint name. " - + "Regenerate the token for this exact endpoint and organization in the Minekube dashboard, " - + "replace the token in token.json or CONNECT_TOKEN, then restart the server. " - + "If you do not own this endpoint name, choose a different endpoint name."; + return TOKEN_MISMATCH_GUIDANCE; } if (message.startsWith(ENDPOINT_ORG_OWNED)) { - return "WatchService rejected this endpoint because the endpoint name belongs to an organization. " - + "Switch to the owning Minekube organization/team, regenerate or import the endpoint token there, " - + "then restart the server."; + return TOKEN_MISMATCH_GUIDANCE + ORG_OWNED_CLAUSE; } return message; } diff --git a/core/src/test/java/com/minekube/connect/watch/WatchAuthFailureMessageTest.java b/core/src/test/java/com/minekube/connect/watch/WatchAuthFailureMessageTest.java index 22baa38d1..d355aa79f 100644 --- a/core/src/test/java/com/minekube/connect/watch/WatchAuthFailureMessageTest.java +++ b/core/src/test/java/com/minekube/connect/watch/WatchAuthFailureMessageTest.java @@ -1,35 +1,67 @@ package com.minekube.connect.watch; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; class WatchAuthFailureMessageTest { + private static final String TOKEN_MISMATCH_GUIDANCE = + "WatchService rejected the endpoint token for this endpoint name: " + + "it does not match the token currently stored for this endpoint. " + + "If you reset the token in the Minekube dashboard, the previous token is invalidated; " + + "put the new token byte-for-byte into the connector token file " + + "(token.json in the connector data directory, or the CONNECT_TOKEN environment variable) " + + "rather than into config.yml, which only holds the endpoint name, then restart the connector. " + + "If you do not own this endpoint name, choose a different endpoint name."; + @Test void explainsEndpointTokenMismatch() { String message = WatchAuthFailureMessage.format( "CONNECT_AUTH_TOKEN_ENDPOINT_MISMATCH: endpoint token does not match the existing endpoint name"); - assertEquals( - "WatchService rejected the endpoint token for this endpoint name. " - + "Regenerate the token for this exact endpoint and organization in the Minekube dashboard, " - + "replace the token in token.json or CONNECT_TOKEN, then restart the server. " - + "If you do not own this endpoint name, choose a different endpoint name.", - message); + assertEquals(TOKEN_MISMATCH_GUIDANCE, message); } @Test - void explainsOrganizationOwnedEndpoint() { + void explainsOrganizationOwnedEndpointAsTokenMismatch() { String message = WatchAuthFailureMessage.format( "CONNECT_AUTH_ENDPOINT_ORG_OWNED: endpoint name belongs to an organization"); + // The org-owned code is returned for ANY non-matching token on an org-owned name, so the primary + // condition reported to the user must be the token mismatch, with the ownership as the secondary clause. assertEquals( - "WatchService rejected this endpoint because the endpoint name belongs to an organization. " - + "Switch to the owning Minekube organization/team, regenerate or import the endpoint token there, " - + "then restart the server.", + TOKEN_MISMATCH_GUIDANCE + + " This endpoint name belongs to an organization, so only a token created for it " + + "inside that organization is accepted; a different token cannot take the name over.", message); } + @Test + void organizationOwnedMessageDoesNotSendUsersToOrgSwitching() { + String message = WatchAuthFailureMessage.format( + "CONNECT_AUTH_ENDPOINT_ORG_OWNED: endpoint name belongs to an organization"); + + assertTrue(message.startsWith("WatchService rejected the endpoint token for this endpoint name:")); + assertFalse(message.contains("Switch to the owning")); + assertFalse(message.contains("organization/team")); + } + + @Test + void bothAuthMessagesDescribeTheTokenFileAndTheConfigSplit() { + for (String responseBody : new String[] { + "CONNECT_AUTH_TOKEN_ENDPOINT_MISMATCH: endpoint token does not match the existing endpoint name", + "CONNECT_AUTH_ENDPOINT_ORG_OWNED: endpoint name belongs to an organization"}) { + String message = WatchAuthFailureMessage.format(responseBody); + + assertTrue(message.contains("token.json"), responseBody); + assertTrue(message.contains("CONNECT_TOKEN"), responseBody); + assertTrue(message.contains("rather than into config.yml"), responseBody); + assertTrue(message.contains("restart the connector"), responseBody); + } + } + @Test void preservesUnknownServerMessages() { assertEquals("Internal Server Error", WatchAuthFailureMessage.format("Internal Server Error"));