137 lines
6.9 KiB
Go
137 lines
6.9 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"agent-system-backend/pkg/extension"
|
|
)
|
|
|
|
type scriptedRecheckProvider struct {
|
|
testProjectContextProvider
|
|
recheck func(context.Context, extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error)
|
|
}
|
|
|
|
func (p scriptedRecheckProvider) Recheck(ctx context.Context, req extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
return p.recheck(ctx, req)
|
|
}
|
|
|
|
func registeredRecheckSlot(t *testing.T, recheck func(context.Context, extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error)) *projectContextSlot {
|
|
t.Helper()
|
|
slot := &projectContextSlot{}
|
|
if err := (pluginRegistry{pluginID: "rdm", caps: hostcaps{contexts: slot}}).RegisterProjectContextProvider(scriptedRecheckProvider{recheck: recheck}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return slot
|
|
}
|
|
|
|
func TestProjectSourceRecheckScopeErrorsAreDistinguishable(t *testing.T) {
|
|
var nilSlot *projectContextSlot
|
|
slot := registeredRecheckSlot(t, func(context.Context, extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
t.Error("unusable recheck scope reached the provider")
|
|
return extension.ProjectContextSourceRecheckResult{}, nil
|
|
})
|
|
cases := map[string]func() (extension.ProjectContextSourceRecheckResult, error){
|
|
"slot missing": func() (extension.ProjectContextSourceRecheckResult, error) {
|
|
return nilSlot.recheck(context.Background(), extension.ProjectContextSourceRecheckRequest{Scope: extension.ProjectContextRequest{Actor: "alice", ProjectID: "p1"}})
|
|
},
|
|
"no actor": func() (extension.ProjectContextSourceRecheckResult, error) {
|
|
return slot.recheck(context.Background(), extension.ProjectContextSourceRecheckRequest{Scope: extension.ProjectContextRequest{ProjectID: "p1"}})
|
|
},
|
|
"no project": func() (extension.ProjectContextSourceRecheckResult, error) {
|
|
return slot.recheck(context.Background(), extension.ProjectContextSourceRecheckRequest{Scope: extension.ProjectContextRequest{Actor: "alice"}})
|
|
},
|
|
"carries goal": func() (extension.ProjectContextSourceRecheckResult, error) {
|
|
return slot.recheck(context.Background(), extension.ProjectContextSourceRecheckRequest{Scope: extension.ProjectContextRequest{Actor: "alice", ProjectID: "p1", Goal: "secret"}})
|
|
},
|
|
}
|
|
seen := map[string]string{}
|
|
for name, call := range cases {
|
|
result, err := call()
|
|
if err == nil {
|
|
t.Fatalf("%s scope accepted", name)
|
|
}
|
|
if result.Bundle.Status != "" || result.Bundle.ProjectID != "" {
|
|
t.Fatalf("%s scope returned a non-empty bundle: %+v", name, result.Bundle)
|
|
}
|
|
if other, dup := seen[err.Error()]; dup {
|
|
t.Fatalf("%s and %s share one error message: %q", other, name, err)
|
|
}
|
|
seen[err.Error()] = name
|
|
}
|
|
if len(seen) != len(cases) {
|
|
t.Fatalf("expected %d distinct errors, got %d: %v", len(cases), len(seen), seen)
|
|
}
|
|
}
|
|
|
|
func TestProjectSourceRecheckFailsClosedAtThreeSecondCeiling(t *testing.T) {
|
|
var remaining time.Duration
|
|
slot := registeredRecheckSlot(t, func(ctx context.Context, req extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
deadline, ok := ctx.Deadline()
|
|
if !ok {
|
|
return extension.ProjectContextSourceRecheckResult{}, errors.New("unbounded recheck")
|
|
}
|
|
remaining = time.Until(deadline)
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(10 * time.Second):
|
|
}
|
|
// A late success must not authorize old machine context.
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.Scope.ProjectID}}, nil
|
|
})
|
|
started := time.Now()
|
|
_, err := slot.recheck(context.Background(), extension.ProjectContextSourceRecheckRequest{Scope: extension.ProjectContextRequest{Actor: "alice", ProjectID: "p1"}})
|
|
if err == nil {
|
|
t.Fatal("source recheck past its deadline returned authorized context")
|
|
}
|
|
if remaining <= 0 || remaining > 3*time.Second {
|
|
t.Fatalf("source recheck deadline is not bounded by 3s: %v", remaining)
|
|
}
|
|
if elapsed := time.Since(started); elapsed > 5*time.Second {
|
|
t.Fatalf("source recheck outlived its 3s ceiling: %v", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestProjectSourceRecheckRejectsProviderRefusalAndForeignBundles(t *testing.T) {
|
|
scope := extension.ProjectContextRequest{Actor: "alice", ProjectID: "p1"}
|
|
answers := map[string]func(extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error){
|
|
"refused": func(extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
return extension.ProjectContextSourceRecheckResult{}, errors.New("provider refused")
|
|
},
|
|
"denied": func(req extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: extension.ProjectContextBundle{Status: extension.ProjectContextDenied, ProjectID: req.Scope.ProjectID}}, nil
|
|
},
|
|
"incomplete": func(req extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: extension.ProjectContextBundle{Status: extension.ProjectContextIncomplete, ProjectID: req.Scope.ProjectID}}, nil
|
|
},
|
|
"other project": func(extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: "p2"}}, nil
|
|
},
|
|
}
|
|
for name, answer := range answers {
|
|
slot := registeredRecheckSlot(t, func(_ context.Context, req extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
return answer(req)
|
|
})
|
|
if _, err := slot.recheck(context.Background(), extension.ProjectContextSourceRecheckRequest{Scope: scope}); err == nil {
|
|
t.Fatalf("%s source recheck accepted", name)
|
|
}
|
|
}
|
|
var sawPlugin string
|
|
slot := registeredRecheckSlot(t, func(_ context.Context, req extension.ProjectContextSourceRecheckRequest) (extension.ProjectContextSourceRecheckResult, error) {
|
|
sawPlugin = req.Scope.PluginID
|
|
return extension.ProjectContextSourceRecheckResult{Bundle: extension.ProjectContextBundle{Status: extension.ProjectContextReady, ProjectID: req.Scope.ProjectID}}, nil
|
|
})
|
|
forged := scope
|
|
forged.PluginID = "forged-plugin"
|
|
if _, err := slot.recheck(context.Background(), extension.ProjectContextSourceRecheckRequest{Scope: forged}); err != nil || sawPlugin != "rdm" {
|
|
t.Fatalf("recheck scope plugin was not the registered holder: plugin=%q err=%v", sawPlugin, err)
|
|
}
|
|
for _, missing := range []extension.ProjectContextRequest{{ProjectID: "p1"}, {Actor: "alice"}} {
|
|
if _, err := slot.recheck(context.Background(), extension.ProjectContextSourceRecheckRequest{Scope: missing}); err == nil {
|
|
t.Fatalf("recheck without trusted actor/project accepted: %+v", missing)
|
|
}
|
|
}
|
|
}
|