feat(license): rewrite LicenseGate around state + effective limits

LicenseGate now exposes getState() (delegates to LicenseStateMachine),
getEffectiveLimits() (merged over DefaultTierLimits in ACTIVE/GRACE,
defaults-only in ABSENT/EXPIRED/INVALID), markInvalid(reason), and
clear(). Internal snapshot is an immutable record-like class swapped
atomically so concurrent reads see a consistent license+reason pair.

Removes the transient openSentinel() and getTier() introduced by
earlier tasks (no production consumers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-26 10:48:56 +02:00
parent ddc0b686c3
commit 0499a54ebc
2 changed files with 85 additions and 24 deletions

View File

@@ -3,30 +3,62 @@ package com.cameleer.server.core.license;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
class LicenseGateTest {
@Test
void noLicense_returnsOpenTier() {
void absent_byDefault() {
LicenseGate gate = new LicenseGate();
assertThat(gate.getTier()).isEqualTo("open");
assertThat(gate.getLimit("max_apps", 99)).isEqualTo(99);
assertThat(gate.getState()).isEqualTo(LicenseState.ABSENT);
assertThat(gate.getEffectiveLimits().get("max_apps"))
.isEqualTo(DefaultTierLimits.DEFAULTS.get("max_apps"));
assertThat(gate.getCurrent()).isNull();
assertThat(gate.getInvalidReason()).isNull();
}
@Test
void loaded_exposesLimits() {
void load_setsActiveAndMergesLimits() {
LicenseGate gate = new LicenseGate();
LicenseInfo info = new LicenseInfo(java.util.UUID.randomUUID(), "acme", "MID",
Map.of("max_agents", 10, "retention_days", 30),
Instant.now(), Instant.now().plus(365, ChronoUnit.DAYS), 0);
LicenseInfo info = new LicenseInfo(UUID.randomUUID(), "acme", "label",
Map.of("max_apps", 50), Instant.now(),
Instant.now().plusSeconds(86400), 0);
gate.load(info);
assertThat(gate.getTier()).isEqualTo("MID");
assertThat(gate.getLimit("max_agents", 0)).isEqualTo(10);
assertThat(gate.getLimit("missing", 7)).isEqualTo(7);
assertThat(gate.getState()).isEqualTo(LicenseState.ACTIVE);
assertThat(gate.getEffectiveLimits().get("max_apps")).isEqualTo(50);
assertThat(gate.getEffectiveLimits().get("max_users"))
.isEqualTo(DefaultTierLimits.DEFAULTS.get("max_users"));
}
@Test
void markInvalid_overridesActive() {
LicenseGate gate = new LicenseGate();
LicenseInfo info = new LicenseInfo(UUID.randomUUID(), "acme", null,
Map.of("max_apps", 50), Instant.now(),
Instant.now().plusSeconds(86400), 0);
gate.load(info);
gate.markInvalid("signature failed");
assertThat(gate.getState()).isEqualTo(LicenseState.INVALID);
assertThat(gate.getEffectiveLimits().get("max_apps"))
.isEqualTo(DefaultTierLimits.DEFAULTS.get("max_apps"));
assertThat(gate.getInvalidReason()).isEqualTo("signature failed");
}
@Test
void clear_returnsToAbsent() {
LicenseGate gate = new LicenseGate();
LicenseInfo info = new LicenseInfo(UUID.randomUUID(), "acme", null,
Map.of(), Instant.now(),
Instant.now().plusSeconds(86400), 0);
gate.load(info);
gate.clear();
assertThat(gate.getState()).isEqualTo(LicenseState.ABSENT);
assertThat(gate.getCurrent()).isNull();
}
}