test(handlers): add instructions_test.go — 17 cases for InstructionsHandler
Covers List (workspace scope, global-only, query error), Create (happy path, missing required, invalid scope, workspace without target, content/title too long, insert error), Update (happy path, partial, content/title too long, not found, update error), Delete (happy path, not found, delete error), Resolve (no instructions, global only, global+workspace, query error, missing workspace ID), and scanInstructions helper (empty rows, scan error). Fixes gap: instructions.go had zero unit test coverage. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
871f8f52b5
commit
f3fd8aa700
654
workspace-server/internal/handlers/instructions_test.go
Normal file
654
workspace-server/internal/handlers/instructions_test.go
Normal file
@ -0,0 +1,654 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// instructions_test.go — unit coverage for InstructionsHandler.
|
||||
//
|
||||
// Coverage targets:
|
||||
// - List: workspace_id scope (returns global + workspace); global-only scope;
|
||||
// query error propagation.
|
||||
// - Create: happy path; missing required fields; invalid scope; workspace scope
|
||||
// without scope_target; content too long; title too long; insert error.
|
||||
// - Update: happy path; partial update; content too long; title too long;
|
||||
// not found; update error.
|
||||
// - Delete: happy path; not found; delete error.
|
||||
// - Resolve: no instructions; global only; global + workspace; query error.
|
||||
|
||||
func setupInstructionsTest(t *testing.T) (*sqlmock.Sqlmock, *gin.Engine) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
mock := setupTestDB(t)
|
||||
r := gin.New()
|
||||
return mock, r
|
||||
}
|
||||
|
||||
// ---------- List ----------
|
||||
|
||||
func TestInstructionsList_WorkspaceScope(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r.GET("/instructions", h.List)
|
||||
|
||||
mock.ExpectQuery(`SELECT id, scope, scope_target, title, content, priority, enabled, created_at, updated_at
|
||||
FROM platform_instructions
|
||||
WHERE enabled = true AND \(\s*scope = 'global'\s*OR \(scope = 'workspace' AND scope_target = \$1\)\s*\)`).
|
||||
WithArgs("ws-uuid-123").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "scope", "scope_target", "title", "content", "priority", "enabled", "created_at", "updated_at"}).
|
||||
AddRow("inst-1", "global", nil, "Global Rule", "Be nice", 10, true, "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z").
|
||||
AddRow("inst-2", "workspace", stringPtr("ws-uuid-123"), "WS Rule", "Use dark mode", 5, true, "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z"))
|
||||
|
||||
req, _ := http.NewRequest("GET", "/instructions?workspace_id=ws-uuid-123", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp []Instruction
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
if len(resp) != 2 {
|
||||
t.Errorf("expected 2 instructions, got %d", len(resp))
|
||||
}
|
||||
if resp[0].Scope != "global" {
|
||||
t.Errorf("expected global scope, got %s", resp[0].Scope)
|
||||
}
|
||||
if resp[1].Scope != "workspace" {
|
||||
t.Errorf("expected workspace scope, got %s", resp[1].Scope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsList_GlobalOnlyScope(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r.GET("/instructions", h.List)
|
||||
|
||||
mock.ExpectQuery(`SELECT id, scope, scope_target, title, content, priority, enabled, created_at, updated_at
|
||||
FROM platform_instructions WHERE 1=1`).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "scope", "scope_target", "title", "content", "priority", "enabled", "created_at", "updated_at"}).
|
||||
AddRow("inst-1", "global", nil, "Global Rule", "Be nice", 10, true, "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z"))
|
||||
|
||||
req, _ := http.NewRequest("GET", "/instructions?scope=global", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsList_QueryError(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r.GET("/instructions", h.List)
|
||||
|
||||
mock.ExpectQuery(`SELECT id, scope, scope_target, title, content, priority, enabled, created_at, updated_at
|
||||
FROM platform_instructions WHERE 1=1`).
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/instructions", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Create ----------
|
||||
|
||||
func TestInstructionsCreate_HappyPath(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r.POST("/instructions", h.List) // Use h.Create when routed
|
||||
|
||||
// Patch routing: create a separate router for POST
|
||||
r2 := gin.New()
|
||||
r2.POST("/instructions", h.Create)
|
||||
|
||||
mock.ExpectQuery(`INSERT INTO platform_instructions`).
|
||||
WithArgs("global", nil, "Test Title", "Test Content", 5).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("new-inst-123"))
|
||||
|
||||
body := map[string]interface{}{
|
||||
"scope": "global",
|
||||
"title": "Test Title",
|
||||
"content": "Test Content",
|
||||
"priority": 5,
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Errorf("expected 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
if resp["id"] != "new-inst-123" {
|
||||
t.Errorf("expected id new-inst-123, got %s", resp["id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsCreate_MissingRequired(t *testing.T) {
|
||||
_, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.POST("/instructions", h.Create)
|
||||
|
||||
// Missing scope
|
||||
body := map[string]interface{}{
|
||||
"title": "Test",
|
||||
"content": "Test",
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsCreate_InvalidScope(t *testing.T) {
|
||||
_, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.POST("/instructions", h.Create)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"scope": "invalid",
|
||||
"title": "Test",
|
||||
"content": "Test",
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsCreate_WorkspaceScopeWithoutTarget(t *testing.T) {
|
||||
_, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.POST("/instructions", h.Create)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"scope": "workspace",
|
||||
"title": "Test",
|
||||
"content": "Test",
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsCreate_ContentTooLong(t *testing.T) {
|
||||
_, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.POST("/instructions", h.Create)
|
||||
|
||||
// Content > 8192 chars
|
||||
longContent := make([]byte, 8193)
|
||||
for i := range longContent {
|
||||
longContent[i] = 'x'
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"scope": "global",
|
||||
"title": "Test",
|
||||
"content": string(longContent),
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsCreate_TitleTooLong(t *testing.T) {
|
||||
_, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.POST("/instructions", h.Create)
|
||||
|
||||
// Title > 200 chars
|
||||
longTitle := make([]byte, 201)
|
||||
for i := range longTitle {
|
||||
longTitle[i] = 'x'
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"scope": "global",
|
||||
"title": string(longTitle),
|
||||
"content": "Test",
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsCreate_InsertError(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.POST("/instructions", h.Create)
|
||||
|
||||
mock.ExpectQuery(`INSERT INTO platform_instructions`).
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"scope": "global",
|
||||
"title": "Test",
|
||||
"content": "Test",
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Update ----------
|
||||
|
||||
func TestInstructionsUpdate_HappyPath(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.PUT("/instructions/:id", h.Update)
|
||||
|
||||
mock.ExpectExec(`UPDATE platform_instructions SET`).
|
||||
WithArgs("New Title", "New Content", sqlmock.AnyArg(), sqlmock.AnyArg(), "inst-123").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
body := map[string]interface{}{
|
||||
"title": "New Title",
|
||||
"content": "New Content",
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("PUT", "/instructions/inst-123", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsUpdate_PartialUpdate(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.PUT("/instructions/:id", h.Update)
|
||||
|
||||
// Only title update — content/priority/enabled stay nil
|
||||
mock.ExpectExec(`UPDATE platform_instructions SET`).
|
||||
WithArgs("Only Title", sqlmock.NilArg(), sqlmock.NilArg(), sqlmock.NilArg(), "inst-123").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
body := map[string]interface{}{
|
||||
"title": "Only Title",
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("PUT", "/instructions/inst-123", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsUpdate_ContentTooLong(t *testing.T) {
|
||||
_, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.PUT("/instructions/:id", h.Update)
|
||||
|
||||
longContent := make([]byte, 8193)
|
||||
for i := range longContent {
|
||||
longContent[i] = 'x'
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"content": string(longContent),
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("PUT", "/instructions/inst-123", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsUpdate_TitleTooLong(t *testing.T) {
|
||||
_, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.PUT("/instructions/:id", h.Update)
|
||||
|
||||
longTitle := make([]byte, 201)
|
||||
for i := range longTitle {
|
||||
longTitle[i] = 'x'
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"title": string(longTitle),
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("PUT", "/instructions/inst-123", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsUpdate_NotFound(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.PUT("/instructions/:id", h.Update)
|
||||
|
||||
mock.ExpectExec(`UPDATE platform_instructions SET`).
|
||||
WillReturnResult(sqlmock.NewResult(0, 0)) // 0 rows affected
|
||||
|
||||
body := map[string]interface{}{
|
||||
"title": "New Title",
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("PUT", "/instructions/nonexistent", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsUpdate_UpdateError(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.PUT("/instructions/:id", h.Update)
|
||||
|
||||
mock.ExpectExec(`UPDATE platform_instructions SET`).
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"title": "New Title",
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("PUT", "/instructions/inst-123", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Delete ----------
|
||||
|
||||
func TestInstructionsDelete_HappyPath(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.DELETE("/instructions/:id", h.Delete)
|
||||
|
||||
mock.ExpectExec(`DELETE FROM platform_instructions WHERE id = \$1`).
|
||||
WithArgs("inst-123").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
req, _ := http.NewRequest("DELETE", "/instructions/inst-123", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsDelete_NotFound(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.DELETE("/instructions/:id", h.Delete)
|
||||
|
||||
mock.ExpectExec(`DELETE FROM platform_instructions WHERE id = \$1`).
|
||||
WithArgs("nonexistent").
|
||||
WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
|
||||
req, _ := http.NewRequest("DELETE", "/instructions/nonexistent", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsDelete_DeleteError(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.DELETE("/instructions/:id", h.Delete)
|
||||
|
||||
mock.ExpectExec(`DELETE FROM platform_instructions WHERE id = \$1`).
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
req, _ := http.NewRequest("DELETE", "/instructions/inst-123", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Resolve ----------
|
||||
|
||||
func TestInstructionsResolve_NoInstructions(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.GET("/workspaces/:id/instructions/resolve", h.Resolve)
|
||||
|
||||
mock.ExpectQuery(`SELECT scope, title, content FROM platform_instructions`).
|
||||
WithArgs("ws-uuid-123").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"scope", "title", "content"}))
|
||||
|
||||
req, _ := http.NewRequest("GET", "/workspaces/ws-uuid-123/instructions/resolve", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
if resp["workspace_id"] != "ws-uuid-123" {
|
||||
t.Errorf("expected workspace_id ws-uuid-123, got %s", resp["workspace_id"])
|
||||
}
|
||||
if resp["instructions"] != "" {
|
||||
t.Errorf("expected empty instructions, got %q", resp["instructions"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsResolve_GlobalOnly(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.GET("/workspaces/:id/instructions/resolve", h.Resolve)
|
||||
|
||||
mock.ExpectQuery(`SELECT scope, title, content FROM platform_instructions`).
|
||||
WithArgs("ws-uuid-123").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"scope", "title", "content"}).
|
||||
AddRow("global", "Be Nice", "Always be nice to users"))
|
||||
|
||||
req, _ := http.NewRequest("GET", "/workspaces/ws-uuid-123/instructions/resolve", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
if resp["instructions"] == "" {
|
||||
t.Error("expected non-empty instructions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsResolve_GlobalPlusWorkspace(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.GET("/workspaces/:id/instructions/resolve", h.Resolve)
|
||||
|
||||
mock.ExpectQuery(`SELECT scope, title, content FROM platform_instructions`).
|
||||
WithArgs("ws-uuid-123").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"scope", "title", "content"}).
|
||||
AddRow("global", "Be Nice", "Global rule content").
|
||||
AddRow("workspace", "Use Dark Mode", "WS specific rule"))
|
||||
|
||||
req, _ := http.NewRequest("GET", "/workspaces/ws-uuid-123/instructions/resolve", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
// Both scopes should be present
|
||||
if !bytes.Contains([]byte(resp["instructions"]), []byte("Platform-Wide Rules")) {
|
||||
t.Error("expected Platform-Wide Rules section")
|
||||
}
|
||||
if !bytes.Contains([]byte(resp["instructions"]), []byte("Role-Specific Rules")) {
|
||||
t.Error("expected Role-Specific Rules section")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsResolve_QueryError(t *testing.T) {
|
||||
mock, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.GET("/workspaces/:id/instructions/resolve", h.Resolve)
|
||||
|
||||
mock.ExpectQuery(`SELECT scope, title, content FROM platform_instructions`).
|
||||
WithArgs("ws-uuid-123").
|
||||
WillReturnError(sql.ErrConnDone)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/workspaces/ws-uuid-123/instructions/resolve", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsResolve_MissingWorkspaceID(t *testing.T) {
|
||||
_, r := setupInstructionsTest(t)
|
||||
h := NewInstructionsHandler()
|
||||
r2 := gin.New()
|
||||
r2.GET("/workspaces/:id/instructions/resolve", h.Resolve)
|
||||
|
||||
// Empty workspace ID
|
||||
req, _ := http.NewRequest("GET", "/workspaces//instructions/resolve", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r2.ServeHTTP(w, req)
|
||||
|
||||
// Gin will return 404 for empty path segment
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- scanInstructions helper ----------
|
||||
|
||||
func TestScanInstructions_EmptyRows(t *testing.T) {
|
||||
rows := sqlmock.NewRows([]string{"id", "scope", "scope_target", "title", "content", "priority", "enabled", "created_at", "updated_at"})
|
||||
result := scanInstructions(rows)
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected 0, got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanInstructions_ScanError(t *testing.T) {
|
||||
// Rows that error on scan — scanInstructions should skip bad rows and continue
|
||||
rows := sqlmock.NewRows([]string{"id", "scope", "scope_target", "title", "content", "priority", "enabled", "created_at", "updated_at"}).
|
||||
AddRow("inst-1", "global", nil, "Good", "Good content", 10, true, "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z").
|
||||
RowError(1, sql.ErrConnDone) // Error on second row
|
||||
result := scanInstructions(rows)
|
||||
// Should return first row, skip second
|
||||
if len(result) != 1 {
|
||||
t.Errorf("expected 1 (skipped bad row), got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Helper ----------
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user