package v1 import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/auth" "github.com/gochat/gochat/internal/middleware" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" ) func newCaptainSkillHandlerTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, *model.CaptainAssistant) { t.Helper() gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&model.CaptainAssistant{}, &model.CaptainSkill{}, &model.CaptainSkillReference{}, &model.CaptainAssistantSkill{})) assistant := &model.CaptainAssistant{AccountID: 1, Name: "Assistant", Status: model.AssistantStatusActive} require.NoError(t, db.Create(assistant).Error) handler := NewCaptainSkillHandler(service.NewCaptainSkillService(repository.NewCaptainSkillRepo(db))) router := gin.New() router.Use(func(c *gin.Context) { role := c.GetHeader("X-Test-Role") if role == "" { role = "administrator" } c.Set("policy_context", auth.NewPolicyContext(1, 1, role, 0, nil)) c.Next() }) captain := router.Group("/api/v1/accounts/:account_id/captain", middleware.RoleCheckAny("administrator", "super_admin")) skills := captain.Group("/skills") skills.GET("", handler.List) skills.POST("", handler.Create) skills.GET("/:skill_id", handler.Get) skills.PUT("/:skill_id", handler.Update) skills.DELETE("/:skill_id", handler.Delete) assistants := captain.Group("/assistants/:assistant_id/skills") assistants.POST("/:skill_id", handler.Bind) assistants.DELETE("/:skill_id", handler.Unbind) return router, db, assistant } func captainSkillRequest(t *testing.T, router http.Handler, method, path, role string, body any) *httptest.ResponseRecorder { t.Helper() var raw []byte if body != nil { raw, _ = json.Marshal(body) } req := httptest.NewRequest(method, path, bytes.NewReader(raw)) req.Header.Set("Content-Type", "application/json") if role != "" { req.Header.Set("X-Test-Role", role) } w := httptest.NewRecorder() router.ServeHTTP(w, req) return w } func captainSkillBody(status string) map[string]any { return map[string]any{"skill": map[string]any{ "name": "refund-policy", "description": "Refund rules", "instructions_md": "Use the policy.", "status": status, "references": []map[string]any{{"reference_key": "standard-policy", "content_md": "Standard terms."}}, }} } func TestCaptainSkillHandlerManagementContract(t *testing.T) { router, _, assistant := newCaptainSkillHandlerTestRouter(t) base := "/api/v1/accounts/1/captain" w := captainSkillRequest(t, router, http.MethodPost, base+"/skills", "agent", captainSkillBody("active")) assert.Equal(t, http.StatusForbidden, w.Code) w = captainSkillRequest(t, router, http.MethodPost, base+"/skills", "super_admin", captainSkillBody("active")) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) var created map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &created)) skillID := strconv.Itoa(int(created["id"].(float64))) assert.Equal(t, "active", created["status"]) require.Len(t, created["references"], 1) w = captainSkillRequest(t, router, http.MethodPost, base+"/assistants/"+strconv.Itoa(int(assistant.ID))+"/skills/"+skillID, "administrator", nil) assert.Equal(t, http.StatusNoContent, w.Code, w.Body.String()) w = captainSkillRequest(t, router, http.MethodGet, base+"/skills?assistant_id="+strconv.Itoa(int(assistant.ID)), "administrator", nil) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) var listed map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &listed)) payload := listed["payload"].([]any) require.Len(t, payload, 1) assert.Equal(t, true, payload[0].(map[string]any)["bound"]) assert.NotContains(t, payload[0].(map[string]any), "instructions_md") w = captainSkillRequest(t, router, http.MethodGet, base+"/skills/"+skillID, "administrator", nil) assert.Equal(t, http.StatusOK, w.Code) updatedBody := captainSkillBody("active") updatedBody["skill"].(map[string]any)["description"] = "Updated refund rules" updatedBody["skill"].(map[string]any)["expected_version"] = 1 w = captainSkillRequest(t, router, http.MethodPut, base+"/skills/"+skillID, "administrator", updatedBody) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) var updated map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &updated)) assert.Equal(t, float64(2), updated["version"]) updatedBody["skill"].(map[string]any)["description"] = "Stale update" w = captainSkillRequest(t, router, http.MethodPut, base+"/skills/"+skillID, "administrator", updatedBody) assert.Equal(t, http.StatusConflict, w.Code, w.Body.String()) w = captainSkillRequest(t, router, http.MethodDelete, base+"/assistants/"+strconv.Itoa(int(assistant.ID))+"/skills/"+skillID, "administrator", nil) assert.Equal(t, http.StatusNoContent, w.Code) w = captainSkillRequest(t, router, http.MethodGet, "/api/v1/accounts/2/captain/skills/"+skillID, "administrator", nil) assert.Equal(t, http.StatusNotFound, w.Code) w = captainSkillRequest(t, router, http.MethodPost, base+"/assistants/999/skills/"+skillID, "administrator", nil) assert.Equal(t, http.StatusNotFound, w.Code) } func TestCaptainSkillHandlerValidationAndConflictStatuses(t *testing.T) { router, _, assistant := newCaptainSkillHandlerTestRouter(t) base := "/api/v1/accounts/1/captain" body := captainSkillBody("active") body["skill"].(map[string]any)["references"] = []map[string]any{{"reference_key": "../unsafe", "content_md": "x"}} w := captainSkillRequest(t, router, http.MethodPost, base+"/skills", "administrator", body) assert.Equal(t, http.StatusUnprocessableEntity, w.Code) w = captainSkillRequest(t, router, http.MethodGet, base+"/skills?assistant_id=bad", "administrator", nil) assert.Equal(t, http.StatusBadRequest, w.Code) w = captainSkillRequest(t, router, http.MethodPost, base+"/skills", "administrator", captainSkillBody("draft")) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) var created map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &created)) skillID := strconv.Itoa(int(created["id"].(float64))) w = captainSkillRequest(t, router, http.MethodPost, base+"/assistants/"+strconv.Itoa(int(assistant.ID))+"/skills/"+skillID, "administrator", nil) assert.Equal(t, http.StatusConflict, w.Code) w = captainSkillRequest(t, router, http.MethodDelete, base+"/skills/"+skillID, "administrator", nil) assert.Equal(t, http.StatusNoContent, w.Code) }