176 lines
5.6 KiB
Go
176 lines
5.6 KiB
Go
package apiapproval
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.julianfamily.org/keepassgo/internal/apitokens"
|
|
)
|
|
|
|
func TestBrokerCreatesPendingRequestAndAllowsOnce(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
broker := NewBroker(time.Minute)
|
|
broker.now = func() time.Time { return time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC) }
|
|
|
|
resultCh := make(chan Result, 1)
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
result, err := broker.Request(context.Background(), apitokens.Token{ID: "token-1", Name: "CLI", ClientName: "grpc-cli"}, apitokens.OperationListEntries, apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root", "Internet"}})
|
|
resultCh <- result
|
|
errCh <- err
|
|
}()
|
|
|
|
waitForPending(t, broker, 1)
|
|
pending := broker.Pending()
|
|
if len(pending) != 1 {
|
|
t.Fatalf("Pending() len = %d, want 1", len(pending))
|
|
}
|
|
if pending[0].TokenID != "token-1" {
|
|
t.Fatalf("Pending()[0].TokenID = %q, want token-1", pending[0].TokenID)
|
|
}
|
|
|
|
if _, _, err := broker.Resolve(pending[0].ID, OutcomeAllowOnce); err != nil {
|
|
t.Fatalf("Resolve(allow once) error = %v", err)
|
|
}
|
|
|
|
result := <-resultCh
|
|
if err := <-errCh; err != nil {
|
|
t.Fatalf("Request() error = %v, want nil", err)
|
|
}
|
|
if result.Outcome != OutcomeAllowOnce {
|
|
t.Fatalf("Request() outcome = %q, want %q", result.Outcome, OutcomeAllowOnce)
|
|
}
|
|
if result.Rule != nil {
|
|
t.Fatalf("Request() rule = %#v, want nil for allow-once", result.Rule)
|
|
}
|
|
if got := broker.Pending(); len(got) != 0 {
|
|
t.Fatalf("Pending() after allow len = %d, want 0", len(got))
|
|
}
|
|
}
|
|
|
|
func TestBrokerReturnsPermanentRuleForDeny(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
broker := NewBroker(time.Minute)
|
|
reqDone := make(chan struct{})
|
|
var result Result
|
|
var err error
|
|
go func() {
|
|
result, err = broker.Request(context.Background(), apitokens.Token{ID: "token-1", Name: "CLI"}, apitokens.OperationReadEntry, apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: "entry-1", Path: []string{"Root", "Internet"}})
|
|
close(reqDone)
|
|
}()
|
|
|
|
waitForPending(t, broker, 1)
|
|
pending := broker.Pending()[0]
|
|
request, rule, resolveErr := broker.Resolve(pending.ID, OutcomeDenyPermanent)
|
|
if resolveErr != nil {
|
|
t.Fatalf("Resolve(deny permanent) error = %v", resolveErr)
|
|
}
|
|
if request.ID != pending.ID {
|
|
t.Fatalf("Resolve().ID = %q, want %q", request.ID, pending.ID)
|
|
}
|
|
if rule == nil || rule.Effect != apitokens.EffectDeny || rule.Operation != apitokens.OperationReadEntry {
|
|
t.Fatalf("Resolve() rule = %#v, want deny read-entry rule", rule)
|
|
}
|
|
|
|
<-reqDone
|
|
if !errors.Is(err, ErrRequestDenied) {
|
|
t.Fatalf("Request() error = %v, want ErrRequestDenied", err)
|
|
}
|
|
if result.Rule == nil || result.Rule.Effect != apitokens.EffectDeny {
|
|
t.Fatalf("Request() rule = %#v, want deny rule", result.Rule)
|
|
}
|
|
if result.Outcome != OutcomeDenyPermanent {
|
|
t.Fatalf("Request() outcome = %q, want %q", result.Outcome, OutcomeDenyPermanent)
|
|
}
|
|
}
|
|
|
|
func TestBrokerSupportsCancellation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
broker := NewBroker(time.Minute)
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := broker.Request(context.Background(), apitokens.Token{ID: "token-1", Name: "CLI"}, apitokens.OperationListGroups, apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}})
|
|
errCh <- err
|
|
}()
|
|
|
|
waitForPending(t, broker, 1)
|
|
if _, _, err := broker.Resolve(broker.Pending()[0].ID, OutcomeCancel); err != nil {
|
|
t.Fatalf("Resolve(cancel) error = %v", err)
|
|
}
|
|
if err := <-errCh; !errors.Is(err, ErrRequestCanceled) {
|
|
t.Fatalf("Request() error = %v, want ErrRequestCanceled", err)
|
|
}
|
|
}
|
|
|
|
func TestBrokerTimesOutPendingRequests(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
broker := NewBroker(10 * time.Millisecond)
|
|
_, err := broker.Request(context.Background(), apitokens.Token{ID: "token-1", Name: "CLI"}, apitokens.OperationListGroups, apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}})
|
|
if !errors.Is(err, ErrRequestTimedOut) {
|
|
t.Fatalf("Request() error = %v, want ErrRequestTimedOut", err)
|
|
}
|
|
if got := broker.Pending(); len(got) != 0 {
|
|
t.Fatalf("Pending() len after timeout = %d, want 0", len(got))
|
|
}
|
|
}
|
|
|
|
func TestNilBrokerReturnsConfigurationError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var broker *Broker
|
|
_, err := broker.Request(context.Background(), apitokens.Token{ID: "token-1", Name: "CLI"}, apitokens.OperationListGroups, apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}})
|
|
if !errors.Is(err, ErrBrokerNotConfigured) {
|
|
t.Fatalf("Request(nil broker) error = %v, want %v", err, ErrBrokerNotConfigured)
|
|
}
|
|
}
|
|
|
|
func TestBrokerNotifiesWhenPendingRequestsChange(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
broker := NewBroker(time.Minute)
|
|
changes := make(chan int, 4)
|
|
broker.SetChangeNotifier(func() {
|
|
changes <- len(broker.Pending())
|
|
})
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := broker.Request(context.Background(), apitokens.Token{ID: "token-1", Name: "CLI"}, apitokens.OperationListGroups, apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}})
|
|
errCh <- err
|
|
}()
|
|
|
|
waitForPending(t, broker, 1)
|
|
if _, _, err := broker.Resolve(broker.Pending()[0].ID, OutcomeAllowOnce); err != nil {
|
|
t.Fatalf("Resolve(allow once) error = %v", err)
|
|
}
|
|
if err := <-errCh; err != nil {
|
|
t.Fatalf("Request() error = %v, want nil", err)
|
|
}
|
|
|
|
got := []int{<-changes, <-changes}
|
|
slices.Sort(got)
|
|
if !slices.Equal(got, []int{0, 1}) {
|
|
t.Fatalf("change notifications = %v, want [0 1]", got)
|
|
}
|
|
}
|
|
|
|
func waitForPending(t *testing.T, broker *Broker, want int) {
|
|
t.Helper()
|
|
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if got := len(broker.Pending()); got == want {
|
|
return
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
t.Fatalf("Pending() never reached len %d", want)
|
|
}
|