Files
github-api/src/test/java/org/kohsuke/github/GHRateLimitTest.java
Alex Taylor 7c495c2177 Fixes after merge
Fixed some failing tests after merge
2020-01-27 14:45:02 -05:00

443 lines
18 KiB
Java

package org.kohsuke.github;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer;
import org.hamcrest.CoreMatchers;
import org.junit.Test;
import java.io.IOException;
import java.util.Date;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.fail;
/**
* Test showing the behavior of OkHttpConnector with and without cache.
* <p>
* Key take aways:
*
* <ul>
* <li>These tests are artificial and intended to highlight the differences in behavior between scenarios. However, the
* differences they indicate are stark.</li>
* <li>Caching reduces rate limit consumption by at least a factor of two in even the simplest case.</li>
* <li>The OkHttp cache is pretty smart and will often connect read and write requests made on the same client and
* invalidate caches.</li>
* <li>Changes made outside the current client cause the OkHttp cache to return stale data. This is expected and correct
* behavior.</li>
* <li>"max-age=0" addresses the problem of external changes by revalidating caches for each request. This produces the
* same number of requests as OkHttp without caching, but those requests only count towards the GitHub rate limit if
* data has changes.</li>
* </ul>
*
* @author Liam Newman
*/
public class GHRateLimitTest extends AbstractGitHubWireMockTest {
GHRateLimit rateLimit = null;
GHRateLimit previousLimit = null;
public GHRateLimitTest() {
useDefaultGitHub = false;
}
@Override
protected WireMockConfiguration getWireMockOptions() {
return super.getWireMockOptions()
.extensions(ResponseTemplateTransformer.builder().global(true).maxCacheEntries(0L).build());
}
@Test
public void testGitHubRateLimit() throws Exception {
// Customized response that templates the date to keep things working
snapshotNotAllowed();
assertThat(mockGitHub.getRequestCount(), equalTo(0));
// 4897 is just the what the limit was when the snapshot was taken
previousLimit = GHRateLimit
.fromHeaderRecord(new GHRateLimit.Record(5000, 4897, System.currentTimeMillis() / 1000L));
// Give this a moment
Thread.sleep(1000);
// -------------------------------------------------------------
// /user gets response with rate limit information
gitHub = getGitHubBuilder().withEndpoint(mockGitHub.apiServer().baseUrl()).build();
gitHub.getMyself();
assertThat(mockGitHub.getRequestCount(), equalTo(1));
// Since we already had rate limit info these don't request again
rateLimit = gitHub.lastRateLimit();
verifyRateLimitValues(previousLimit, previousLimit.getRemaining());
previousLimit = rateLimit;
GHRateLimit headerRateLimit = rateLimit;
// Give this a moment
Thread.sleep(1000);
// ratelimit() uses headerRateLimit if available and headerRateLimit is not expired
assertThat(gitHub.rateLimit(), equalTo(headerRateLimit));
assertThat(mockGitHub.getRequestCount(), equalTo(1));
// Give this a moment
Thread.sleep(1000);
// Always requests new info
rateLimit = gitHub.getRateLimit();
assertThat(mockGitHub.getRequestCount(), equalTo(2));
// rate limit request is free, remaining is unchanged
verifyRateLimitValues(previousLimit, previousLimit.getRemaining());
previousLimit = rateLimit;
// Give this a moment
Thread.sleep(1000);
// Always requests new info
rateLimit = gitHub.getRateLimit();
assertThat(mockGitHub.getRequestCount(), equalTo(3));
// rate limit request is free, remaining is unchanged
verifyRateLimitValues(previousLimit, previousLimit.getRemaining());
previousLimit = rateLimit;
gitHub.getOrganization(GITHUB_API_TEST_ORG);
assertThat(mockGitHub.getRequestCount(), equalTo(4));
assertThat(gitHub.lastRateLimit(), not(equalTo(headerRateLimit)));
rateLimit = gitHub.lastRateLimit();
// Org costs limit to query
verifyRateLimitValues(previousLimit, previousLimit.getRemaining() - 1);
previousLimit = rateLimit;
headerRateLimit = rateLimit;
// ratelimit() should prefer headerRateLimit when it is most recent and not expired
assertThat(gitHub.rateLimit(), equalTo(headerRateLimit));
assertThat(mockGitHub.getRequestCount(), equalTo(4));
// Give this a moment
Thread.sleep(2000);
// Always requests new info
rateLimit = gitHub.getRateLimit();
assertThat(mockGitHub.getRequestCount(), equalTo(5));
// rate limit request is free, remaining is unchanged
verifyRateLimitValues(previousLimit, previousLimit.getRemaining());
previousLimit = rateLimit;
// When getRateLimit() succeeds, headerRateLimit updates as usual as well (if needed)
// These are separate instances, but should be equal
assertThat(gitHub.rateLimit(), not(sameInstance(rateLimit)));
// Verify different record instances can be compared
assertThat(gitHub.rateLimit().getCore(), equalTo(rateLimit.getCore()));
// Verify different instances can be compared
// TODO: This is not work currently because the header rate limit has unknowns for records other than core.
// assertThat(gitHub.rateLimit().getCore(), equalTo(rateLimit.getCore()));
assertThat(gitHub.rateLimit(), not(sameInstance(headerRateLimit)));
assertThat(gitHub.rateLimit(), sameInstance(gitHub.lastRateLimit()));
assertThat(mockGitHub.getRequestCount(), equalTo(5));
}
private void verifyRateLimitValues(GHRateLimit previousLimit, int remaining) {
// Basic checks of values
assertThat(rateLimit, notNullValue());
assertThat(rateLimit.getLimit(), equalTo(previousLimit.getLimit()));
assertThat(rateLimit.getRemaining(), equalTo(remaining));
// Check that the reset date of the current limit is not older than the previous one
assertThat(rateLimit.getResetDate().compareTo(previousLimit.getResetDate()), greaterThanOrEqualTo(0));
// Additional checks for record values
assertThat(rateLimit.getCore().getLimit(), equalTo(rateLimit.getLimit()));
assertThat(rateLimit.getCore().getRemaining(), equalTo(rateLimit.getRemaining()));
assertThat(rateLimit.getCore().getResetEpochSeconds(), equalTo(rateLimit.getResetEpochSeconds()));
assertThat(rateLimit.getCore().getResetDate(), equalTo(rateLimit.getResetDate()));
// Additional checks for deprecated values
assertThat(rateLimit.limit, equalTo(rateLimit.getLimit()));
assertThat(rateLimit.remaining, equalTo(rateLimit.getRemaining()));
assertThat(rateLimit.reset.getTime(), equalTo(rateLimit.getResetEpochSeconds()));
}
@Test
public void testGitHubEnterpriseDoesNotHaveRateLimit() throws Exception {
// Customized response that results in file not found the same as GitHub Enterprise
snapshotNotAllowed();
assertThat(mockGitHub.getRequestCount(), equalTo(0));
GHRateLimit rateLimit = null;
Date lastReset = new Date(System.currentTimeMillis() / 1000L);
// Give this a moment
Thread.sleep(1000);
// -------------------------------------------------------------
// Before any queries, rate limit starts as null but may be requested
gitHub = GitHub.connectToEnterprise(mockGitHub.apiServer().baseUrl(), "bogus", "bogus");
assertThat(mockGitHub.getRequestCount(), equalTo(0));
assertThat(gitHub.lastRateLimit(), CoreMatchers.nullValue());
rateLimit = gitHub.rateLimit();
assertThat(rateLimit.getCore(), instanceOf(GHRateLimit.UnknownLimitRecord.class));
assertThat(rateLimit.getLimit(), equalTo(GHRateLimit.UnknownLimitRecord.unknownLimit));
assertThat(rateLimit.getRemaining(), equalTo(GHRateLimit.UnknownLimitRecord.unknownRemaining));
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(1));
lastReset = rateLimit.getResetDate();
assertThat(mockGitHub.getRequestCount(), equalTo(1));
// last is still null, because it actually means lastHeaderRateLimit
assertThat(gitHub.lastRateLimit(), CoreMatchers.nullValue());
assertThat(mockGitHub.getRequestCount(), equalTo(1));
// Give this a moment
Thread.sleep(1000);
// -------------------------------------------------------------
// First call to /user gets response without rate limit information
gitHub = GitHub.connectToEnterprise(mockGitHub.apiServer().baseUrl(), "bogus", "bogus");
gitHub.getMyself();
assertThat(mockGitHub.getRequestCount(), equalTo(2));
assertThat(gitHub.lastRateLimit(), CoreMatchers.nullValue());
rateLimit = gitHub.rateLimit();
assertThat(rateLimit.getCore(), instanceOf(GHRateLimit.UnknownLimitRecord.class));
assertThat(rateLimit.getLimit(), equalTo(GHRateLimit.UnknownLimitRecord.unknownLimit));
assertThat(rateLimit.getRemaining(), equalTo(GHRateLimit.UnknownLimitRecord.unknownRemaining));
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(1));
lastReset = rateLimit.getResetDate();
assertThat(mockGitHub.getRequestCount(), equalTo(3));
// Give this a moment
Thread.sleep(1000);
// Always requests new info
rateLimit = gitHub.getRateLimit();
assertThat(mockGitHub.getRequestCount(), equalTo(4));
assertThat(rateLimit.getCore(), instanceOf(GHRateLimit.UnknownLimitRecord.class));
assertThat(rateLimit.getLimit(), equalTo(GHRateLimit.UnknownLimitRecord.unknownLimit));
assertThat(rateLimit.getRemaining(), equalTo(GHRateLimit.UnknownLimitRecord.unknownRemaining));
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(1));
// Give this a moment
Thread.sleep(1000);
// last is still null, because it actually means lastHeaderRateLimit
assertThat(gitHub.lastRateLimit(), CoreMatchers.nullValue());
// ratelimit() tries not to make additional requests, uses queried rate limit since header not available
Thread.sleep(1000);
assertThat(gitHub.rateLimit(), sameInstance(rateLimit));
// -------------------------------------------------------------
// Second call to /user gets response with rate limit information
gitHub = GitHub.connectToEnterprise(mockGitHub.apiServer().baseUrl(), "bogus", "bogus");
gitHub.getMyself();
assertThat(mockGitHub.getRequestCount(), equalTo(5));
// Since we already had rate limit info these don't request again
rateLimit = gitHub.lastRateLimit();
assertThat(rateLimit, notNullValue());
assertThat(rateLimit.getLimit(), equalTo(5000));
assertThat(rateLimit.getRemaining(), equalTo(4978));
assertThat(rateLimit.getResetDate().compareTo(lastReset), greaterThanOrEqualTo(0));
lastReset = rateLimit.getResetDate();
GHRateLimit headerRateLimit = rateLimit;
// Give this a moment
Thread.sleep(1000);
// ratelimit() uses headerRateLimit if available and headerRateLimit is not expired
assertThat(gitHub.rateLimit(), equalTo(headerRateLimit));
assertThat(mockGitHub.getRequestCount(), equalTo(5));
// Give this a moment
Thread.sleep(1000);
// Always requests new info
rateLimit = gitHub.getRateLimit();
assertThat(mockGitHub.getRequestCount(), equalTo(6));
assertThat(rateLimit.getCore(), instanceOf(GHRateLimit.UnknownLimitRecord.class));
assertThat(rateLimit.getLimit(), equalTo(GHRateLimit.UnknownLimitRecord.unknownLimit));
assertThat(rateLimit.getRemaining(), equalTo(GHRateLimit.UnknownLimitRecord.unknownRemaining));
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(1));
// ratelimit() should prefer headerRateLimit when getRateLimit fails and headerRateLimit is not expired
assertThat(gitHub.rateLimit(), equalTo(headerRateLimit));
assertThat(mockGitHub.getRequestCount(), equalTo(6));
// Wait for the header
Thread.sleep(1000);
}
@Test
public void testGitHubRateLimitWithBadData() throws Exception {
snapshotNotAllowed();
gitHub = getGitHubBuilder().withEndpoint(mockGitHub.apiServer().baseUrl()).build();
gitHub.getMyself();
try {
gitHub.getRateLimit();
fail("Invalid rate limit missing some records should throw");
} catch (Exception e) {
assertThat(e, instanceOf(HttpException.class));
assertThat(e.getCause(), instanceOf(IOException.class));
assertThat(e.getCause().getCause(), instanceOf(ValueInstantiationException.class));
assertThat(e.getCause().getCause().getMessage(),
containsString(
"Cannot construct instance of `org.kohsuke.github.GHRateLimit`, problem: `java.lang.NullPointerException`"));
}
try {
gitHub.getRateLimit();
fail("Invalid rate limit record missing a value should throw");
} catch (Exception e) {
assertThat(e, instanceOf(HttpException.class));
assertThat(e.getCause(), instanceOf(IOException.class));
assertThat(e.getCause().getCause(), instanceOf(MismatchedInputException.class));
assertThat(e.getCause().getCause().getMessage(),
containsString("Missing required creator property 'reset' (index 2)"));
}
}
// These tests should behave the same, showing server time adjustment working
@Test
public void testGitHubRateLimitExpirationServerFiveMinutesAhead() throws Exception {
executeExpirationTest();
}
@Test
public void testGitHubRateLimitExpirationServerFiveMinutesBehind() throws Exception {
executeExpirationTest();
}
private void executeExpirationTest() throws Exception {
// Customized response that templates the date to keep things working
snapshotNotAllowed();
assertThat(mockGitHub.getRequestCount(), equalTo(0));
GHRateLimit rateLimit = null;
GHRateLimit headerRateLimit = null;
// Give this a moment
Thread.sleep(1000);
// -------------------------------------------------------------
// /user gets response with rate limit information
gitHub = getGitHubBuilder().withEndpoint(mockGitHub.apiServer().baseUrl()).build();
gitHub.getMyself();
assertThat(mockGitHub.getRequestCount(), equalTo(1));
// Since we already had rate limit info these don't request again
headerRateLimit = gitHub.lastRateLimit();
rateLimit = gitHub.rateLimit();
assertThat(rateLimit, notNullValue());
assertThat("rateLimit() selects header instance when not expired, does not ask server",
rateLimit,
sameInstance(headerRateLimit));
// Nothing changes still valid
Thread.sleep(1000);
assertThat("rateLimit() selects header instance when not expired, does not ask server",
gitHub.rateLimit(),
sameInstance(headerRateLimit));
assertThat("rateLimit() selects header instance when not expired, does not ask server",
gitHub.lastRateLimit(),
sameInstance(headerRateLimit));
assertThat(mockGitHub.getRequestCount(), equalTo(1));
// This time, rateLimit() should find an expired record and get a new one.
Thread.sleep(3000);
assertThat("Header instance has expired", gitHub.lastRateLimit().isExpired(), is(true));
assertThat("rateLimit() will ask server when header instance expires and it has not called getRateLimit() yet",
gitHub.rateLimit(),
not(sameInstance(rateLimit)));
assertThat("lastRateLimit() (header instance) is populated as part of internal call to getRateLimit()",
gitHub.lastRateLimit(),
not(sameInstance(rateLimit)));
assertThat("After request, rateLimit() selects header instance since it has been refreshed",
gitHub.rateLimit(),
sameInstance(gitHub.lastRateLimit()));
headerRateLimit = gitHub.lastRateLimit();
assertThat(mockGitHub.getRequestCount(), equalTo(2));
// This time, rateLimit() should find an expired header record, but a valid returned record
Thread.sleep(4000);
rateLimit = gitHub.rateLimit();
// Using custom data to have a header instance that expires before the queried instance
assertThat(
"if header instance expires but queried instance is valid, ratelimit() uses it without asking server",
gitHub.rateLimit(),
not(sameInstance(gitHub.lastRateLimit())));
assertThat("ratelimit() should almost never return a return a GHRateLimit that is already expired",
gitHub.rateLimit().isExpired(),
is(false));
assertThat("Header instance hasn't been reloaded", gitHub.lastRateLimit(), sameInstance(headerRateLimit));
assertThat("Header instance has expired", gitHub.lastRateLimit().isExpired(), is(true));
assertThat(mockGitHub.getRequestCount(), equalTo(2));
// Finally they both expire and rateLimit() should find both expired and get a new record
Thread.sleep(2000);
headerRateLimit = gitHub.rateLimit();
assertThat("rateLimit() has asked server for new information",
gitHub.rateLimit(),
not(sameInstance(rateLimit)));
assertThat("rateLimit() has asked server for new information",
gitHub.lastRateLimit(),
not(sameInstance(rateLimit)));
assertThat("rateLimit() selects header instance when not expired, does not ask server",
gitHub.rateLimit(),
sameInstance((gitHub.lastRateLimit())));
assertThat(mockGitHub.getRequestCount(), equalTo(3));
}
private static GHRepository getRepository(GitHub gitHub) throws IOException {
return gitHub.getOrganization("github-api-test-org").getRepository("github-api");
}
}