mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-20 02:17:24 +00:00
Push two deferred packages above their coverage floors: - internal/providers 57.0% -> 62.2% (+5.2%, target ≥60%) Targeted pure-logic adapter transformation code: registry, openai/dashscope/codex ToRequest/FromResponse/FromStreamChunk, Azure header branch, token source propagation, function_call + incomplete + reasoning parse branches. - internal/channels/zalo 7.2% -> 65.3% (+58.1%, target ≥20%) Factory table-driven creds/config cases, New() defaults, callAPI success/error/malformed, getMe/getUpdates, sendMessage/sendPhoto, Send routing + photo extraction, Stop signal, processUpdate dispatch, handleTextMessage empty-sender drop, downloadMedia four branches (png/jpg fallback/404/empty). Minimal refactor: converted package-level apiBase const to var in internal/channels/zalo/zalo.go so tests can point at httptest server. Zero runtime behavior change; tests restore the original value via t.Cleanup. Ratchet bumps (scripts/coverage_thresholds.json): - internal/providers: 57.00 -> 62.15 - internal/channels/zalo: 7.20 -> 65.25 Race clean, dual build (default + -tags sqliteonly) clean, personal/ sub-packages untouched.
192 lines
5.2 KiB
Go
192 lines
5.2 KiB
Go
package zalo
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
|
"github.com/nextlevelbuilder/goclaw/internal/config"
|
|
)
|
|
|
|
// TestFactory_ValidCredsProducesChannel verifies Factory constructs a live
|
|
// Channel when credentials and config JSON are well-formed.
|
|
func TestFactory_ValidCredsProducesChannel(t *testing.T) {
|
|
creds := []byte(`{"token":"fake-zalo-token","webhook_secret":"hook-sec"}`)
|
|
cfg := []byte(`{"dm_policy":"open","media_max_mb":7,"allow_from":["+84900000000"],"webhook_url":"https://example.test/hook","block_reply":true}`)
|
|
|
|
mb := bus.New()
|
|
ch, err := Factory("my-zalo", creds, cfg, mb, nil)
|
|
if err != nil {
|
|
t.Fatalf("Factory error: %v", err)
|
|
}
|
|
if ch == nil {
|
|
t.Fatal("Factory returned nil channel")
|
|
}
|
|
if ch.Name() != "my-zalo" {
|
|
t.Errorf("Name() = %q, want my-zalo", ch.Name())
|
|
}
|
|
zc, ok := ch.(*Channel)
|
|
if !ok {
|
|
t.Fatalf("unexpected channel type %T", ch)
|
|
}
|
|
if zc.token != "fake-zalo-token" {
|
|
t.Errorf("token = %q, want fake-zalo-token", zc.token)
|
|
}
|
|
if zc.dmPolicy != "open" {
|
|
t.Errorf("dmPolicy = %q, want open", zc.dmPolicy)
|
|
}
|
|
if zc.mediaMaxMB != 7 {
|
|
t.Errorf("mediaMaxMB = %d, want 7", zc.mediaMaxMB)
|
|
}
|
|
if zc.blockReply == nil || !*zc.blockReply {
|
|
t.Errorf("blockReply = %v, want true", zc.blockReply)
|
|
}
|
|
}
|
|
|
|
// TestFactory_ErrorCases covers the error branches in order:
|
|
// - missing token in creds
|
|
// - malformed creds JSON
|
|
// - malformed config JSON
|
|
// - empty creds bytes (missing required token after unmarshal)
|
|
func TestFactory_ErrorCases(t *testing.T) {
|
|
mb := bus.New()
|
|
cases := []struct {
|
|
name string
|
|
creds []byte
|
|
cfg []byte
|
|
wantSub string
|
|
}{
|
|
{
|
|
name: "missing token",
|
|
creds: []byte(`{"webhook_secret":"x"}`),
|
|
cfg: []byte(`{}`),
|
|
wantSub: "token is required",
|
|
},
|
|
{
|
|
name: "malformed creds",
|
|
creds: []byte(`{not-json`),
|
|
cfg: []byte(`{}`),
|
|
wantSub: "decode zalo credentials",
|
|
},
|
|
{
|
|
name: "malformed config",
|
|
creds: []byte(`{"token":"t"}`),
|
|
cfg: []byte(`{malformed`),
|
|
wantSub: "decode zalo config",
|
|
},
|
|
{
|
|
name: "empty creds bytes",
|
|
creds: nil,
|
|
cfg: []byte(`{}`),
|
|
wantSub: "token is required",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := Factory("zalo-x", tc.creds, tc.cfg, mb, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantSub) {
|
|
t.Errorf("err = %v, want substring %q", err, tc.wantSub)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNew_DefaultsWhenFieldsUnset verifies New() applies defaults:
|
|
// - dm_policy defaults to "pairing"
|
|
// - media_max_mb defaults to defaultMediaMaxMB when ≤0
|
|
func TestNew_DefaultsWhenFieldsUnset(t *testing.T) {
|
|
mb := bus.New()
|
|
ch, err := New(config.ZaloConfig{Token: "t"}, mb, nil)
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
if ch.dmPolicy != "pairing" {
|
|
t.Errorf("dmPolicy = %q, want pairing (default)", ch.dmPolicy)
|
|
}
|
|
if ch.mediaMaxMB != defaultMediaMaxMB {
|
|
t.Errorf("mediaMaxMB = %d, want %d", ch.mediaMaxMB, defaultMediaMaxMB)
|
|
}
|
|
// block_reply should be nil (inherit gateway default).
|
|
if got := ch.BlockReplyEnabled(); got != nil {
|
|
t.Errorf("BlockReplyEnabled() = %v, want nil", got)
|
|
}
|
|
}
|
|
|
|
// TestNew_EmptyTokenRejected verifies New() rejects empty token.
|
|
func TestNew_EmptyTokenRejected(t *testing.T) {
|
|
mb := bus.New()
|
|
if _, err := New(config.ZaloConfig{}, mb, nil); err == nil {
|
|
t.Fatal("expected error on empty token, got nil")
|
|
}
|
|
}
|
|
|
|
// TestNew_CustomValuesPreserved verifies caller-provided values stick.
|
|
func TestNew_CustomValuesPreserved(t *testing.T) {
|
|
mb := bus.New()
|
|
blockReply := false
|
|
ch, err := New(config.ZaloConfig{
|
|
Token: "xyz",
|
|
DMPolicy: "open",
|
|
MediaMaxMB: 12,
|
|
BlockReply: &blockReply,
|
|
}, mb, nil)
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
if ch.dmPolicy != "open" {
|
|
t.Errorf("dmPolicy = %q, want open", ch.dmPolicy)
|
|
}
|
|
if ch.mediaMaxMB != 12 {
|
|
t.Errorf("mediaMaxMB = %d, want 12", ch.mediaMaxMB)
|
|
}
|
|
if got := ch.BlockReplyEnabled(); got == nil || *got != false {
|
|
t.Errorf("BlockReplyEnabled() = %v, want false pointer", got)
|
|
}
|
|
}
|
|
|
|
// TestFactoryConfigWithoutOptionals verifies minimal config still parses —
|
|
// zero values for non-required fields are accepted.
|
|
func TestFactoryConfigWithoutOptionals(t *testing.T) {
|
|
mb := bus.New()
|
|
creds := []byte(`{"token":"t"}`)
|
|
cfg := []byte(`{}`)
|
|
|
|
ch, err := Factory("zalo", creds, cfg, mb, nil)
|
|
if err != nil {
|
|
t.Fatalf("Factory: %v", err)
|
|
}
|
|
zc := ch.(*Channel)
|
|
if zc.dmPolicy != "pairing" {
|
|
t.Errorf("dmPolicy = %q, want pairing (default)", zc.dmPolicy)
|
|
}
|
|
}
|
|
|
|
// Sanity guard: factory must accept the same JSON shapes that
|
|
// zaloInstanceConfig declares; ensure deserialization matches struct tags.
|
|
func TestZaloInstanceConfigRoundTrip(t *testing.T) {
|
|
src := zaloInstanceConfig{
|
|
DMPolicy: "pairing",
|
|
WebhookURL: "https://example.test",
|
|
MediaMaxMB: 3,
|
|
AllowFrom: []string{"user1", "user2"},
|
|
}
|
|
data, err := json.Marshal(src)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got zaloInstanceConfig
|
|
if err := json.Unmarshal(data, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.DMPolicy != src.DMPolicy || got.MediaMaxMB != src.MediaMaxMB {
|
|
t.Errorf("round-trip mismatch: got %+v, want %+v", got, src)
|
|
}
|
|
if len(got.AllowFrom) != 2 {
|
|
t.Errorf("AllowFrom len = %d, want 2", len(got.AllowFrom))
|
|
}
|
|
}
|