Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 792327afd6 |
+13
-21
@@ -145,11 +145,10 @@ jobs:
|
||||
# the diagnostic step with its own continue-on-error: true (line 203).
|
||||
# Flip confirmed by CI / Platform (Go) status = success on main HEAD 363905d3.
|
||||
continue-on-error: false
|
||||
# Job-level ceiling. The go test step below runs with a per-step 70m timeout;
|
||||
# this cap catches any step that leaks past that. Set well above 70m so
|
||||
# the per-step timeout is the active constraint. Raised to 75m
|
||||
# to account for golangci-lint ~17m + test suite ~20-30m on cold runner (mc#1099).
|
||||
timeout-minutes: 75
|
||||
# Job-level ceiling. The go test step below runs with a per-step 10m timeout;
|
||||
# this cap catches any step that leaks past that. Set well above 10m so
|
||||
# the per-step timeout is the active constraint.
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: workspace-server
|
||||
@@ -175,20 +174,14 @@ jobs:
|
||||
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
|
||||
- if: always()
|
||||
name: Run golangci-lint
|
||||
# mc#1099: --no-config bypasses .golangci.yaml ceiling; --timeout 30m
|
||||
# is the active constraint. Cold runner: fetch-depth:0 clone (5-10m) + Go
|
||||
# toolchain (5-10m) + mod download (2-5m) + build + vet + install lint
|
||||
# (5m) = ~15-20m before linting even starts. 30m gives headroom.
|
||||
run: $(go env GOPATH)/bin/golangci-lint run --no-config --timeout 30m ./...
|
||||
run: $(go env GOPATH)/bin/golangci-lint run --timeout 3m ./...
|
||||
- if: always()
|
||||
name: Diagnostic — per-package verbose 600s
|
||||
# mc#1099: step-level ceiling above the 600s Go timeout for cold-runner headroom.
|
||||
timeout-minutes: 20
|
||||
name: Diagnostic — per-package verbose 60s
|
||||
run: |
|
||||
set +e
|
||||
go test -race -v -timeout 600s ./internal/handlers/... 2>&1 | tee /tmp/test-handlers.log
|
||||
go test -race -v -timeout 60s ./internal/handlers/... 2>&1 | tee /tmp/test-handlers.log
|
||||
handlers_exit=$?
|
||||
go test -race -v -timeout 600s ./internal/pendinguploads/... 2>&1 | tee /tmp/test-pu.log
|
||||
go test -race -v -timeout 60s ./internal/pendinguploads/... 2>&1 | tee /tmp/test-pu.log
|
||||
pu_exit=$?
|
||||
echo "::group::handlers exit=$handlers_exit (last 100 lines)"
|
||||
tail -100 /tmp/test-handlers.log
|
||||
@@ -200,12 +193,11 @@ jobs:
|
||||
continue-on-error: true
|
||||
- if: always()
|
||||
name: Run tests with race detection and coverage
|
||||
# mc#1099: cold runner (~5-20m) + race detector (3-5x overhead) can push
|
||||
# the suite past 10m. Per-step ceiling must exceed Go-level timeout so
|
||||
# Go's timeout fires first (clean interrupt) rather than the step ceiling
|
||||
# (SIGKILL). Job-level ceiling (75m) is the outer backstop.
|
||||
timeout-minutes: 70
|
||||
run: go test -race -timeout 60m -coverprofile=coverage.out ./...
|
||||
# Explicit timeout: cold runner cache causes OOM kills at ~4m39s on the
|
||||
# full ./... suite with race detection + coverage. A 10m per-step timeout
|
||||
# lets the suite complete on cold cache (~5-7m) while failing cleanly
|
||||
# instead of OOM-killing. The job-level timeout (15m) is a backstop.
|
||||
run: go test -race -timeout 10m -coverprofile=coverage.out ./...
|
||||
|
||||
- if: always()
|
||||
name: Per-file coverage report
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
sqlmock "github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
|
||||
)
|
||||
|
||||
// TestExtractExpiresInSeconds covers the JSON parser used at enqueue time
|
||||
@@ -58,3 +63,207 @@ func TestExtractExpiresInSeconds(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── QueueStatusByID ─────────────────────────────────────────────────────────────
|
||||
|
||||
func setupQueueStatusDB(t *testing.T) sqlmock.Sqlmock {
|
||||
t.Helper()
|
||||
mockDB, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create sqlmock: %v", err)
|
||||
}
|
||||
prevDB := db.DB
|
||||
db.DB = mockDB
|
||||
t.Cleanup(func() { db.DB = prevDB; mockDB.Close() })
|
||||
return mock
|
||||
}
|
||||
|
||||
func TestQueueStatusByID_Success(t *testing.T) {
|
||||
mock := setupQueueStatusDB(t)
|
||||
queueID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
wsID := "cccccccc-cccc-cccc-cccc-cccccccccccc"
|
||||
|
||||
rows := sqlmock.NewRows([]string{
|
||||
"id", "workspace_id", "status", "priority", "attempts",
|
||||
"last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at",
|
||||
"response_body",
|
||||
}).AddRow(
|
||||
queueID, wsID, "queued", 50, 0,
|
||||
nil, // last_error
|
||||
"2026-01-01T00:00:00Z", // enqueued_at
|
||||
nil, // dispatched_at
|
||||
nil, // completed_at
|
||||
nil, // expires_at
|
||||
nil, // response_body
|
||||
)
|
||||
mock.ExpectQuery(`SELECT`).
|
||||
WithArgs(queueID).
|
||||
WillReturnRows(rows)
|
||||
|
||||
qs, err := QueueStatusByID(context.Background(), queueID)
|
||||
if err != nil {
|
||||
t.Fatalf("QueueStatusByID returned error: %v", err)
|
||||
}
|
||||
if qs.ID != queueID {
|
||||
t.Errorf("ID = %q, want %q", qs.ID, queueID)
|
||||
}
|
||||
if qs.WorkspaceID != wsID {
|
||||
t.Errorf("WorkspaceID = %q, want %q", qs.WorkspaceID, wsID)
|
||||
}
|
||||
if qs.Status != "queued" {
|
||||
t.Errorf("Status = %q, want %q", qs.Status, "queued")
|
||||
}
|
||||
if qs.Priority != 50 {
|
||||
t.Errorf("Priority = %d, want 50", qs.Priority)
|
||||
}
|
||||
if qs.LastError != nil {
|
||||
t.Errorf("LastError = %v, want nil", qs.LastError)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueStatusByID_NotFound(t *testing.T) {
|
||||
mock := setupQueueStatusDB(t)
|
||||
queueID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
|
||||
mock.ExpectQuery(`SELECT`).
|
||||
WithArgs(queueID).
|
||||
WillReturnError(sql.ErrNoRows)
|
||||
|
||||
qs, err := QueueStatusByID(context.Background(), queueID)
|
||||
if err != sql.ErrNoRows {
|
||||
t.Errorf("expected sql.ErrNoRows, got %v", err)
|
||||
}
|
||||
if qs != nil {
|
||||
t.Errorf("expected nil queue status, got %+v", qs)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueStatusByID_DBError(t *testing.T) {
|
||||
mock := setupQueueStatusDB(t)
|
||||
queueID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
|
||||
mock.ExpectQuery(`SELECT`).
|
||||
WithArgs(queueID).
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
qs, err := QueueStatusByID(context.Background(), queueID)
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
if qs != nil {
|
||||
t.Errorf("expected nil queue status, got %+v", qs)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueStatusByID_CompletedWithResponse(t *testing.T) {
|
||||
mock := setupQueueStatusDB(t)
|
||||
queueID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
wsID := "cccccccc-cccc-cccc-cccc-cccccccccccc"
|
||||
|
||||
respBody := []byte(`{"text":"delegation result"}`)
|
||||
rows := sqlmock.NewRows([]string{
|
||||
"id", "workspace_id", "status", "priority", "attempts",
|
||||
"last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at",
|
||||
"response_body",
|
||||
}).AddRow(
|
||||
queueID, wsID, "completed", 50, 1,
|
||||
nil,
|
||||
"2026-01-01T00:00:00Z",
|
||||
"2026-01-01T00:01:00Z",
|
||||
"2026-01-01T00:02:00Z",
|
||||
nil,
|
||||
respBody,
|
||||
)
|
||||
mock.ExpectQuery(`SELECT`).
|
||||
WithArgs(queueID).
|
||||
WillReturnRows(rows)
|
||||
|
||||
qs, err := QueueStatusByID(context.Background(), queueID)
|
||||
if err != nil {
|
||||
t.Fatalf("QueueStatusByID returned error: %v", err)
|
||||
}
|
||||
if qs.Status != "completed" {
|
||||
t.Errorf("Status = %q, want completed", qs.Status)
|
||||
}
|
||||
if qs.ResponseBody == nil {
|
||||
t.Fatal("ResponseBody should be set for completed status")
|
||||
}
|
||||
if string(qs.ResponseBody) != `{"text":"delegation result"}` {
|
||||
t.Errorf("ResponseBody = %q, want %q", string(qs.ResponseBody), `{"text":"delegation result"}`)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ── queueRowAuthFields ──────────────────────────────────────────────────────────
|
||||
|
||||
func TestQueueRowAuthFields_Success(t *testing.T) {
|
||||
mock := setupQueueStatusDB(t)
|
||||
queueID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
callerID := "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
|
||||
wsID := "cccccccc-cccc-cccc-cccc-cccccccccccc"
|
||||
|
||||
rows := sqlmock.NewRows([]string{"caller_id", "workspace_id"}).
|
||||
AddRow(callerID, wsID)
|
||||
mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id`).
|
||||
WithArgs(queueID).
|
||||
WillReturnRows(rows)
|
||||
|
||||
gotCaller, gotWs, err := queueRowAuthFields(context.Background(), queueID)
|
||||
if err != nil {
|
||||
t.Fatalf("queueRowAuthFields returned error: %v", err)
|
||||
}
|
||||
if gotCaller != callerID {
|
||||
t.Errorf("callerID = %q, want %q", gotCaller, callerID)
|
||||
}
|
||||
if gotWs != wsID {
|
||||
t.Errorf("workspaceID = %q, want %q", gotWs, wsID)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueRowAuthFields_NotFound(t *testing.T) {
|
||||
mock := setupQueueStatusDB(t)
|
||||
queueID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
|
||||
mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id`).
|
||||
WithArgs(queueID).
|
||||
WillReturnError(sql.ErrNoRows)
|
||||
|
||||
_, _, err := queueRowAuthFields(context.Background(), queueID)
|
||||
if err != sql.ErrNoRows {
|
||||
t.Errorf("expected sql.ErrNoRows, got %v", err)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueRowAuthFields_DBError(t *testing.T) {
|
||||
mock := setupQueueStatusDB(t)
|
||||
queueID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
|
||||
mock.ExpectQuery(`SELECT caller_id, workspace_id FROM a2a_queue WHERE id`).
|
||||
WithArgs(queueID).
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
_, _, err := queueRowAuthFields(context.Background(), queueID)
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,3 +516,51 @@ func TestDrainQueueForWorkspace_ClaimGuarding_SecondDrainGetsEmpty(t *testing.T)
|
||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ── QueueDepth ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestQueueDepth_ReturnsCount(t *testing.T) {
|
||||
mockDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create sqlmock: %v", err)
|
||||
}
|
||||
prevDB := db.DB
|
||||
db.DB = mockDB
|
||||
t.Cleanup(func() { db.DB = prevDB; mockDB.Close() })
|
||||
|
||||
wsID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
mock.ExpectQuery(`SELECT COUNT(*) FROM a2a_queue WHERE workspace_id = $1 AND status = 'queued'`).
|
||||
WithArgs(wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(42))
|
||||
|
||||
got := QueueDepth(context.Background(), wsID)
|
||||
if got != 42 {
|
||||
t.Errorf("QueueDepth returned %d, want 42", got)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueDepth_ZeroWhenEmpty(t *testing.T) {
|
||||
mockDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create sqlmock: %v", err)
|
||||
}
|
||||
prevDB := db.DB
|
||||
db.DB = mockDB
|
||||
t.Cleanup(func() { db.DB = prevDB; mockDB.Close() })
|
||||
|
||||
wsID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
mock.ExpectQuery(`SELECT COUNT(*) FROM a2a_queue WHERE workspace_id = $1 AND status = 'queued'`).
|
||||
WithArgs(wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
|
||||
|
||||
got := QueueDepth(context.Background(), wsID)
|
||||
if got != 0 {
|
||||
t.Errorf("QueueDepth returned %d, want 0", got)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func (h *ChannelHandler) List(c *gin.Context) {
|
||||
last_message_at, message_count, created_at, updated_at
|
||||
FROM workspace_channels WHERE workspace_id = $1
|
||||
ORDER BY created_at
|
||||
`, workspaceID) // CI re-trigger: push to re-run golangci-lint with cold runner timeout fix (mc#1099)
|
||||
`, workspaceID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
||||
return
|
||||
@@ -104,9 +104,6 @@ func (h *ChannelHandler) List(c *gin.Context) {
|
||||
}
|
||||
result = append(result, entry)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Printf("Channels list rows.Err workspace=%s: %v", workspaceID, err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
@@ -517,9 +514,6 @@ func (h *ChannelHandler) Webhook(c *gin.Context) {
|
||||
candidates = append(candidates, row)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Printf("Channels webhook rows.Err channel_type=%s: %v", channelType, err)
|
||||
}
|
||||
|
||||
if targetSlug != "" {
|
||||
// [slug] routing — match against config username (lowercased)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -948,6 +947,73 @@ func TestVerifyDiscordSignature_WrongLengthPubKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== matchesChatID pure function ====================
|
||||
|
||||
func TestMatchesChatID_ExactMatch(t *testing.T) {
|
||||
cfg := map[string]interface{}{"chat_id": "123456"}
|
||||
if !matchesChatID(cfg, "123456") {
|
||||
t.Error("expected true for exact match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesChatID_NoMatch(t *testing.T) {
|
||||
cfg := map[string]interface{}{"chat_id": "123456"}
|
||||
if matchesChatID(cfg, "654321") {
|
||||
t.Error("expected false for non-matching chat ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesChatID_PrefixNoMatch(t *testing.T) {
|
||||
// "123" is a prefix of "123456" but not an exact match.
|
||||
cfg := map[string]interface{}{"chat_id": "123456"}
|
||||
if matchesChatID(cfg, "123") {
|
||||
t.Error("expected false for prefix of stored chat ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesChatID_CommaSeparatedMultiple(t *testing.T) {
|
||||
cfg := map[string]interface{}{"chat_id": "111,222,333"}
|
||||
for _, id := range []string{"111", "222", "333"} {
|
||||
if !matchesChatID(cfg, id) {
|
||||
t.Errorf("expected true for %q in comma-separated list", id)
|
||||
}
|
||||
}
|
||||
if matchesChatID(cfg, "444") {
|
||||
t.Error("expected false for ID not in list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesChatID_WhitespaceTrimmed(t *testing.T) {
|
||||
cfg := map[string]interface{}{"chat_id": "111, 222 , 333"}
|
||||
if !matchesChatID(cfg, "222") {
|
||||
t.Error("expected true for whitespace-trimmed match")
|
||||
}
|
||||
if matchesChatID(cfg, " 222") {
|
||||
t.Error("expected false for whitespace in query (not trimmed from query)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesChatID_EmptyChatID(t *testing.T) {
|
||||
cfg := map[string]interface{}{"chat_id": ""}
|
||||
if matchesChatID(cfg, "123456") {
|
||||
t.Error("expected false for empty chat_id in config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesChatID_MissingChatIDKey(t *testing.T) {
|
||||
cfg := map[string]interface{}{}
|
||||
if matchesChatID(cfg, "123456") {
|
||||
t.Error("expected false when chat_id key is missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesChatID_NonStringChatID(t *testing.T) {
|
||||
cfg := map[string]interface{}{"chat_id": 123456} // wrong type
|
||||
if matchesChatID(cfg, "123456") {
|
||||
t.Error("expected false when chat_id is not a string")
|
||||
}
|
||||
}
|
||||
|
||||
// TestChannelHandler_Webhook_Discord_NoKey_Returns401 verifies that a Discord
|
||||
// webhook request is rejected with 401 when no public key is configured in the
|
||||
// DB and DISCORD_APP_PUBLIC_KEY env var is not set.
|
||||
@@ -1014,54 +1080,6 @@ func TestChannelHandler_Webhook_Discord_InvalidSig_Returns401(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestChannelHandler_List_RowsErr_LogsError verifies that when the row iterator
|
||||
// returns an error after the last row (mid-stream DB error), rows.Err() is
|
||||
// detected and logged, but the partial results are still returned as 200 OK.
|
||||
// This is the fix for the missing rows.Err() check in List().
|
||||
func TestChannelHandler_List_RowsErr_LogsError(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
handler := NewChannelHandler(newTestChannelManager())
|
||||
|
||||
// Return one valid row, then mark row 0 as having a scan error.
|
||||
// RowError(n, err) causes Scan() to fail on row n, and sets rows.Err()
|
||||
// to the error. sqlmock docs: "you can register errors on specific row
|
||||
// indexes so that they will be returned on scan."
|
||||
rows := sqlmock.NewRows([]string{
|
||||
"id", "workspace_id", "channel_type", "channel_config", "enabled",
|
||||
"allowed_users", "last_message_at", "message_count", "created_at", "updated_at",
|
||||
}).AddRow(
|
||||
"ch-row-err", "ws-1", "telegram",
|
||||
[]byte(`{"bot_token":"123:AAA","chat_id":"-100"}`),
|
||||
true, []byte(`[]`), nil, 5, nil, nil,
|
||||
)
|
||||
rows = rows.RowError(0, errors.New("connection lost"))
|
||||
|
||||
mock.ExpectQuery("SELECT .* FROM workspace_channels WHERE workspace_id").
|
||||
WithArgs("ws-1").
|
||||
WillReturnRows(rows)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request, _ = http.NewRequest("GET", "/workspaces/ws-1/channels", nil)
|
||||
c.Params = gin.Params{{Key: "id", Value: "ws-1"}}
|
||||
|
||||
handler.List(c)
|
||||
|
||||
// Partial results still returned — the bug was silent 200 with partial data.
|
||||
if w.Code != 200 {
|
||||
t.Errorf("expected 200 (partial results on rows.Err), got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
// The rows.Err() is logged, not surfaced to the client (non-fatal).
|
||||
var result []map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &result)
|
||||
if len(result) == 0 {
|
||||
t.Error("expected at least partial results despite rows.Err")
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("sqlmock expectations not met: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestChannelHandler_Webhook_Discord_ValidSig_PingAccepted verifies that a
|
||||
// correctly signed Discord PING (type=1) passes the signature gate and the
|
||||
// handler returns 200 (PING returns nil msg → "ignored" status).
|
||||
|
||||
Reference in New Issue
Block a user