156 lines
6.9 KiB
Go
156 lines
6.9 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
|
|
"git.ipao.vip/rogee/go-sip/internal/configread"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func approvedTestRequest(t *testing.T, now time.Time) *agentpb.ExecuteApprovedRequest {
|
|
t.Helper()
|
|
taskJSON, err := os.ReadFile("../../contracts/local/examples/config-read-task-asr.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var task configread.CurrentTask
|
|
if err := json.Unmarshal(taskJSON, &task); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rawProviders, err := os.ReadFile("../../contracts/local/examples/config-read-providers.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var payload struct {
|
|
Providers []configread.CurrentProvider `json:"providers"`
|
|
}
|
|
if err := json.Unmarshal(rawProviders, &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
providers := make(map[string]configread.CurrentProvider)
|
|
for _, p := range payload.Providers {
|
|
providers[p.ProviderRef] = p
|
|
}
|
|
providerJSON, err := json.Marshal(providers)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hash, err := configread.ExecutionBindingSHA256(taskJSON, providerJSON, 8)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &agentpb.ExecuteApprovedRequest{
|
|
Meta: testMeta("event-1", "event-1", 1),
|
|
DispatcherId: task.DispatcherID, TenantId: task.TenantID, TaskId: task.TaskID,
|
|
SourceEventId: "event-1", CallId: "event-1", SelectedTrunkId: task.AllowedTrunkIDs[0],
|
|
CallerId: "BD93205882", Callee: "15003164745", DialedCallee: "708915003164745",
|
|
RingTimeoutMs: 10000, MaxCallDurationMs: 20000, DialBeforeUnixMs: now.Add(time.Minute).UnixMilli(),
|
|
SipRevision: 8, TaskConfigJson: taskJSON, ProvidersJson: providerJSON, BindingSha256: hash,
|
|
}
|
|
}
|
|
|
|
func activatedApprovedServer(t *testing.T, now time.Time, statePath string, dispatcherID string, generation uint64, originate func(context.Context, ApprovedExecution) error) *Server {
|
|
t.Helper()
|
|
s := NewServer(ServerOptions{
|
|
Mode: "mock", StatePath: statePath, Now: func() time.Time { return now },
|
|
Status: &agentpb.AgentStatus{AgentId: "agent-1", CellId: "cell-1", BootId: "boot-1"},
|
|
LoadedSIP: func(context.Context) (map[string]int64, error) { return map[string]int64{"trunk-mock": 8}, nil },
|
|
MockApprovedOriginate: originate,
|
|
})
|
|
_, err := s.ActivateAgent(context.Background(), &agentpb.ActivateAgentRequest{
|
|
Meta: testMeta("activate-approved", "", 0),
|
|
Binding: &agentpb.AgentBinding{AgentId: "agent-1", CellId: "cell-1", ExpectedBootId: "boot-1", DispatcherEpoch: "epoch-1", SessionGeneration: generation, DispatcherId: dispatcherID},
|
|
ActivationOperationId: "activate-approved",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestExecuteApprovedBindsConfigAndPreventsRedialAcrossRestart(t *testing.T) {
|
|
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
|
|
req := approvedTestRequest(t, now)
|
|
statePath := filepath.Join(t.TempDir(), "agent-session.json")
|
|
attempts := 0
|
|
originate := func(context.Context, ApprovedExecution) error {
|
|
attempts++
|
|
return errors.New("mock originated; outcome unknown")
|
|
}
|
|
s := activatedApprovedServer(t, now, statePath, req.DispatcherId, 1, originate)
|
|
if _, err := s.ExecuteApproved(context.Background(), req); err == nil || attempts != 1 {
|
|
t.Fatalf("first attempt must preserve unknown outcome: attempts=%d err=%v", attempts, err)
|
|
}
|
|
if _, err := s.ExecuteApproved(context.Background(), req); status.Code(err) != codes.FailedPrecondition || attempts != 1 {
|
|
t.Fatalf("repeat must not redial: attempts=%d err=%v", attempts, err)
|
|
}
|
|
restarted := activatedApprovedServer(t, now, statePath, req.DispatcherId, 2, originate)
|
|
req.Meta.SessionGeneration = 2
|
|
if _, err := restarted.ExecuteApproved(context.Background(), req); status.Code(err) != codes.FailedPrecondition || attempts != 1 {
|
|
t.Fatalf("restart must retain unknown outcome: attempts=%d err=%v", attempts, err)
|
|
}
|
|
entries, err := os.ReadDir(statePath + ".approved")
|
|
if err != nil || len(entries) != 1 {
|
|
t.Fatalf("one durable execution identity expected: %d %v", len(entries), err)
|
|
}
|
|
body, err := os.ReadFile(filepath.Join(statePath+".approved", entries[0].Name()))
|
|
if err != nil || strings.Contains(string(body), "example-only-not-a-real-secret") {
|
|
t.Fatalf("journal must not persist raw provider credentials: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestExecuteApprovedRejectsMismatchedIdentityAndCredentialsBeforeAttempt(t *testing.T) {
|
|
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
|
|
req := approvedTestRequest(t, now)
|
|
attempts := 0
|
|
s := activatedApprovedServer(t, now, filepath.Join(t.TempDir(), "agent-session.json"), req.DispatcherId, 1,
|
|
func(context.Context, ApprovedExecution) error { attempts++; return nil })
|
|
original := req.BindingSha256
|
|
req.ProvidersJson = append(append([]byte(nil), req.ProvidersJson...), ' ')
|
|
if _, err := s.ExecuteApproved(context.Background(), req); status.Code(err) != codes.FailedPrecondition || attempts != 0 {
|
|
t.Fatalf("altered provider bytes must fail binding: attempts=%d err=%v", attempts, err)
|
|
}
|
|
req.ProvidersJson = req.ProvidersJson[:len(req.ProvidersJson)-1]
|
|
req.DispatcherId = "unauthorized-dispatcher"
|
|
if _, err := s.ExecuteApproved(context.Background(), req); status.Code(err) != codes.PermissionDenied || attempts != 0 {
|
|
t.Fatalf("unauthorized Dispatcher must fail session binding: attempts=%d err=%v", attempts, err)
|
|
}
|
|
req.DispatcherId = approvedTestRequest(t, now).DispatcherId
|
|
req.BindingSha256 = original
|
|
req.SipRevision = 9
|
|
if _, err := s.ExecuteApproved(context.Background(), req); status.Code(err) != codes.FailedPrecondition || attempts != 0 {
|
|
t.Fatalf("unloaded SIP revision must reject: attempts=%d err=%v", attempts, err)
|
|
}
|
|
}
|
|
|
|
func TestGetLoadedSIPRequiresActiveSessionAndReportsActualRevision(t *testing.T) {
|
|
now := time.Date(2026, 9, 20, 10, 0, 0, 0, time.UTC)
|
|
req := approvedTestRequest(t, now)
|
|
s := NewServer(ServerOptions{Mode: "mock", StatePath: filepath.Join(t.TempDir(), "agent-session.json"),
|
|
Now: func() time.Time { return now }, Status: &agentpb.AgentStatus{AgentId: "agent-1", CellId: "cell-1", BootId: "boot-1"},
|
|
LoadedSIP: func(context.Context) (map[string]int64, error) { return map[string]int64{req.SelectedTrunkId: 8}, nil },
|
|
})
|
|
if _, err := s.GetLoadedSIP(context.Background(), &agentpb.GetLoadedSIPRequest{Meta: req.Meta}); err == nil {
|
|
t.Fatal("unactivated caller must not inspect loaded SIP")
|
|
}
|
|
_, err := s.ActivateAgent(context.Background(), &agentpb.ActivateAgentRequest{Meta: testMeta("activate-loaded", "", 0),
|
|
Binding: &agentpb.AgentBinding{AgentId: "agent-1", CellId: "cell-1", ExpectedBootId: "boot-1", DispatcherEpoch: "epoch-1", SessionGeneration: 1, DispatcherId: req.DispatcherId},
|
|
ActivationOperationId: "activate-loaded"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := s.GetLoadedSIP(context.Background(), &agentpb.GetLoadedSIPRequest{Meta: req.Meta})
|
|
if err != nil || result.TrunkRevision[req.SelectedTrunkId] != 8 {
|
|
t.Fatalf("must report live loaded revision: %v %v", result, err)
|
|
}
|
|
}
|