246 lines
11 KiB
Go
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)
|
|
}
|
|
}
|