mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
230 lines
7.8 KiB
Go
230 lines
7.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func concatExtra(t *testing.T, reference, total, sequence int) json.RawMessage {
|
|
t.Helper()
|
|
extra, err := json.Marshal(map[string]any{
|
|
"concat": map[string]any{"reference": reference, "total": total, "sequence": sequence},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal concat extra: %v", err)
|
|
}
|
|
return extra
|
|
}
|
|
|
|
func TestMergeConcatSegmentJoinsOutOfOrderInSequenceOrder(t *testing.T) {
|
|
// UCS-2 long SMS (the customer case) whose segments arrive 2, 1, 3.
|
|
var body string
|
|
var extra json.RawMessage
|
|
var changed, complete bool
|
|
|
|
body, extra, changed, err := mergeConcatSegment(extra, "中段", concatExtra(t, 9, 3, 2))
|
|
if err != nil || !changed {
|
|
t.Fatalf("segment 2: body=%q changed=%v err=%v", body, changed, err)
|
|
}
|
|
if body != "中段" {
|
|
t.Fatalf("after segment 2 body = %q, want partial %q", body, "中段")
|
|
}
|
|
|
|
body, extra, changed, err = mergeConcatSegment(extra, "前段", concatExtra(t, 9, 3, 1))
|
|
if err != nil || !changed {
|
|
t.Fatalf("segment 1: body=%q changed=%v err=%v", body, changed, err)
|
|
}
|
|
if body != "前段中段" {
|
|
t.Fatalf("after segment 1 body = %q, want %q", body, "前段中段")
|
|
}
|
|
|
|
body, extra, changed, err = mergeConcatSegment(extra, "尾段", concatExtra(t, 9, 3, 3))
|
|
if err != nil || !changed {
|
|
t.Fatalf("segment 3: body=%q changed=%v err=%v", body, changed, err)
|
|
}
|
|
if body != "前段中段尾段" {
|
|
t.Fatalf("complete body = %q, want %q", body, "前段中段尾段")
|
|
}
|
|
document, err := decodeJSONObject(extra)
|
|
if err != nil {
|
|
t.Fatalf("decode merged extra: %v", err)
|
|
}
|
|
complete, _ = document["concat_complete"].(bool)
|
|
if !complete {
|
|
t.Fatalf("concat_complete = %v, want true; extra=%s", complete, extra)
|
|
}
|
|
if got := numberAsInt(document["concat_received"]); got != 3 {
|
|
t.Fatalf("concat_received = %d, want 3", got)
|
|
}
|
|
}
|
|
|
|
func TestMergeConcatSegmentKeepsURLContiguous(t *testing.T) {
|
|
// A GSM-7 tracking link split mid-token (the OFCA "garbled" report) must
|
|
// reassemble with no break.
|
|
first := "https://ofca.gov.hk/track?tok=ab"
|
|
second := "cdef1234&lang=zh"
|
|
_, extra, _, err := mergeConcatSegment(nil, first, concatExtra(t, 4, 2, 1))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _, _, err := mergeConcatSegment(extra, second, concatExtra(t, 4, 2, 2))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "https://ofca.gov.hk/track?tok=abcdef1234&lang=zh"
|
|
if body != want {
|
|
t.Fatalf("body = %q, want %q", body, want)
|
|
}
|
|
}
|
|
|
|
func TestMergeConcatSegmentRedeliveryIsIdempotent(t *testing.T) {
|
|
_, extra, _, err := mergeConcatSegment(nil, "甲", concatExtra(t, 3, 2, 1))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A modem rescan redelivers the identical segment: no change, no growth.
|
|
body, extra2, changed, err := mergeConcatSegment(extra, "甲", concatExtra(t, 3, 2, 1))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if changed {
|
|
t.Fatalf("redelivered segment reported changed=true; body=%q", body)
|
|
}
|
|
if body != "甲" {
|
|
t.Fatalf("body = %q, want %q", body, "甲")
|
|
}
|
|
if string(extra2) == "" {
|
|
t.Fatal("merged extra lost on idempotent redelivery")
|
|
}
|
|
}
|
|
|
|
func TestMergeConcatSegmentWithoutHeaderPassesThrough(t *testing.T) {
|
|
extra, err := json.Marshal(map[string]any{"encoding": "gsm7"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _, changed, err := mergeConcatSegment(nil, "plain", extra)
|
|
if err != nil || !changed || body != "plain" {
|
|
t.Fatalf("body=%q changed=%v err=%v, want passthrough", body, changed, err)
|
|
}
|
|
}
|
|
|
|
func TestStableConcatMessageIDScopesByPeerReferenceTotal(t *testing.T) {
|
|
a := StableConcatMessageID("cellular_at", "imei-1", "ec20", "+10086", 7, 2)
|
|
if !isConcatSMSMessageID(a) {
|
|
t.Fatalf("id %q missing concat prefix", a)
|
|
}
|
|
if again := StableConcatMessageID("cellular_at", "imei-1", "ec20", "+10086", 7, 2); again != a {
|
|
t.Fatalf("id unstable: %q vs %q", a, again)
|
|
}
|
|
for _, different := range []string{
|
|
StableConcatMessageID("cellular_at", "imei-1", "ec20", "+10086", 8, 2), // other reference
|
|
StableConcatMessageID("cellular_at", "imei-1", "ec20", "+10010", 7, 2), // other peer
|
|
StableConcatMessageID("cellular_at", "imei-1", "ec20", "+10086", 7, 3), // other total
|
|
StableConcatMessageID("ims", "imei-1", "ec20", "+10086", 7, 2), // other source
|
|
} {
|
|
if different == a {
|
|
t.Fatalf("id %q collides across distinct concat groups", a)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConcatSMSReadyToNotify(t *testing.T) {
|
|
if !ConcatSMSReadyToNotify("modem:SM:3:abcd", json.RawMessage(`{}`)) {
|
|
t.Fatal("plain message should always be ready")
|
|
}
|
|
incomplete := StableConcatMessageID("cellular_at", "imei", "ec20", "peer", 1, 2)
|
|
if ConcatSMSReadyToNotify(incomplete, json.RawMessage(`{"concat_complete":false}`)) {
|
|
t.Fatal("incomplete long SMS must not notify")
|
|
}
|
|
if ConcatSMSReadyToNotify(incomplete, json.RawMessage(`not-json`)) {
|
|
t.Fatal("unparseable concat extra must not notify")
|
|
}
|
|
if !ConcatSMSReadyToNotify(incomplete, json.RawMessage(`{"concat_complete":true}`)) {
|
|
t.Fatal("complete long SMS should notify")
|
|
}
|
|
}
|
|
|
|
func TestSaveConcatSMSFoldsSegmentsIntoOneRow(t *testing.T) {
|
|
ctx := context.Background()
|
|
database := openTestStore(t, ":memory:")
|
|
mustSaveDevice(t, database, "ec20-1", "测试设备")
|
|
const imei = "867394042309830"
|
|
base := time.Unix(1_700_000_000, 0).UTC()
|
|
|
|
save := func(sequence int, text string, at time.Time) SMSMessage {
|
|
t.Helper()
|
|
saved, err := database.SaveSMSMessage(ctx, SMSMessage{
|
|
MessageID: StableConcatMessageID("cellular_at", imei, "ec20-1", "+8520000", 5, 2),
|
|
DeviceID: "ec20-1", ModemIMEI: imei, IMSI: "45400",
|
|
Peer: "+8520000", Direction: "inbound", Body: text,
|
|
Timestamp: at, Status: "received", Source: "cellular_at",
|
|
PartsTotal: 2,
|
|
Extra: concatExtra(t, 5, 2, sequence),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SaveSMSMessage(seq=%d) error = %v", sequence, err)
|
|
}
|
|
return saved
|
|
}
|
|
|
|
first := save(1, "【检测】您的结果为", base)
|
|
if first.PartsTotal != 2 {
|
|
t.Fatalf("PartsTotal = %d, want 2", first.PartsTotal)
|
|
}
|
|
if ConcatSMSReadyToNotify(first.MessageID, first.Extra) {
|
|
t.Fatal("first segment alone should not be ready to notify")
|
|
}
|
|
|
|
second := save(2, "合格,请查收报告", base.Add(30*time.Second))
|
|
if second.ID <= first.ID {
|
|
t.Fatalf("completed row id = %d, want a fresh id greater than %d", second.ID, first.ID)
|
|
}
|
|
if second.Body != "【检测】您的结果为合格,请查收报告" {
|
|
t.Fatalf("merged body = %q", second.Body)
|
|
}
|
|
if !second.Timestamp.Equal(base) {
|
|
t.Fatalf("merged timestamp = %v, want earliest segment %v", second.Timestamp, base)
|
|
}
|
|
if !ConcatSMSReadyToNotify(second.MessageID, second.Extra) {
|
|
t.Fatal("completed long SMS should be ready to notify")
|
|
}
|
|
|
|
// Exactly one stored row represents the whole long SMS.
|
|
messages, err := database.ListSMSMessages(ctx, SMSFilter{DeviceID: "ec20-1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(messages) != 1 {
|
|
t.Fatalf("stored rows = %d, want 1 merged row: %+v", len(messages), messages)
|
|
}
|
|
|
|
// The completed row re-enters after the earlier partial id, so the Telegram
|
|
// id-cursor surfaces it once, complete.
|
|
fresh, err := database.ListInboundSMSAfterID(ctx, first.ID, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(fresh) != 1 || fresh[0].ID != second.ID || !strings.Contains(fresh[0].Body, "合格") {
|
|
t.Fatalf("ListInboundSMSAfterID = %+v, want the completed row", fresh)
|
|
}
|
|
|
|
// A modem rescan redelivers an already-folded segment: no write, no id churn.
|
|
rescan := save(1, "【检测】您的结果为", base)
|
|
if rescan.ID != second.ID {
|
|
t.Fatalf("rescan churned the row id: got %d, want stable %d", rescan.ID, second.ID)
|
|
}
|
|
if rescan.Body != second.Body {
|
|
t.Fatalf("rescan changed body to %q", rescan.Body)
|
|
}
|
|
if after, err := database.ListInboundSMSAfterID(ctx, second.ID, 10); err != nil || len(after) != 0 {
|
|
t.Fatalf("rescan produced new rows: %+v, %v", after, err)
|
|
}
|
|
if count, err := database.ListSMSMessages(ctx, SMSFilter{DeviceID: "ec20-1"}); err != nil || len(count) != 1 {
|
|
t.Fatalf("rows after rescan = %d, %v; want still 1", len(count), err)
|
|
}
|
|
}
|