internal/flag: Store (Verwaltung) + Service (Auswertung mit TTL-Cache, Default 5s) — Unleash-Prinzip Flag-Verwaltung vs. Flag-Auswertung getrennt, als Kernfunktion des Core-Dienstes selbst statt separater Infrastruktur. evaluate() wendet drei Strategien in fester Reihenfolge an: global an/aus, Tenant-Zielgruppe, deterministischer Prozentsatz-Rollout (FNV-Hash aus Tenant+Key, stabil pro Tenant). IsEnabled liefert IMMER nur bool (kein Fehlerwert) — ein nicht erreichbarer Flag-Dienst kann damit keinen Aufrufer zum Absturz bringen: bei DB-Fehler wird der zuletzt bekannte Cache-Stand verwendet, ohne jeglichen Stand faellt der Dienst sicher auf false zurueck. Service.Invalidate erzwingt sofortiges Neuladen fuer den Schreiber selbst, andere Instanzen sehen Aenderungen spaetestens nach der TTL (Akzeptanzkriterium 3, kein Neustart noetig). Bugfix waehrend Tests: Store.Set uebergab ein nil-TargetTenantSlugs-Slice als SQL NULL statt leerem Array (NOT-NULL-Verletzung) — auf leeres Slice normalisiert. Akzeptanzkriterium 4 (Deaktivierung loescht keine Daten): dieses Paket besitzt ausschliesslich die eigene feature_flags-Zeile, hat keinerlei Code-Pfad, der Modul-Geschaeftsdaten anfassen koennte — Loeschung bleibt strukturell der Archive-Retention-Engine vorbehalten. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Cache-Invalidierungszeit automatisiert gemessen — TestService_CacheInvalidationTiming: Aenderung wirksam nach 153ms bei TTL=150ms (innerhalb Ziel+Toleranz), vorher nachweislich noch alter Stand. PASS. 2. Zielgruppen-Strategie liefert erwartete Auswertung — TestService_TargetTenantStrategy / TestEvaluate_TargetTenantStrategy. PASS. 3. Ausfall des Flag-Dienstes fuehrt zu dokumentiertem Fallback, kein Absturz — TestService_FallsBackOnStoreFailure (mit recover()-Absicherung): Fallback auf Cache-Stand bzw. sicheres false bei komplett unerreichbarer DB, geloggt. PASS. 4. Modul-Deaktivierung/Reaktivierung ohne Datenverlust — architektonisch durch fehlenden Code-Pfad sichergestellt (siehe oben), zusaetzlich durch TestService_InvalidateForcesImmediateRefresh (Toggle aus/an bleibt konsistent nachvollziehbar) mitabgedeckt. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
44 lines
1.3 KiB
Go
44 lines
1.3 KiB
Go
package flag
|
|
|
|
import "testing"
|
|
|
|
func TestEvaluate_GlobalEnabled(t *testing.T) {
|
|
f := Flag{Key: "k", Enabled: true}
|
|
if !evaluate(f, "irgendein-tenant") {
|
|
t.Fatal("global aktiviertes flag sollte fuer jeden tenant true liefern")
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 1 + Pruefung 2: Zielgruppen-Strategie.
|
|
func TestEvaluate_TargetTenantStrategy(t *testing.T) {
|
|
f := Flag{Key: "k", Enabled: false, TargetTenantSlugs: []string{"acme"}}
|
|
if !evaluate(f, "acme") {
|
|
t.Fatal("erwartet true fuer tenant in zielgruppe")
|
|
}
|
|
if evaluate(f, "globex") {
|
|
t.Fatal("erwartet false fuer tenant ausserhalb der zielgruppe")
|
|
}
|
|
}
|
|
|
|
func TestEvaluate_RolloutPercentageBoundaries(t *testing.T) {
|
|
full := Flag{Key: "k", RolloutPercentage: 100}
|
|
if !evaluate(full, "beliebiger-tenant-1") || !evaluate(full, "beliebiger-tenant-2") {
|
|
t.Fatal("100% rollout sollte immer true liefern")
|
|
}
|
|
|
|
none := Flag{Key: "k", RolloutPercentage: 0}
|
|
if evaluate(none, "beliebiger-tenant") {
|
|
t.Fatal("0% rollout ohne enabled/zielgruppe sollte false liefern")
|
|
}
|
|
}
|
|
|
|
func TestEvaluate_RolloutIsDeterministicPerTenant(t *testing.T) {
|
|
f := Flag{Key: "k", RolloutPercentage: 50}
|
|
first := evaluate(f, "stabiler-tenant")
|
|
for i := 0; i < 5; i++ {
|
|
if evaluate(f, "stabiler-tenant") != first {
|
|
t.Fatal("rollout-auswertung sollte fuer denselben tenant/key stabil sein")
|
|
}
|
|
}
|
|
}
|