Implement authorization import and revocation checks (T-503)
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"softbox.local/core/licensing"
|
||||
)
|
||||
|
||||
const applicationTestMachineHash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
||||
func TestAuthorizationServiceUsesCurrentGraceAndRevocationStates(t *testing.T) {
|
||||
now := time.Date(2026, 7, 20, 12, 0, 0, 0, time.UTC)
|
||||
store := &authorizationStoreFake{
|
||||
licenses: []licensing.License{{
|
||||
LicenseID: "lic-active", Products: []string{"product-active"}, Perpetual: true, RebindPolicy: "support-only",
|
||||
}, {
|
||||
LicenseID: "lic-revoked", Products: []string{"product-revoked"}, RebindPolicy: "support-only",
|
||||
}},
|
||||
revocations: licensing.RevocationList{
|
||||
GeneratedAt: now.Add(-2 * time.Hour), ExpiresAt: now.Add(time.Hour), RevokedLicenseIDs: []string{"lic-revoked"},
|
||||
},
|
||||
found: true,
|
||||
}
|
||||
service := newAuthorizationServiceForTest(t, store, now)
|
||||
snapshot, err := service.Snapshot(context.Background())
|
||||
if err != nil || snapshot.State != AuthorizationStateReady || len(snapshot.Products) != 1 || snapshot.Products[0].ProductID != "product-active" {
|
||||
t.Fatalf("Snapshot() = (%#v, %v)", snapshot, err)
|
||||
}
|
||||
if authorized, err := service.IsAuthorized("product-active"); err != nil || !authorized {
|
||||
t.Fatalf("IsAuthorized(active) = (%t, %v)", authorized, err)
|
||||
}
|
||||
if authorized, err := service.IsAuthorized("product-revoked"); err != nil || authorized {
|
||||
t.Fatalf("IsAuthorized(revoked) = (%t, %v)", authorized, err)
|
||||
}
|
||||
|
||||
store.revocations.ExpiresAt = now.Add(-time.Hour)
|
||||
snapshot, err = service.Snapshot(context.Background())
|
||||
if err != nil || snapshot.State != AuthorizationStateGrace {
|
||||
t.Fatalf("Snapshot(grace) = (%#v, %v)", snapshot, err)
|
||||
}
|
||||
store.revocations.ExpiresAt = now.Add(-licensing.RevocationGrace - time.Second)
|
||||
snapshot, err = service.Snapshot(context.Background())
|
||||
if !errors.Is(err, ErrAuthorizationUnavailable) || snapshot.State != AuthorizationStateUnavailable {
|
||||
t.Fatalf("Snapshot(stale) = (%#v, %v)", snapshot, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizationServiceFailsClosedWithoutRevocations(t *testing.T) {
|
||||
now := time.Date(2026, 7, 20, 12, 0, 0, 0, time.UTC)
|
||||
store := &authorizationStoreFake{licenses: []licensing.License{{
|
||||
LicenseID: "lic-active", Products: []string{"product-active"}, RebindPolicy: "support-only",
|
||||
}}}
|
||||
service := newAuthorizationServiceForTest(t, store, now)
|
||||
snapshot, err := service.Snapshot(context.Background())
|
||||
if !errors.Is(err, ErrAuthorizationUnavailable) || snapshot.State != AuthorizationStateUnavailable {
|
||||
t.Fatalf("Snapshot() = (%#v, %v)", snapshot, err)
|
||||
}
|
||||
if authorized, err := service.IsAuthorized("product-active"); authorized || !errors.Is(err, ErrAuthorizationUnavailable) {
|
||||
t.Fatalf("IsAuthorized() = (%t, %v)", authorized, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizationEventsAndBootstrapAreSanitized(t *testing.T) {
|
||||
snapshot := AuthorizationSnapshot{
|
||||
State: AuthorizationStateReady, MachineHash: applicationTestMachineHash,
|
||||
Products: []AuthorizedProduct{{ProductID: "product-test", Kind: LicenseKindPerpetual, RebindPolicy: "support-only"}},
|
||||
}
|
||||
event := NewAuthorizationEvent(snapshot)
|
||||
snapshot.Products[0].ProductID = "mutated"
|
||||
parsed, handled, err := ParseAuthorizationEvent(event)
|
||||
if err != nil || !handled || parsed.Snapshot.Products[0].ProductID != "product-test" {
|
||||
t.Fatalf("ParseAuthorizationEvent() = (%#v, %t, %v)", parsed, handled, err)
|
||||
}
|
||||
if _, _, err := ParseAuthorizationEvent(Event{Type: EventLicenseChanged, Payload: "raw license"}); !errors.Is(err, ErrAuthorizationEventPayload) {
|
||||
t.Fatalf("ParseAuthorizationEvent(bad) error = %v", err)
|
||||
}
|
||||
|
||||
runtime := NewRuntime(1)
|
||||
err = NewAuthorizationBootstrap(UnconfiguredAuthorizationLoader{}, runtime).Run(context.Background())
|
||||
if !errors.Is(err, ErrAuthorizationSourceUnconfigured) {
|
||||
t.Fatalf("Bootstrap.Run() error = %v", err)
|
||||
}
|
||||
event = <-runtime.Events()
|
||||
payload, handled, err := ParseAuthorizationEvent(event)
|
||||
if err != nil || !handled || payload.Snapshot.State != AuthorizationStateUnconfigured {
|
||||
t.Fatalf("unconfigured event = (%#v, %t, %v)", payload, handled, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLicenseImportPublishesSanitizedFailure(t *testing.T) {
|
||||
now := time.Date(2026, 7, 20, 12, 0, 0, 0, time.UTC)
|
||||
service := newAuthorizationServiceForTest(t, &authorizationStoreFake{}, now)
|
||||
runtime := NewRuntime(1)
|
||||
err := NewLicenseImport(service, licenseSourceFake{document: []byte(`{"secret":"never publish"}`)}, runtime).Run(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("Run() unexpectedly succeeded")
|
||||
}
|
||||
payload, handled, parseErr := ParseAuthorizationEvent(<-runtime.Events())
|
||||
if parseErr != nil || !handled || payload.Snapshot.State != AuthorizationStateImportFailed ||
|
||||
payload.Snapshot.MachineHash != applicationTestMachineHash || len(payload.Snapshot.Products) != 0 {
|
||||
t.Fatalf("import event = (%#v, %t, %v)", payload, handled, parseErr)
|
||||
}
|
||||
}
|
||||
|
||||
func newAuthorizationServiceForTest(t *testing.T, store *authorizationStoreFake, now time.Time) *AuthorizationService {
|
||||
t.Helper()
|
||||
service, err := NewAuthorizationService(AuthorizationServiceConfig{
|
||||
Store: store, MachineHash: applicationTestMachineHash, Clock: func() time.Time { return now },
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
type authorizationStoreFake struct {
|
||||
licenses []licensing.License
|
||||
revocations licensing.RevocationList
|
||||
found bool
|
||||
listErr error
|
||||
revokedErr error
|
||||
}
|
||||
|
||||
type licenseSourceFake struct {
|
||||
document []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (source licenseSourceFake) ReadLicense(context.Context) ([]byte, error) {
|
||||
return append([]byte(nil), source.document...), source.err
|
||||
}
|
||||
|
||||
func (store *authorizationStoreFake) Import(_ []byte, _ licensing.Verifier, _ string) (licensing.License, bool, error) {
|
||||
return licensing.License{}, false, errors.New("not used")
|
||||
}
|
||||
|
||||
func (store *authorizationStoreFake) List(_ licensing.Verifier, _ string) ([]licensing.License, error) {
|
||||
if store.listErr != nil {
|
||||
return nil, store.listErr
|
||||
}
|
||||
licenses := append([]licensing.License(nil), store.licenses...)
|
||||
for index := range licenses {
|
||||
licenses[index].Products = append([]string(nil), licenses[index].Products...)
|
||||
}
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
func (store *authorizationStoreFake) StoreRevocations(_ []byte, _ licensing.RevocationVerifier) (licensing.RevocationList, error) {
|
||||
return licensing.RevocationList{}, errors.New("not used")
|
||||
}
|
||||
|
||||
func (store *authorizationStoreFake) LoadRevocations(_ licensing.RevocationVerifier) (licensing.RevocationList, bool, error) {
|
||||
if store.revokedErr != nil {
|
||||
return licensing.RevocationList{}, false, store.revokedErr
|
||||
}
|
||||
list := store.revocations
|
||||
list.RevokedLicenseIDs = append([]string(nil), list.RevokedLicenseIDs...)
|
||||
return list, store.found, nil
|
||||
}
|
||||
Reference in New Issue
Block a user