Skip to content

Commit 4484d3d

Browse files
Add a Test Connection button to TestingBot credentials (#36)
* Add a Test Connection button to TestingBot credentials Adds a "Test Connection" validate button to the credentials form that calls the TestingBot REST API (getUserInfo) with the entered key/secret and reports whether they authenticate — e.g. "Connection successful — signed in as <email> (<plan> plan)" or a clear error. This is the credential verification Sauce/BrowserStack offer and we lacked. The doVerifyCredentials descriptor method is @post (CSRF-safe), gated to admins or users who can configure the surrounding item, and resolves the secret via Secret.fromString so it works whether the form submits plaintext or the encrypted value. The REST client is closed via try-with-resources. * Catch the REST client's typed exceptions, not broad Exception (review on #36) doVerifyCredentials caught Exception, masking unexpected bugs. The testingbotrest client signals failures via two specific (unchecked) types — TestingbotUnauthorized Exception (bad key/secret) and TestingbotApiException (which wraps network/parse IOException) — so catch exactly those and let any other runtime exception propagate. Note: getUserInfo()/close() declare no checked exceptions, so narrowing to checked types (as literally worded) isn't possible; these typed unchecked exceptions are the client's declared failure signals.
1 parent b0cfb0e commit 4484d3d

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

src/main/java/testingbot/TestingBotCredentials.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
import com.cloudbees.plugins.credentials.domains.Domain;
2424
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
2525
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
26+
import com.testingbot.models.TestingbotUser;
27+
import com.testingbot.testingbotrest.TestingbotApiException;
28+
import com.testingbot.testingbotrest.TestingbotREST;
29+
import com.testingbot.testingbotrest.TestingbotUnauthorizedException;
2630
import edu.umd.cs.findbugs.annotations.CheckForNull;
2731
import edu.umd.cs.findbugs.annotations.NonNull;
2832
import hudson.Extension;
@@ -305,6 +309,39 @@ public final FormValidation doCheckId(@QueryParameter String value, @AncestorInP
305309
}
306310
return FormValidation.ok();
307311
}
312+
313+
/**
314+
* Verifies a key/secret against the TestingBot API and reports whether they authenticate.
315+
* Gated so only users who can configure the surrounding item (or admins) can probe.
316+
*/
317+
@POST
318+
public FormValidation doVerifyCredentials(@QueryParameter String key, @QueryParameter String secret,
319+
@AncestorInPath ModelObject context) {
320+
boolean allowed = Jenkins.get().hasPermission(Jenkins.ADMINISTER)
321+
|| (context instanceof Item && ((Item) context).hasPermission(Item.CONFIGURE));
322+
if (!allowed) {
323+
return FormValidation.error("You do not have permission to verify credentials.");
324+
}
325+
if (Util.fixEmptyAndTrim(key) == null || Util.fixEmptyAndTrim(secret) == null) {
326+
return FormValidation.error("Enter both a key and a secret first.");
327+
}
328+
// The password field may submit the encrypted Secret when editing an existing credential;
329+
// fromString transparently yields the plaintext either way.
330+
String plainSecret = Secret.fromString(secret).getPlainText();
331+
try (TestingbotREST rest = new TestingbotREST(key.trim(), plainSecret)) {
332+
TestingbotUser user = rest.getUserInfo();
333+
if (user == null || Util.fixEmptyAndTrim(user.getEmail()) == null) {
334+
return FormValidation.error("Could not verify these credentials with TestingBot.");
335+
}
336+
String plan = Util.fixEmptyAndTrim(user.getPlan());
337+
return FormValidation.ok("Connection successful — signed in as %s%s",
338+
user.getEmail(), plan != null ? " (" + plan + " plan)" : "");
339+
} catch (TestingbotUnauthorizedException | TestingbotApiException e) {
340+
// The REST client's typed failures: bad key/secret, and network/parse errors (it wraps
341+
// IOException into TestingbotApiException). Unexpected runtime exceptions propagate.
342+
return FormValidation.error("TestingBot authentication failed: " + e.getMessage());
343+
}
344+
}
308345
}
309346

310347
public static class NameProvider extends CredentialsNameProvider<TestingBotCredentials> {

src/main/resources/testingbot/TestingBotCredentials/credentials.jelly

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
<f:password/>
99
</f:entry>
1010

11+
<f:validateButton title="${%Test Connection}" progress="${%Testing…}"
12+
method="verifyCredentials" with="key,secret"/>
13+
1114
<f:entry title="${%Description}" field="description">
1215
<f:textbox/>
1316
</f:entry>

0 commit comments

Comments
 (0)