diff --git a/pom.xml b/pom.xml index 57bf50fc9..6b928a194 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.jenkins-ci.plugins plugin - 4.29 + 4.38 github-branch-source @@ -48,6 +48,11 @@ github 1.34.3 + + org.jenkins-ci.plugins + credentials + 1087.v16065d268466 + org.jenkins-ci.plugins.workflow workflow-support diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java index 8e0dd871e..3167d9152 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java @@ -152,7 +152,7 @@ public static ListBoxModel listScanCredentials(@CheckForNull Item context, Strin * @param apiUri the api endpoint. * @param scanCredentialsId the credentials ID. * @return the {@link FormValidation} results. - * @deprecated use {@link #checkScanCredentials(Item, String, String)} + * @deprecated use {@link #checkScanCredentials(Item, String, String, String)} */ @Deprecated public static FormValidation checkScanCredentials( @@ -168,9 +168,29 @@ public static FormValidation checkScanCredentials( * @param apiUri the api endpoint. * @param scanCredentialsId the credentials ID. * @return the {@link FormValidation} results. + * @deprecated use {@link #checkScanCredentials(Item, String, String, String)} */ + @Deprecated public static FormValidation checkScanCredentials( @CheckForNull Item context, String apiUri, String scanCredentialsId) { + return checkScanCredentials(context, apiUri, scanCredentialsId, null); + } + + /** + * Checks the credential ID for use as scan credentials in the supplied context against the + * supplied API endpoint. + * + * @param context the context. + * @param apiUri the api endpoint. + * @param scanCredentialsId the credentials ID. + * @param repoOwner the org/user + * @return the {@link FormValidation} results. + */ + public static FormValidation checkScanCredentials( + @CheckForNull Item context, + String apiUri, + String scanCredentialsId, + @CheckForNull String repoOwner) { if (context == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) || context != null && !context.hasPermission(Item.EXTENDED_READ)) { return FormValidation.ok(); @@ -194,7 +214,8 @@ public static FormValidation checkScanCredentials( Connector.lookupScanCredentials( context, StringUtils.defaultIfEmpty(apiUri, GitHubServerConfig.GITHUB_URL), - scanCredentialsId); + scanCredentialsId, + repoOwner); if (credentials == null) { return FormValidation.error("Credentials not found"); } else { @@ -241,7 +262,7 @@ public static FormValidation checkScanCredentials( * @param apiUri the API endpoint. * @param scanCredentialsId the credentials to resolve. * @return the {@link StandardCredentials} or {@code null} - * @deprecated use {@link #lookupScanCredentials(Item, String, String)} + * @deprecated use {@link #lookupScanCredentials(Item, String, String, String)} */ @Deprecated @CheckForNull @@ -260,25 +281,52 @@ public static StandardCredentials lookupScanCredentials( * @param apiUri the API endpoint. * @param scanCredentialsId the credentials to resolve. * @return the {@link StandardCredentials} or {@code null} + * @deprecated use {@link #lookupScanCredentials(Item, String, String, String)} */ + @Deprecated @CheckForNull public static StandardCredentials lookupScanCredentials( @CheckForNull Item context, @CheckForNull String apiUri, @CheckForNull String scanCredentialsId) { + return lookupScanCredentials(context, apiUri, scanCredentialsId, null); + } + + /** + * Resolves the specified scan credentials in the specified context for use against the specified + * API endpoint. + * + * @param context the context. + * @param apiUri the API endpoint. + * @param scanCredentialsId the credentials to resolve. + * @param repoOwner the org/user + * @return the {@link StandardCredentials} or {@code null} + */ + @CheckForNull + public static StandardCredentials lookupScanCredentials( + @CheckForNull Item context, + @CheckForNull String apiUri, + @CheckForNull String scanCredentialsId, + @CheckForNull String repoOwner) { if (Util.fixEmpty(scanCredentialsId) == null) { return null; } else { - return CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentials( - StandardUsernameCredentials.class, - context, - context instanceof Queue.Task - ? ((Queue.Task) context).getDefaultAuthentication() - : ACL.SYSTEM, - githubDomainRequirements(apiUri)), - CredentialsMatchers.allOf( - CredentialsMatchers.withId(scanCredentialsId), githubScanCredentialsMatcher())); + StandardCredentials c = + CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials( + StandardUsernameCredentials.class, + context, + context instanceof Queue.Task + ? ((Queue.Task) context).getDefaultAuthentication() + : ACL.SYSTEM, + githubDomainRequirements(apiUri)), + CredentialsMatchers.allOf( + CredentialsMatchers.withId(scanCredentialsId), githubScanCredentialsMatcher())); + if (c instanceof GitHubAppCredentials && repoOwner != null) { + return ((GitHubAppCredentials) c).withOwner(repoOwner); + } else { + return c; + } } } diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java index cade19be3..7fadf8f92 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java @@ -2,15 +2,20 @@ import static org.jenkinsci.plugins.github_branch_source.GitHubSCMNavigator.DescriptorImpl.getPossibleApiUriItems; +import com.cloudbees.jenkins.GitHubRepositoryName; +import com.cloudbees.plugins.credentials.Credentials; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; +import com.coravy.hudson.plugins.github.GithubProjectProperty; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.Functions; import hudson.Util; +import hudson.model.Job; +import hudson.model.Run; import hudson.remoting.Channel; import hudson.util.FormValidation; import hudson.util.ListBoxModel; @@ -20,10 +25,13 @@ import java.security.GeneralSecurityException; import java.time.Duration; import java.time.Instant; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.scm.api.SCMSource; import jenkins.security.SlaveToMasterCallable; import jenkins.util.JenkinsJVM; import net.sf.json.JSONObject; @@ -73,10 +81,18 @@ public class GitHubAppCredentials extends BaseStandardCredentials private String apiUri; + @SuppressFBWarnings( + value = "IS2_INCONSISTENT_SYNC", + justification = "#withOwner locking only for #byOwner") private String owner; private transient AppInstallationToken cachedToken; + /** + * Cache of credentials specialized by {@link #owner}, so that {@link #cachedToken} is preserved. + */ + private transient Map byOwner; + @DataBoundConstructor @SuppressWarnings("unused") // by stapler public GitHubAppCredentials( @@ -310,6 +326,47 @@ public String getUsername() { return appID; } + @NonNull + public synchronized GitHubAppCredentials withOwner(@NonNull String owner) { + if (this.owner != null) { + if (!owner.equals(this.owner)) { + throw new IllegalArgumentException("Owner mismatch: " + this.owner + " vs. " + owner); + } + return this; + } + if (byOwner == null) { + byOwner = new HashMap<>(); + } + return byOwner.computeIfAbsent( + owner, + k -> { + GitHubAppCredentials clone = + new GitHubAppCredentials(getScope(), getId(), getDescription(), appID, privateKey); + clone.apiUri = apiUri; + clone.owner = owner; + return clone; + }); + } + + @NonNull + @Override + public Credentials forRun(Run context) { + if (owner != null) { + return this; + } + Job job = context.getParent(); + SCMSource src = SCMSource.SourceByItem.findSource(job); + if (src instanceof GitHubSCMSource) { + return withOwner(((GitHubSCMSource) src).getRepoOwner()); + } + GitHubRepositoryName ghrn = + GitHubRepositoryName.create(job.getProperty(GithubProjectProperty.class)); + if (ghrn != null) { + return withOwner(ghrn.userName); + } + return this; + } + private AppInstallationToken getCachedToken() { synchronized (this) { return cachedToken; diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubBuildStatusNotification.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubBuildStatusNotification.java index f38ab10e9..20600f1df 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubBuildStatusNotification.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubBuildStatusNotification.java @@ -191,7 +191,7 @@ private static GitHub lookUpGitHub(@NonNull Job job) throws IOException { return Connector.connect( source.getApiUri(), Connector.lookupScanCredentials( - job, source.getApiUri(), source.getScanCredentialsId())); + job, source.getApiUri(), source.getScanCredentialsId(), source.getRepoOwner())); } } return null; diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMFileSystem.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMFileSystem.java index 7f37df602..29bf35a43 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMFileSystem.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMFileSystem.java @@ -241,7 +241,7 @@ public SCMFileSystem build( String apiUri = src.getApiUri(); StandardCredentials credentials = Connector.lookupScanCredentials( - (Item) src.getOwner(), apiUri, src.getScanCredentialsId()); + (Item) src.getOwner(), apiUri, src.getScanCredentialsId(), src.getRepoOwner()); // Github client and validation GitHub github = Connector.connect(apiUri, credentials); diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java index 818c8c792..ff1f40690 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java @@ -935,7 +935,8 @@ public void visitSources(SCMSourceObserver observer) throws IOException, Interru } StandardCredentials credentials = - Connector.lookupScanCredentials((Item) observer.getContext(), apiUri, credentialsId); + Connector.lookupScanCredentials( + (Item) observer.getContext(), apiUri, credentialsId, repoOwner); // Github client and validation GitHub github = Connector.connect(apiUri, credentials); @@ -1262,7 +1263,8 @@ public void visitSource(String sourceName, SCMSourceObserver observer) } StandardCredentials credentials = - Connector.lookupScanCredentials((Item) observer.getContext(), apiUri, credentialsId); + Connector.lookupScanCredentials( + (Item) observer.getContext(), apiUri, credentialsId, repoOwner); // Github client and validation GitHub github; @@ -1577,8 +1579,8 @@ public List retrieveActions( List result = new ArrayList<>(); String apiUri = Util.fixEmptyAndTrim(getApiUri()); StandardCredentials credentials = - Connector.lookupScanCredentials((Item) owner, apiUri, credentialsId); - GitHub hub = Connector.connect(apiUri, credentials); + Connector.lookupScanCredentials((Item) owner, getApiUri(), credentialsId, repoOwner); + GitHub hub = Connector.connect(getApiUri(), credentials); boolean privateMode = determinePrivateMode(apiUri); try { Connector.configureLocalRateLimitChecker(listener, hub); @@ -1631,7 +1633,7 @@ public void afterSave(@NonNull SCMNavigatorOwner owner) { try { // FIXME MINOR HACK ALERT StandardCredentials credentials = - Connector.lookupScanCredentials((Item) owner, getApiUri(), credentialsId); + Connector.lookupScanCredentials((Item) owner, getApiUri(), credentialsId, repoOwner); GitHub hub = Connector.connect(getApiUri(), credentials); try { GitHubOrgWebHook.register(hub, repoOwner); @@ -1757,8 +1759,9 @@ protected SCMSourceCategory[] createCategories() { public FormValidation doCheckCredentialsId( @CheckForNull @AncestorInPath Item context, @QueryParameter String apiUri, - @QueryParameter String credentialsId) { - return Connector.checkScanCredentials(context, apiUri, credentialsId); + @QueryParameter String credentialsId, + @QueryParameter String repoOwner) { + return Connector.checkScanCredentials(context, apiUri, credentialsId, repoOwner); } /** diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java index 90447deb6..d4fa4975c 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java @@ -986,7 +986,7 @@ protected final void retrieve( @NonNull final TaskListener listener) throws IOException, InterruptedException { StandardCredentials credentials = - Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId); + Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId, repoOwner); // Github client and validation final GitHub github = Connector.connect(apiUri, credentials); try { @@ -1296,7 +1296,7 @@ public SCMRevision create(@NonNull PullRequestSCMHead head, @Nullable Void ignor protected Set retrieveRevisions(@NonNull TaskListener listener, Item retrieveContext) throws IOException, InterruptedException { StandardCredentials credentials = - Connector.lookupScanCredentials(retrieveContext, apiUri, credentialsId); + Connector.lookupScanCredentials(retrieveContext, apiUri, credentialsId, repoOwner); // Github client and validation final GitHub github = Connector.connect(apiUri, credentials); try { @@ -1406,7 +1406,7 @@ protected SCMRevision retrieve( @NonNull String headName, @NonNull TaskListener listener, Item retrieveContext) throws IOException, InterruptedException { StandardCredentials credentials = - Connector.lookupScanCredentials(retrieveContext, apiUri, credentialsId); + Connector.lookupScanCredentials(retrieveContext, apiUri, credentialsId, repoOwner); // Github client and validation final GitHub github = Connector.connect(apiUri, credentials); try { @@ -1654,7 +1654,7 @@ public void unwrap() throws IOException, InterruptedException { protected SCMProbe createProbe(@NonNull SCMHead head, @CheckForNull final SCMRevision revision) throws IOException { StandardCredentials credentials = - Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId); + Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId, repoOwner); // Github client and validation GitHub github = Connector.connect(apiUri, credentials); try { @@ -1673,7 +1673,7 @@ protected SCMProbe createProbe(@NonNull SCMHead head, @CheckForNull final SCMRev protected SCMRevision retrieve(SCMHead head, TaskListener listener) throws IOException, InterruptedException { StandardCredentials credentials = - Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId); + Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId, repoOwner); // Github client and validation GitHub github = Connector.connect(apiUri, credentials); @@ -1825,7 +1825,7 @@ PullRequestSource retrievePullRequestSource(int number) { String fullName = repoOwner + "/" + repository; LOGGER.log(Level.INFO, "Getting remote pull requests from {0}", fullName); StandardCredentials credentials = - Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId); + Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId, repoOwner); LogTaskListener listener = new LogTaskListener(LOGGER, Level.INFO); try { GitHub github = Connector.connect(apiUri, credentials); @@ -1995,7 +1995,7 @@ protected List retrieveActions( String repository = this.repository; StandardCredentials credentials = - Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId); + Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId, repoOwner); GitHub hub = Connector.connect(apiUri, credentials); try { Connector.checkConnectionValidity(apiUri, listener, credentials, hub); @@ -2129,8 +2129,9 @@ public ListBoxModel doFillCredentialsIdItems( public FormValidation doCheckCredentialsId( @CheckForNull @AncestorInPath Item context, @QueryParameter String apiUri, + @QueryParameter String repoOwner, @QueryParameter String value) { - return Connector.checkScanCredentials(context, apiUri, value); + return Connector.checkScanCredentials(context, apiUri, value, repoOwner); } @RequirePOST @@ -2138,7 +2139,8 @@ public FormValidation doCheckCredentialsId( public FormValidation doValidateRepositoryUrlAndCredentials( @CheckForNull @AncestorInPath Item context, @QueryParameter String repositoryUrl, - @QueryParameter String credentialsId) { + @QueryParameter String credentialsId, + @QueryParameter String repoOwner) { if (context == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) || context != null && !context.hasPermission(Item.EXTENDED_READ)) { return FormValidation.error( @@ -2158,7 +2160,7 @@ public FormValidation doValidateRepositoryUrlAndCredentials( } StandardCredentials credentials = - Connector.lookupScanCredentials(context, info.getApiUri(), credentialsId); + Connector.lookupScanCredentials(context, info.getApiUri(), credentialsId, repoOwner); StringBuilder sb = new StringBuilder(); try { GitHub github = Connector.connect(info.getApiUri(), credentials); @@ -2197,8 +2199,9 @@ public FormValidation doCheckIncludes(@QueryParameter String value) { public FormValidation doCheckScanCredentialsId( @CheckForNull @AncestorInPath Item context, @QueryParameter String apiUri, - @QueryParameter String scanCredentialsId) { - return doCheckCredentialsId(context, apiUri, scanCredentialsId); + @QueryParameter String scanCredentialsId, + @QueryParameter String repoOwner) { + return doCheckCredentialsId(context, apiUri, scanCredentialsId, repoOwner); } @Restricted(NoExternalUse.class) @@ -2282,7 +2285,8 @@ public boolean isApiUriSelectable() { public ListBoxModel doFillOrganizationItems( @CheckForNull @AncestorInPath Item context, @QueryParameter String apiUri, - @QueryParameter String credentialsId) + @QueryParameter String credentialsId, + @QueryParameter String repoOwner) throws IOException { if (credentialsId == null) { return new ListBoxModel(); @@ -2296,7 +2300,7 @@ public ListBoxModel doFillOrganizationItems( } try { StandardCredentials credentials = - Connector.lookupScanCredentials(context, apiUri, credentialsId); + Connector.lookupScanCredentials(context, apiUri, credentialsId, repoOwner); GitHub github = Connector.connect(apiUri, credentials); try { if (!github.isAnonymous()) { @@ -2344,7 +2348,7 @@ public ListBoxModel doFillRepositoryItems( } try { StandardCredentials credentials = - Connector.lookupScanCredentials(context, apiUri, credentialsId); + Connector.lookupScanCredentials(context, apiUri, credentialsId, repoOwner); GitHub github = Connector.connect(apiUri, credentials); try { @@ -2918,7 +2922,7 @@ protected Set create() { "Connecting to %s to obtain list of collaborators for %s/%s%n", apiUri, repoOwner, repository); StandardCredentials credentials = - Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId); + Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId, repoOwner); // Github client and validation try { GitHub github = Connector.connect(apiUri, credentials); @@ -2986,7 +2990,7 @@ public GHPermissionType fetch(String username) throws IOException, InterruptedEx "Connecting to %s to check permissions of obtain list of %s for %s/%s%n", apiUri, username, repoOwner, repository); StandardCredentials credentials = - Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId); + Connector.lookupScanCredentials((Item) getOwner(), apiUri, credentialsId, repoOwner); github = Connector.connect(apiUri, credentials); String fullName = repoOwner + "/" + repository; repo = github.getRepository(fullName); diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-owner.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-owner.html index 2097e42f0..86a279f3f 100644 --- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-owner.html +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials/help-owner.html @@ -1,3 +1,6 @@

- The organisation or user that this app is to be used for. Only required if this app is installed to multiple organisations. + The organisation or user that this app is to be used for. + Only required if this app is installed to multiple organisations. + May be omitted in case credentials are used from GitHub multibranch projects + (in that case the account is determined from the branch source where the credentials are used).

diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/config-detail.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/config-detail.jelly index ad84a0a01..5af7baa48 100644 --- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/config-detail.jelly +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/config-detail.jelly @@ -17,7 +17,7 @@ - + diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsAppInstallationTokenTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsAppInstallationTokenTest.java index 814b738dc..a58cb6890 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsAppInstallationTokenTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsAppInstallationTokenTest.java @@ -1,11 +1,16 @@ package org.jenkinsci.plugins.github_branch_source; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; import hudson.util.Secret; +import java.math.BigDecimal; import java.time.Duration; import java.time.Instant; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.hamcrest.number.BigDecimalCloseTo; import org.junit.Test; public class GithubAppCredentialsAppInstallationTokenTest { @@ -22,7 +27,7 @@ public void testAppInstallationTokenStale() throws Exception { assertThat(token.isStale(), is(false)); assertThat( token.getTokenStaleEpochSeconds(), - equalTo(now + GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS)); + closeTo(now + GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS, 3)); now = Instant.now().getEpochSecond(); token = @@ -31,7 +36,7 @@ public void testAppInstallationTokenStale() throws Exception { assertThat(token.isStale(), is(false)); assertThat( token.getTokenStaleEpochSeconds(), - equalTo(now + GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS)); + closeTo(now + GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS, 3)); now = Instant.now().getEpochSecond(); token = @@ -41,7 +46,7 @@ public void testAppInstallationTokenStale() throws Exception { assertThat(token.isStale(), is(false)); assertThat( token.getTokenStaleEpochSeconds(), - equalTo(now + GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS)); + closeTo(now + GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS, 3)); now = Instant.now().getEpochSecond(); token = @@ -52,7 +57,7 @@ public void testAppInstallationTokenStale() throws Exception { + Duration.ofMinutes(7).getSeconds()); assertThat(token.isStale(), is(false)); assertThat( - token.getTokenStaleEpochSeconds(), equalTo(now + Duration.ofMinutes(7).getSeconds())); + token.getTokenStaleEpochSeconds(), closeTo(now + Duration.ofMinutes(7).getSeconds(), 3)); now = Instant.now().getEpochSecond(); token = @@ -61,8 +66,9 @@ public void testAppInstallationTokenStale() throws Exception { assertThat(token.isStale(), is(false)); assertThat( token.getTokenStaleEpochSeconds(), - equalTo(now + GitHubAppCredentials.AppInstallationToken.STALE_AFTER_SECONDS + 1)); + closeTo(now + GitHubAppCredentials.AppInstallationToken.STALE_AFTER_SECONDS + 1, 3)); + // TODO use FlagRule long notStaleSeconds = GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS; try { // Should revert to 1 second minimum @@ -71,7 +77,7 @@ public void testAppInstallationTokenStale() throws Exception { now = Instant.now().getEpochSecond(); token = new GitHubAppCredentials.AppInstallationToken(secret, now); assertThat(token.isStale(), is(false)); - assertThat(token.getTokenStaleEpochSeconds(), equalTo(now + 1)); + assertThat(token.getTokenStaleEpochSeconds(), closeTo(now + 1, 3)); // Verify goes stale Thread.sleep(1000); @@ -80,4 +86,25 @@ public void testAppInstallationTokenStale() throws Exception { GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS = notStaleSeconds; } } + + private static Matcher closeTo(long operand, long error) { + BigDecimalCloseTo delegate = + new BigDecimalCloseTo(new BigDecimal(operand), new BigDecimal(error)); + return new TypeSafeMatcher(Long.class) { + @Override + protected boolean matchesSafely(Long item) { + return delegate.matches(new BigDecimal(item)); + } + + @Override + protected void describeMismatchSafely(Long item, Description mismatchDescription) { + delegate.describeMismatchSafely(new BigDecimal(item), mismatchDescription); + } + + @Override + public void describeTo(Description description) { + delegate.describeTo(description); + } + }; + } }