mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 06:17:41 +00:00
Phase 03 - facebook 23.1% -> 81.9% (+58.8%, target ≥60%) graph_api_test.go: NewGraphClient, VerifyToken, SubscribeApp, GetPost, GetComment, GetCommentThread, ReplyToComment, SendMessage, SendTypingOn, doRequest retry/backoff matrix (5xx retry, 401 non-retry, 429+Retry-After, context cancel during backoff, transport error + retry), 551/subcode 24h window non-retry, logRateLimit parse branches. handlers_test.go: New required-field validation (4 branches), Factory valid+malformed, WebhookHandler single-shot route, handleAPIError health state mapping, handleCommentEvent (feature gate, edit/remove drop, page routing, self-reply skip, dedup, enriched content path), handleMessagingEvent (feature gate, page routing, self skip, receipt drop, text+postback dispatch, dedup), Send Messenger+comment paths + missing-metadata error, sendFirstInbox one-shot dedup, runDedupCleaner stale eviction, Stop closes deps, webhookRouter register/unregister/route-once/ServeHTTP-no-instances, PostFetcher (defaults, empty-id, cache hit/miss/expired, error propagation, GetCommentThread empty+delegate). Minimal refactor: converted package-level graphAPIBase const to var so tests can point at httptest server. KISS, zero runtime behavior change. Phase 04 - store/pg 1.3% -> 3.5% (+2.2%, unit-test-only) scan_rows_test.go: pure-logic transformation tests for agentShareRow, userInstanceRow, documentInfoRow, documentDetailRow, chunkInfoRow, scoredChunkRow, episodicSummaryRow, episodicScoredRow, sessionListRow, sessionPagedRow, sessionRichRow, entityRow, entityTemporalRow, relationRow, relationExportRow, traversalRow, dedupCandidateRow, mcpAccessRequestRow, cronRunLogRow, skillInfoRow+skillInfoRowWithFrontmatter, customSkillExportRow, parseDepsColumn, parseFrontmatterAuthor, marshalFrontmatter, buildSkillInfo, mergeEpisodicScores, hybridMerge. pure_logic_test.go: matchWildcard pattern matching, evalPermRows priority resolution (individual deny > individual allow > group deny > group allow), contactResolveCache get/set/expired/negative cache. Finding: 30% target not reachable with unit tests alone. Store/pg is fundamentally DB-bound (~6000 LOC of SQL code vs ~200 LOC pure logic). Reaching 30% requires running integration tests with TEST_DATABASE_URL and -coverpkg=./internal/store/pg/ — which the current CI ratchet does NOT do. Confirmed: integration tests yield 32% coverage on store/pg, but that requires a wired-up test database in CI. Deferred as infrastructure change outside Phase 04 scope. Ratchet bumps (scripts/coverage_thresholds.json): - internal/channels/facebook: 23.08 -> 81.80 - internal/store/pg: 1.28 -> 3.45 Race clean, dual build (default + -tags sqliteonly) clean.
178 lines
4.8 KiB
Go
178 lines
4.8 KiB
Go
package pg
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// --- config_permissions.go: evalPermRows + matchWildcard + itoa ---
|
|
|
|
func TestMatchWildcard(t *testing.T) {
|
|
cases := []struct {
|
|
pattern string
|
|
value string
|
|
want bool
|
|
}{
|
|
{"*", "anything", true},
|
|
{"*", "", true},
|
|
{"group:*", "group:telegram:123", true},
|
|
{"group:*", "group:telegram", true},
|
|
{"group:*", "dm:telegram", false},
|
|
{"group:telegram:*", "group:telegram:123", true},
|
|
{"group:telegram:*", "group:telegram", true},
|
|
{"group:telegram:*", "group:slack:123", false},
|
|
{"exact", "exact", true},
|
|
{"exact", "other", false},
|
|
{"group:telegram:-100456", "group:telegram:-100456", true},
|
|
{"group:telegram:-100456", "group:telegram:-100457", false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.pattern+" vs "+tc.value, func(t *testing.T) {
|
|
if got := matchWildcard(tc.pattern, tc.value); got != tc.want {
|
|
t.Errorf("matchWildcard(%q, %q) = %v, want %v", tc.pattern, tc.value, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvalPermRows_IndividualDenyWinsOverGroupAllow(t *testing.T) {
|
|
rows := []permRow{
|
|
{Scope: "*", ConfigType: "*", UserID: "*", Permission: "allow"},
|
|
{Scope: "*", ConfigType: "*", UserID: "alice", Permission: "deny"},
|
|
}
|
|
if got := evalPermRows(rows, "any", "any", "alice"); got {
|
|
t.Error("individual deny should win over group allow")
|
|
}
|
|
}
|
|
|
|
func TestEvalPermRows_IndividualAllowWinsOverGroupDeny(t *testing.T) {
|
|
rows := []permRow{
|
|
{Scope: "*", ConfigType: "*", UserID: "*", Permission: "deny"},
|
|
{Scope: "*", ConfigType: "*", UserID: "alice", Permission: "allow"},
|
|
}
|
|
if got := evalPermRows(rows, "any", "any", "alice"); !got {
|
|
t.Error("individual allow should win over group deny")
|
|
}
|
|
}
|
|
|
|
func TestEvalPermRows_GroupAllowWhenNoIndividual(t *testing.T) {
|
|
rows := []permRow{
|
|
{Scope: "*", ConfigType: "*", UserID: "*", Permission: "allow"},
|
|
}
|
|
if got := evalPermRows(rows, "any", "any", "bob"); !got {
|
|
t.Error("group allow should apply when no individual rule")
|
|
}
|
|
}
|
|
|
|
func TestEvalPermRows_GroupDenyWhenNoIndividual(t *testing.T) {
|
|
rows := []permRow{
|
|
{Scope: "*", ConfigType: "*", UserID: "*", Permission: "deny"},
|
|
}
|
|
if got := evalPermRows(rows, "any", "any", "bob"); got {
|
|
t.Error("group deny should apply when no individual rule")
|
|
}
|
|
}
|
|
|
|
func TestEvalPermRows_NoMatchDefaultsDeny(t *testing.T) {
|
|
rows := []permRow{
|
|
{Scope: "different", ConfigType: "*", UserID: "alice", Permission: "allow"},
|
|
}
|
|
// scope doesn't match → no rule applies → implicit deny.
|
|
if got := evalPermRows(rows, "other", "any", "alice"); got {
|
|
t.Error("no matching rule should default to deny")
|
|
}
|
|
}
|
|
|
|
func TestEvalPermRows_ScopeFiltering(t *testing.T) {
|
|
rows := []permRow{
|
|
{Scope: "group:*", ConfigType: "*", UserID: "*", Permission: "allow"},
|
|
}
|
|
if !evalPermRows(rows, "group:telegram:1", "skill", "u") {
|
|
t.Error("wildcarded scope should match")
|
|
}
|
|
if evalPermRows(rows, "dm:telegram", "skill", "u") {
|
|
t.Error("wildcarded scope should NOT match different prefix")
|
|
}
|
|
}
|
|
|
|
func TestEvalPermRows_ConfigTypeFiltering(t *testing.T) {
|
|
rows := []permRow{
|
|
{Scope: "*", ConfigType: "skill", UserID: "u", Permission: "allow"},
|
|
}
|
|
if !evalPermRows(rows, "any", "skill", "u") {
|
|
t.Error("exact config_type match should allow")
|
|
}
|
|
if evalPermRows(rows, "any", "other", "u") {
|
|
t.Error("different config_type should not allow")
|
|
}
|
|
}
|
|
|
|
func TestItoa(t *testing.T) {
|
|
cases := []struct {
|
|
n int
|
|
want string
|
|
}{
|
|
{0, "0"},
|
|
{1, "1"},
|
|
{42, "42"},
|
|
{-5, "-5"},
|
|
{1000, "1000"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := itoa(tc.n); got != tc.want {
|
|
t.Errorf("itoa(%d) = %q, want %q", tc.n, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- contact_resolve.go: contactResolveCache ---
|
|
|
|
func TestContactResolveCache_GetSet(t *testing.T) {
|
|
c := newContactResolveCache()
|
|
|
|
// Miss.
|
|
if v, ok := c.get("k1"); ok {
|
|
t.Errorf("expected miss, got %q", v)
|
|
}
|
|
|
|
// Set and hit.
|
|
c.set("k1", "tenant-user-1")
|
|
if v, ok := c.get("k1"); !ok || v != "tenant-user-1" {
|
|
t.Errorf("get after set: got %q ok=%v", v, ok)
|
|
}
|
|
|
|
// Set another key without interfering.
|
|
c.set("k2", "tenant-user-2")
|
|
if v, ok := c.get("k2"); !ok || v != "tenant-user-2" {
|
|
t.Errorf("k2: got %q ok=%v", v, ok)
|
|
}
|
|
if v, ok := c.get("k1"); !ok || v != "tenant-user-1" {
|
|
t.Errorf("k1 after k2 set: got %q ok=%v", v, ok)
|
|
}
|
|
}
|
|
|
|
func TestContactResolveCache_NegativeResult(t *testing.T) {
|
|
c := newContactResolveCache()
|
|
// Negative results are cached as empty string with ok=true.
|
|
c.set("k1", "")
|
|
v, ok := c.get("k1")
|
|
if !ok {
|
|
t.Error("negative cache entry should be present")
|
|
}
|
|
if v != "" {
|
|
t.Errorf("negative v = %q, want empty", v)
|
|
}
|
|
}
|
|
|
|
func TestContactResolveCache_ExpiredEntry(t *testing.T) {
|
|
c := newContactResolveCache()
|
|
// Manually insert a stale entry.
|
|
c.items["k1"] = contactResolveEntry{
|
|
tenantUserID: "old-user",
|
|
fetched: time.Now().Add(-2 * contactResolveCacheTTL),
|
|
}
|
|
if _, ok := c.get("k1"); ok {
|
|
t.Error("expired entry should not be returned as hit")
|
|
}
|
|
}
|