Files
goclaw/internal/http/channel_instance_contexts_test.go
Duy /zuey/andGitHub 2cd750ac44 feat(channels): add context capability admin surface
Squash merge PR #115 after resolving changelog and SQLite migration-map conflicts with current dev. Renumbered channel-context PostgreSQL migration to 000075 and bumped PG required schema to 75 plus SQLite schema to 44 so it follows the run timeline migration. Local checks passed: go test ./..., go build ./..., go build -tags sqliteonly ./..., go vet ./..., and pnpm -C ui/web build. PR CI run 26705617311 passed release-versioning, go, and web.
2026-05-31 13:50:31 +07:00

190 lines
6.4 KiB
Go

package http
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/crypto"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
type fakeChannelContextContactStore struct {
contacts []store.ChannelContact
seenOpts []store.ContactListOpts
}
func (s *fakeChannelContextContactStore) UpsertContact(context.Context, string, string, string, string, string, string, string, string, string, string) error {
return nil
}
func (s *fakeChannelContextContactStore) ListContacts(_ context.Context, opts store.ContactListOpts) ([]store.ChannelContact, error) {
s.seenOpts = append(s.seenOpts, opts)
var out []store.ChannelContact
for _, c := range s.contacts {
if opts.ChannelType != "" && c.ChannelType != opts.ChannelType {
continue
}
if opts.ChannelInstance != "" && (c.ChannelInstance == nil || *c.ChannelInstance != opts.ChannelInstance) {
continue
}
if opts.ContactType != "" && c.ContactType != opts.ContactType {
continue
}
if opts.PeerKind != "" && (c.PeerKind == nil || *c.PeerKind != opts.PeerKind) {
continue
}
out = append(out, c)
}
return out, nil
}
func (s *fakeChannelContextContactStore) CountContacts(context.Context, store.ContactListOpts) (int, error) {
return len(s.contacts), nil
}
func (s *fakeChannelContextContactStore) GetContactsBySenderIDs(context.Context, []string) (map[string]store.ChannelContact, error) {
return map[string]store.ChannelContact{}, nil
}
func (s *fakeChannelContextContactStore) GetContactByID(context.Context, uuid.UUID) (*store.ChannelContact, error) {
return nil, nil
}
func (s *fakeChannelContextContactStore) GetSenderIDsByContactIDs(context.Context, []uuid.UUID) ([]string, error) {
return nil, nil
}
func (s *fakeChannelContextContactStore) MergeContacts(context.Context, []uuid.UUID, uuid.UUID) error {
return nil
}
func (s *fakeChannelContextContactStore) UnmergeContacts(context.Context, []uuid.UUID) error {
return nil
}
func (s *fakeChannelContextContactStore) GetContactsByMergedID(context.Context, uuid.UUID) ([]store.ChannelContact, error) {
return nil, nil
}
func (s *fakeChannelContextContactStore) ResolveTenantUserID(context.Context, string, string) (string, error) {
return "", nil
}
func TestChannelContextsListUsesStoredGroupsAndMasksToContextShape(t *testing.T) {
token := "channel-contexts-key"
setupTestCache(t, map[string]*store.APIKeyData{
crypto.HashAPIKey(token): {Scopes: []string{"operator.write"}, OwnerID: "caller"},
})
instID := uuid.New()
channelName := "discord-admin"
displayName := "Ops Discord"
peerKind := "group"
lastSeen := time.Date(2026, 5, 29, 10, 0, 0, 0, time.UTC)
contacts := &fakeChannelContextContactStore{contacts: []store.ChannelContact{{
ID: uuid.New(),
ChannelType: "discord",
ChannelInstance: &channelName,
SenderID: "guild-123/channel-456",
DisplayName: &displayName,
PeerKind: &peerKind,
ContactType: "group",
LastSeenAt: lastSeen,
}}}
handler := NewChannelInstancesHandler(
&stubChannelInstanceStore{inst: &store.ChannelInstanceData{BaseModel: store.BaseModel{ID: instID}, Name: channelName, DisplayName: "Discord Admin", ChannelType: "discord", AgentID: uuid.New()}},
nil, nil, contacts, nil, nil,
)
mux := http.NewServeMux()
handler.RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/v1/channels/instances/"+instID.String()+"/contexts", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
var body struct {
Contexts []channelContextDTO `json:"contexts"`
}
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if len(body.Contexts) != 2 {
t.Fatalf("contexts len = %d, want 2: %+v", len(body.Contexts), body.Contexts)
}
if body.Contexts[0].ScopeType != store.ChannelScopeTypeChannel || body.Contexts[0].ScopeKey != channelName {
t.Fatalf("base context = %+v", body.Contexts[0])
}
gotGroup := body.Contexts[1]
if gotGroup.ScopeType != store.ChannelScopeTypeGroup || gotGroup.ScopeKey != "guild-123/channel-456" || gotGroup.Source != "stored_contact" {
t.Fatalf("group context = %+v", gotGroup)
}
if len(contacts.seenOpts) == 0 || contacts.seenOpts[0].ChannelInstance != channelName {
t.Fatalf("expected channel_instance filter %q, got %+v", channelName, contacts.seenOpts)
}
}
func TestChannelContextMembersDoNotShowChannelWideContactsAsGroupMembers(t *testing.T) {
token := "channel-context-members-key"
setupTestCache(t, map[string]*store.APIKeyData{
crypto.HashAPIKey(token): {Scopes: []string{"operator.write"}, OwnerID: "caller"},
})
instID := uuid.New()
channelName := "telegram-main"
peerKind := "group"
displayName := "Duy"
username := "duy"
userID := "tenant-user-1"
contacts := &fakeChannelContextContactStore{contacts: []store.ChannelContact{{
ID: uuid.New(),
ChannelType: "telegram",
ChannelInstance: &channelName,
SenderID: "386246614",
UserID: &userID,
DisplayName: &displayName,
Username: &username,
PeerKind: &peerKind,
ContactType: "user",
LastSeenAt: time.Date(2026, 5, 29, 10, 5, 0, 0, time.UTC),
}}}
handler := NewChannelInstancesHandler(
&stubChannelInstanceStore{inst: &store.ChannelInstanceData{BaseModel: store.BaseModel{ID: instID}, Name: channelName, ChannelType: "telegram", AgentID: uuid.New()}},
nil, nil, contacts, nil, nil,
)
mux := http.NewServeMux()
handler.RegisterRoutes(mux)
req := httptest.NewRequest(http.MethodGet, "/v1/channels/instances/"+instID.String()+"/contexts/group/group:telegram:-100123/members", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
var body struct {
Members []channelContextMemberDTO `json:"members"`
LiveMembersSupported bool `json:"live_members_supported"`
Source string `json:"source"`
}
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.LiveMembersSupported || body.Source != "stored_contacts" {
t.Fatalf("expected unsupported stored contact response, got %+v", body)
}
if len(body.Members) != 0 {
t.Fatalf("members = %+v", body.Members)
}
}