package dispatcher import ( "context" "encoding/json" "errors" "log/slog" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "git.ipao.vip/rogee/go-sip/internal/configread" ) func TestSIPNotificationPersistsBarrierAndWaitsForLoadedFullSnapshot(t *testing.T) { executor, _, _, s := newExecuteFixture(t) approved := policySnapshot(t).SIP changed := approved changed.Revision = 9 newSIP, err := json.Marshal(changed) if err != nil { t.Fatal(err) } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") var body []byte switch r.URL.Path { case "/internal/v1/dispatcher/sip": body = newSIP default: t.Errorf("unexpected HTTP path %s", r.URL.Path) w.WriteHeader(http.StatusNotFound) return } _, _ = w.Write(body) })) defer server.Close() client, err := configread.NewClient(server.URL, executor.DispatcherID, "test-secret", server.Client()) if err != nil { t.Fatal(err) } var loaded atomic.Bool var applied atomic.Int32 verify := func(_ context.Context, sip configread.SIP) error { if applied.Load() == 0 { return errors.New("SIP load checked before native apply") } if sip.Revision != 9 || !loaded.Load() { return errors.New("Agent and Asterisk have not applied SIP revision 9") } return nil } runtime := &Runtime{Bootstrap: Bootstrap{DispatcherID: executor.DispatcherID, Client: client, Store: s, ApplySIP: func(_ context.Context, sip configread.SIP) error { if sip.Revision != 9 { return errors.New("wrong SIP revision sent to Agent") } applied.Add(1) if !loaded.Load() { return errors.New("native Asterisk SIP reload is still unavailable") } return nil }, VerifySIP: verify}, Logger: slog.Default()} body := []byte(strings.Replace(string(configExample(t, "mq-sip-change")), `"revision":8`, `"revision":9`, 1)) if err := runtime.handleControl(context.Background(), "", body); err != nil { t.Fatal(err) } if admitted, err := s.CanAdmit(executor.DispatcherID, 1001, "task-asr"); err != nil || admitted { t.Fatalf("SIP notification did not close admission: %v %v", admitted, err) } if applied, pending, err := s.SIPState(executor.DispatcherID); err != nil || applied != 8 || pending != 9 { t.Fatalf("notification not durable before ACK: %d %d %v", applied, pending, err) } if err := runtime.refreshSIP(context.Background(), &approved); err != nil { t.Fatal(err) } if admitted, err := s.CanAdmit(executor.DispatcherID, 1001, "task-asr"); err != nil || admitted { t.Fatalf("unloaded SIP reopened admission: %v %v", admitted, err) } if applied.Load() == 0 { t.Fatal("notification never sent to Agent") } loaded.Store(true) if err := runtime.refreshSIP(context.Background(), &approved); err != nil { t.Fatal(err) } if admitted, err := s.CanAdmit(executor.DispatcherID, 1001, "task-asr"); err != nil || !admitted { t.Fatalf("verified SIP did not reopen: %v %v", admitted, err) } if approved.Revision != 9 { t.Fatalf("reloaded SIP not bound: rev=%d", approved.Revision) } bound, err := s.ReadSnapshot(executor.DispatcherID, 1001, "task-asr") if err != nil || bound.SIP.Revision != 9 { t.Fatalf("existing task did not inherit the approved SIP revision: %+v %v", bound.SIP, err) } }