Skip to content

Commit b8923cb

Browse files
Fix TestingBot credential resolution and Test Connection (#40)
* Fix TestingBot credential resolution and Test Connection Two correctness fixes surfaced by an end-to-end run against a real account. 1. Credentials never resolved at build time. TestingBotCredentials extended BaseCredentials (implements StandardCredentials). CredentialsProvider.lookupCredentialsInItem(TestingBotCredentials.class, ...) — used by the build wrapper's getCredentials(), availableCredentials() and every credential dropdown — returned an empty list for it, even though the stored objects are instances of the class (a StandardCredentials.class lookup found them; a UsernamePasswordCredentialsImpl, which extends BaseStandardCredentials, is found by concrete class). As a result the freestyle wrapper silently resolved null credentials, so TB_KEY/TB_SECRET/TESTINGBOT_BUILD were never injected and the dropdowns were empty. Make TestingBotCredentials extend BaseStandardCredentials, which owns id and description (and the id-based equals/hashCode), so concrete-type lookups work. Old-format credentials.xml (scope/id/description/key/secret) still deserializes unchanged — field names are identical. 2. Test Connection failed for accounts without an email. doVerifyCredentials required a non-null user email; a valid account can return 200 with a null email (e.g. sub-accounts), which produced a false 'Could not verify these credentials' for working key/secret. Treat any authenticated user as success, preferring the email and falling back to the account first name. * Add regression tests for Test Connection result branches Extract the user-facing result building of doVerifyCredentials into a pure, package-private verificationResult(TestingbotUser) helper (behavior unchanged) so it can be unit-tested without the network call or the Jenkins permission check (a JenkinsRule test is not viable here — see the harness/Jetty conflict). Cover: null user -> error; user with email -> success naming the email; user without email but with a first name -> success naming the first name; user missing email, first name and plan -> still success, with no identity or plan fragment in the message.
1 parent 6c594a9 commit b8923cb

2 files changed

Lines changed: 82 additions & 37 deletions

File tree

src/main/java/testingbot/TestingBotCredentials.java

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import org.kohsuke.stapler.export.Exported;
88
import org.kohsuke.stapler.verb.POST;
99

10-
import com.cloudbees.plugins.credentials.BaseCredentials;
1110
import com.cloudbees.plugins.credentials.Credentials;
1211
import com.cloudbees.plugins.credentials.CredentialsDescriptor;
1312
import com.cloudbees.plugins.credentials.CredentialsMatcher;
@@ -28,7 +27,6 @@
2827
import com.testingbot.testingbotrest.TestingbotREST;
2928
import com.testingbot.testingbotrest.TestingbotUnauthorizedException;
3029
import edu.umd.cs.findbugs.annotations.CheckForNull;
31-
import edu.umd.cs.findbugs.annotations.NonNull;
3230
import hudson.Extension;
3331
import hudson.Util;
3432
import hudson.model.AbstractItem;
@@ -56,19 +54,15 @@
5654
import java.util.logging.Logger;
5755

5856
@NameWith(value = TestingBotCredentials.NameProvider.class)
59-
public class TestingBotCredentials extends BaseCredentials implements StandardCredentials {
57+
public class TestingBotCredentials extends BaseStandardCredentials {
6058
private static final String CREDENTIAL_DISPLAY_NAME = "TestingBot";
6159

62-
private final String id;
63-
private final String description;
6460
private final String key;
6561
private final Secret secret;
6662

6763
@DataBoundConstructor
6864
public TestingBotCredentials(String id, String description, String key, String secret) {
69-
super(CredentialsScope.GLOBAL);
70-
this.id = IdCredentials.Helpers.fixEmptyId(id);
71-
this.description = Util.fixNull(description);
65+
super(CredentialsScope.GLOBAL, IdCredentials.Helpers.fixEmptyId(id), Util.fixNull(description));
7266
this.key = Util.fixNull(key);
7367
this.secret = Secret.fromString(secret);
7468
}
@@ -86,28 +80,6 @@ public String getDecryptedSecret() {
8680
return secret.getPlainText();
8781
}
8882

89-
@NonNull
90-
@Exported
91-
public String getDescription() {
92-
return description;
93-
}
94-
95-
@NonNull
96-
@Exported
97-
public String getId() {
98-
return id;
99-
}
100-
101-
@Override
102-
public final boolean equals(Object o) {
103-
return IdCredentials.Helpers.equals(this, o);
104-
}
105-
106-
@Override
107-
public final int hashCode() {
108-
return IdCredentials.Helpers.hashCode(this);
109-
}
110-
11183
private static List<String> getLegacyCredentials() {
11284
DataInputStream in = null;
11385
BufferedReader br = null;
@@ -329,19 +301,33 @@ public FormValidation doVerifyCredentials(@QueryParameter String key, @QueryPara
329301
// fromString transparently yields the plaintext either way.
330302
String plainSecret = Secret.fromString(secret).getPlainText();
331303
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)" : "");
304+
return verificationResult(rest.getUserInfo());
339305
} catch (TestingbotUnauthorizedException | TestingbotApiException e) {
340306
// The REST client's typed failures: bad key/secret, and network/parse errors (it wraps
341307
// IOException into TestingbotApiException). Unexpected runtime exceptions propagate.
342308
return FormValidation.error("TestingBot authentication failed: " + e.getMessage());
343309
}
344310
}
311+
312+
/**
313+
* Builds the user-facing result for a fetched {@link TestingbotUser}. A null user means the
314+
* credentials could not be verified; otherwise any authenticated user is a success. A valid
315+
* account may not expose an email (e.g. sub-accounts), so prefer the email but fall back to
316+
* the account's first name, and treat both the identity and the plan as optional.
317+
*/
318+
static FormValidation verificationResult(TestingbotUser user) {
319+
if (user == null) {
320+
return FormValidation.error("Could not verify these credentials with TestingBot.");
321+
}
322+
String who = Util.fixEmptyAndTrim(user.getEmail());
323+
if (who == null) {
324+
who = Util.fixEmptyAndTrim(user.getFirstName());
325+
}
326+
String plan = Util.fixEmptyAndTrim(user.getPlan());
327+
return FormValidation.ok("Connection successful%s%s",
328+
who != null ? " — signed in as " + who : "",
329+
plan != null ? " (" + plan + " plan)" : "");
330+
}
345331
}
346332

347333
public static class NameProvider extends CredentialsNameProvider<TestingBotCredentials> {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package testingbot;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.testingbot.models.TestingbotUser;
6+
import hudson.util.FormValidation;
7+
import org.junit.Test;
8+
9+
/**
10+
* Covers {@link TestingBotCredentials.DescriptorImpl#verificationResult} — the Test Connection
11+
* result branches — without touching the network or the Jenkins permission check: a null user
12+
* fails, and any authenticated user succeeds even when the email, first name or plan are absent.
13+
*/
14+
public class TestingBotCredentialsVerificationTest {
15+
16+
@Test
17+
public void nullUserFailsVerification() {
18+
FormValidation r = TestingBotCredentials.DescriptorImpl.verificationResult(null);
19+
assertThat(r.kind).isEqualTo(FormValidation.Kind.ERROR);
20+
assertThat(r.getMessage()).contains("Could not verify");
21+
}
22+
23+
@Test
24+
public void userWithEmailSucceeds() {
25+
TestingbotUser user = new TestingbotUser();
26+
user.setEmail("jane@example.com");
27+
user.setFirstName("Jane");
28+
user.setPlan("Enterprise Plan");
29+
FormValidation r = TestingBotCredentials.DescriptorImpl.verificationResult(user);
30+
assertThat(r.kind).isEqualTo(FormValidation.Kind.OK);
31+
assertThat(r.getMessage())
32+
.contains("Connection successful")
33+
.contains("jane@example.com")
34+
.contains("Enterprise Plan plan");
35+
}
36+
37+
@Test
38+
public void userWithoutEmailFallsBackToFirstName() {
39+
TestingbotUser user = new TestingbotUser();
40+
user.setFirstName("demo");
41+
user.setPlan("Free");
42+
FormValidation r = TestingBotCredentials.DescriptorImpl.verificationResult(user);
43+
assertThat(r.kind).isEqualTo(FormValidation.Kind.OK);
44+
assertThat(r.getMessage())
45+
.contains("signed in as demo")
46+
.contains("Free plan")
47+
.doesNotContain("@");
48+
}
49+
50+
@Test
51+
public void userMissingIdentityAndPlanStillSucceeds() {
52+
TestingbotUser user = new TestingbotUser(); // no email, first name or plan
53+
FormValidation r = TestingBotCredentials.DescriptorImpl.verificationResult(user);
54+
assertThat(r.kind).isEqualTo(FormValidation.Kind.OK);
55+
assertThat(r.getMessage()).contains("Connection successful");
56+
assertThat(r.getMessage()).doesNotContain("signed in as");
57+
assertThat(r.getMessage()).doesNotContain(" plan)");
58+
}
59+
}

0 commit comments

Comments
 (0)