diff --git a/octopus-common/src/main/java/octopus/teamcity/common/connection/ConnectionPropertyNames.java b/octopus-common/src/main/java/octopus/teamcity/common/connection/ConnectionPropertyNames.java index d38f10ca..afae6062 100644 --- a/octopus-common/src/main/java/octopus/teamcity/common/connection/ConnectionPropertyNames.java +++ b/octopus-common/src/main/java/octopus/teamcity/common/connection/ConnectionPropertyNames.java @@ -42,6 +42,12 @@ public class ConnectionPropertyNames { public static final String OIDC_CONNECTOR_TOKEN_VARIABLE_NAME = "token_variable_name"; public static final String OIDC_DEFAULT_TOKEN_VARIABLE = "jwt.token"; + // The teamcity-oidc-plugin's build feature (type "oidc-plugin", display name "OIDC Identity + // Token") that publishes the JWT. + // Its optional "connection_id" param references an oidc-identity-token OAuth connection. + public static final String OIDC_BUILD_FEATURE_TYPE = "oidc-plugin"; + public static final String OIDC_BUILD_FEATURE_CONNECTION_ID_PARAM = "connection_id"; + public ConnectionPropertyNames() {} public String getDisplayName() { diff --git a/octopus-server/src/main/java/octopus/teamcity/server/connection/OctopusConnectionUiData.java b/octopus-server/src/main/java/octopus/teamcity/server/connection/OctopusConnectionUiData.java index 2707d518..2db70714 100644 --- a/octopus-server/src/main/java/octopus/teamcity/server/connection/OctopusConnectionUiData.java +++ b/octopus-server/src/main/java/octopus/teamcity/server/connection/OctopusConnectionUiData.java @@ -16,13 +16,16 @@ package octopus.teamcity.server.connection; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import jetbrains.buildServer.serverSide.ProjectManager; +import jetbrains.buildServer.serverSide.SBuildFeatureDescriptor; import jetbrains.buildServer.serverSide.SBuildType; +import jetbrains.buildServer.serverSide.SProject; import jetbrains.buildServer.serverSide.oauth.OAuthConnectionDescriptor; import jetbrains.buildServer.users.SUser; import jetbrains.buildServer.web.util.SessionUser; @@ -58,15 +61,25 @@ public OctopusConnectionUiData( */ @NotNull public static List> availableConnections(final HttpServletRequest request) { - return availableConnections(SessionUser.getUser(request)); + return availableConnections(SessionUser.getUser(request), currentBuildType(request)); } /** As {@link #availableConnections(HttpServletRequest)} but for an already-resolved user. */ static List> availableConnections(final SUser user) { + return availableConnections(user, null); + } + + static List> availableConnections( + final SUser user, final SBuildType buildType) { final List> result = new ArrayList<>(); if (user == null || connectionsManager == null) { return result; } + + final List oidcFeatures = + buildType == null ? Collections.emptyList() : collectOidcFeatures(buildType); + final SProject project = buildType == null ? null : buildType.getProject(); + for (final OAuthConnectionDescriptor descriptor : connectionsManager.listAvailableConnections(user)) { final Map params = descriptor.getParameters(); @@ -76,11 +89,59 @@ static List> availableConnections(final SUser user) { view.put("url", params.getOrDefault(CONNECTION_KEYS.getServerUrlPropertyName(), "")); view.put("version", params.getOrDefault(CONNECTION_KEYS.getVersionPropertyName(), "")); view.put("space", params.getOrDefault(CONNECTION_KEYS.getSpaceNamePropertyName(), "")); + + final String apiKeySource = + params.getOrDefault(CONNECTION_KEYS.getApiKeySourcePropertyName(), ""); + final boolean isOidc = ConnectionPropertyNames.API_KEY_SOURCE_OIDC.equals(apiKeySource); + String connectorTokenVariable = ""; + OidcFeatureWarningEvaluator.Warning warning = OidcFeatureWarningEvaluator.Warning.NONE; + if (isOidc && buildType != null) { + final String oidcConnectionId = + params.getOrDefault(CONNECTION_KEYS.getOidcConnectionIdPropertyName(), ""); + connectorTokenVariable = connectorTokenVariable(project, oidcConnectionId); + warning = + OidcFeatureWarningEvaluator.evaluate( + apiKeySource, oidcConnectionId, connectorTokenVariable, oidcFeatures); + } + view.put("oidcWarning", warning.attributeValue()); + view.put( + "oidcExpectedTokenVariable", + isOidc ? OidcFeatureWarningEvaluator.expectedTokenVariable(connectorTokenVariable) : ""); result.add(view); } return result; } + private static List collectOidcFeatures( + final SBuildType buildType) { + final List features = new ArrayList<>(); + for (final SBuildFeatureDescriptor descriptor : + buildType.getBuildFeaturesOfType(ConnectionPropertyNames.OIDC_BUILD_FEATURE_TYPE)) { + final Map params = descriptor.getParameters(); + features.add( + new OidcFeatureWarningEvaluator.BuildFeature( + params.getOrDefault( + ConnectionPropertyNames.OIDC_BUILD_FEATURE_CONNECTION_ID_PARAM, ""), + params.getOrDefault(ConnectionPropertyNames.OIDC_CONNECTOR_TOKEN_VARIABLE_NAME, ""))); + } + return features; + } + + private static String connectorTokenVariable( + final SProject project, final String oidcConnectionId) { + if (project == null) { + return ""; + } + return connectionsManager + .resolve(project, oidcConnectionId) + .map( + connector -> + connector + .getParameters() + .getOrDefault(ConnectionPropertyNames.OIDC_CONNECTOR_TOKEN_VARIABLE_NAME, "")) + .orElse(""); + } + /** * URL of the current project's Connections tab (where connections are added). Built relative to * the request's context path so the browser resolves it against the host it is actually viewing — @@ -95,7 +156,26 @@ public static String editConnectionUrl(final HttpServletRequest request) { : base + "?projectId=" + projectExternalId + "&tab=oauthConnections"; } + /** + * Context-relative URL of the current build configuration's Build Features tab, where the OIDC + * Identity Token build feature is added. Empty when the build configuration cannot be resolved. + */ + public static String buildFeaturesUrl(final HttpServletRequest request) { + final SBuildType buildType = currentBuildType(request); + if (buildType == null) { + return ""; + } + return request.getContextPath() + + "/admin/editBuildFeatures.html?id=buildType:" + + buildType.getExternalId(); + } + private static String currentProjectExternalId(final HttpServletRequest request) { + final SBuildType buildType = currentBuildType(request); + return buildType == null ? null : buildType.getProject().getExternalId(); + } + + private static SBuildType currentBuildType(final HttpServletRequest request) { if (projectManager == null) { return null; } @@ -103,8 +183,6 @@ private static String currentProjectExternalId(final HttpServletRequest request) if (idParam == null || !idParam.startsWith("buildType:")) { return null; } - final SBuildType buildType = - projectManager.findBuildTypeByExternalId(idParam.substring("buildType:".length())); - return buildType == null ? null : buildType.getProject().getExternalId(); + return projectManager.findBuildTypeByExternalId(idParam.substring("buildType:".length())); } } diff --git a/octopus-server/src/main/java/octopus/teamcity/server/connection/OidcFeatureWarningEvaluator.java b/octopus-server/src/main/java/octopus/teamcity/server/connection/OidcFeatureWarningEvaluator.java new file mode 100644 index 00000000..62f3c52a --- /dev/null +++ b/octopus-server/src/main/java/octopus/teamcity/server/connection/OidcFeatureWarningEvaluator.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) Octopus Deploy and contributors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * these files except in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package octopus.teamcity.server.connection; + +import java.util.List; + +import octopus.teamcity.common.connection.ConnectionPropertyNames; + +/** + * Decides which advisory OIDC warning (if any) a step edit form should show for a selected + * connection, from plain data. Kept free of TeamCity types so the branching logic is unit-tested in + * isolation; {@link OctopusConnectionUiData} supplies the inputs. + * + *

Supported OIDC setup: the Octopus connection references a connector, and an {@code + * oidc-plugin} build feature references that same connector via {@code connection_id}. An inline + * feature (no {@code connection_id}) is not a supported Octopus OIDC path, so it never satisfies + * the check. + */ +public final class OidcFeatureWarningEvaluator { + + /** The warning to display; {@link #attributeValue()} is the DOM data-attribute form. */ + public enum Warning { + NONE("none"), + FEATURE_MISSING("feature-missing"), + TOKEN_MISMATCH("token-mismatch"); + + private final String attributeValue; + + Warning(final String attributeValue) { + this.attributeValue = attributeValue; + } + + public String attributeValue() { + return attributeValue; + } + } + + /** Minimal view of one {@code oidc-plugin} build feature on the build configuration. */ + public static final class BuildFeature { + private final String connectionId; + private final String tokenVariableName; + + public BuildFeature(final String connectionId, final String tokenVariableName) { + this.connectionId = connectionId == null ? "" : connectionId; + this.tokenVariableName = tokenVariableName == null ? "" : tokenVariableName; + } + + public String getConnectionId() { + return connectionId; + } + + public String getTokenVariableName() { + return tokenVariableName; + } + } + + private OidcFeatureWarningEvaluator() {} + + public static Warning evaluate( + final String apiKeySource, + final String oidcConnectionId, + final String connectorTokenVariableName, + final List oidcFeatures) { + if (!ConnectionPropertyNames.API_KEY_SOURCE_OIDC.equals(apiKeySource)) { + return Warning.NONE; + } + + final String referenced = expectedTokenVariable(connectorTokenVariableName); + boolean anyMatchingFeature = false; + for (final BuildFeature feature : oidcFeatures) { + if (isBlank(feature.getConnectionId()) + || !feature.getConnectionId().trim().equals(oidcConnectionId)) { + continue; + } + anyMatchingFeature = true; + final String published = + isBlank(feature.getTokenVariableName()) ? referenced : feature.getTokenVariableName(); + if (published.equals(referenced)) { + return Warning.NONE; + } + } + + return anyMatchingFeature ? Warning.TOKEN_MISMATCH : Warning.FEATURE_MISSING; + } + + public static String expectedTokenVariable(final String connectorTokenVariableName) { + return isBlank(connectorTokenVariableName) + ? ConnectionPropertyNames.OIDC_DEFAULT_TOKEN_VARIABLE + : connectorTokenVariableName; + } + + private static boolean isBlank(final String value) { + return value == null || value.trim().isEmpty(); + } +} diff --git a/octopus-server/src/main/resources/buildServerResources/connectionSelector.jsp b/octopus-server/src/main/resources/buildServerResources/connectionSelector.jsp index f578a5a4..114777c2 100644 --- a/octopus-server/src/main/resources/buildServerResources/connectionSelector.jsp +++ b/octopus-server/src/main/resources/buildServerResources/connectionSelector.jsp @@ -8,6 +8,7 @@ <% pageContext.setAttribute("octopusConnections", OctopusConnectionUiData.availableConnections(request)); pageContext.setAttribute("editConnectionUrl", OctopusConnectionUiData.editConnectionUrl(request)); + pageContext.setAttribute("buildFeaturesUrl", OctopusConnectionUiData.buildFeaturesUrl(request)); %> @@ -27,13 +28,34 @@ + data-conn-version="" + data-conn-oidc-warning="" + data-conn-oidc-expected-var=""> Reuse a connection defined under Project Settings » Connections. + + @@ -72,11 +94,39 @@ } } + function updateOctopusOidcWarning() { + const select = document.getElementById("octopusConnectionId"); + if (!select) return; + const missingEl = document.querySelector(".octopusOidcFeatureMissing"); + const mismatchEl = document.querySelector(".octopusOidcTokenMismatch"); + let warning = "none"; + let expectedVar = ""; + if (select.value !== "") { + const meta = getOctopusConnectionMetadataFor(select.value); + if (meta) { + warning = meta.getAttribute("data-conn-oidc-warning") || "none"; + expectedVar = meta.getAttribute("data-conn-oidc-expected-var") || ""; + } + } + if (missingEl) { + missingEl.style.display = warning === "feature-missing" ? "" : "none"; + } + if (mismatchEl) { + mismatchEl.style.display = warning === "token-mismatch" ? "" : "none"; + const varSpans = mismatchEl.querySelectorAll(".octopusExpectedVar"); + for (let i = 0; i < varSpans.length; i++) { + varSpans[i].textContent = expectedVar; + } + } + } + $j(document).ready(function () { const select = document.getElementById("octopusConnectionId"); if (select) { select.addEventListener("change", toggleOctopusInlineConnectionFields); + select.addEventListener("change", updateOctopusOidcWarning); toggleOctopusInlineConnectionFields(); + updateOctopusOidcWarning(); } }); diff --git a/octopus-server/src/test/java/octopus/teamcity/server/connection/OctopusConnectionUiDataTest.java b/octopus-server/src/test/java/octopus/teamcity/server/connection/OctopusConnectionUiDataTest.java index d7d6951d..7ca6e8b3 100644 --- a/octopus-server/src/test/java/octopus/teamcity/server/connection/OctopusConnectionUiDataTest.java +++ b/octopus-server/src/test/java/octopus/teamcity/server/connection/OctopusConnectionUiDataTest.java @@ -5,12 +5,14 @@ import static org.mockito.Mockito.when; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import jetbrains.buildServer.serverSide.ProjectManager; +import jetbrains.buildServer.serverSide.SBuildFeatureDescriptor; import jetbrains.buildServer.serverSide.SBuildType; import jetbrains.buildServer.serverSide.SProject; import jetbrains.buildServer.serverSide.oauth.OAuthConnectionDescriptor; @@ -113,4 +115,142 @@ void editConnectionUrlFallsBackWhenProjectCannotBeResolved() { assertThat(OctopusConnectionUiData.editConnectionUrl(request)) .isEqualTo("/admin/editProject.html?tab=oauthConnections"); } + + private OAuthConnectionDescriptor oidcConnection(final String id, final String oidcConnectionId) { + final OAuthConnectionDescriptor descriptor = mock(OAuthConnectionDescriptor.class); + when(descriptor.getId()).thenReturn(id); + when(descriptor.getConnectionDisplayName()).thenReturn("OIDC conn"); + final Map params = new HashMap<>(); + params.put(ConnectionPropertyNames.API_KEY_SOURCE, ConnectionPropertyNames.API_KEY_SOURCE_OIDC); + params.put(ConnectionPropertyNames.OIDC_CONNECTION_ID, oidcConnectionId); + when(descriptor.getParameters()).thenReturn(params); + return descriptor; + } + + private SBuildFeatureDescriptor oidcFeature( + final String connectionId, final String tokenVariableName) { + final SBuildFeatureDescriptor feature = mock(SBuildFeatureDescriptor.class); + final Map params = new HashMap<>(); + params.put(ConnectionPropertyNames.OIDC_BUILD_FEATURE_CONNECTION_ID_PARAM, connectionId); + params.put(ConnectionPropertyNames.OIDC_CONNECTOR_TOKEN_VARIABLE_NAME, tokenVariableName); + when(feature.getParameters()).thenReturn(params); + return feature; + } + + private OAuthConnectionDescriptor connector(final String tokenVariableName) { + final OAuthConnectionDescriptor descriptor = mock(OAuthConnectionDescriptor.class); + final Map params = new HashMap<>(); + params.put(ConnectionPropertyNames.OIDC_CONNECTOR_TOKEN_VARIABLE_NAME, tokenVariableName); + when(descriptor.getParameters()).thenReturn(params); + return descriptor; + } + + @Test + void oidcConnectionWithNoFeatureWarnsFeatureMissing() { + final SBuildType buildType = mock(SBuildType.class); + final SProject project = mock(SProject.class); + when(buildType.getProject()).thenReturn(project); + when(buildType.getBuildFeaturesOfType(ConnectionPropertyNames.OIDC_BUILD_FEATURE_TYPE)) + .thenReturn(Collections.emptyList()); + when(connectionsManager.resolve(project, "CONNECTOR_1")).thenReturn(java.util.Optional.empty()); + final OAuthConnectionDescriptor descriptor = oidcConnection("OCT_1", "CONNECTOR_1"); + when(connectionsManager.listAvailableConnections(user)).thenReturn(Arrays.asList(descriptor)); + + final Map view = + OctopusConnectionUiData.availableConnections(user, buildType).get(0); + + assertThat(view.get("oidcWarning")).isEqualTo("feature-missing"); + assertThat(view.get("oidcExpectedTokenVariable")).isEqualTo("jwt.token"); + } + + @Test + void oidcConnectionWithMatchingFeatureWarnsNone() { + final SBuildType buildType = mock(SBuildType.class); + final SProject project = mock(SProject.class); + when(buildType.getProject()).thenReturn(project); + final SBuildFeatureDescriptor feature = oidcFeature("CONNECTOR_1", ""); + when(buildType.getBuildFeaturesOfType(ConnectionPropertyNames.OIDC_BUILD_FEATURE_TYPE)) + .thenReturn(Arrays.asList(feature)); + final OAuthConnectionDescriptor connector = connector(""); + when(connectionsManager.resolve(project, "CONNECTOR_1")) + .thenReturn(java.util.Optional.of(connector)); + final OAuthConnectionDescriptor descriptor = oidcConnection("OCT_1", "CONNECTOR_1"); + when(connectionsManager.listAvailableConnections(user)).thenReturn(Arrays.asList(descriptor)); + + final Map view = + OctopusConnectionUiData.availableConnections(user, buildType).get(0); + + assertThat(view.get("oidcWarning")).isEqualTo("none"); + } + + @Test + void oidcConnectionWithMismatchedTokenVariableWarnsTokenMismatch() { + final SBuildType buildType = mock(SBuildType.class); + final SProject project = mock(SProject.class); + when(buildType.getProject()).thenReturn(project); + final SBuildFeatureDescriptor feature = oidcFeature("CONNECTOR_1", "custom.var"); + when(buildType.getBuildFeaturesOfType(ConnectionPropertyNames.OIDC_BUILD_FEATURE_TYPE)) + .thenReturn(Arrays.asList(feature)); + final OAuthConnectionDescriptor connector = connector(""); + when(connectionsManager.resolve(project, "CONNECTOR_1")) + .thenReturn(java.util.Optional.of(connector)); + final OAuthConnectionDescriptor descriptor = oidcConnection("OCT_1", "CONNECTOR_1"); + when(connectionsManager.listAvailableConnections(user)).thenReturn(Arrays.asList(descriptor)); + + final Map view = + OctopusConnectionUiData.availableConnections(user, buildType).get(0); + + assertThat(view.get("oidcWarning")).isEqualTo("token-mismatch"); + assertThat(view.get("oidcExpectedTokenVariable")).isEqualTo("jwt.token"); + } + + @Test + void nonOidcConnectionHasNoWarning() { + final SBuildType buildType = mock(SBuildType.class); + when(buildType.getProject()).thenReturn(mock(SProject.class)); + when(buildType.getBuildFeaturesOfType(ConnectionPropertyNames.OIDC_BUILD_FEATURE_TYPE)) + .thenReturn(Collections.emptyList()); + final OAuthConnectionDescriptor descriptor = + connection("OCT_1", "Prod", "https://octo", "3.0+", "Spaces-1"); + when(connectionsManager.listAvailableConnections(user)).thenReturn(Arrays.asList(descriptor)); + + final Map view = + OctopusConnectionUiData.availableConnections(user, buildType).get(0); + + assertThat(view.get("oidcWarning")).isEqualTo("none"); + assertThat(view.get("oidcExpectedTokenVariable")).isEqualTo(""); + } + + @Test + void unresolvedBuildTypeSuppressesWarning() { + final OAuthConnectionDescriptor descriptor = oidcConnection("OCT_1", "CONNECTOR_1"); + when(connectionsManager.listAvailableConnections(user)).thenReturn(Arrays.asList(descriptor)); + + final Map view = + OctopusConnectionUiData.availableConnections(user, (SBuildType) null).get(0); + + assertThat(view.get("oidcWarning")).isEqualTo("none"); + } + + @Test + void buildFeaturesUrlPointsAtBuildFeaturesTab() { + final SBuildType buildType = mock(SBuildType.class); + when(projectManager.findBuildTypeByExternalId("Rtest_Build")).thenReturn(buildType); + when(buildType.getExternalId()).thenReturn("Rtest_Build"); + + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getContextPath()).thenReturn(""); + when(request.getParameter("id")).thenReturn("buildType:Rtest_Build"); + + assertThat(OctopusConnectionUiData.buildFeaturesUrl(request)) + .isEqualTo("/admin/editBuildFeatures.html?id=buildType:Rtest_Build"); + } + + @Test + void buildFeaturesUrlEmptyWhenUnresolved() { + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameter("id")).thenReturn(null); + + assertThat(OctopusConnectionUiData.buildFeaturesUrl(request)).isEqualTo(""); + } } diff --git a/octopus-server/src/test/java/octopus/teamcity/server/connection/OidcFeatureWarningEvaluatorTest.java b/octopus-server/src/test/java/octopus/teamcity/server/connection/OidcFeatureWarningEvaluatorTest.java new file mode 100644 index 00000000..16276f9d --- /dev/null +++ b/octopus-server/src/test/java/octopus/teamcity/server/connection/OidcFeatureWarningEvaluatorTest.java @@ -0,0 +1,107 @@ +package octopus.teamcity.server.connection; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import octopus.teamcity.common.connection.ConnectionPropertyNames; +import octopus.teamcity.server.connection.OidcFeatureWarningEvaluator.BuildFeature; +import octopus.teamcity.server.connection.OidcFeatureWarningEvaluator.Warning; +import org.junit.jupiter.api.Test; + +class OidcFeatureWarningEvaluatorTest { + private static final String OIDC = ConnectionPropertyNames.API_KEY_SOURCE_OIDC; + + private static BuildFeature feature(final String connectionId, final String tokenVariableName) { + return new BuildFeature(connectionId, tokenVariableName); + } + + @Test + void nonOidcSourceIsNone() { + assertThat(evaluateWith("key", "conn-1", "", Collections.emptyList())).isEqualTo(Warning.NONE); + } + + @Test + void oidcWithNoFeaturesIsFeatureMissing() { + assertThat(evaluateWith(OIDC, "conn-1", "", Collections.emptyList())) + .isEqualTo(Warning.FEATURE_MISSING); + } + + @Test + void oidcWithNoMatchingFeatureIsFeatureMissing() { + assertThat(evaluateWith(OIDC, "conn-1", "", Arrays.asList(feature("other", "")))) + .isEqualTo(Warning.FEATURE_MISSING); + } + + @Test + void inlineFeatureWithoutConnectionIdDoesNotMatch() { + assertThat(evaluateWith(OIDC, "conn-1", "", Arrays.asList(feature("", "jwt.token")))) + .isEqualTo(Warning.FEATURE_MISSING); + } + + @Test + void matchingFeatureBlankOverrideConnectorBlankIsNone() { + assertThat(evaluateWith(OIDC, "conn-1", "", Arrays.asList(feature("conn-1", "")))) + .isEqualTo(Warning.NONE); + } + + @Test + void matchingFeatureOverrideEqualsReferencedIsNone() { + assertThat(evaluateWith(OIDC, "conn-1", "", Arrays.asList(feature("conn-1", "jwt.token")))) + .isEqualTo(Warning.NONE); + } + + @Test + void matchingFeatureOverrideDiffersConnectorBlankIsMismatch() { + assertThat(evaluateWith(OIDC, "conn-1", "", Arrays.asList(feature("conn-1", "custom")))) + .isEqualTo(Warning.TOKEN_MISMATCH); + } + + @Test + void connectorVariableSetFeatureNoOverrideIsNone() { + assertThat(evaluateWith(OIDC, "conn-1", "foo", Arrays.asList(feature("conn-1", "")))) + .isEqualTo(Warning.NONE); + } + + @Test + void connectorVariableSetFeatureOverrideDiffersIsMismatch() { + assertThat(evaluateWith(OIDC, "conn-1", "foo", Arrays.asList(feature("conn-1", "bar")))) + .isEqualTo(Warning.TOKEN_MISMATCH); + } + + @Test + void multipleMatchingFeaturesOnePublishesReferencedIsNone() { + assertThat( + evaluateWith( + OIDC, + "conn-1", + "foo", + Arrays.asList(feature("conn-1", "bar"), feature("conn-1", "")))) + .isEqualTo(Warning.NONE); + } + + @Test + void expectedTokenVariableFallsBackToDefault() { + assertThat(OidcFeatureWarningEvaluator.expectedTokenVariable("")).isEqualTo("jwt.token"); + assertThat(OidcFeatureWarningEvaluator.expectedTokenVariable(" ")).isEqualTo("jwt.token"); + assertThat(OidcFeatureWarningEvaluator.expectedTokenVariable("foo")).isEqualTo("foo"); + } + + @Test + void warningAttributeValues() { + assertThat(Warning.NONE.attributeValue()).isEqualTo("none"); + assertThat(Warning.FEATURE_MISSING.attributeValue()).isEqualTo("feature-missing"); + assertThat(Warning.TOKEN_MISMATCH.attributeValue()).isEqualTo("token-mismatch"); + } + + private static Warning evaluateWith( + final String apiKeySource, + final String oidcConnectionId, + final String connectorTokenVariableName, + final List features) { + return OidcFeatureWarningEvaluator.evaluate( + apiKeySource, oidcConnectionId, connectorTokenVariableName, features); + } +}