Merge pull request 'test(handlers): add HTTP handler coverage for schedules.go — 21 cases' (#980) from feat/976-schedules-handler-test-coverage into staging
Some checks failed
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (push) Waiting to run
CI / Platform (Go) (push) Blocked by required conditions
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Canvas Deploy Reminder (push) Blocked by required conditions
CI / Python Lint & Test (push) Blocked by required conditions
CI / all-required (push) Blocked by required conditions
E2E API Smoke Test / detect-changes (push) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (push) Blocked by required conditions
Handlers Postgres Integration / detect-changes (push) Waiting to run
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
Harness Replays / Harness Replays (push) Blocked by required conditions
Runtime PR-Built Compatibility / detect-changes (push) Waiting to run
Runtime PR-Built Compatibility / PR-built wheel + import smoke (push) Blocked by required conditions
Harness Replays / detect-changes (push) Has been cancelled
Secret scan / Scan diff for credential-shaped strings (push) Successful in 16s
Some checks failed
Block internal-flavored paths / Block forbidden paths (push) Waiting to run
CI / Detect changes (push) Waiting to run
CI / Platform (Go) (push) Blocked by required conditions
CI / Canvas (Next.js) (push) Blocked by required conditions
CI / Shellcheck (E2E scripts) (push) Blocked by required conditions
CI / Canvas Deploy Reminder (push) Blocked by required conditions
CI / Python Lint & Test (push) Blocked by required conditions
CI / all-required (push) Blocked by required conditions
E2E API Smoke Test / detect-changes (push) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (push) Blocked by required conditions
Handlers Postgres Integration / detect-changes (push) Waiting to run
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
Harness Replays / Harness Replays (push) Blocked by required conditions
Runtime PR-Built Compatibility / detect-changes (push) Waiting to run
Runtime PR-Built Compatibility / PR-built wheel + import smoke (push) Blocked by required conditions
Harness Replays / detect-changes (push) Has been cancelled
Secret scan / Scan diff for credential-shaped strings (push) Successful in 16s
This commit is contained in:
commit
c2aacaef2e
911
workspace-server/internal/handlers/schedules_handler_test.go
Normal file
911
workspace-server/internal/handlers/schedules_handler_test.go
Normal file
@ -0,0 +1,911 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ─── List ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestList_EmptyResult(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
mock.ExpectQuery(`SELECT .* FROM workspace_schedules WHERE workspace_id = \$1`).
|
||||
WithArgs(wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"id", "workspace_id", "name", "cron_expr", "timezone", "prompt",
|
||||
"enabled", "last_run_at", "next_run_at", "run_count", "last_status",
|
||||
"last_error", "source", "created_at", "updated_at",
|
||||
}))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: wsID}}
|
||||
c.Request = httptest.NewRequest("GET", "/workspaces/"+wsID+"/schedules", nil)
|
||||
|
||||
handler.List(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var schedules []scheduleResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &schedules); err != nil {
|
||||
t.Fatalf("response not JSON: %v", err)
|
||||
}
|
||||
if len(schedules) != 0 {
|
||||
t.Errorf("expected empty list, got %d items", len(schedules))
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestList_QueryError_Returns500(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
mock.ExpectQuery(`SELECT .* FROM workspace_schedules WHERE workspace_id = \$1`).
|
||||
WithArgs(wsID).
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: wsID}}
|
||||
c.Request = httptest.NewRequest("GET", "/workspaces/"+wsID+"/schedules", nil)
|
||||
|
||||
handler.List(c)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestList_ScanError_Continues is not directly testable with sqlmock because
|
||||
// sqlmock panics when a row has the wrong number of columns (rather than
|
||||
// returning a scan error the way a real DB driver would). The handler's scan
|
||||
// error handling (log + continue) is implicitly covered by the multi-row test
|
||||
// TestList_IncludesSourceColumn — the handler's scan loop uses `continue` on
|
||||
// error, so correctly-shaped rows are always returned regardless of what
|
||||
// earlier rows did.
|
||||
|
||||
// ─── Create ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestCreate_MissingCronExpr_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
body := []byte(`{"prompt":"do thing"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "ws-1"}}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/ws-1/schedules", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_MissingPrompt_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
body := []byte(`{"cron_expr":"*/5 * * * *"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "ws-1"}}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/ws-1/schedules", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_InvalidTimezone_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
body := []byte(`{"cron_expr":"*/5 * * * *","prompt":"do thing","timezone":"Not/A/Zone"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "ws-1"}}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/ws-1/schedules", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "invalid timezone") {
|
||||
t.Errorf("error message should mention 'invalid timezone': %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_InvalidCronExpr_Returns400(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
body := []byte(`{"cron_expr":"not-a-cron","prompt":"do thing"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "ws-1"}}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/ws-1/schedules", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_CRLFStrippedFromPrompt(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
// The prompt in the DB should NOT contain \r.
|
||||
mock.ExpectQuery("INSERT INTO workspace_schedules").
|
||||
WithArgs(wsID, "test", "*/5 * * * *", "UTC", "line1\nline2", true, sqlmock.AnyArg()).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("sched-1"))
|
||||
|
||||
body := []byte(`{"name":"test","cron_expr":"*/5 * * * *","prompt":"line1\r\nline2"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: wsID}}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/"+wsID+"/schedules", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v — the \r must be stripped before INSERT", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_DefaultsEnabledTrue(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
// enabled=true is the default when body.enabled is nil.
|
||||
mock.ExpectQuery("INSERT INTO workspace_schedules").
|
||||
WithArgs(wsID, "test", "*/5 * * * *", "UTC", "do thing", true, sqlmock.AnyArg()).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("sched-1"))
|
||||
|
||||
body := []byte(`{"name":"test","cron_expr":"*/5 * * * *","prompt":"do thing"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: wsID}}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/"+wsID+"/schedules", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_DefaultsTimezoneUTC(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
// Timezone defaults to UTC when not specified.
|
||||
mock.ExpectQuery("INSERT INTO workspace_schedules").
|
||||
WithArgs(wsID, "test", "*/5 * * * *", "UTC", "do thing", true, sqlmock.AnyArg()).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("sched-1"))
|
||||
|
||||
body := []byte(`{"name":"test","cron_expr":"*/5 * * * *","prompt":"do thing"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: wsID}}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/"+wsID+"/schedules", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_ExplicitEnabledFalse(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
// enabled=false when explicitly set.
|
||||
mock.ExpectQuery("INSERT INTO workspace_schedules").
|
||||
WithArgs(wsID, "test", "*/5 * * * *", "UTC", "do thing", false, sqlmock.AnyArg()).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("sched-1"))
|
||||
|
||||
body := []byte(`{"name":"test","cron_expr":"*/5 * * * *","prompt":"do thing","enabled":false}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: wsID}}
|
||||
req := httptest.NewRequest("POST", "/workspaces/"+wsID+"/schedules", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.Request = req
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_DBError_Returns500(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
mock.ExpectQuery("INSERT INTO workspace_schedules").
|
||||
WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(),
|
||||
sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
body := []byte(`{"cron_expr":"*/5 * * * *","prompt":"do thing"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "ws-1"}}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/ws-1/schedules", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_ReturnsNextRunAt(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
mock.ExpectQuery("INSERT INTO workspace_schedules").
|
||||
WithArgs(wsID, "test", "*/5 * * * *", "UTC", "do thing", true, sqlmock.AnyArg()).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("sched-1"))
|
||||
|
||||
body := []byte(`{"name":"test","cron_expr":"*/5 * * * *","prompt":"do thing"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: wsID}}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/"+wsID+"/schedules", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Create(c)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("response not JSON: %v", err)
|
||||
}
|
||||
if resp["status"] != "created" {
|
||||
t.Errorf("status=created: got %v", resp["status"])
|
||||
}
|
||||
if _, ok := resp["id"]; !ok {
|
||||
t.Errorf("response missing id field")
|
||||
}
|
||||
if _, ok := resp["next_run_at"]; !ok {
|
||||
t.Errorf("response missing next_run_at field")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Update ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestUpdate_PartialUpdate_CRONChangeRecomputesNextRun(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
// 1. Lookup current cron + timezone.
|
||||
mock.ExpectQuery(`SELECT cron_expr, timezone FROM workspace_schedules WHERE id = \$1 AND workspace_id = \$2`).
|
||||
WithArgs(schedID, wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"cron_expr", "timezone"}).
|
||||
AddRow("0 * * * *", "UTC"))
|
||||
|
||||
// 2. UPDATE with new cron_expr but old timezone; next_run_at = new computed.
|
||||
mock.ExpectExec(`UPDATE workspace_schedules SET`).
|
||||
WithArgs(schedID, sqlmock.AnyArg(), "*/5 * * * *", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), wsID).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
body := []byte(`{"cron_expr":"*/5 * * * *"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("PATCH", "/workspaces/"+wsID+"/schedules/"+schedID, bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Update(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_PartialUpdate_TimezoneChangeRecomputesNextRun(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
mock.ExpectQuery(`SELECT cron_expr, timezone FROM workspace_schedules WHERE id = \$1 AND workspace_id = \$2`).
|
||||
WithArgs(schedID, wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"cron_expr", "timezone"}).
|
||||
AddRow("0 * * * *", "UTC"))
|
||||
|
||||
mock.ExpectExec(`UPDATE workspace_schedules SET`).
|
||||
WithArgs(schedID, sqlmock.AnyArg(), sqlmock.AnyArg(), "America/New_York", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), wsID).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
body := []byte(`{"timezone":"America/New_York"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("PATCH", "/workspaces/"+wsID+"/schedules/"+schedID, bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Update(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_NoScheduleMatch_Returns404(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
// body={} means CronExpr=nil AND Timezone=nil → handler skips the lookup
|
||||
// and goes straight to UPDATE. Expect UPDATE with 0 rows affected → 404.
|
||||
mock.ExpectExec(`UPDATE workspace_schedules SET`).
|
||||
WithArgs(schedID, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), wsID).
|
||||
WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("PATCH", "/workspaces/"+wsID+"/schedules/"+schedID, bytes.NewReader([]byte(`{}`)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Update(c)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_InvalidTimezone_Returns400(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
mock.ExpectQuery(`SELECT cron_expr, timezone FROM workspace_schedules WHERE id = \$1 AND workspace_id = \$2`).
|
||||
WithArgs(schedID, wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"cron_expr", "timezone"}).
|
||||
AddRow("0 * * * *", "UTC"))
|
||||
|
||||
body := []byte(`{"timezone":"Mars/Olympus"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("PATCH", "/workspaces/"+wsID+"/schedules/"+schedID, bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Update(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "invalid timezone") {
|
||||
t.Errorf("error should mention 'invalid timezone': %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_InvalidCronExpr_Returns400(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
mock.ExpectQuery(`SELECT cron_expr, timezone FROM workspace_schedules WHERE id = \$1 AND workspace_id = \$2`).
|
||||
WithArgs(schedID, wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"cron_expr", "timezone"}).
|
||||
AddRow("0 * * * *", "UTC"))
|
||||
|
||||
body := []byte(`{"cron_expr":"[invalid"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("PATCH", "/workspaces/"+wsID+"/schedules/"+schedID, bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Update(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_ScheduleNotFoundOnExec_Returns404(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
// No cron/timezone change → no lookup; goes straight to UPDATE.
|
||||
// RowsAffected=0 means no matching row → 404.
|
||||
mock.ExpectExec(`UPDATE workspace_schedules SET`).
|
||||
WithArgs(schedID, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), wsID).
|
||||
WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
|
||||
body := []byte(`{"name":"new name"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("PATCH", "/workspaces/"+wsID+"/schedules/"+schedID, bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Update(c)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_DBError_Returns500(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
mock.ExpectExec(`UPDATE workspace_schedules SET`).
|
||||
WithArgs(schedID, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), wsID).
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
body := []byte(`{"name":"new name"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("PATCH", "/workspaces/"+wsID+"/schedules/"+schedID, bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Update(c)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_PromptCRLFStripped(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
// No cron/timezone change → no lookup; UPDATE directly.
|
||||
// The prompt arg must have \r stripped.
|
||||
mock.ExpectExec(`UPDATE workspace_schedules SET`).
|
||||
WithArgs(schedID, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "line1\nline2", sqlmock.AnyArg(), sqlmock.AnyArg(), wsID).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
body := []byte(`{"prompt":"line1\r\nline2"}`)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("PATCH", "/workspaces/"+wsID+"/schedules/"+schedID, bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.Update(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v — \\r must be stripped before UPDATE", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Delete ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestDelete_Success(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
mock.ExpectExec(`DELETE FROM workspace_schedules WHERE id = \$1 AND workspace_id = \$2`).
|
||||
WithArgs(schedID, wsID).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("DELETE", "/workspaces/"+wsID+"/schedules/"+schedID, nil)
|
||||
|
||||
handler.Delete(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "deleted") {
|
||||
t.Errorf("response should contain 'deleted': %s", w.Body.String())
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete_NotFound_Returns404(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
// IDOR: schedule belongs to a different workspace → no rows deleted.
|
||||
mock.ExpectExec(`DELETE FROM workspace_schedules WHERE id = \$1 AND workspace_id = \$2`).
|
||||
WithArgs(schedID, wsID).
|
||||
WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("DELETE", "/workspaces/"+wsID+"/schedules/"+schedID, nil)
|
||||
|
||||
handler.Delete(c)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete_DBError_Returns500(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
mock.ExpectExec(`DELETE FROM workspace_schedules WHERE id = \$1 AND workspace_id = \$2`).
|
||||
WithArgs(schedID, wsID).
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("DELETE", "/workspaces/"+wsID+"/schedules/"+schedID, nil)
|
||||
|
||||
handler.Delete(c)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ─── RunNow ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestRunNow_Success(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
mock.ExpectQuery(`SELECT prompt FROM workspace_schedules WHERE id = \$1 AND workspace_id = \$2`).
|
||||
WithArgs(schedID, wsID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"prompt"}).AddRow("do the thing"))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/"+wsID+"/schedules/"+schedID+"/run", nil)
|
||||
|
||||
handler.RunNow(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("response not JSON: %v", err)
|
||||
}
|
||||
if resp["status"] != "fired" {
|
||||
t.Errorf("status=fired: got %v", resp["status"])
|
||||
}
|
||||
if resp["prompt"] != "do the thing" {
|
||||
t.Errorf("prompt: got %v", resp["prompt"])
|
||||
}
|
||||
if resp["workspace_id"] != wsID {
|
||||
t.Errorf("workspace_id: got %v", resp["workspace_id"])
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunNow_NotFound_Returns404(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
mock.ExpectQuery(`SELECT prompt FROM workspace_schedules WHERE id = \$1 AND workspace_id = \$2`).
|
||||
WithArgs(schedID, wsID).
|
||||
WillReturnError(sql.ErrNoRows)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/"+wsID+"/schedules/"+schedID+"/run", nil)
|
||||
|
||||
handler.RunNow(c)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunNow_DBError_Returns500(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
mock.ExpectQuery(`SELECT prompt FROM workspace_schedules WHERE id = \$1 AND workspace_id = \$2`).
|
||||
WithArgs(schedID, wsID).
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/"+wsID+"/schedules/"+schedID+"/run", nil)
|
||||
|
||||
handler.RunNow(c)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ─── History ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestHistory_EmptyResult(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
cols := []string{"created_at", "duration_ms", "status", "error_detail", "request_body"}
|
||||
mock.ExpectQuery(`SELECT created_at, duration_ms, status`).
|
||||
WithArgs(wsID, schedID).
|
||||
WillReturnRows(sqlmock.NewRows(cols))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("GET",
|
||||
"/workspaces/"+wsID+"/schedules/"+schedID+"/history", nil)
|
||||
|
||||
handler.History(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var entries []map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &entries); err != nil {
|
||||
t.Fatalf("response not JSON: %v", err)
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
t.Errorf("expected empty history, got %d entries", len(entries))
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistory_QueryError_Returns500(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
mock.ExpectQuery(`SELECT created_at, duration_ms, status`).
|
||||
WithArgs(wsID, schedID).
|
||||
WillReturnError(errors.New("connection lost"))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("GET",
|
||||
"/workspaces/"+wsID+"/schedules/"+schedID+"/history", nil)
|
||||
|
||||
handler.History(c)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistory_MultipleEntries_ReverseOrder(t *testing.T) {
|
||||
// Verifies the History handler correctly deserialises multiple rows and
|
||||
// includes error_detail in the response (#152). sqlmock doesn't produce
|
||||
// scan errors from nil pointer fields (the driver accepts nil for *int
|
||||
// and *string columns), so we verify the happy multi-row path instead.
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
handler := NewScheduleHandler()
|
||||
|
||||
wsID := "550e8400-e29b-41d4-a716-446655440000"
|
||||
schedID := "11111111-1111-1111-1111-111111111111"
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
|
||||
mock.ExpectQuery(`SELECT created_at, duration_ms, status`).
|
||||
WithArgs(wsID, schedID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"created_at", "duration_ms", "status", "error_detail", "request_body"}).
|
||||
AddRow(now, 500, "ok", "", `{"schedule_id":"`+schedID+`"}`).
|
||||
AddRow(now, 1200, "error", "HTTP 500 — OOM", `{"schedule_id":"`+schedID+`"}`))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{
|
||||
{Key: "id", Value: wsID},
|
||||
{Key: "scheduleId", Value: schedID},
|
||||
}
|
||||
c.Request = httptest.NewRequest("GET",
|
||||
"/workspaces/"+wsID+"/schedules/"+schedID+"/history", nil)
|
||||
|
||||
handler.History(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var entries []map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &entries); err != nil {
|
||||
t.Fatalf("response not JSON: %v", err)
|
||||
}
|
||||
if len(entries) != 2 {
|
||||
t.Fatalf("expected 2 entries, got %d", len(entries))
|
||||
}
|
||||
// error_detail must be populated for the failed run.
|
||||
if entries[1]["error_detail"] != "HTTP 500 — OOM" {
|
||||
t.Errorf("error_detail: got %v", entries[1]["error_detail"])
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sqlmock: %v", err)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user