348 lines
12 KiB
Go
348 lines
12 KiB
Go
package msglib
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestSelfCheckSendsProtocolEnvelopeAndDecodesResponse(t *testing.T) {
|
|
client := newFakeClient(t, "self_check_ok", Config{})
|
|
|
|
got, err := client.SelfCheck(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("SelfCheck returned error: %v", err)
|
|
}
|
|
if got.SidecarName != "MsgLibReadSidecar" {
|
|
t.Fatalf("SidecarName = %q, want MsgLibReadSidecar", got.SidecarName)
|
|
}
|
|
if got.Protocol != Protocol {
|
|
t.Fatalf("Protocol = %q, want %q", got.Protocol, Protocol)
|
|
}
|
|
if got.ProcessBits != 32 || !got.ReadOnly || got.MutatesDB {
|
|
t.Fatalf("unexpected safety fields: bits=%d read_only=%v mutates_db=%v", got.ProcessBits, got.ReadOnly, got.MutatesDB)
|
|
}
|
|
}
|
|
|
|
func TestDisplaySourcesPassesConfiguredPathsAndDecodesMetadata(t *testing.T) {
|
|
cfg := Config{
|
|
SQLiteDLLPath: `C:\fixture\System.Data.SQLite.dll`,
|
|
DBPath: `C:\fixture\MsgLib.db`,
|
|
Password: "123",
|
|
}
|
|
client := newFakeClient(t, "display_sources_ok", cfg)
|
|
|
|
got, err := client.DisplaySources(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("DisplaySources returned error: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("len(DisplaySources) = %d, want 2", len(got))
|
|
}
|
|
if got[0].Source != "contacts_roster" || got[0].Table != "TD_Roster" || !got[0].Available {
|
|
t.Fatalf("unexpected first display source: %+v", got[0])
|
|
}
|
|
if strings.Join(got[0].RequiredColumns, ",") != "UserJID,Name" {
|
|
t.Fatalf("required columns = %#v", got[0].RequiredColumns)
|
|
}
|
|
if got[1].Source != "work_group_auth" || got[1].Available {
|
|
t.Fatalf("unexpected second display source: %+v", got[1])
|
|
}
|
|
}
|
|
|
|
func TestMessageSourcesPassesConfiguredPathsAndDecodesMetadata(t *testing.T) {
|
|
cfg := Config{
|
|
SQLiteDLLPath: `C:\fixture\System.Data.SQLite.dll`,
|
|
DBPath: `C:\fixture\MsgLib.db`,
|
|
Password: "123",
|
|
}
|
|
client := newFakeClient(t, "message_sources_ok", cfg)
|
|
|
|
got, err := client.MessageSources(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("MessageSources returned error: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("len(MessageSources) = %d, want 2", len(got))
|
|
}
|
|
if got[0].Source != "direct_messages" || got[0].Table != "tblPersonMsg" || got[0].Role != "direct" || !got[0].Available {
|
|
t.Fatalf("unexpected first message source: %+v", got[0])
|
|
}
|
|
if got[0].RowCount != "2132" || strings.Join(got[0].RequiredColumns, ",") != "Id,SndPersonId,RecvPersonId,ObjPersonId,ActionTime,MsgText,MessageBody" {
|
|
t.Fatalf("unexpected first message source metadata: %+v", got[0])
|
|
}
|
|
if got[1].Source != "group_messages" || got[1].Table != "tblMsgGroupPersonMsg" || got[1].Role != "group" || got[1].RowCount != "2884" {
|
|
t.Fatalf("unexpected second message source: %+v", got[1])
|
|
}
|
|
}
|
|
|
|
func TestDisplayEntitiesPassesBoundedArgsAndDecodesMetadata(t *testing.T) {
|
|
cfg := Config{
|
|
SQLiteDLLPath: `C:\fixture\System.Data.SQLite.dll`,
|
|
DBPath: `C:\fixture\MsgLib.db`,
|
|
Password: "123",
|
|
}
|
|
client := newFakeClient(t, "display_entities_ok", cfg)
|
|
|
|
got, err := client.DisplayEntities(context.Background(), DisplayEntitiesOptions{EntityType: "contacts", Query: "alice", Limit: 2})
|
|
if err != nil {
|
|
t.Fatalf("DisplayEntities returned error: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("len(DisplayEntities) = %d, want 2", len(got))
|
|
}
|
|
if got[0].EntityType != "contacts" || got[0].SourceTable != "TD_Roster" || got[0].JID != "alice@example" || got[0].DisplayName != "Alice" {
|
|
t.Fatalf("unexpected first entity: %+v", got[0])
|
|
}
|
|
if got[0].Confidence <= 0 || strings.Join(got[0].MatchedColumns, ",") != "JId,Nick" {
|
|
t.Fatalf("unexpected first entity metadata: %+v", got[0])
|
|
}
|
|
if got[1].SourceTable != "tblRecent" || got[1].JID != "recent@example" || got[1].DisplayName != "Recent Alice" {
|
|
t.Fatalf("unexpected second entity: %+v", got[1])
|
|
}
|
|
}
|
|
|
|
func TestDisplayEntitiesRejectsUnsupportedEntityType(t *testing.T) {
|
|
client := newFakeClient(t, "display_entities_ok", Config{})
|
|
|
|
_, err := client.DisplayEntities(context.Background(), DisplayEntitiesOptions{EntityType: "messages", Limit: 5})
|
|
if err == nil {
|
|
t.Fatal("DisplayEntities returned nil error, want unsupported entity_type error")
|
|
}
|
|
if !strings.Contains(err.Error(), "entity_type") {
|
|
t.Fatalf("DisplayEntities error = %v, want entity_type context", err)
|
|
}
|
|
}
|
|
|
|
func TestClientReturnsStructuredSidecarError(t *testing.T) {
|
|
client := newFakeClient(t, "sidecar_error", Config{})
|
|
|
|
_, err := client.SelfCheck(context.Background())
|
|
if err == nil {
|
|
t.Fatal("SelfCheck returned nil error, want sidecar error")
|
|
}
|
|
var sidecarErr *SidecarError
|
|
if !errors.As(err, &sidecarErr) {
|
|
t.Fatalf("error %T %[1]v does not wrap *SidecarError", err)
|
|
}
|
|
if sidecarErr.Code != "DB_OPEN_FAILED" || !strings.Contains(sidecarErr.Message, "read-only open failed") {
|
|
t.Fatalf("unexpected sidecar error: %+v", sidecarErr)
|
|
}
|
|
}
|
|
|
|
func TestClientTimeoutStopsSidecar(t *testing.T) {
|
|
client := newFakeClient(t, "hang", Config{Timeout: 150 * time.Millisecond})
|
|
|
|
_, err := client.SelfCheck(context.Background())
|
|
if err == nil {
|
|
t.Fatal("SelfCheck returned nil error, want timeout")
|
|
}
|
|
if !errors.Is(err, context.DeadlineExceeded) && !strings.Contains(strings.ToLower(err.Error()), "deadline") {
|
|
t.Fatalf("SelfCheck error = %v, want deadline exceeded", err)
|
|
}
|
|
}
|
|
|
|
func TestConfigFromEnvReadsSidecarShape(t *testing.T) {
|
|
t.Setenv("ISPHERE_MSGLIB_SIDECAR_EXE", `C:\tools\MsgLibReadSidecar.exe`)
|
|
t.Setenv("ISPHERE_MSGLIB_SQLITE_DLL", `C:\tools\System.Data.SQLite.dll`)
|
|
t.Setenv("ISPHERE_MSGLIB_DB", `C:\copy\MsgLib.db`)
|
|
|
|
cfg, err := ConfigFromEnv()
|
|
if err != nil {
|
|
t.Fatalf("ConfigFromEnv returned error: %v", err)
|
|
}
|
|
if cfg.SidecarPath != `C:\tools\MsgLibReadSidecar.exe` {
|
|
t.Fatalf("SidecarPath = %q", cfg.SidecarPath)
|
|
}
|
|
if cfg.SQLiteDLLPath != `C:\tools\System.Data.SQLite.dll` || cfg.DBPath != `C:\copy\MsgLib.db` {
|
|
t.Fatalf("unexpected DB config: %+v", cfg)
|
|
}
|
|
if cfg.Password != "123" {
|
|
t.Fatalf("Password = %q, want default 123", cfg.Password)
|
|
}
|
|
}
|
|
|
|
func newFakeClient(t *testing.T, mode string, overrides Config) *Client {
|
|
t.Helper()
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatalf("os.Executable: %v", err)
|
|
}
|
|
cfg := overrides
|
|
cfg.SidecarPath = exe
|
|
cfg.SidecarArgs = []string{"-test.run=TestFakeSidecarProcess", "--"}
|
|
cfg.ExtraEnv = append(cfg.ExtraEnv, "ISPHERE_MSGLIB_FAKE_SIDECAR=1", "ISPHERE_MSGLIB_FAKE_MODE="+mode)
|
|
if cfg.Timeout == 0 {
|
|
cfg.Timeout = 2 * time.Second
|
|
}
|
|
return NewClient(cfg)
|
|
}
|
|
|
|
func TestFakeSidecarProcess(t *testing.T) {
|
|
if os.Getenv("ISPHERE_MSGLIB_FAKE_SIDECAR") != "1" {
|
|
return
|
|
}
|
|
os.Exit(fakeSidecarMain())
|
|
}
|
|
|
|
func fakeSidecarMain() int {
|
|
mode := os.Getenv("ISPHERE_MSGLIB_FAKE_MODE")
|
|
if mode == "hang" {
|
|
time.Sleep(5 * time.Second)
|
|
return 0
|
|
}
|
|
|
|
var req map[string]any
|
|
if err := json.NewDecoder(os.Stdin).Decode(&req); err != nil {
|
|
fmt.Fprintf(os.Stderr, "decode request: %v\n", err)
|
|
return 2
|
|
}
|
|
protocol, _ := req["protocol"].(string)
|
|
op, _ := req["op"].(string)
|
|
requestID, _ := req["request_id"].(string)
|
|
if protocol != Protocol || op == "" || requestID == "" {
|
|
fmt.Fprintf(os.Stderr, "bad envelope: protocol=%q op=%q request_id=%q\n", protocol, op, requestID)
|
|
return 2
|
|
}
|
|
|
|
if mode == "sidecar_error" {
|
|
return writeFakeResponse(map[string]any{
|
|
"protocol": Protocol,
|
|
"request_id": requestID,
|
|
"op": op,
|
|
"ok": false,
|
|
"data": nil,
|
|
"error": map[string]any{
|
|
"code": "DB_OPEN_FAILED",
|
|
"message": "read-only open failed",
|
|
},
|
|
"warnings": []any{},
|
|
})
|
|
}
|
|
|
|
switch op {
|
|
case "self_check":
|
|
return writeFakeResponse(map[string]any{
|
|
"protocol": Protocol,
|
|
"request_id": requestID,
|
|
"op": op,
|
|
"ok": true,
|
|
"data": map[string]any{
|
|
"sidecar_name": "MsgLibReadSidecar",
|
|
"sidecar_version": "0.1.0",
|
|
"protocol": Protocol,
|
|
"process_bits": 32,
|
|
"read_only": true,
|
|
"mutates_db": false,
|
|
},
|
|
"error": nil,
|
|
"warnings": []any{},
|
|
})
|
|
case "display_sources":
|
|
args, _ := req["args"].(map[string]any)
|
|
if args["sqlite_dll_path"] != `C:\fixture\System.Data.SQLite.dll` || args["db_path"] != `C:\fixture\MsgLib.db` || args["password"] != "123" {
|
|
fmt.Fprintf(os.Stderr, "bad display_sources args: %#v\n", args)
|
|
return 2
|
|
}
|
|
return writeFakeResponse(map[string]any{
|
|
"protocol": Protocol,
|
|
"request_id": requestID,
|
|
"op": op,
|
|
"ok": true,
|
|
"data": map[string]any{
|
|
"metadata_only": true,
|
|
"read_only": true,
|
|
"message_body_values_returned": false,
|
|
"display_sources": []any{
|
|
map[string]any{"source": "contacts_roster", "table": "TD_Roster", "available": true, "required_columns": []any{"UserJID", "Name"}, "present_columns": []any{"UserJID", "Name", "Mobile"}},
|
|
map[string]any{"source": "work_group_auth", "table": "TD_WorkGroupAuth", "available": false, "required_columns": []any{"GroupJID", "GroupName"}, "present_columns": []any{"GroupJID"}},
|
|
},
|
|
},
|
|
"error": nil,
|
|
"warnings": []any{},
|
|
})
|
|
case "message_sources":
|
|
args, _ := req["args"].(map[string]any)
|
|
if args["sqlite_dll_path"] != `C:\fixture\System.Data.SQLite.dll` || args["db_path"] != `C:\fixture\MsgLib.db` || args["password"] != "123" {
|
|
fmt.Fprintf(os.Stderr, "bad message_sources args: %#v\n", args)
|
|
return 2
|
|
}
|
|
return writeFakeResponse(map[string]any{
|
|
"protocol": Protocol,
|
|
"request_id": requestID,
|
|
"op": op,
|
|
"ok": true,
|
|
"data": map[string]any{
|
|
"metadata_only": true,
|
|
"read_only": true,
|
|
"message_body_values_returned": false,
|
|
"raw_rows_returned": false,
|
|
"file_paths_returned": false,
|
|
"message_sources": []any{
|
|
map[string]any{
|
|
"source": "direct_messages",
|
|
"table": "tblPersonMsg",
|
|
"role": "direct",
|
|
"available": true,
|
|
"required_columns": []any{"Id", "SndPersonId", "RecvPersonId", "ObjPersonId", "ActionTime", "MsgText", "MessageBody"},
|
|
"present_columns": []any{"Id", "SndPersonId", "RecvPersonId", "ObjPersonId", "ActionTime", "MsgText", "MessageBody"},
|
|
"row_count": "2132",
|
|
},
|
|
map[string]any{
|
|
"source": "group_messages",
|
|
"table": "tblMsgGroupPersonMsg",
|
|
"role": "group",
|
|
"available": true,
|
|
"required_columns": []any{"Id", "SndPersonId", "MsgGroupId", "MsgGroupName", "MsgTime", "MsgText", "MessageBody"},
|
|
"present_columns": []any{"Id", "SndPersonId", "MsgGroupId", "MsgGroupName", "MsgTime", "MsgText", "MessageBody"},
|
|
"row_count": "2884",
|
|
},
|
|
},
|
|
},
|
|
"error": nil,
|
|
"warnings": []any{},
|
|
})
|
|
case "display_entities":
|
|
args, _ := req["args"].(map[string]any)
|
|
if args["sqlite_dll_path"] != `C:\fixture\System.Data.SQLite.dll` || args["db_path"] != `C:\fixture\MsgLib.db` || args["password"] != "123" || args["entity_type"] != "contacts" || args["query"] != "alice" || args["limit"] != float64(2) {
|
|
fmt.Fprintf(os.Stderr, "bad display_entities args: %#v\n", args)
|
|
return 2
|
|
}
|
|
return writeFakeResponse(map[string]any{
|
|
"protocol": Protocol,
|
|
"request_id": requestID,
|
|
"op": op,
|
|
"ok": true,
|
|
"data": map[string]any{
|
|
"metadata_only": true,
|
|
"read_only": true,
|
|
"entity_type": "contacts",
|
|
"limit": 2,
|
|
"result_count": 2,
|
|
"entities": []any{
|
|
map[string]any{"entity_type": "contacts", "source_table": "TD_Roster", "jid": "alice@example", "display_name": "Alice", "confidence": 0.95, "matched_columns": []any{"JId", "Nick"}},
|
|
map[string]any{"entity_type": "contacts", "source_table": "tblRecent", "jid": "recent@example", "display_name": "Recent Alice", "confidence": 0.70, "matched_columns": []any{"PersonId", "PersonName"}},
|
|
},
|
|
},
|
|
"error": nil,
|
|
"warnings": []any{},
|
|
})
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unexpected op: %s\n", op)
|
|
return 2
|
|
}
|
|
}
|
|
|
|
func writeFakeResponse(resp map[string]any) int {
|
|
if err := json.NewEncoder(os.Stdout).Encode(resp); err != nil {
|
|
fmt.Fprintf(os.Stderr, "encode response: %v\n", err)
|
|
return 2
|
|
}
|
|
return 0
|
|
}
|