Files
go-sip/internal/store/event_identity_test.go
T

246 lines
11 KiB
Go

package store
import (
"bytes"
"database/sql"
"encoding/json"
"errors"
"testing"
"git.ipao.vip/rogee/go-sip/internal/contract"
)
func TestSourceEventIdentitySurvivesResultAndRestart(t *testing.T) {
for _, id := range []string{"call-numberID-0000123", "result-numberID-0000123", "NumberID:0000123/Case", " 接收-numberID-0000123 "} {
t.Run(id, func(t *testing.T) {
s := preparedCurrentCallStore(t)
cmd := currentResultCall(t, s, id, true)
result, created, err := s.RecordCallResult(cmd.DispatcherID, id, currentResultPayload(t))
if err != nil || !created || result.EventID != id {
t.Fatalf("source event ID changed: want=%q got=%q created=%t err=%v", id, result.EventID, created, err)
}
pending, err := s.ListPendingOutbox(cmd.DispatcherID)
if err != nil || len(pending) != 2 {
t.Fatalf("ack/result missing: %+v %v", pending, err)
}
for _, event := range pending {
var envelope struct {
EventID string `json:"event_id"`
}
if err := json.Unmarshal(event.Body, &envelope); err != nil || envelope.EventID != id || event.EventID != id {
t.Fatalf("feedback changed source ID: %+v %v", event, err)
}
}
path := s.path
if err := s.Close(); err != nil {
t.Fatal(err)
}
restarted, err := Open(path)
if err != nil {
t.Fatal(err)
}
defer restarted.Close()
again, created, err := restarted.RecordCallResult(cmd.DispatcherID, id, currentResultPayload(t))
if err != nil || created || again.EventID != id || !bytes.Equal(again.Body, result.Body) {
t.Fatalf("restart changed original result: %+v created=%t err=%v", again, created, err)
}
if _, created, err := restarted.RecordExecute(cmd); err != nil || created {
t.Fatalf("duplicate command became a new execution: created=%t err=%v", created, err)
}
})
}
}
func TestSourceEventIdentityAllowsIndependentFeedback(t *testing.T) {
s := preparedCurrentCallStore(t)
cmd := currentResultCall(t, s, "numberID-shared-0000123", true)
// Reproduce the required result identity independently of the generator so
// the old two-column primary key also fails before implementation changes.
if _, err := s.db.Exec(`INSERT INTO dispatcher_outbox(dispatcher_id,event_id,event_type,routing_key,body) VALUES(?,?,?,?,?)`, cmd.DispatcherID, cmd.EventID, "call.execute.result", "mock.route", []byte(`{"source":"original"}`)); err != nil {
t.Fatalf("different feedback types cannot share source ID: %v", err)
}
if err := s.MarkOutboxConfirmed(cmd.DispatcherID, "call.execute", cmd.EventID); err != nil {
t.Fatal(err)
}
pending, err := s.ListPendingOutbox(cmd.DispatcherID)
if err != nil || len(pending) != 1 || pending[0].EventType != "call.execute.result" {
t.Fatalf("ack confirmation consumed a different feedback: %+v %v", pending, err)
}
}
func TestSourceEventIdentityConfirmationCannotConsumeAnotherType(t *testing.T) {
for _, first := range []string{"call.execute", "call.execute.result"} {
t.Run(first, func(t *testing.T) {
s := preparedCurrentCallStore(t)
cmd := currentResultCall(t, s, "numberID-confirm-0000123", true)
if _, _, err := s.RecordCallResult(cmd.DispatcherID, cmd.EventID, currentResultPayload(t)); err != nil {
t.Fatal(err)
}
if err := s.MarkOutboxConfirmed(cmd.DispatcherID, "task.control", cmd.EventID); err == nil {
t.Fatal("nonexistent feedback falsely confirmed")
}
if pending, err := s.ListPendingOutbox(cmd.DispatcherID); err != nil || len(pending) != 2 {
t.Fatalf("wrong type changed confirmations: %+v %v", pending, err)
}
if err := s.MarkOutboxConfirmed(cmd.DispatcherID, first, cmd.EventID); err != nil {
t.Fatal(err)
}
pending, err := s.ListPendingOutbox(cmd.DispatcherID)
if err != nil || len(pending) != 1 || pending[0].EventType == first || pending[0].EventID != cmd.EventID {
t.Fatalf("one confirmation consumed both feedback types: %+v %v", pending, err)
}
if err := s.MarkOutboxConfirmed(cmd.DispatcherID, first, cmd.EventID); err == nil {
t.Fatal("already-confirmed feedback falsely reported another transition")
}
if err := s.MarkExecuteDispatched(cmd.DispatcherID, cmd.EventID); err != nil {
t.Fatalf("late ack confused final result with execute receipt: %v", err)
}
if pending, err := s.ListPendingOutbox(cmd.DispatcherID); err != nil || len(pending) != 1 {
t.Fatalf("late ack recreated feedback: %+v %v", pending, err)
}
})
}
}
func TestSourceEventIdentityUploadedResultRetainsOriginal(t *testing.T) {
s := preparedCurrentCallStore(t)
cmd := currentResultCall(t, s, "numberID-uploaded-0000123", true)
grant := currentUploadBinding(cmd)
if _, _, err := s.BindRecordingUpload(grant); err != nil {
t.Fatal(err)
}
proof := UploadProof{UploadID: grant.UploadID, RecordingID: grant.RecordingID, StatusCode: 200, SizeBytes: grant.SizeBytes, SHA256: grant.ChecksumSHA256}
result, created, err := s.RecordUploadedCallResult(cmd.DispatcherID, cmd.EventID, currentUploadedResultPayload(t, grant), proof)
if err != nil || !created || result.EventID != cmd.EventID {
t.Fatalf("uploaded result changed source identity: %+v created=%t err=%v", result, created, err)
}
if err := s.MarkOutboxConfirmed(cmd.DispatcherID, "call.execute.result", cmd.EventID); err != nil {
t.Fatal(err)
}
again, created, err := s.RecordUploadedCallResult(cmd.DispatcherID, cmd.EventID, currentUploadedResultPayload(t, grant), proof)
if err != nil || created || again.EventID != cmd.EventID || !bytes.Equal(result.Body, again.Body) {
t.Fatalf("upload retry changed original result: %+v created=%t err=%v", again, created, err)
}
if pending, err := s.ListPendingOutbox(cmd.DispatcherID); err != nil || len(pending) != 1 || pending[0].EventType != "call.execute" {
t.Fatalf("uploaded result confirmation consumed execute receipt: %+v %v", pending, err)
}
}
func TestSourceEventIdentityUploadUsesOnlyFinalResult(t *testing.T) {
s := preparedCurrentCallStore(t)
cmd := currentResultCall(t, s, "numberID-recording-0000123", true)
// A pending or confirmed execute receipt must not deny the first recording.
if err := s.MarkOutboxConfirmed(cmd.DispatcherID, "call.execute", cmd.EventID); err != nil {
t.Fatal(err)
}
grant := currentUploadBinding(cmd)
if _, created, err := s.BindRecordingUpload(grant); err != nil || !created {
t.Fatalf("execute receipt was mistaken for final result: created=%t err=%v", created, err)
}
withoutRecording := currentResultCall(t, s, "numberID-no-recording-0000456", true)
result, _, err := s.RecordCallResult(withoutRecording.DispatcherID, withoutRecording.EventID, currentResultPayload(t))
if err != nil || result.EventID != withoutRecording.EventID {
t.Fatalf("final source ID changed: %+v %v", result, err)
}
if _, _, err := s.BindRecordingUpload(currentUploadBinding(withoutRecording)); !errors.Is(err, ErrResultConflict) {
t.Fatalf("new recording accepted after final result: %v", err)
}
if _, err := s.LoadRecordingUpload(withoutRecording.DispatcherID, withoutRecording.EventID); !errors.Is(err, ErrUploadNotFound) {
t.Fatalf("refused grant left partial recording: %v", err)
}
}
func TestSourceEventIdentityRejectionReplayOnlyRequeuesItsType(t *testing.T) {
for _, kind := range []string{"call.execute", "task.control", "sip.config"} {
t.Run(kind, func(t *testing.T) {
s := preparedCurrentCallStore(t)
id := "numberID-rejected-0000123"
raw, err := json.Marshal(map[string]any{"event_id": id, "event_type": kind, "dispatcher_id": currentDispatcherID, "tenant_id": 1001, "payload": map[string]any{}})
if err != nil {
t.Fatal(err)
}
cause := contract.ValidateCurrent("mq", raw)
if cause == nil {
t.Fatal("fixture unexpectedly valid")
}
if err := s.RejectData(currentDispatcherID, raw, cause); err != nil {
t.Fatal(err)
}
pending, err := s.ListPendingOutbox(currentDispatcherID)
if err != nil || len(pending) != 1 || pending[0].EventID != id || pending[0].EventType != kind {
t.Fatalf("rejection changed source identity/type: %+v %v", pending, err)
}
original := bytes.Clone(pending[0].Body)
if err := s.MarkOutboxConfirmed(currentDispatcherID, kind, id); err != nil {
t.Fatal(err)
}
// A different confirmed feedback sharing the identity must stay
// confirmed while the rejected receipt is requested again.
if _, err := s.db.Exec(`INSERT INTO dispatcher_outbox(dispatcher_id,event_id,event_type,routing_key,body,confirmed) VALUES(?,?,?,?,?,1)`, currentDispatcherID, id, "call.execute.result", "mock.route", []byte(`{}`)); err != nil {
t.Fatal(err)
}
for _, replay := range []bool{false, true} {
if replay {
tenantID := int64(1001)
if kind == "sip.config" {
tenantID = 0
}
if rejected, err := s.ReplayRejectedEvent(currentDispatcherID, id, kind, tenantID); err != nil || !rejected {
t.Fatalf("original rejection not replayed: %t %v", rejected, err)
}
} else if err := s.RejectData(currentDispatcherID, raw, cause); err != nil {
t.Fatal(err)
}
pending, err := s.ListPendingOutbox(currentDispatcherID)
if err != nil || len(pending) != 1 || pending[0].EventID != id || pending[0].EventType != kind || !bytes.Equal(original, pending[0].Body) {
t.Fatalf("rejection requeued another feedback or changed original: %+v %v", pending, err)
}
if err := s.MarkOutboxConfirmed(currentDispatcherID, kind, id); err != nil {
t.Fatal(err)
}
}
})
}
}
func TestSourceEventIdentityRejectsVersionThreeWithoutWriting(t *testing.T) {
s := preparedCurrentCallStore(t)
cmd := currentResultCall(t, s, "numberID-old-layout", true)
if _, err := s.db.Exec(`PRAGMA user_version=3`); err != nil {
t.Fatal(err)
}
var schemaBefore string
if err := s.db.QueryRow(`SELECT sql FROM sqlite_master WHERE name='dispatcher_outbox'`).Scan(&schemaBefore); err != nil {
t.Fatal(err)
}
pendingBefore, err := s.ListPendingOutbox(cmd.DispatcherID)
if err != nil {
t.Fatal(err)
}
path := s.path
if err := s.Close(); err != nil {
t.Fatal(err)
}
if reopened, err := Open(path); err == nil {
reopened.Close()
t.Fatal("old layout admitted or silently migrated")
}
db, err := sql.Open("sqlite", path)
if err != nil {
t.Fatal(err)
}
defer db.Close()
var version int
var schemaAfter string
var body []byte
if err := db.QueryRow(`PRAGMA user_version`).Scan(&version); err != nil || version != 3 {
t.Fatalf("old version changed: %d %v", version, err)
}
if err := db.QueryRow(`SELECT sql FROM sqlite_master WHERE name='dispatcher_outbox'`).Scan(&schemaAfter); err != nil || schemaBefore != schemaAfter {
t.Fatalf("old schema changed: %v", err)
}
if err := db.QueryRow(`SELECT body FROM dispatcher_outbox WHERE dispatcher_id=? AND event_type='call.execute' AND event_id=?`, cmd.DispatcherID, cmd.EventID).Scan(&body); err != nil || !bytes.Equal(body, pendingBefore[0].Body) {
t.Fatalf("old feedback changed: %v", err)
}
}