This is an automated email from the ASF dual-hosted git repository.

dspavlov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite-teamcity-bot.git


The following commit(s) were added to refs/heads/master by this push:
     new d80ba656 IGNITE-28679: [TCBot] Support JIRA tokens instead of basic 
auth (#230)
d80ba656 is described below

commit d80ba65619b2c45b6f708965b644bc4c49874671
Author: ignitetcbot <[email protected]>
AuthorDate: Fri May 15 10:21:22 2026 +0300

    IGNITE-28679: [TCBot] Support JIRA tokens instead of basic auth (#230)
    
    Codex co-authored-by: Dmitriy Pavlov <[email protected]>
---
 README.md                                          |   7 +-
 conf/branches.json                                 |  22 ++-
 .../tcbot/common/conf/IJiraServerConfig.java       |  12 +-
 .../ignite/tcbot/common/conf/PasswordEncoder.java  |  45 +++++
 .../apache/ignite/tcbot/common/util/HttpUtil.java  |  15 +-
 .../ignite/tcbot/engine/conf/GitHubConfig.java     |  54 +++++-
 .../ignite/tcbot/engine/conf/JiraServerConfig.java | 127 ++++++++++++-
 .../tcbot/engine/conf/AuthTokenConfigTest.java     | 201 +++++++++++++++++++++
 tcbot-github/README.md                             |   6 +-
 .../src/integrationTest/resources/branches.json    |   2 +-
 tcbot-jira/README.md                               |   8 +-
 .../java/org/apache/ignite/jiraservice/Jira.java   |   4 +-
 12 files changed, 468 insertions(+), 35 deletions(-)

diff --git a/README.md b/README.md
index a9890fd4..b5856bf9 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,12 @@ Main config file is 
[conf/branches.json](conf/branches.json). This file needs to
 The running bot reloads `branches.json` lazily: configuration reads are cached 
for up to 3 minutes, so most changes
 become visible without a restart after the cache expires. Restart the bot only 
when you need the change to take effect
 immediately.
-Extra setup is required using security-sensitive information using 
PasswordEncoder. No TeamCity credentials are required because TC bot asks users 
to enter creds.
+JIRA and GitHub tokens can be specified as plain text in `branches.json` or 
protected with `PasswordEncoder`.
+When `authTokEncoded` is not set, the bot auto-detects encoded hex values and 
otherwise treats tokens as plain.
+Set `authTokEncoded` only when you need to force a mode. For JIRA Personal 
Access Tokens, use
+`authScheme: "Bearer"`; legacy base64 username/password tokens can still use 
`authScheme: "Basic"`. If
+JIRA `authScheme` is omitted, encoded tokens default to Basic for 
compatibility and plain tokens default to Bearer.
+No TeamCity credentials are required because TC bot asks users to enter creds.
 
 Minimal local run checklist:
 * Import the Gradle project into IntelliJ IDEA.
diff --git a/conf/branches.json b/conf/branches.json
index efbc5da4..f9f8d1cf 100644
--- a/conf/branches.json
+++ b/conf/branches.json
@@ -66,12 +66,17 @@
       "projectCode": "IGNITE",
       /* Following prefix is to be specified only if it is necessary to 
separate project code and branches markup in tickets. */
       // "branchNumPrefix": "IGNITE-",
-      /* JIRA Url, HTTPs is highly recommended because of Basic Auth used. */
+      /* JIRA Url, HTTPs is highly recommended. */
       "url": "https://issues.apache.org/jira/";,
-      /** JIRA Auth token to access,
-        Base-64 pre-encoded username and password, which will be included into 
Basic Auth requests.
-        Encoded token needs to be encrypted using 
org.apache.ignite.ci.tcbot.conf.PasswordEncoder,
-        use PasswordEncoder#encodeJiraTok to get from clear username/password 
*/
+      /** JIRA authorization scheme. Use Bearer for personal access tokens, or 
Basic for
+        base64 pre-encoded username/password tokens. If omitted, plain tokens 
default to Bearer
+        and PasswordEncoder-protected tokens default to Basic for backward 
compatibility. */
+      "authScheme": "Bearer",
+      /** JIRA Auth token to access. Personal Access Token can be specified as 
plain text by default.
+        PasswordEncoder-protected hex values are auto-detected. Set 
authTokEncoded to true or false
+        only to force a mode. For Basic auth, use 
PasswordEncoder#userPwdToToken to get token value
+        from clear username/password. */
+      // "authTokEncoded": false,
       "authTok": ""
       //todo     ^ specify token
     }
@@ -88,10 +93,11 @@
 
       /*
         Specify GitHub Auth token (if needed), go to 
User->Settings->Developers options->create new.
-        Created token needs to be encrypted using 
org.apache.ignite.ci.tcbot.conf.PasswordEncoder,
-        use {@link org.apache.ignite.ci.conf.PasswordEncoder#encode} to set up 
value in a config.
-        Git Auth token encoded to access non-public GitHub repos. For public 
GitHub repos token gives more velocity.
+        Created token can be specified as plain text. 
PasswordEncoder-protected hex values are
+        auto-detected. Set authTokEncoded to true or false only to force a 
mode.
+        Git Auth token is used to access non-public GitHub repos. For public 
GitHub repos token gives more velocity.
        */
+      // "authTokEncoded": false,
       "authTok": "",
       //todo     ^ specify token
 
diff --git 
a/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/conf/IJiraServerConfig.java
 
b/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/conf/IJiraServerConfig.java
index bf1fd298..4c3a9177 100644
--- 
a/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/conf/IJiraServerConfig.java
+++ 
b/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/conf/IJiraServerConfig.java
@@ -60,11 +60,21 @@ public interface IJiraServerConfig {
      */
     @Nullable String decodedHttpAuthToken();
 
+    /**
+     * @return Null or full HTTP Authorization header value for JIRA.
+     */
+    @Nullable
+    default String httpAuthorizationHeader() {
+        String tok = decodedHttpAuthToken();
+
+        return Strings.isNullOrEmpty(tok) ? null : "Basic " + tok;
+    }
+
     /**
      * @return {@code True} if JIRA authorization token is available.
      */
     default boolean isJiraTokenAvailable() {
-        return !Strings.isNullOrEmpty(decodedHttpAuthToken());
+        return !Strings.isNullOrEmpty(httpAuthorizationHeader());
     }
 
     default String restApiUrl() {
diff --git 
a/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/conf/PasswordEncoder.java
 
b/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/conf/PasswordEncoder.java
index 092ccf5b..d03e53e3 100644
--- 
a/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/conf/PasswordEncoder.java
+++ 
b/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/conf/PasswordEncoder.java
@@ -47,6 +47,51 @@ public class PasswordEncoder {
         return new String(parseHexBinary(p), CryptUtil.CHARSET);
     }
 
+    public static String decodeIfEncoded(String tok) {
+        if (!mayBeEncoded(tok))
+            return tok;
+
+        try {
+            return decode(tok);
+        }
+        catch (RuntimeException ignored) {
+            return tok;
+        }
+    }
+
+    public static boolean isEncoded(String tok) {
+        if (!mayBeEncoded(tok))
+            return false;
+
+        try {
+            decode(tok);
+
+            return true;
+        }
+        catch (RuntimeException ignored) {
+            return false;
+        }
+    }
+
+    private static boolean mayBeEncoded(String tok) {
+        if (Strings.isNullOrEmpty(tok))
+            return false;
+
+        String trimmed = tok.trim();
+
+        if (trimmed.length() % 2 != 0)
+            return false;
+
+        for (int i = 0; i < trimmed.length(); i++) {
+            char ch = trimmed.charAt(i);
+
+            if (!((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch 
>= 'A' && ch <= 'F')))
+                return false;
+        }
+
+        return true;
+    }
+
     public static String encode(String pass) {
         byte[] bytes = pass.getBytes(CryptUtil.CHARSET);
         SecureRandom random = new SecureRandom();
diff --git 
a/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/util/HttpUtil.java 
b/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/util/HttpUtil.java
index 7261c59c..b62b4c17 100644
--- 
a/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/util/HttpUtil.java
+++ 
b/tcbot-common/src/main/java/org/apache/ignite/tcbot/common/util/HttpUtil.java
@@ -396,13 +396,14 @@ public class HttpUtil {
     /**
      * Send POST request to the JIRA url.
      *
-     * @param jiraAuthTok Authorization Base64 token.
+     * @param jiraAuthHeader Authorization header value.
      * @param url URL.
      * @param body Request POST params.
      * @return Response body from given url.
      * @throws IOException If failed.
      */
-    public static String sendPostAsStringToJira(String jiraAuthTok, String 
url, String body) throws IOException {
+    public static String sendPostAsStringToJira(@Nullable String 
jiraAuthHeader, String url, String body)
+        throws IOException {
         URL obj = new URL(url);
         ensureIntegrationTestTarget(obj);
         HttpURLConnection con = (HttpURLConnection)obj.openConnection();
@@ -411,7 +412,8 @@ public class HttpUtil {
         Charset charset = StandardCharsets.UTF_8;
 
         con.setRequestProperty("accept-charset", charset.toString());
-        con.setRequestProperty("Authorization", "Basic " + jiraAuthTok);
+        if (jiraAuthHeader != null)
+            con.setRequestProperty("Authorization", jiraAuthHeader);
         con.setRequestProperty("content-type", "application/json");
         con.setInstanceFollowRedirects(false);
         useKeepAlive(con);
@@ -434,10 +436,10 @@ public class HttpUtil {
     /**
      * Send GET request to the JIRA url.
      *
-     * @param jiraAuthTok Jira auth token.
+     * @param jiraAuthHeader Jira authorization header value.
      * @param url Url.
      */
-    public static String sendGetToJira(String jiraAuthTok, String url) throws 
IOException {
+    public static String sendGetToJira(@Nullable String jiraAuthHeader, String 
url) throws IOException {
         Stopwatch started = Stopwatch.createStarted();
         URL obj = new URL(url);
         ensureIntegrationTestTarget(obj);
@@ -447,7 +449,8 @@ public class HttpUtil {
         Charset charset = StandardCharsets.UTF_8;
 
         con.setRequestProperty("accept-charset", charset.toString());
-        con.setRequestProperty("Authorization", "Basic " + jiraAuthTok);
+        if (jiraAuthHeader != null)
+            con.setRequestProperty("Authorization", jiraAuthHeader);
         con.setRequestProperty("content-type", "application/json");
         con.setInstanceFollowRedirects(false);
         useKeepAlive(con);
diff --git 
a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/GitHubConfig.java
 
b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/GitHubConfig.java
index dae7647a..016ccfcb 100644
--- 
a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/GitHubConfig.java
+++ 
b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/GitHubConfig.java
@@ -33,6 +33,9 @@ public class GitHubConfig implements IGitHubConfig {
     /** GitHub authorization token property name. */
     public static final String GITHUB_AUTH_TOKEN = "github.auth_token";
 
+    /** GitHub authorization token PasswordEncoder flag property name. */
+    public static final String GITHUB_AUTH_TOKEN_ENCODED = 
"github.auth_token.encoded";
+
     /** Git branch naming prefix for PRLess contributions. */
     public static final String GIT_BRANCH_PREFIX = "git.branch_prefix";
 
@@ -49,11 +52,17 @@ public class GitHubConfig implements IGitHubConfig {
     private String branchPrefix;
 
     /**
-     * Git Auth token encoded to access non-public git repos, use {@link 
PasswordEncoder#encodeJiraTok(String,
-     * String)} to set up value in a config.
+     * Git Auth token to access non-public git repos. Plain and {@link 
PasswordEncoder}-encoded tokens are
+     * auto-detected unless {@link #authTokEncoded} is set explicitly.
      */
     private String authTok;
 
+    /**
+     * {@code True} if {@link #authTok} is encoded with {@link 
PasswordEncoder}, {@code false} if it is plain, or
+     * {@code null} to auto-detect.
+     */
+    private Boolean authTokEncoded;
+
     private Properties props;
 
     /**
@@ -102,9 +111,17 @@ public class GitHubConfig implements IGitHubConfig {
     /** {@inheritDoc} */
     @Nullable
     @Override public String gitAuthTok() {
-        String encAuth = gitAuthTokenEncoded();
+        String authTok = gitAuthTokenConfigured();
+
+        if (isNullOrEmpty(authTok))
+            return null;
+
+        Boolean encoded = isAuthTokenEncoded();
 
-        return isNullOrEmpty(encAuth) ? null : PasswordEncoder.decode(encAuth);
+        if (encoded == null)
+            return PasswordEncoder.decodeIfEncoded(authTok);
+
+        return encoded ? PasswordEncoder.decode(authTok) : authTok;
     }
 
     /** {@inheritDoc} */
@@ -145,7 +162,7 @@ public class GitHubConfig implements IGitHubConfig {
      *
      */
     @Nullable
-    public String gitAuthTokenEncoded() {
+    public String gitAuthTokenConfigured() {
         if (!Strings.isNullOrEmpty(authTok))
             return authTok;
 
@@ -153,4 +170,31 @@ public class GitHubConfig implements IGitHubConfig {
             ? props.getProperty(GITHUB_AUTH_TOKEN)
             : null;
     }
+
+    /**
+     * @return Configured Github auth token.
+     */
+    @Deprecated
+    @Nullable
+    public String gitAuthTokenEncoded() {
+        return gitAuthTokenConfigured();
+    }
+
+    /**
+     * @return {@code True} if configured auth token is encoded with {@link 
PasswordEncoder}, {@code false} if it is
+     * plain, or {@code null} if it should be auto-detected.
+     */
+    @Nullable
+    private Boolean isAuthTokenEncoded() {
+        if (authTokEncoded != null)
+            return authTokEncoded;
+
+        if (props != null && Strings.isNullOrEmpty(authTok)) {
+            String encoded = props.getProperty(GITHUB_AUTH_TOKEN_ENCODED);
+
+            return Strings.isNullOrEmpty(encoded) ? null : 
Boolean.parseBoolean(encoded);
+        }
+
+        return null;
+    }
 }
diff --git 
a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/JiraServerConfig.java
 
b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/JiraServerConfig.java
index 72d94e32..c37affbc 100644
--- 
a/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/JiraServerConfig.java
+++ 
b/tcbot-engine/src/main/java/org/apache/ignite/tcbot/engine/conf/JiraServerConfig.java
@@ -33,6 +33,17 @@ public class JiraServerConfig implements IJiraServerConfig {
     /** JIRA authorization token property name. */
     public static final String JIRA_AUTH_TOKEN = "jira.auth_token";
 
+    /** JIRA authorization token PasswordEncoder flag property name. */
+    public static final String JIRA_AUTH_TOKEN_ENCODED = 
"jira.auth_token.encoded";
+
+    /** JIRA authorization scheme property name. */
+    public static final String JIRA_AUTH_SCHEME = "jira.auth_scheme";
+
+    /** JIRA Basic authorization scheme. */
+    public static final String JIRA_AUTH_SCHEME_BASIC = "Basic";
+
+    /** JIRA Bearer authorization scheme. */
+    public static final String JIRA_AUTH_SCHEME_BEARER = "Bearer";
 
     /** JIRA URL to build links to tickets. */
     public static final String JIRA_URL = "jira.url";
@@ -63,11 +74,23 @@ public class JiraServerConfig implements IJiraServerConfig {
     private Properties props;
 
     /**
-     * JIRA Auth token encoded to access JIRA, use {@link 
PasswordEncoder#encodeJiraTok(String,
-     * String)} to set up value in a config.
+     * JIRA Auth token to access JIRA. Plain and {@link 
PasswordEncoder}-encoded tokens are auto-detected unless
+     * {@link #authTokEncoded} is set explicitly.
      */
     private String authTok;
 
+    /**
+     * {@code True} if {@link #authTok} is encoded with {@link 
PasswordEncoder}, {@code false} if it is plain, or
+     * {@code null} to auto-detect.
+     */
+    private Boolean authTokEncoded;
+
+    /**
+     * HTTP Authorization scheme. Use {@code Bearer} for JIRA personal access 
tokens and {@code Basic} for legacy
+     * base64 username/password tokens. Encoded tokens default to Basic when 
the scheme is not set.
+     */
+    private String authScheme;
+
     /**
      * JIRA Server URL. HTTPs is highly recommended.
      */
@@ -143,16 +166,102 @@ public class JiraServerConfig implements 
IJiraServerConfig {
     @Nullable
     @Override
     public String decodedHttpAuthToken() {
-        String tok;
-
-        if (Strings.isNullOrEmpty(authTok) && props != null)
-            tok = props.getProperty(JIRA_AUTH_TOKEN);
-        else
-            tok = authTok;
+        String tok = authTokenConfigured();
 
         if (isNullOrEmpty(tok))
             return null;
 
-        return PasswordEncoder.decode(tok);
+        Boolean encoded = isAuthTokenEncoded();
+
+        if (encoded == null)
+            return PasswordEncoder.decodeIfEncoded(tok);
+
+        return encoded ? PasswordEncoder.decode(tok) : tok;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable
+    @Override
+    public String httpAuthorizationHeader() {
+        String tok = decodedHttpAuthToken();
+
+        return isNullOrEmpty(tok) ? null : authScheme() + " " + tok;
+    }
+
+    /**
+     * @return Configured auth token.
+     */
+    @Nullable
+    private String authTokenConfigured() {
+        if (!Strings.isNullOrEmpty(authTok))
+            return authTok;
+
+        return props != null ? props.getProperty(JIRA_AUTH_TOKEN) : null;
+    }
+
+    /**
+     * @return {@code True} if configured auth token is encoded with {@link 
PasswordEncoder}, {@code false} if it is
+     * plain, or {@code null} if it should be auto-detected.
+     */
+    @Nullable
+    private Boolean isAuthTokenEncoded() {
+        if (authTokEncoded != null)
+            return authTokEncoded;
+
+        if (props != null && Strings.isNullOrEmpty(authTok)) {
+            String encoded = props.getProperty(JIRA_AUTH_TOKEN_ENCODED);
+
+            return Strings.isNullOrEmpty(encoded) ? null : 
Boolean.parseBoolean(encoded);
+        }
+
+        return null;
+    }
+
+    /**
+     * @return HTTP Authorization scheme.
+     */
+    private String authScheme() {
+        String scheme = authSchemeConfigured();
+
+        if (Strings.isNullOrEmpty(scheme))
+            scheme = defaultAuthScheme();
+
+        if (JIRA_AUTH_SCHEME_BASIC.equalsIgnoreCase(scheme))
+            return JIRA_AUTH_SCHEME_BASIC;
+
+        if (JIRA_AUTH_SCHEME_BEARER.equalsIgnoreCase(scheme))
+            return JIRA_AUTH_SCHEME_BEARER;
+
+        throw new IllegalStateException("Unsupported JIRA auth scheme: " + 
scheme);
+    }
+
+    /**
+     * @return Configured HTTP Authorization scheme.
+     */
+    @Nullable
+    private String authSchemeConfigured() {
+        if (!Strings.isNullOrEmpty(authScheme))
+            return authScheme;
+
+        return props != null && Strings.isNullOrEmpty(authTok) ? 
props.getProperty(JIRA_AUTH_SCHEME) : null;
+    }
+
+    /**
+     * @return Default HTTP Authorization scheme.
+     */
+    private String defaultAuthScheme() {
+        if (props != null && Strings.isNullOrEmpty(authTok))
+            return JIRA_AUTH_SCHEME_BASIC;
+
+        return isAuthTokenEncodedOrAutoDetected() ? JIRA_AUTH_SCHEME_BASIC : 
JIRA_AUTH_SCHEME_BEARER;
+    }
+
+    /**
+     * @return {@code True} if configured token is known or detected as 
encoded.
+     */
+    private boolean isAuthTokenEncodedOrAutoDetected() {
+        Boolean encoded = isAuthTokenEncoded();
+
+        return encoded != null ? encoded : 
PasswordEncoder.isEncoded(authTokenConfigured());
     }
 }
diff --git 
a/tcbot-engine/src/test/java/org/apache/ignite/tcbot/engine/conf/AuthTokenConfigTest.java
 
b/tcbot-engine/src/test/java/org/apache/ignite/tcbot/engine/conf/AuthTokenConfigTest.java
new file mode 100644
index 00000000..51cc689a
--- /dev/null
+++ 
b/tcbot-engine/src/test/java/org/apache/ignite/tcbot/engine/conf/AuthTokenConfigTest.java
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file 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 org.apache.ignite.tcbot.engine.conf;
+
+import java.lang.reflect.Field;
+import java.util.Properties;
+import org.apache.ignite.tcbot.common.conf.PasswordEncoder;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Checks auth token configuration compatibility.
+ */
+public class AuthTokenConfigTest {
+    /** */
+    @Test
+    public void jiraJsonTokenIsPlainBearerByDefault() throws Exception {
+        JiraServerConfig cfg = withField(new JiraServerConfig(), "authTok", 
"jira-pat");
+
+        assertEquals("jira-pat", cfg.decodedHttpAuthToken());
+        assertEquals("Bearer jira-pat", cfg.httpAuthorizationHeader());
+    }
+
+    /** */
+    @Test
+    public void jiraJsonTokenCanBeEncodedBasic() throws Exception {
+        String basicTok = PasswordEncoder.userPwdToToken("user", "password");
+        JiraServerConfig cfg = withField(new JiraServerConfig(), "authTok", 
PasswordEncoder.encode(basicTok));
+
+        withField(cfg, "authTokEncoded", true);
+        withField(cfg, "authScheme", "Basic");
+
+        assertEquals(basicTok, cfg.decodedHttpAuthToken());
+        assertEquals("Basic " + basicTok, cfg.httpAuthorizationHeader());
+    }
+
+    /** */
+    @Test
+    public void jiraJsonTokenCanBeAutoDetectedAsEncoded() throws Exception {
+        String basicTok = PasswordEncoder.userPwdToToken("user", "password");
+        JiraServerConfig cfg = withField(new JiraServerConfig(), "authTok", 
PasswordEncoder.encode(basicTok));
+
+        assertEquals(basicTok, cfg.decodedHttpAuthToken());
+        assertEquals("Basic " + basicTok, cfg.httpAuthorizationHeader());
+    }
+
+    /** */
+    @Test
+    public void jiraJsonTokenExplicitlyEncodedDefaultsToBasic() throws 
Exception {
+        String basicTok = PasswordEncoder.userPwdToToken("user", "password");
+        JiraServerConfig cfg = withField(new JiraServerConfig(), "authTok", 
PasswordEncoder.encode(basicTok));
+
+        withField(cfg, "authTokEncoded", true);
+
+        assertEquals(basicTok, cfg.decodedHttpAuthToken());
+        assertEquals("Basic " + basicTok, cfg.httpAuthorizationHeader());
+    }
+
+    /** */
+    @Test
+    public void jiraLegacyPropertyTokenIsEncodedBasicByDefault() {
+        String basicTok = PasswordEncoder.userPwdToToken("user", "password");
+        Properties props = new Properties();
+
+        props.setProperty(JiraServerConfig.JIRA_AUTH_TOKEN, 
PasswordEncoder.encode(basicTok));
+
+        JiraServerConfig cfg = new JiraServerConfig("apache", props);
+
+        assertEquals(basicTok, cfg.decodedHttpAuthToken());
+        assertEquals("Basic " + basicTok, cfg.httpAuthorizationHeader());
+    }
+
+    /** */
+    @Test
+    public void jiraLegacyPropertyTokenCanBePlainBearer() {
+        Properties props = new Properties();
+
+        props.setProperty(JiraServerConfig.JIRA_AUTH_TOKEN, "jira-pat");
+        props.setProperty(JiraServerConfig.JIRA_AUTH_SCHEME, "Bearer");
+
+        JiraServerConfig cfg = new JiraServerConfig("apache", props);
+
+        assertEquals("jira-pat", cfg.decodedHttpAuthToken());
+        assertEquals("Bearer jira-pat", cfg.httpAuthorizationHeader());
+    }
+
+    /** */
+    @Test
+    public void jiraHexLikePlainPropertyTokenStaysPlainWhenDecodeFails() {
+        Properties props = new Properties();
+
+        props.setProperty(JiraServerConfig.JIRA_AUTH_TOKEN, "abcdef1234");
+        props.setProperty(JiraServerConfig.JIRA_AUTH_SCHEME, "Bearer");
+
+        JiraServerConfig cfg = new JiraServerConfig("apache", props);
+
+        assertEquals("abcdef1234", cfg.decodedHttpAuthToken());
+        assertEquals("Bearer abcdef1234", cfg.httpAuthorizationHeader());
+    }
+
+    /** */
+    @Test
+    public void jiraMissingTokenHasNoAuthorizationHeader() {
+        JiraServerConfig cfg = new JiraServerConfig();
+
+        assertNull(cfg.decodedHttpAuthToken());
+        assertNull(cfg.httpAuthorizationHeader());
+    }
+
+    /** */
+    @Test
+    public void githubJsonTokenIsPlainByDefault() throws Exception {
+        GitHubConfig cfg = withField(new GitHubConfig(), "authTok", 
"github-pat");
+
+        assertEquals("github-pat", cfg.gitAuthTok());
+    }
+
+    /** */
+    @Test
+    public void githubJsonTokenCanBeEncoded() throws Exception {
+        GitHubConfig cfg = withField(new GitHubConfig(), "authTok", 
PasswordEncoder.encode("github-pat"));
+
+        withField(cfg, "authTokEncoded", true);
+
+        assertEquals("github-pat", cfg.gitAuthTok());
+    }
+
+    /** */
+    @Test
+    public void githubJsonTokenCanBeAutoDetectedAsEncoded() throws Exception {
+        GitHubConfig cfg = withField(new GitHubConfig(), "authTok", 
PasswordEncoder.encode("github-pat"));
+
+        assertEquals("github-pat", cfg.gitAuthTok());
+    }
+
+    /** */
+    @Test
+    public void githubLegacyPropertyTokenIsEncodedByDefault() {
+        Properties props = new Properties();
+
+        props.setProperty(GitHubConfig.GITHUB_AUTH_TOKEN, 
PasswordEncoder.encode("github-pat"));
+
+        GitHubConfig cfg = new GitHubConfig().properties(props);
+
+        assertEquals("github-pat", cfg.gitAuthTok());
+    }
+
+    /** */
+    @Test
+    public void githubLegacyPropertyTokenCanBePlain() {
+        Properties props = new Properties();
+
+        props.setProperty(GitHubConfig.GITHUB_AUTH_TOKEN, "github-pat");
+
+        GitHubConfig cfg = new GitHubConfig().properties(props);
+
+        assertEquals("github-pat", cfg.gitAuthTok());
+    }
+
+    /** */
+    @Test
+    public void githubHexLikePlainPropertyTokenStaysPlainWhenDecodeFails() {
+        Properties props = new Properties();
+
+        props.setProperty(GitHubConfig.GITHUB_AUTH_TOKEN, "abcdef1234");
+
+        GitHubConfig cfg = new GitHubConfig().properties(props);
+
+        assertEquals("abcdef1234", cfg.gitAuthTok());
+    }
+
+    /**
+     * @param target Target object.
+     * @param fieldName Field name.
+     * @param val Field value.
+     */
+    private static <T> T withField(T target, String fieldName, Object val) 
throws Exception {
+        Field field = target.getClass().getDeclaredField(fieldName);
+
+        field.setAccessible(true);
+        field.set(target, val);
+
+        return target;
+    }
+}
diff --git a/tcbot-github/README.md b/tcbot-github/README.md
index c1322e46..da65d8d3 100644
--- a/tcbot-github/README.md
+++ b/tcbot-github/README.md
@@ -1,3 +1,7 @@
 TC Bot GitHub (service) module
 --------------------------------
-Module for pure (non cached) requests to GitHub service
\ No newline at end of file
+Module for pure (non cached) requests to GitHub service
+
+GitHub authentication is configured in `branches.json` under `gitHubConfigs`.
+Personal access tokens can be stored directly in `authTok`.
+PasswordEncoder-protected hex values are auto-detected; set `authTokEncoded` 
only to force a mode.
diff --git 
a/tcbot-integration-tests/src/integrationTest/resources/branches.json 
b/tcbot-integration-tests/src/integrationTest/resources/branches.json
index 93ea9aaf..96370349 100644
--- a/tcbot-integration-tests/src/integrationTest/resources/branches.json
+++ b/tcbot-integration-tests/src/integrationTest/resources/branches.json
@@ -20,7 +20,7 @@
       "code": "apache",
       "projectCode": "IGNITE",
       "url": "http://127.0.0.1:8012/";,
-      "authTok": 
"745AA699283F105E69FD8CDE51DDA66229D7D21E890C80004BBDABA4184C3CE09F5BF48E1924286A77B33B905EE84EA559ED65D22C3D0538C9B76580B8016D653154A66E880292923461E2846FFB3D10"
+      "authTok": "jira-test-token"
     }
   ],
   "gitHubConfigs": [
diff --git a/tcbot-jira/README.md b/tcbot-jira/README.md
index 241f9706..92eafdac 100644
--- a/tcbot-jira/README.md
+++ b/tcbot-jira/README.md
@@ -1,3 +1,9 @@
 TC Bot JIRA (service) module
 --------------------------------
-Module for pure (non cached) requests to JIRA service
\ No newline at end of file
+Module for pure (non cached) requests to JIRA service
+
+JIRA authentication is configured in `branches.json` under `jiraServers`.
+Personal Access Tokens use `authScheme: "Bearer"` and can be stored directly 
in `authTok`.
+PasswordEncoder-protected hex values are auto-detected; set `authTokEncoded` 
only to force a mode.
+Legacy Basic auth remains available with `authScheme: "Basic"` and a base64 
`user:password` token.
+If `authScheme` is omitted, encoded tokens default to Basic for compatibility 
and plain tokens default to Bearer.
diff --git a/tcbot-jira/src/main/java/org/apache/ignite/jiraservice/Jira.java 
b/tcbot-jira/src/main/java/org/apache/ignite/jiraservice/Jira.java
index 33f4f068..455fc65e 100644
--- a/tcbot-jira/src/main/java/org/apache/ignite/jiraservice/Jira.java
+++ b/tcbot-jira/src/main/java/org/apache/ignite/jiraservice/Jira.java
@@ -106,7 +106,7 @@ class Jira implements IJiraIntegration {
 
         String url = jiraApiUrl + "issue/" + ticket + "/comment";
 
-        return 
HttpUtil.sendPostAsStringToJira(config().decodedHttpAuthToken(), url, 
"{\"body\": " + comment + "}");
+        return 
HttpUtil.sendPostAsStringToJira(config().httpAuthorizationHeader(), url, 
"{\"body\": " + comment + "}");
     }
 
     /** {@inheritDoc} */
@@ -120,6 +120,6 @@ class Jira implements IJiraIntegration {
      * @return Response as gson string.
      */
     public String sendGetToJira(String url) throws IOException {
-        return HttpUtil.sendGetToJira(config().decodedHttpAuthToken(), 
config().restApiUrl() + url);
+        return HttpUtil.sendGetToJira(config().httpAuthorizationHeader(), 
config().restApiUrl() + url);
     }
 }

Reply via email to