This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
heroagent_go_old/pkg/heroagent/handlers/job_handlers_test.go
2025-04-23 04:18:28 +02:00

639 lines
18 KiB
Go

package handlers
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/freeflowuniverse/heroagent/pkg/herojobs"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockHeroJobsClient is a mock implementation of the HeroJobs client
type MockHeroJobsClient struct {
mock.Mock
}
// Connect mocks the Connect method
func (m *MockHeroJobsClient) Connect() error {
args := m.Called()
return args.Error(0)
}
// Close mocks the Close method
func (m *MockHeroJobsClient) Close() error {
args := m.Called()
return args.Error(0)
}
// SubmitJob mocks the SubmitJob method
func (m *MockHeroJobsClient) SubmitJob(job *herojobs.Job) (*herojobs.Job, error) {
args := m.Called(job)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*herojobs.Job), args.Error(1)
}
// GetJob mocks the GetJob method
func (m *MockHeroJobsClient) GetJob(jobID string) (*herojobs.Job, error) {
args := m.Called(jobID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*herojobs.Job), args.Error(1)
}
// DeleteJob mocks the DeleteJob method
func (m *MockHeroJobsClient) DeleteJob(jobID string) error {
args := m.Called(jobID)
return args.Error(0)
}
// ListJobs mocks the ListJobs method
func (m *MockHeroJobsClient) ListJobs(circleID, topic string) ([]string, error) {
args := m.Called(circleID, topic)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]string), args.Error(1)
}
// QueueSize mocks the QueueSize method
func (m *MockHeroJobsClient) QueueSize(circleID, topic string) (int64, error) {
args := m.Called(circleID, topic)
return args.Get(0).(int64), args.Error(1)
}
// QueueEmpty mocks the QueueEmpty method
func (m *MockHeroJobsClient) QueueEmpty(circleID, topic string) error {
args := m.Called(circleID, topic)
return args.Error(0)
}
// QueueGet mocks the QueueGet method
func (m *MockHeroJobsClient) QueueGet(circleID, topic string) (*herojobs.Job, error) {
args := m.Called(circleID, topic)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*herojobs.Job), args.Error(1)
}
// CreateJob mocks the CreateJob method
func (m *MockHeroJobsClient) CreateJob(circleID, topic, sessionKey, heroScript, rhaiScript string) (*herojobs.Job, error) {
args := m.Called(circleID, topic, sessionKey, heroScript, rhaiScript)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*herojobs.Job), args.Error(1)
}
// setupTest initializes a test environment with a mock client
func setupTest() (*JobHandler, *MockHeroJobsClient, *fiber.App) {
mockClient := new(MockHeroJobsClient)
handler := &JobHandler{
client: mockClient,
}
app := fiber.New()
// Register routes
api := app.Group("/api")
jobs := api.Group("/jobs")
jobs.Post("/create", handler.createJob)
jobs.Get("/queue/get", handler.queueGet)
jobs.Post("/queue/empty", handler.queueEmpty)
jobs.Post("/submit", handler.submitJob)
jobs.Get("/get/:jobid", handler.getJob)
jobs.Delete("/delete/:jobid", handler.deleteJob)
jobs.Get("/list", handler.listJobs)
jobs.Get("/queue/size", handler.queueSize)
return handler, mockClient, app
}
// createTestRequest creates a test request with the given method, path, and body
func createTestRequest(method, path string, body io.Reader) (*http.Request, error) {
req := httptest.NewRequest(method, path, body)
req.Header.Set("Content-Type", "application/json")
return req, nil
}
// TestQueueEmpty tests the queueEmpty handler
func TestQueueEmpty(t *testing.T) {
// Test cases
tests := []struct {
name string
circleID string
topic string
connectError error
emptyError error
expectedStatus int
expectedBody string
}{
{
name: "Success",
circleID: "test-circle",
topic: "test-topic",
connectError: nil,
emptyError: nil,
expectedStatus: fiber.StatusOK,
expectedBody: `{"status":"success","message":"Queue for circle test-circle and topic test-topic emptied successfully"}`,
},
{
name: "Connection Error",
circleID: "test-circle",
topic: "test-topic",
connectError: errors.New("connection error"),
emptyError: nil,
expectedStatus: fiber.StatusInternalServerError,
expectedBody: `{"error":"Failed to connect to HeroJobs server: connection error"}`,
},
{
name: "Empty Error",
circleID: "test-circle",
topic: "test-topic",
connectError: nil,
emptyError: errors.New("empty error"),
expectedStatus: fiber.StatusInternalServerError,
expectedBody: `{"error":"Failed to empty queue: empty error"}`,
},
{
name: "Empty Circle ID",
circleID: "",
topic: "test-topic",
connectError: nil,
emptyError: nil,
expectedStatus: fiber.StatusBadRequest,
expectedBody: `{"error":"Circle ID is required"}`,
},
{
name: "Empty Topic",
circleID: "test-circle",
topic: "",
connectError: nil,
emptyError: nil,
expectedStatus: fiber.StatusBadRequest,
expectedBody: `{"error":"Topic is required"}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create a new mock client for each test
mockClient := new(MockHeroJobsClient)
// Setup mock expectations - Connect is always called in the handler
mockClient.On("Connect").Return(tc.connectError)
// QueueEmpty and Close are only called if Connect succeeds and parameters are valid
if tc.connectError == nil && tc.circleID != "" && tc.topic != "" {
mockClient.On("QueueEmpty", tc.circleID, tc.topic).Return(tc.emptyError)
mockClient.On("Close").Return(nil)
} else {
// Close is still called via defer even if we return early
mockClient.On("Close").Return(nil).Maybe()
}
// Create a new handler with the mock client
handler := &JobHandler{
client: mockClient,
}
// Create a new app for each test
app := fiber.New()
api := app.Group("/api")
jobs := api.Group("/jobs")
jobs.Post("/queue/empty", handler.queueEmpty)
// Create request body
reqBody := map[string]string{
"circleid": tc.circleID,
"topic": tc.topic,
}
reqBodyBytes, err := json.Marshal(reqBody)
assert.NoError(t, err)
// Create test request
req, err := createTestRequest(http.MethodPost, "/api/jobs/queue/empty", bytes.NewReader(reqBodyBytes))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
// Perform the request
resp, err := app.Test(req)
assert.NoError(t, err)
// Check status code
assert.Equal(t, tc.expectedStatus, resp.StatusCode)
// Check response body
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.JSONEq(t, tc.expectedBody, string(body))
// Verify that all expectations were met
mockClient.AssertExpectations(t)
})
}
}
// TestQueueGet tests the queueGet handler
func TestQueueGet(t *testing.T) {
// Create a test job
testJob := &herojobs.Job{
JobID: "test-job-id",
CircleID: "test-circle",
Topic: "test-topic",
}
// Test cases
tests := []struct {
name string
circleID string
topic string
connectError error
getError error
getResponse *herojobs.Job
expectedStatus int
expectedBody string
}{
{
name: "Success",
circleID: "test-circle",
topic: "test-topic",
connectError: nil,
getError: nil,
getResponse: testJob,
expectedStatus: fiber.StatusOK,
// Include all fields in the response, even empty ones
expectedBody: `{"jobid":"test-job-id","circleid":"test-circle","topic":"test-topic","error":"","heroscript":"","result":"","rhaiscript":"","sessionkey":"","status":"","time_end":0,"time_scheduled":0,"time_start":0,"timeout":0}`,
},
{
name: "Connection Error",
circleID: "test-circle",
topic: "test-topic",
connectError: errors.New("connection error"),
getError: nil,
getResponse: nil,
expectedStatus: fiber.StatusInternalServerError,
expectedBody: `{"error":"Failed to connect to HeroJobs server: connection error"}`,
},
{
name: "Get Error",
circleID: "test-circle",
topic: "test-topic",
connectError: nil,
getError: errors.New("get error"),
getResponse: nil,
expectedStatus: fiber.StatusInternalServerError,
expectedBody: `{"error":"Failed to get job from queue: get error"}`,
},
{
name: "Empty Circle ID",
circleID: "",
topic: "test-topic",
connectError: nil,
getError: nil,
getResponse: nil,
expectedStatus: fiber.StatusBadRequest,
expectedBody: `{"error":"Circle ID is required"}`,
},
{
name: "Empty Topic",
circleID: "test-circle",
topic: "",
connectError: nil,
getError: nil,
getResponse: nil,
expectedStatus: fiber.StatusBadRequest,
expectedBody: `{"error":"Topic is required"}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create a new mock client for each test
mockClient := new(MockHeroJobsClient)
// Setup mock expectations - Connect is always called in the handler
mockClient.On("Connect").Return(tc.connectError)
// QueueGet and Close are only called if Connect succeeds and parameters are valid
if tc.connectError == nil && tc.circleID != "" && tc.topic != "" {
mockClient.On("QueueGet", tc.circleID, tc.topic).Return(tc.getResponse, tc.getError)
mockClient.On("Close").Return(nil)
} else {
// Close is still called via defer even if we return early
mockClient.On("Close").Return(nil).Maybe()
}
// Create a new handler with the mock client
handler := &JobHandler{
client: mockClient,
}
// Create a new app for each test
app := fiber.New()
api := app.Group("/api")
jobs := api.Group("/jobs")
jobs.Get("/queue/get", handler.queueGet)
// Create test request
path := fmt.Sprintf("/api/jobs/queue/get?circleid=%s&topic=%s", tc.circleID, tc.topic)
req, err := createTestRequest(http.MethodGet, path, nil)
assert.NoError(t, err)
// Perform the request
resp, err := app.Test(req)
assert.NoError(t, err)
// Check status code
assert.Equal(t, tc.expectedStatus, resp.StatusCode)
// Check response body
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.JSONEq(t, tc.expectedBody, string(body))
// Verify that all expectations were met
mockClient.AssertExpectations(t)
})
}
}
// TestCreateJob tests the createJob handler
func TestCreateJob(t *testing.T) {
// Create a test job
testJob := &herojobs.Job{
JobID: "test-job-id",
CircleID: "test-circle",
Topic: "test-topic",
}
// Test cases
tests := []struct {
name string
circleID string
topic string
sessionKey string
heroScript string
rhaiScript string
connectError error
createError error
createResponse *herojobs.Job
expectedStatus int
expectedBody string
}{
{
name: "Success",
circleID: "test-circle",
topic: "test-topic",
sessionKey: "test-key",
heroScript: "test-hero-script",
rhaiScript: "test-rhai-script",
connectError: nil,
createError: nil,
createResponse: testJob,
expectedStatus: fiber.StatusOK,
expectedBody: `{"jobid":"test-job-id","circleid":"test-circle","topic":"test-topic","error":"","heroscript":"","result":"","rhaiscript":"","sessionkey":"","status":"","time_end":0,"time_scheduled":0,"time_start":0,"timeout":0}`,
},
{
name: "Connection Error",
circleID: "test-circle",
topic: "test-topic",
sessionKey: "test-key",
heroScript: "test-hero-script",
rhaiScript: "test-rhai-script",
connectError: errors.New("connection error"),
createError: nil,
createResponse: nil,
expectedStatus: fiber.StatusInternalServerError,
expectedBody: `{"error":"Failed to connect to HeroJobs server: connection error"}`,
},
{
name: "Create Error",
circleID: "test-circle",
topic: "test-topic",
sessionKey: "test-key",
heroScript: "test-hero-script",
rhaiScript: "test-rhai-script",
connectError: nil,
createError: errors.New("create error"),
createResponse: nil,
expectedStatus: fiber.StatusInternalServerError,
expectedBody: `{"error":"Failed to create job: create error"}`,
},
{
name: "Empty Circle ID",
circleID: "",
topic: "test-topic",
sessionKey: "test-key",
heroScript: "test-hero-script",
rhaiScript: "test-rhai-script",
connectError: nil,
createError: nil,
createResponse: nil,
expectedStatus: fiber.StatusBadRequest,
expectedBody: `{"error":"Circle ID is required"}`,
},
{
name: "Empty Topic",
circleID: "test-circle",
topic: "",
sessionKey: "test-key",
heroScript: "test-hero-script",
rhaiScript: "test-rhai-script",
connectError: nil,
createError: nil,
createResponse: nil,
expectedStatus: fiber.StatusBadRequest,
expectedBody: `{"error":"Topic is required"}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create a new mock client for each test
mockClient := new(MockHeroJobsClient)
// Setup mock expectations - Connect is always called in the handler
mockClient.On("Connect").Return(tc.connectError)
// CreateJob and Close are only called if Connect succeeds and parameters are valid
if tc.connectError == nil && tc.circleID != "" && tc.topic != "" {
mockClient.On("CreateJob", tc.circleID, tc.topic, tc.sessionKey, tc.heroScript, tc.rhaiScript).Return(tc.createResponse, tc.createError)
mockClient.On("Close").Return(nil)
} else {
// Close is still called via defer even if we return early
mockClient.On("Close").Return(nil).Maybe()
}
// Create a new handler with the mock client
handler := &JobHandler{
client: mockClient,
}
// Create a new app for each test
app := fiber.New()
api := app.Group("/api")
jobs := api.Group("/jobs")
jobs.Post("/create", handler.createJob)
// Create request body
reqBody := map[string]string{
"circleid": tc.circleID,
"topic": tc.topic,
"sessionkey": tc.sessionKey,
"heroscript": tc.heroScript,
"rhaiscript": tc.rhaiScript,
}
reqBodyBytes, err := json.Marshal(reqBody)
assert.NoError(t, err)
// Create test request
req, err := createTestRequest(http.MethodPost, "/api/jobs/create", bytes.NewReader(reqBodyBytes))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
// Perform the request
resp, err := app.Test(req)
assert.NoError(t, err)
// Check status code
assert.Equal(t, tc.expectedStatus, resp.StatusCode)
// Check response body
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.JSONEq(t, tc.expectedBody, string(body))
// Verify that all expectations were met
mockClient.AssertExpectations(t)
})
}
}
// TestSubmitJob tests the submitJob handler
func TestSubmitJob(t *testing.T) {
// Create a test job
testJob := &herojobs.Job{
JobID: "test-job-id",
CircleID: "test-circle",
Topic: "test-topic",
}
// Test cases
tests := []struct {
name string
job *herojobs.Job
connectError error
submitError error
submitResponse *herojobs.Job
expectedStatus int
expectedBody string
}{
{
name: "Success",
job: testJob,
connectError: nil,
submitError: nil,
submitResponse: testJob,
expectedStatus: fiber.StatusOK,
expectedBody: `{"jobid":"test-job-id","circleid":"test-circle","topic":"test-topic","error":"","heroscript":"","result":"","rhaiscript":"","sessionkey":"","status":"","time_end":0,"time_scheduled":0,"time_start":0,"timeout":0}`,
},
{
name: "Connection Error",
job: testJob,
connectError: errors.New("connection error"),
submitError: nil,
submitResponse: nil,
expectedStatus: fiber.StatusInternalServerError,
expectedBody: `{"error":"Failed to connect to HeroJobs server: connection error"}`,
},
{
name: "Submit Error",
job: testJob,
connectError: nil,
submitError: errors.New("submit error"),
submitResponse: nil,
expectedStatus: fiber.StatusInternalServerError,
expectedBody: `{"error":"Failed to submit job: submit error"}`,
},
{
name: "Empty Job",
job: nil,
connectError: nil,
submitError: nil,
submitResponse: nil,
expectedStatus: fiber.StatusBadRequest,
expectedBody: `{"error":"Failed to parse job data: unexpected end of JSON input"}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create a new mock client for each test
mockClient := new(MockHeroJobsClient)
// Setup mock expectations - Connect is always called in the handler
mockClient.On("Connect").Return(tc.connectError)
// SubmitJob and Close are only called if Connect succeeds and job is not nil
if tc.connectError == nil && tc.job != nil {
mockClient.On("SubmitJob", tc.job).Return(tc.submitResponse, tc.submitError)
mockClient.On("Close").Return(nil)
} else {
// Close is still called via defer even if we return early
mockClient.On("Close").Return(nil).Maybe()
}
// Create a new handler with the mock client
handler := &JobHandler{
client: mockClient,
}
// Create a new app for each test
app := fiber.New()
api := app.Group("/api")
jobs := api.Group("/jobs")
jobs.Post("/submit", handler.submitJob)
// Create request body
var reqBodyBytes []byte
var err error
if tc.job != nil {
reqBodyBytes, err = json.Marshal(tc.job)
assert.NoError(t, err)
}
// Create test request
req, err := createTestRequest(http.MethodPost, "/api/jobs/submit", bytes.NewReader(reqBodyBytes))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
// Perform the request
resp, err := app.Test(req)
assert.NoError(t, err)
// Check status code
assert.Equal(t, tc.expectedStatus, resp.StatusCode)
// Check response body
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.JSONEq(t, tc.expectedBody, string(body))
// Verify that all expectations were met
mockClient.AssertExpectations(t)
})
}
}