feat(self-operated): persist continuous collection and frozen coverage
This commit is contained in:
+13
-2
@@ -7,12 +7,23 @@ import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class LingxingEgressPropertiesTest {
|
||||
@Test void continuousBusinessFlagBindsOnlyWhenExplicitlyEnabled() {
|
||||
String prefix = "baiyue.integration.lingxing-wms.egress";
|
||||
var input = new java.util.HashMap<String, String>();
|
||||
input.put(prefix + ".collection-mode", "BUSINESS");
|
||||
var source = new org.springframework.boot.context.properties.source.MapConfigurationPropertySource(input);
|
||||
var binder = new org.springframework.boot.context.properties.bind.Binder(source);
|
||||
var target = org.springframework.boot.context.properties.bind.Bindable.of(LingxingEgressProperties.class);
|
||||
assertThat(binder.bind(prefix, target).get().continuousBusinessCollectionEnabled()).isFalse();
|
||||
source.put(prefix + ".continuous-business-collection-enabled", "true");
|
||||
assertThat(binder.bind(prefix, target).get().continuousBusinessCollectionEnabled()).isTrue();
|
||||
}
|
||||
private LingxingEgressProperties properties(CollectionMode mode, Duration interval, Duration cooldown) {
|
||||
return new LingxingEgressProperties(mode, "test", null, null, null, null, interval, cooldown);
|
||||
return new LingxingEgressProperties(mode, "test", null, null, null, null, interval, cooldown, false);
|
||||
}
|
||||
|
||||
@Test void businessAcceptsPositiveMillisecondIntervalsIncludingProbeSteps() {
|
||||
for (long millis : new long[]{1, 1500, 2000, 3000, 5000, 300000}) {
|
||||
for (long millis : new long[]{1, 1200, 1500, 2000, 3000, 5000, 300000}) {
|
||||
assertThat(properties(CollectionMode.BUSINESS, Duration.ofMillis(millis), null).minStartInterval())
|
||||
.isEqualTo(Duration.ofMillis(millis));
|
||||
}
|
||||
|
||||
+85
-3
@@ -54,7 +54,88 @@ class LingxingEgressGateTest {
|
||||
private LingxingEgressGate gate() { return new LingxingEgressGate(props(CollectionMode.VALIDATION, "test"), ledger, clock, clock::advanceMillis); }
|
||||
static EgressContext context(String attempt) { return new EgressContext("demo-account", CollectionMode.VALIDATION, "root", "session", attempt, "demo-grant"); }
|
||||
static LingxingEgressProperties props(CollectionMode mode, String environment) {
|
||||
return new LingxingEgressProperties(mode, environment, "demo-account", "demo-grant", NOW.plusSeconds(3600), URI.create("http://127.0.0.1"), null, null);
|
||||
return new LingxingEgressProperties(mode, environment, "demo-account", "demo-grant", NOW.plusSeconds(3600), URI.create("http://127.0.0.1"), null, null, false);
|
||||
}
|
||||
private LingxingEgressGate continuousGate(CollectionMode mode, String grant, Instant until) {
|
||||
return new LingxingEgressGate(new LingxingEgressProperties(mode, "test", "demo-account", grant, until,
|
||||
URI.create("http://127.0.0.1"), null, null, true), ledger, clock, clock::advanceMillis);
|
||||
}
|
||||
@Test void continuousBusinessCrossesOldGrantExpiryButKeepsFiniteSingleUsePermits() {
|
||||
var gate = continuousGate(CollectionMode.BUSINESS, "demo-grant", NOW.minusSeconds(1));
|
||||
assertThat(gate.execute(businessContext("continuous"), Operation.WAREHOUSES, HASH,
|
||||
h -> { h.startHttp(); return h.metadata(); }).actualHttpAttempts()).isEqualTo(1);
|
||||
clock.advanceMillis(86400000);
|
||||
var restarted = continuousGate(CollectionMode.BUSINESS, "demo-grant", null);
|
||||
assertThat(restarted.execute(businessContext("next-day"), Operation.WAREHOUSES, HASH,
|
||||
h -> { h.startHttp(); return h.metadata(); }).actualHttpAttempts()).isEqualTo(1);
|
||||
ledger.mutation = p -> new AttemptPermit(p.context(), p.operation(), p.requestHash(), 1,
|
||||
clock.instant().plusSeconds(31), true, true);
|
||||
assertThatThrownBy(() -> restarted.execute(businessContext("too-long"), Operation.WAREHOUSES, HASH, h -> true))
|
||||
.hasMessageContaining("BUDGET_PERMIT_INVALID");
|
||||
assertThat(ledger.completions).hasSize(2);
|
||||
}
|
||||
@Test void continuousFlagNeverReplacesMatchingGrantOrFiniteValidationWindow() {
|
||||
for (String grant : Arrays.asList(null, "", "other-grant")) {
|
||||
var gate = continuousGate(CollectionMode.BUSINESS, grant, null);
|
||||
assertThatThrownBy(() -> gate.execute(businessContext("missing-grant"), Operation.WAREHOUSES, HASH, h -> true))
|
||||
.hasMessageContaining("EXCLUSIVE_WINDOW_UNCONFIRMED");
|
||||
}
|
||||
for (Instant until : Arrays.asList(null, NOW)) {
|
||||
var gate = continuousGate(CollectionMode.VALIDATION, "demo-grant", until);
|
||||
assertThatThrownBy(() -> gate.execute(context("validation"), Operation.WAREHOUSES, HASH, h -> true))
|
||||
.hasMessageContaining("EXCLUSIVE_WINDOW_UNCONFIRMED");
|
||||
}
|
||||
assertThat(ledger.authorizations).isZero();
|
||||
}
|
||||
@Test void continuousBusinessStillPausesOnUnconfirmedCompletion() {
|
||||
var gate = continuousGate(CollectionMode.BUSINESS, "demo-grant", null);
|
||||
ledger.failComplete = true;
|
||||
assertThatThrownBy(() -> gate.execute(businessContext("lost"), Operation.WAREHOUSES, HASH,
|
||||
h -> { h.startHttp(); return true; })).hasMessageContaining("BUDGET_COMPLETION_UNCONFIRMED");
|
||||
ledger.failComplete = false; clock.advanceMillis(86400000);
|
||||
assertThatThrownBy(() -> gate.execute(businessContext("after-lost"), Operation.WAREHOUSES, HASH, h -> true))
|
||||
.hasMessageContaining("ACCOUNT_PAUSED");
|
||||
assertThat(ledger.authorizations).isEqualTo(1);
|
||||
}
|
||||
@Test void disabledContinuousBusinessStillRequiresUnexpiredGrant() {
|
||||
var gate = new LingxingEgressGate(new LingxingEgressProperties(CollectionMode.BUSINESS, "test",
|
||||
"demo-account", "demo-grant", NOW, URI.create("http://127.0.0.1"), Duration.ofMillis(1200), null, false),
|
||||
ledger, clock, clock::advanceMillis);
|
||||
assertThatThrownBy(() -> gate.execute(businessContext("disabled"), Operation.WAREHOUSES, HASH, h -> true))
|
||||
.hasMessageContaining("EXCLUSIVE_WINDOW_UNCONFIRMED");
|
||||
assertThat(ledger.authorizations).isZero();
|
||||
}
|
||||
@Test void continuousBusinessRetains1200MillisSpacingAndOneHttpPerPermit() {
|
||||
var gate = new LingxingEgressGate(new LingxingEgressProperties(CollectionMode.BUSINESS, "test",
|
||||
"demo-account", "demo-grant", null, URI.create("http://127.0.0.1"), Duration.ofMillis(1200), null, true),
|
||||
ledger, clock, clock::advanceMillis);
|
||||
gate.execute(businessContext("first-spaced"), Operation.WAREHOUSES, HASH, h -> { h.startHttp(); return true; });
|
||||
gate.execute(businessContext("second-spaced"), Operation.WAREHOUSES, HASH, h -> { h.startHttp(); return true; });
|
||||
assertThat(Duration.between(NOW, clock.instant()).toMillis()).isGreaterThanOrEqualTo(1200);
|
||||
assertThatThrownBy(() -> gate.execute(businessContext("one-send"), Operation.WAREHOUSES, HASH,
|
||||
h -> { h.startHttp(); h.startHttp(); return true; })).hasMessageContaining("ATTEMPT_NOT_SENDABLE");
|
||||
assertThat(ledger.completions).hasSize(3).allSatisfy(c -> assertThat(c.actualHttpAttempts()).isEqualTo(1));
|
||||
}
|
||||
@Test void continuousBusinessCannotSendWithExpiredPermitAfterSigning() {
|
||||
var gate = continuousGate(CollectionMode.BUSINESS, "demo-grant", null);
|
||||
assertThatThrownBy(() -> gate.execute(businessContext("expired-permit"), Operation.OUTBOUND_DETAIL, HASH,
|
||||
h -> { clock.advanceMillis(30000); h.startHttp(); return true; })).hasMessageContaining("ATTEMPT_NOT_SENDABLE");
|
||||
assertThat(ledger.completions).singleElement().satisfies(c -> assertThat(c.actualHttpAttempts()).isZero());
|
||||
}
|
||||
@Test void continuousBusinessRateCooldownStillRequiresFreshAuthorityApproval() {
|
||||
var gate = continuousGate(CollectionMode.BUSINESS, "demo-grant", null);
|
||||
assertThrows(LingxingWmsException.class, () -> gate.execute(businessContext("continuous-rate"), Operation.WAREHOUSES, HASH,
|
||||
h -> { h.startHttp(); h.rateLimited(null); throw rateLimit(); }));
|
||||
assertThatThrownBy(() -> gate.execute(businessContext("during-cooldown"), Operation.WAREHOUSES, HASH, h -> true))
|
||||
.hasMessageContaining("ACCOUNT_PAUSED");
|
||||
clock.advanceMillis(300000);ledger.denial = new AuthorizationDenial("ACCOUNT_REQUIRES_REVIEW", false, null);
|
||||
assertThatThrownBy(() -> gate.execute(businessContext("authority-denied"), Operation.WAREHOUSES, HASH, h -> true))
|
||||
.hasMessageContaining("BUDGET_AUTHORIZATION_DENIED");
|
||||
assertThat(ledger.completions).hasSize(1);
|
||||
ledger.denial = null;
|
||||
assertThat(gate.execute(businessContext("fresh-approved"), Operation.WAREHOUSES, HASH,
|
||||
h -> { h.startHttp(); return h.metadata(); }).accountPaused()).isFalse();
|
||||
assertThat(ledger.completions).hasSize(2);
|
||||
}
|
||||
@Test void failureCompletionPreservesNullableTransportAndSourceCodes() {
|
||||
for (Integer status : new Integer[] {null, 503}) {
|
||||
@@ -72,8 +153,9 @@ class LingxingEgressGateTest {
|
||||
}
|
||||
}
|
||||
@Test void defaultOffAndUnknownBusinessEnvironmentsNeverAuthorize() {
|
||||
var defaults = new LingxingEgressProperties(null, null, null, null, null, null, null, null);
|
||||
var defaults = new LingxingEgressProperties(null, null, null, null, null, null, null, null, false);
|
||||
assertThat(defaults.collectionMode()).isEqualTo(CollectionMode.OFF);
|
||||
assertThat(defaults.continuousBusinessCollectionEnabled()).isFalse();
|
||||
assertThat(defaults.minStartInterval()).isEqualTo(Duration.ofSeconds(5));
|
||||
var off = new LingxingEgressGate(defaults, ledger, clock, clock::advanceMillis);
|
||||
assertThatThrownBy(() -> off.execute(context("a"), Operation.WAREHOUSES, HASH, h -> { h.startHttp(); return true; })).hasMessageContaining("EGRESS_OFF");
|
||||
@@ -193,7 +275,7 @@ class LingxingEgressGateTest {
|
||||
} finally { release.countDown(); executor.shutdownNow(); }
|
||||
}
|
||||
@Test void intervalCannotBeConfiguredBelowFiveSecondsAndPropertiesHideGrant() {
|
||||
assertThatThrownBy(() -> new LingxingEgressProperties(CollectionMode.VALIDATION, null, null, null, null, null, Duration.ofMillis(4999), null)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> new LingxingEgressProperties(CollectionMode.VALIDATION, null, null, null, null, null, Duration.ofMillis(4999), null, false)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(props(CollectionMode.VALIDATION, "test").toString()).doesNotContain("demo-grant", "demo-account", "127.0.0.1");
|
||||
}
|
||||
@Test void knownBudgetDenialDoesNotPermanentlyPauseOrConsumeHttp() {
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ class LingxingHttpBudgetAuthorityTest {
|
||||
@AfterEach void stop() { server.stop(0); }
|
||||
private LingxingHttpBudgetAuthority authority(String serviceToken) {
|
||||
return new LingxingHttpBudgetAuthority(new LingxingEgressProperties(CollectionMode.VALIDATION, "test", "demo-account", "demo-grant",
|
||||
LingxingEgressGateTest.NOW.plusSeconds(3600), URI.create("http://127.0.0.1:" + server.getAddress().getPort()), null, null), mapper, serviceToken);
|
||||
LingxingEgressGateTest.NOW.plusSeconds(3600), URI.create("http://127.0.0.1:" + server.getAddress().getPort()), null, null, false), mapper, serviceToken);
|
||||
}
|
||||
private CompletionRequest completion() { return new CompletionRequest(request.context(), request.operation(), request.requestHash(), 1, null, 200, 200, false, null); }
|
||||
private String permit() throws Exception { return mapper.writeValueAsString(new AttemptPermit(request.context(), request.operation(), request.requestHash(), 1,
|
||||
@@ -86,7 +86,7 @@ class LingxingHttpBudgetAuthorityTest {
|
||||
}
|
||||
@Test void missingServerConfiguredAuthorityPreventsHttp() {
|
||||
var authority = new LingxingHttpBudgetAuthority(new LingxingEgressProperties(CollectionMode.VALIDATION, "test", "demo-account", "demo-grant",
|
||||
LingxingEgressGateTest.NOW.plusSeconds(3600), null, null, null), mapper, "synthetic-token");
|
||||
LingxingEgressGateTest.NOW.plusSeconds(3600), null, null, null, false), mapper, "synthetic-token");
|
||||
assertThrows(LingxingWmsException.class, () -> authority.authorize(request)); assertThat(calls.get()).isZero();
|
||||
}
|
||||
@Test void permitRejectsCoercedBooleansExtraFieldsAndInvalidAttemptCount() throws Exception {
|
||||
|
||||
+1
-1
@@ -254,7 +254,7 @@ class LingxingWmsClientTest {
|
||||
var p = new LingxingWmsProperties(true, URI.create("http://127.0.0.1:" + server.getAddress().getPort()),
|
||||
"synthetic-key", "synthetic-secret", Duration.ofSeconds(2), Duration.ofSeconds(2));
|
||||
var props = new LingxingEgressProperties(mode, "production", "demo-account", grant ? "demo-grant" : null,
|
||||
NOW.plusSeconds(3600), budget ? URI.create("http://127.0.0.1") : null, null, null);
|
||||
NOW.plusSeconds(3600), budget ? URI.create("http://127.0.0.1") : null, null, null, false);
|
||||
var ledger = new LingxingEgressGateTest.LedgerAuthority(clock) {
|
||||
@Override public void complete(CompletionRequest request) { super.complete(request); completion.set(request); }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user