Files
Memoh/internal/channel/adapters/qq/target_resolver_test.go
T
Ringo.Typowriter e6a6dbe3f6 feat(channel): add QQ channel support and image message pipeline (#199)
* feat(channel): add qq adapter and outbound delivery

* feat(channel): ingest inbound qq messages

* feat(web): expose qq channel in management ui

* feat(channel): support qq attachment ingestion

* fix(mcp): fail read raw immediately for missing files

* fix(agent): parse inline image data into native image parts

* test(agent): align read_media tool tests with SDK options

* fix(channel): harden qq image delivery and reconnect loop

Avoid data URLs for qq channel images, reset reconnect backoff after healthy sessions, and fall back gracefully for malformed public image URLs.

* fix(channel): restore qq media delivery and target resolution

* fix(qq,mcp,agent): fix message/qq regressions and pass go lint

* fix(qq,agent): validate inline base64 and sync heartbeat seq

* fix(qq): validate remote voice mime for upload checks

* fix(qq): fall back intents and restore adapter wiring

* fix(qq): prevent final text leakage and dedupe persisted inbound query
2026-03-07 17:12:06 +08:00

202 lines
5.5 KiB
Go

package qq
import (
"context"
"errors"
"strings"
"testing"
"github.com/jackc/pgx/v5"
identitypkg "github.com/memohai/memoh/internal/channel/identities"
routepkg "github.com/memohai/memoh/internal/channel/route"
)
const testQQOpenID = "00112233445566778899AABBCCDDEEFF"
func TestQQResolveTargetMapsRouteID(t *testing.T) {
t.Parallel()
adapter := NewQQAdapter(nil)
adapter.SetRouteResolver(&fakeQQRouteResolver{
byID: map[string]routepkg.Route{
"3fe2bad9-3eae-4f23-872c-b7a63662aa00": {
ID: "3fe2bad9-3eae-4f23-872c-b7a63662aa00",
Platform: "qq",
ReplyTarget: "c2c:" + testQQOpenID,
},
},
})
got, err := adapter.resolveTarget(context.Background(), "3fe2bad9-3eae-4f23-872c-b7a63662aa00")
if err != nil {
t.Fatalf("resolveTarget returned error: %v", err)
}
if got != "c2c:"+testQQOpenID {
t.Fatalf("unexpected mapped target: %q", got)
}
}
func TestQQResolveTargetMapsIdentityID(t *testing.T) {
t.Parallel()
adapter := NewQQAdapter(nil)
adapter.SetChannelIdentityResolver(&fakeQQIdentityResolver{
canonical: map[string][]identitypkg.ChannelIdentity{
"3fe2bad9-3eae-4f23-872c-b7a63662aa00": {
{ID: "qq-identity-1", Channel: "qq", ChannelSubjectID: testQQOpenID},
},
},
})
got, err := adapter.resolveTarget(context.Background(), "3fe2bad9-3eae-4f23-872c-b7a63662aa00")
if err != nil {
t.Fatalf("resolveTarget returned error: %v", err)
}
if got != "c2c:"+testQQOpenID {
t.Fatalf("unexpected mapped target: %q", got)
}
}
func TestQQResolveTargetMapsUserID(t *testing.T) {
t.Parallel()
adapter := NewQQAdapter(nil)
adapter.SetChannelIdentityResolver(&fakeQQIdentityResolver{
userScoped: map[string][]identitypkg.ChannelIdentity{
"3fe2bad9-3eae-4f23-872c-b7a63662aa00": {
{ID: "qq-identity-1", Channel: "qq", ChannelSubjectID: testQQOpenID},
},
},
})
got, err := adapter.resolveTarget(context.Background(), "3fe2bad9-3eae-4f23-872c-b7a63662aa00")
if err != nil {
t.Fatalf("resolveTarget returned error: %v", err)
}
if got != "c2c:"+testQQOpenID {
t.Fatalf("unexpected mapped target: %q", got)
}
}
func TestQQResolveTargetSkipsNonOpenIDQQIdentity(t *testing.T) {
t.Parallel()
adapter := NewQQAdapter(nil)
adapter.SetChannelIdentityResolver(&fakeQQIdentityResolver{
canonical: map[string][]identitypkg.ChannelIdentity{
"3fe2bad9-3eae-4f23-872c-b7a63662aa00": {
{ID: "qq-guild-identity-1", Channel: "qq", ChannelSubjectID: "guild-user-id"},
},
},
})
got, err := adapter.resolveTarget(context.Background(), "3fe2bad9-3eae-4f23-872c-b7a63662aa00")
if err != nil {
t.Fatalf("resolveTarget returned error: %v", err)
}
if got != "c2c:3fe2bad9-3eae-4f23-872c-b7a63662aa00" {
t.Fatalf("unexpected mapped target: %q", got)
}
}
func TestQQResolveTargetReturnsRouteResolverErrors(t *testing.T) {
t.Parallel()
adapter := NewQQAdapter(nil)
adapter.SetRouteResolver(&fakeQQRouteResolver{err: errors.New("route store unavailable")})
_, err := adapter.resolveTarget(context.Background(), "3fe2bad9-3eae-4f23-872c-b7a63662aa00")
if err == nil {
t.Fatal("expected route resolver error")
}
if !strings.Contains(err.Error(), "route store unavailable") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestQQResolveTargetReturnsIdentityResolverErrors(t *testing.T) {
t.Parallel()
adapter := NewQQAdapter(nil)
adapter.SetChannelIdentityResolver(&fakeQQIdentityResolver{canonicalErr: errors.New("identity store unavailable")})
_, err := adapter.resolveTarget(context.Background(), "3fe2bad9-3eae-4f23-872c-b7a63662aa00")
if err == nil {
t.Fatal("expected identity resolver error")
}
if !strings.Contains(err.Error(), "identity store unavailable") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestParseTargetRejectsUUIDForC2C(t *testing.T) {
t.Parallel()
_, err := parseTarget("3fe2bad9-3eae-4f23-872c-b7a63662aa00")
if err == nil {
t.Fatal("expected c2c uuid target error")
}
if !strings.Contains(err.Error(), "user_openid") {
t.Fatalf("unexpected error: %v", err)
}
}
type fakeQQIdentityResolver struct {
byID map[string]identitypkg.ChannelIdentity
canonical map[string][]identitypkg.ChannelIdentity
userScoped map[string][]identitypkg.ChannelIdentity
byIDErr error
canonicalErr error
userErr error
}
func (f *fakeQQIdentityResolver) GetByID(_ context.Context, channelIdentityID string) (identitypkg.ChannelIdentity, error) {
if f.byIDErr != nil {
return identitypkg.ChannelIdentity{}, f.byIDErr
}
item, ok := f.byID[channelIdentityID]
if !ok {
return identitypkg.ChannelIdentity{}, identitypkg.ErrChannelIdentityNotFound
}
return item, nil
}
func (f *fakeQQIdentityResolver) ListCanonicalChannelIdentities(_ context.Context, channelIdentityID string) ([]identitypkg.ChannelIdentity, error) {
if f.canonicalErr != nil {
return nil, f.canonicalErr
}
items, ok := f.canonical[channelIdentityID]
if !ok {
return nil, identitypkg.ErrChannelIdentityNotFound
}
return items, nil
}
func (f *fakeQQIdentityResolver) ListUserChannelIdentities(_ context.Context, userID string) ([]identitypkg.ChannelIdentity, error) {
if f.userErr != nil {
return nil, f.userErr
}
items, ok := f.userScoped[userID]
if !ok {
return nil, nil
}
return items, nil
}
type fakeQQRouteResolver struct {
byID map[string]routepkg.Route
err error
}
func (f *fakeQQRouteResolver) GetByID(_ context.Context, routeID string) (routepkg.Route, error) {
if f.err != nil {
return routepkg.Route{}, f.err
}
item, ok := f.byID[routeID]
if !ok {
return routepkg.Route{}, pgx.ErrNoRows
}
return item, nil
}