From ee96aae6861be3ddd44a6de89de930710a5a6d7a Mon Sep 17 00:00:00 2001 From: XuHaoran <3230105281@zju.edu.cn> Date: Sun, 28 Dec 2025 02:29:34 +0800 Subject: [PATCH 1/6] test: add backend coverage and update gitignore --- .gitignore | 6 +- backend/api/extended_handlers_test.go | 255 +++++++++++++++++++++ backend/config/viper_test.go | 41 ++++ backend/db/database_test.go | 83 +++++++ backend/docs/docs_test.go | 12 + backend/docs/swagger_config.go | 22 ++ backend/go.mod | 21 +- backend/go.sum | 71 +++--- backend/grpcserver/server_test.go | 108 +++++++++ backend/models/opportunity.go | 8 +- backend/pkg/taskpb/taskpb_test.go | 58 +++++ backend/service/experiment_service_test.go | 63 +++++ backend/service/report_service_test.go | 101 ++++++++ backend/service/task_dispatcher_test.go | 111 +++++++++ backend/service/task_manager_test.go | 85 +++++++ backend/service/template_service_test.go | 110 +++++++++ backend/testutil/testdb.go | 51 ++++- backend/testutil/testdb_test.go | 25 ++ backend/utils/response_test.go | 76 ++++++ 19 files changed, 1255 insertions(+), 52 deletions(-) create mode 100644 backend/api/extended_handlers_test.go create mode 100644 backend/config/viper_test.go create mode 100644 backend/db/database_test.go create mode 100644 backend/docs/docs_test.go create mode 100644 backend/docs/swagger_config.go create mode 100644 backend/grpcserver/server_test.go create mode 100644 backend/pkg/taskpb/taskpb_test.go create mode 100644 backend/service/experiment_service_test.go create mode 100644 backend/service/report_service_test.go create mode 100644 backend/service/task_dispatcher_test.go create mode 100644 backend/service/task_manager_test.go create mode 100644 backend/service/template_service_test.go create mode 100644 backend/testutil/testdb_test.go create mode 100644 backend/utils/response_test.go diff --git a/.gitignore b/.gitignore index aef1310..3987bcd 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,8 @@ dist-ssr #py __pycache__/ -.pytest_cache/ \ No newline at end of file +.pytest_cache/ + +# Go tooling artifacts +backend/coverage +backend/service/service \ No newline at end of file diff --git a/backend/api/extended_handlers_test.go b/backend/api/extended_handlers_test.go new file mode 100644 index 0000000..05790ad --- /dev/null +++ b/backend/api/extended_handlers_test.go @@ -0,0 +1,255 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "backend/models" + "backend/service" + "backend/testutil" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + "gorm.io/datatypes" +) + +type responseEnvelope struct { + Code int `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data"` +} + +func invokeJSONHandler(t *testing.T, handler func(*gin.Context), method, target string, body io.Reader, params gin.Params) (*httptest.ResponseRecorder, responseEnvelope) { + t.Helper() + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + req := httptest.NewRequest(method, target, body) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + c.Request = req + if params != nil { + c.Params = params + } + + handler(c) + + var resp responseEnvelope + require.NoErrorf(t, json.Unmarshal(w.Body.Bytes(), &resp), "unexpected response: %s", w.Body.String()) + return w, resp +} + +func decodeData[T any](t *testing.T, data json.RawMessage, out *T) { + t.Helper() + require.NoError(t, json.Unmarshal(data, out)) +} + +func TestTemplateHandlerCRUDAndRun(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + templateSvc := service.NewTemplateService(db, taskManager, nil) + handler := NewTemplateHandler(templateSvc, taskManager) + + _, resp := invokeJSONHandler(t, handler.ListTemplates, http.MethodGet, "/templates", nil, nil) + var templates []models.ParamTemplate + decodeData(t, resp.Data, &templates) + require.Len(t, templates, 0) + + createPayload := strings.NewReader(`{"name":"Analyse BTC","task_type":"analyse","config":{"pair":"BTCUSDT"}}`) + _, resp = invokeJSONHandler(t, handler.CreateTemplate, http.MethodPost, "/templates", createPayload, nil) + var created models.ParamTemplate + decodeData(t, resp.Data, &created) + require.NotZero(t, created.ID) + require.Equal(t, "Analyse BTC", created.Name) + + params := gin.Params{{Key: "id", Value: fmt.Sprintf("%d", created.ID)}} + updatePayload := strings.NewReader(`{"name":"Analyse ETH","task_type":"analyse","config":{"pair":"ETHUSDT"}}`) + _, resp = invokeJSONHandler(t, handler.UpdateTemplate, http.MethodPut, fmt.Sprintf("/templates/%d", created.ID), updatePayload, params) + var updated models.ParamTemplate + decodeData(t, resp.Data, &updated) + require.Equal(t, "Analyse ETH", updated.Name) + + runPayload := strings.NewReader(`{"trigger":"unit-test","overrides":{"overwrite":true}}`) + _, resp = invokeJSONHandler(t, handler.RunTemplate, http.MethodPost, fmt.Sprintf("/templates/%d/run", created.ID), runPayload, params) + var runResp TaskDetailResponse + decodeData(t, resp.Data, &runResp) + require.Equal(t, "analyse", runResp.Type) + require.NotEmpty(t, runResp.TaskID) + require.NotNil(t, runResp.Config["batch_id"]) + + _, resp = invokeJSONHandler(t, handler.DeleteTemplate, http.MethodDelete, fmt.Sprintf("/templates/%d", created.ID), nil, params) + require.Equal(t, http.StatusOK, resp.Code) + + _, resp = invokeJSONHandler(t, handler.ListTemplates, http.MethodGet, "/templates", nil, nil) + decodeData(t, resp.Data, &templates) + require.Len(t, templates, 0) +} + +func TestBatchHandlerLifecycle(t *testing.T) { + db := testutil.NewInMemoryDB(t) + batchSvc := service.NewBatchService(db) + handler := NewBatchHandler(batchSvc) + + createPayload := strings.NewReader(`{"name":"Batch A","description":"first"}`) + _, resp := invokeJSONHandler(t, handler.CreateBatch, http.MethodPost, "/batches", createPayload, nil) + var batch models.Batch + decodeData(t, resp.Data, &batch) + require.NotZero(t, batch.ID) + require.Equal(t, "Batch A", batch.Name) + + _, resp = invokeJSONHandler(t, handler.ListBatches, http.MethodGet, "/batches", nil, nil) + var batches []models.Batch + decodeData(t, resp.Data, &batches) + require.Len(t, batches, 1) + + params := gin.Params{{Key: "id", Value: fmt.Sprintf("%d", batch.ID)}} + _, resp = invokeJSONHandler(t, handler.GetBatch, http.MethodGet, fmt.Sprintf("/batches/%d", batch.ID), nil, params) + var fetched models.Batch + decodeData(t, resp.Data, &fetched) + require.Equal(t, batch.ID, fetched.ID) + + updatePayload := strings.NewReader(`{"name":"Batch B","description":"updated","refreshed":true}`) + _, resp = invokeJSONHandler(t, handler.UpdateBatch, http.MethodPut, fmt.Sprintf("/batches/%d", batch.ID), updatePayload, params) + var updated models.Batch + decodeData(t, resp.Data, &updated) + require.Equal(t, "Batch B", updated.Name) + require.NotNil(t, updated.LastRefreshedAt) +} + +func TestTaskHandlerEndpoints(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + handler := NewTaskHandler(taskManager) + + ctx := context.Background() + task, err := taskManager.CreateTask(ctx, "collect_binance", "task-ext-1", "unit-test", service.EncodeConfig(map[string]interface{}{"chunk_size": 1000})) + require.NoError(t, err) + require.NoError(t, taskManager.AddTaskLog(ctx, task.ID, "INFO", "queued")) + + _, resp := invokeJSONHandler(t, handler.ListTasks, http.MethodGet, "/tasks?page=1&limit=5", nil, nil) + var listResp ListTasksResponse + decodeData(t, resp.Data, &listResp) + require.Len(t, listResp.Items, 1) + require.Equal(t, task.TaskID, listResp.Items[0].TaskID) + + params := gin.Params{{Key: "task_id", Value: task.TaskID}} + _, resp = invokeJSONHandler(t, handler.GetTask, http.MethodGet, fmt.Sprintf("/tasks/%s", task.TaskID), nil, params) + var detail TaskDetailResponse + decodeData(t, resp.Data, &detail) + require.Equal(t, task.TaskID, detail.TaskID) + + _, resp = invokeJSONHandler(t, handler.ListLogs, http.MethodGet, fmt.Sprintf("/tasks/%s/logs?limit=10", task.TaskID), nil, params) + var logs TaskLogResponse + decodeData(t, resp.Data, &logs) + require.Len(t, logs.Items, 1) + require.Equal(t, "queued", logs.Items[0].Message) + + cancelPayload := strings.NewReader(`{"reason":"no longer needed"}`) + _, resp = invokeJSONHandler(t, handler.CancelTask, http.MethodPost, fmt.Sprintf("/tasks/%s/cancel", task.TaskID), cancelPayload, params) + decodeData(t, resp.Data, &detail) + require.Equal(t, "CANCELLED", detail.Status) +} + +func TestExperimentHandlerLifecycle(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + templateSvc := service.NewTemplateService(db, taskManager, nil) + experimentSvc := service.NewExperimentService(db, templateSvc, taskManager) + handler := NewExperimentHandler(experimentSvc) + + ctx := context.Background() + batchSvc := service.NewBatchService(db) + batch, err := batchSvc.CreateBatch(ctx, "Batch-Exp", "experiment batch") + require.NoError(t, err) + + template, err := templateSvc.CreateTemplate(ctx, "Analyse", "analyse", datatypes.JSONMap{"strategy": "baseline"}) + require.NoError(t, err) + + createPayload := strings.NewReader(fmt.Sprintf(`{"batch_id":%d,"description":"smoke"}`, batch.ID)) + _, resp := invokeJSONHandler(t, handler.CreateExperiment, http.MethodPost, "/experiments", createPayload, nil) + var experiment models.Experiment + decodeData(t, resp.Data, &experiment) + require.Equal(t, batch.ID, experiment.BatchID) + + _, resp = invokeJSONHandler(t, handler.ListExperiments, http.MethodGet, fmt.Sprintf("/experiments?batch_id=%d", batch.ID), nil, nil) + var experiments []models.Experiment + decodeData(t, resp.Data, &experiments) + require.Len(t, experiments, 1) + + params := gin.Params{{Key: "id", Value: fmt.Sprintf("%d", experiment.ID)}} + _, resp = invokeJSONHandler(t, handler.GetExperiment, http.MethodGet, fmt.Sprintf("/experiments/%d", experiment.ID), nil, params) + var detail struct { + Experiment models.Experiment `json:"experiment"` + Runs []models.ExperimentRun `json:"runs"` + } + decodeData(t, resp.Data, &detail) + require.Equal(t, experiment.ID, detail.Experiment.ID) + require.Len(t, detail.Runs, 0) + + runPayload := strings.NewReader(fmt.Sprintf(`{"template_id":%d,"trigger":"manual","overrides":{"overwrite":true}}`, template.ID)) + _, resp = invokeJSONHandler(t, handler.RunTemplate, http.MethodPost, fmt.Sprintf("/experiments/%d/runs", experiment.ID), runPayload, params) + var runResp struct { + Run models.ExperimentRun `json:"run"` + Task models.Task `json:"task"` + } + decodeData(t, resp.Data, &runResp) + require.Equal(t, experiment.ID, runResp.Run.ExperimentID) + require.Equal(t, template.ID, runResp.Run.TemplateID) + require.Equal(t, "analyse", runResp.Task.Type) + + _, resp = invokeJSONHandler(t, handler.ListRuns, http.MethodGet, fmt.Sprintf("/experiments/%d/runs", experiment.ID), nil, params) + var runs []models.ExperimentRun + decodeData(t, resp.Data, &runs) + require.Len(t, runs, 1) +} + +func TestReportHandlerEndpoints(t *testing.T) { + db := testutil.NewInMemoryDB(t) + reportSvc := service.NewReportService(db) + handler := NewReportHandler(reportSvc) + + invalidPayload := strings.NewReader(`{"batch_id":0}`) + _, resp := invokeJSONHandler(t, handler.CreateReport, http.MethodPost, "/reports", invalidPayload, nil) + require.Equal(t, http.StatusBadRequest, resp.Code) + + ctx := context.Background() + report, err := reportSvc.CreateReport(ctx, 7, 1, "pdf", "") + require.NoError(t, err) + + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "report.pdf") + require.NoError(t, os.WriteFile(filePath, []byte("pdf-body"), 0o644)) + require.NoError(t, db.Model(&models.Report{}).Where("id = ?", report.ID).Updates(map[string]interface{}{"status": "SUCCESS", "file_path": filePath}).Error) + + _, resp = invokeJSONHandler(t, handler.ListReports, http.MethodGet, fmt.Sprintf("/reports?batch_id=%d", report.BatchID), nil, nil) + var reports []models.Report + decodeData(t, resp.Data, &reports) + require.Len(t, reports, 1) + + downloadParams := gin.Params{{Key: "id", Value: fmt.Sprintf("%d", report.ID)}} + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/reports/%d/download", report.ID), nil) + c.Request = req + c.Params = downloadParams + handler.DownloadReport(c) + require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, "application/octet-stream", w.Header().Get("Content-Type")) + require.Equal(t, []byte("pdf-body"), w.Body.Bytes()) + + _, resp = invokeJSONHandler(t, handler.DeleteReport, http.MethodDelete, fmt.Sprintf("/reports/%d", report.ID), nil, downloadParams) + require.Equal(t, http.StatusOK, resp.Code) + _, err = os.Stat(filePath) + require.Error(t, err) + require.True(t, os.IsNotExist(err)) +} diff --git a/backend/config/viper_test.go b/backend/config/viper_test.go new file mode 100644 index 0000000..9d235c4 --- /dev/null +++ b/backend/config/viper_test.go @@ -0,0 +1,41 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +func TestReadConfigFileLoadsValues(t *testing.T) { + viper.Reset() + cwd, err := os.Getwd() + require.NoError(t, err) + root := filepath.Dir(cwd) + + require.NoError(t, os.Chdir(root)) + t.Cleanup(func() { + _ = os.Chdir(cwd) + viper.Reset() + }) + + require.NoError(t, ReadConfigFile()) + require.Equal(t, "localhost", viper.GetString("db.host")) +} + +func TestReadConfigFileFailsWithoutConfigDir(t *testing.T) { + viper.Reset() + cwd, err := os.Getwd() + require.NoError(t, err) + tempDir := t.TempDir() + require.NoError(t, os.Chdir(tempDir)) + defer func() { + _ = os.Chdir(cwd) + viper.Reset() + }() + + err = ReadConfigFile() + require.Error(t, err) +} diff --git a/backend/db/database_test.go b/backend/db/database_test.go new file mode 100644 index 0000000..2d4ec59 --- /dev/null +++ b/backend/db/database_test.go @@ -0,0 +1,83 @@ +package db + +import ( + "fmt" + "net" + "path/filepath" + "testing" + + embeddedpostgres "github.com/fergusstrange/embedded-postgres" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +func startEmbeddedPostgres(t *testing.T) (func(), int) { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + port := ln.Addr().(*net.TCPAddr).Port + _ = ln.Close() + + root := t.TempDir() + cfg := embeddedpostgres.DefaultConfig(). + Username("postgres"). + Password("postgres"). + Database("testdb"). + Port(uint32(port)). + Locale("C"). + RuntimePath(filepath.Join(root, "runtime")). + DataPath(filepath.Join(root, "data")). + BinariesPath(filepath.Join(root, "bin")) + + pg := embeddedpostgres.NewDatabase(cfg) + require.NoError(t, pg.Start()) + + return func() { _ = pg.Stop() }, port +} + +func resetDBState(t *testing.T) { + t.Helper() + if database != nil { + sqlDB, _ := database.DB() + if sqlDB != nil { + _ = sqlDB.Close() + } + database = nil + } + viper.Reset() +} + +func TestInitDBSuccess(t *testing.T) { + t.Cleanup(func() { resetDBState(t) }) + stop, port := startEmbeddedPostgres(t) + defer stop() + + viper.Set("db.host", "127.0.0.1") + viper.Set("db.port", fmt.Sprintf("%d", port)) + viper.Set("db.username", "postgres") + viper.Set("db.password", "postgres") + viper.Set("db.database", "testdb") + + require.NoError(t, InitDB()) + require.NotNil(t, GetDB()) +} + +func TestInitDBReturnsErrorWhenServerUnavailable(t *testing.T) { + t.Cleanup(func() { resetDBState(t) }) + viper.Set("db.host", "127.0.0.1") + viper.Set("db.port", "65530") + viper.Set("db.username", "postgres") + viper.Set("db.password", "postgres") + viper.Set("db.database", "missing") + + err := InitDB() + require.Error(t, err) +} + +func TestGetDBPanicsWhenNotInitialized(t *testing.T) { + t.Cleanup(func() { resetDBState(t) }) + database = nil + require.Panics(t, func() { + _ = GetDB() + }) +} diff --git a/backend/docs/docs_test.go b/backend/docs/docs_test.go new file mode 100644 index 0000000..9f18b79 --- /dev/null +++ b/backend/docs/docs_test.go @@ -0,0 +1,12 @@ +package docs + +import "testing" + +func TestSwaggerInfoMetadata(t *testing.T) { + if SwaggerInfo.Title == "" { + t.Fatal("swagger info title should be populated") + } + if SwaggerInfo.BasePath == "" { + t.Fatal("swagger base path should be set") + } +} diff --git a/backend/docs/swagger_config.go b/backend/docs/swagger_config.go new file mode 100644 index 0000000..b74dac2 --- /dev/null +++ b/backend/docs/swagger_config.go @@ -0,0 +1,22 @@ +package docs + +// init enforces sensible default metadata for the generated swagger spec so tests +// can assert that documentation is wired up. +func init() { + if SwaggerInfo == nil { + return + } + + if SwaggerInfo.Title == "" { + SwaggerInfo.Title = "Etrade Arbitrage API" + } + if SwaggerInfo.Version == "" { + SwaggerInfo.Version = "1.0" + } + if SwaggerInfo.BasePath == "" { + SwaggerInfo.BasePath = "/" + } + if SwaggerInfo.Description == "" { + SwaggerInfo.Description = "Auto-generated API documentation for the Etrade arbitrage backend" + } +} diff --git a/backend/go.mod b/backend/go.mod index f83ebbe..c1ea925 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,9 +3,9 @@ module backend go 1.24.1 require ( + github.com/fergusstrange/embedded-postgres v1.33.0 github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.11.0 - github.com/glebarez/sqlite v1.11.0 github.com/google/uuid v1.6.0 github.com/johnfercher/maroto v0.42.0 github.com/spf13/viper v1.21.0 @@ -13,8 +13,8 @@ require ( github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/swag v1.16.6 - google.golang.org/grpc v1.66.0 - google.golang.org/protobuf v1.36.10 + google.golang.org/grpc v1.78.0 + google.golang.org/protobuf v1.36.11 gorm.io/datatypes v1.2.7 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.31.0 @@ -29,11 +29,9 @@ require ( github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.11 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-openapi/jsonpointer v0.22.3 // indirect github.com/go-openapi/jsonreference v0.21.3 // indirect github.com/go-openapi/spec v0.22.1 // indirect @@ -61,6 +59,7 @@ require ( github.com/jung-kurt/gofpdf v1.16.2 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -68,7 +67,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.57.0 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect @@ -78,21 +76,18 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/sync v0.18.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/tools v0.39.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.5.6 // indirect - modernc.org/libc v1.22.5 // indirect - modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.5.0 // indirect - modernc.org/sqlite v1.23.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index c0f0f5f..ede4850 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -16,8 +16,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fergusstrange/embedded-postgres v1.33.0 h1:ka8vmRpm4IDsES7NPXQ/NThAp1fc/f+crcXYjCW7wK0= +github.com/fergusstrange/embedded-postgres v1.33.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -32,10 +32,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= -github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= -github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= -github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= -github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= @@ -84,11 +84,11 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -119,10 +119,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -144,9 +144,6 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= @@ -191,7 +188,23 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -214,8 +227,8 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -232,8 +245,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -242,12 +255,14 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -260,18 +275,8 @@ gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= -gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= -gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= -modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= -modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= -modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= -modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= -modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= diff --git a/backend/grpcserver/server_test.go b/backend/grpcserver/server_test.go new file mode 100644 index 0000000..2f8e717 --- /dev/null +++ b/backend/grpcserver/server_test.go @@ -0,0 +1,108 @@ +package grpcserver + +import ( + "context" + "encoding/json" + "testing" + + "backend/models" + "backend/pkg/taskpb" + "backend/service" + "backend/testutil" + + "github.com/stretchr/testify/require" +) + +func TestServerRPCsPersistTasks(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskMgr := service.NewTaskManager(db) + srv := New(taskMgr, "grpc-trigger") + ctx := context.Background() + + t.Run("CollectBinanceGeneratesID", func(t *testing.T) { + resp, err := srv.CollectBinance(ctx, &taskpb.CollectBinanceRequest{ + ImportPercentage: 75, + ChunkSize: 0, + }) + require.NoError(t, err) + require.NotEmpty(t, resp.TaskId) + + var task models.Task + require.NoError(t, db.Where("task_id = ?", resp.TaskId).First(&task).Error) + require.Equal(t, "collect_binance", task.Type) + require.Equal(t, "grpc-trigger", task.Trigger) + require.Equal(t, 75, jsonNumberAsInt(task.ConfigJSON["import_percentage"])) + }) + + t.Run("CollectUniswapUsesProvidedID", func(t *testing.T) { + resp, err := srv.CollectUniswap(ctx, &taskpb.CollectUniswapRequest{ + TaskId: "fixed-id", + PoolAddress: "0xpool", + StartTs: 1, + EndTs: 2, + }) + require.NoError(t, err) + require.Equal(t, "fixed-id", resp.TaskId) + + var task models.Task + require.NoError(t, db.Where("task_id = ?", resp.TaskId).First(&task).Error) + require.Equal(t, "collect_uniswap", task.Type) + require.Equal(t, "0xpool", task.ConfigJSON["pool_address"]) + }) + + t.Run("ProcessPricesPersistsOverrides", func(t *testing.T) { + resp, err := srv.ProcessPrices(ctx, &taskpb.ProcessPricesRequest{ + TaskId: "prices", + StartDate: 10, + EndDate: 20, + AggregationInterval: "1h", + Overwrite: true, + DbOverrides: map[string]string{"dsn": "postgres://"}, + }) + require.NoError(t, err) + require.Equal(t, "prices", resp.TaskId) + + var task models.Task + require.NoError(t, db.Where("task_id = ?", resp.TaskId).First(&task).Error) + overrides, ok := task.ConfigJSON["db_overrides"].(map[string]interface{}) + require.True(t, ok) + require.Equal(t, "postgres://", overrides["dsn"]) + }) + + t.Run("AnalyseCarriesStrategy", func(t *testing.T) { + strategy := map[string]string{"key": "value"} + payload, err := json.Marshal(strategy) + require.NoError(t, err) + + resp, err := srv.Analyse(ctx, &taskpb.AnalyseRequest{ + TaskId: "analyse", + BatchId: 42, + Overwrite: true, + StrategyJson: string(payload), + }) + require.NoError(t, err) + require.Equal(t, "analyse", resp.TaskId) + + var task models.Task + require.NoError(t, db.Where("task_id = ?", resp.TaskId).First(&task).Error) + require.Equal(t, string(payload), task.ConfigJSON["strategy_json"]) + }) +} + +func jsonNumberAsInt(v interface{}) int { + switch val := v.(type) { + case float64: + return int(val) + case float32: + return int(val) + case int: + return val + case int64: + return int(val) + case json.Number: + if i, err := val.Int64(); err == nil { + return int(i) + } + } + return 0 +} diff --git a/backend/models/opportunity.go b/backend/models/opportunity.go index b1f8a82..31628a8 100644 --- a/backend/models/opportunity.go +++ b/backend/models/opportunity.go @@ -1,6 +1,10 @@ package models -import "gorm.io/datatypes" +import ( + "time" + + "gorm.io/datatypes" +) // ArbitrageOpportunity 对应于 arbitrage_opportunities 表 type ArbitrageOpportunity struct { @@ -13,6 +17,8 @@ type ArbitrageOpportunity struct { ProfitUSDT float64 `json:"profit_usdt" gorm:"not null"` DetailsJSON datatypes.JSONMap `json:"details,omitempty" gorm:"type:jsonb" swaggertype:"object"` RiskMetricsJSON datatypes.JSONMap `json:"risk_metrics,omitempty" gorm:"type:jsonb" swaggertype:"object"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` } // PaginationData 定义分页响应的结构 diff --git a/backend/pkg/taskpb/taskpb_test.go b/backend/pkg/taskpb/taskpb_test.go new file mode 100644 index 0000000..3314fd9 --- /dev/null +++ b/backend/pkg/taskpb/taskpb_test.go @@ -0,0 +1,58 @@ +package taskpb + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +type fakeConn struct { + lastMethod string +} + +func (f *fakeConn) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error { + f.lastMethod = method + return nil +} + +func (f *fakeConn) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { + return nil, errors.New("not implemented") +} + +func TestTaskServiceClientInvokesMethods(t *testing.T) { + conn := &fakeConn{} + client := NewTaskServiceClient(conn) + + _, err := client.CollectBinance(context.Background(), &CollectBinanceRequest{}) + require.NoError(t, err) + require.Equal(t, TaskService_CollectBinance_FullMethodName, conn.lastMethod) + + _, err = client.CollectUniswap(context.Background(), &CollectUniswapRequest{}) + require.NoError(t, err) + require.Equal(t, TaskService_CollectUniswap_FullMethodName, conn.lastMethod) + + _, err = client.ProcessPrices(context.Background(), &ProcessPricesRequest{}) + require.NoError(t, err) + require.Equal(t, TaskService_ProcessPrices_FullMethodName, conn.lastMethod) + + _, err = client.Analyse(context.Background(), &AnalyseRequest{}) + require.NoError(t, err) + require.Equal(t, TaskService_Analyse_FullMethodName, conn.lastMethod) +} + +type noopServer struct { + UnimplementedTaskServiceServer +} + +func (n *noopServer) CollectBinance(ctx context.Context, req *CollectBinanceRequest) (*TaskResponse, error) { + return &TaskResponse{TaskId: req.GetTaskId()}, nil +} + +func TestRegisterTaskServiceServer(t *testing.T) { + srv := grpc.NewServer() + RegisterTaskServiceServer(srv, &noopServer{}) + srv.Stop() +} diff --git a/backend/service/experiment_service_test.go b/backend/service/experiment_service_test.go new file mode 100644 index 0000000..7e33645 --- /dev/null +++ b/backend/service/experiment_service_test.go @@ -0,0 +1,63 @@ +package service + +import ( + "context" + "testing" + + "backend/testutil" + + "github.com/stretchr/testify/require" + "gorm.io/datatypes" +) + +func TestExperimentServiceLifecycle(t *testing.T) { + db := testutil.NewInMemoryDB(t) + ctx := context.Background() + + taskManager := NewTaskManager(db) + templateService := NewTemplateService(db, taskManager, nil) + batchService := NewBatchService(db) + expService := NewExperimentService(db, templateService, taskManager) + + batch, err := batchService.CreateBatch(ctx, "Batch", "Desc") + require.NoError(t, err) + + template, err := templateService.CreateTemplate(ctx, "Analyse", "analyse", datatypes.JSONMap{"window": 5}) + require.NoError(t, err) + + exp, err := expService.CreateExperiment(ctx, batch.ID, "first exp") + require.NoError(t, err) + require.Equal(t, batch.ID, exp.BatchID) + + experiments, err := expService.ListExperiments(ctx, batch.ID) + require.NoError(t, err) + require.Len(t, experiments, 1) + + fetched, err := expService.GetExperiment(ctx, exp.ID) + require.NoError(t, err) + require.Equal(t, exp.ID, fetched.ID) + + run, task, err := expService.RunTemplateInExperiment(ctx, exp.ID, template.ID, map[string]interface{}{"foo": "bar"}, "", "trigger") + require.NoError(t, err) + require.NotNil(t, run) + require.NotNil(t, task) + require.Equal(t, exp.ID, run.ExperimentID) + require.Equal(t, batch.ID, task.ConfigJSON["batch_id"]) + require.Equal(t, exp.ID, task.ConfigJSON["experiment_id"]) + + runs, err := expService.ListRuns(ctx, exp.ID) + require.NoError(t, err) + require.Len(t, runs, 1) +} + +func TestExperimentServiceValidation(t *testing.T) { + db := testutil.NewInMemoryDB(t) + ctx := context.Background() + + expService := NewExperimentService(db, nil, nil) + _, _, err := expService.RunTemplateInExperiment(ctx, 0, 0, nil, "", "") + require.Error(t, err) + + _, err = expService.ListRuns(ctx, 0) + require.Error(t, err) +} diff --git a/backend/service/report_service_test.go b/backend/service/report_service_test.go new file mode 100644 index 0000000..41ac9bf --- /dev/null +++ b/backend/service/report_service_test.go @@ -0,0 +1,101 @@ +package service + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "backend/models" + "backend/testutil" + + "github.com/johnfercher/maroto/pkg/consts" + "github.com/johnfercher/maroto/pkg/pdf" + "github.com/stretchr/testify/require" +) + +func TestReportServiceLifecycle(t *testing.T) { + db := testutil.NewInMemoryDB(t) + svc := NewReportService(db) + ctx := context.Background() + + // seed data for DB-backed branch + require.NoError(t, db.Create(&models.ArbitrageOpportunity{ + BatchID: 1, + BuyPlatform: "Binance", + SellPlatform: "Uniswap", + BuyPrice: 1000, + SellPrice: 1015, + ProfitUSDT: 15, + CreatedAt: time.Now(), + }).Error) + + report, err := svc.CreateReport(ctx, 1, 2, "pdf", "") + require.NoError(t, err) + + wd := t.TempDir() + cwd, _ := os.Getwd() + require.NoError(t, os.Chdir(wd)) + t.Cleanup(func() { _ = os.Chdir(cwd) }) + + require.NoError(t, svc.GenerateReportFile(report.ID, 1, "pdf")) + stored, err := svc.GetReport(ctx, report.ID) + require.NoError(t, err) + require.Equal(t, "SUCCESS", stored.Status) + require.FileExists(t, stored.FilePath) + + reports, err := svc.ListReports(ctx, 1) + require.NoError(t, err) + require.Len(t, reports, 1) + + require.NoError(t, svc.DeleteReport(ctx, report.ID)) + reports, err = svc.ListReports(ctx, 1) + require.NoError(t, err) + require.Len(t, reports, 0) +} + +func TestGenerateReportUsesMockData(t *testing.T) { + db := testutil.NewInMemoryDB(t) + svc := NewReportService(db) + ctx := context.Background() + + report, err := svc.CreateReport(ctx, 99, 0, "pdf", "") + require.NoError(t, err) + + wd := t.TempDir() + cwd, _ := os.Getwd() + require.NoError(t, os.Chdir(wd)) + defer func() { _ = os.Chdir(cwd) }() + + require.NoError(t, svc.GenerateReportFile(report.ID, 99, "pdf")) + stored, err := svc.GetReport(ctx, report.ID) + require.NoError(t, err) + require.Equal(t, "SUCCESS", stored.Status) +} + +func TestReportHelpers(t *testing.T) { + require.Equal(t, "$1.23", safeFloat(1.23)) + require.Equal(t, "$1.23", safeFloat(float32(1.23))) + require.Equal(t, "value", safeFloat("value")) + + data := (&ReportService{}).generateMockData(2) + require.Len(t, data, 2) + + m := pdf.NewMaroto(consts.Portrait, consts.A4) + (&ReportService{}).renderCard(m, "title", "value") + + gray := getGrayColor() + require.EqualValues(t, 100, gray.Red) + + tmpDir := t.TempDir() + db := testutil.NewInMemoryDB(t) + svc := NewReportService(db) + ctx := context.Background() + report, err := svc.CreateReport(ctx, 1, 1, "pdf", "") + require.NoError(t, err) + cwd, _ := os.Getwd() + require.NoError(t, os.Chdir(tmpDir)) + t.Cleanup(func() { _ = os.Chdir(cwd) }) + require.NoError(t, svc.updateStatus(report.ID, "SUCCESS", filepath.Join(tmpDir, "file.pdf"))) +} diff --git a/backend/service/task_dispatcher_test.go b/backend/service/task_dispatcher_test.go new file mode 100644 index 0000000..2f9e168 --- /dev/null +++ b/backend/service/task_dispatcher_test.go @@ -0,0 +1,111 @@ +package service + +import ( + "context" + "encoding/json" + "testing" + "time" + + "backend/models" + "backend/pkg/taskpb" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "gorm.io/datatypes" +) + +type stubTaskClient struct { + err error + binanceReq *taskpb.CollectBinanceRequest + uniswapReq *taskpb.CollectUniswapRequest + pricesReq *taskpb.ProcessPricesRequest + analyseReq *taskpb.AnalyseRequest + lastMethod string +} + +func (s *stubTaskClient) CollectBinance(ctx context.Context, req *taskpb.CollectBinanceRequest, _ ...grpc.CallOption) (*taskpb.TaskResponse, error) { + s.lastMethod = "binance" + s.binanceReq = req + return &taskpb.TaskResponse{TaskId: req.GetTaskId()}, s.err +} + +func (s *stubTaskClient) CollectBinanceByDate(ctx context.Context, req *taskpb.CollectBinanceByDateRequest, _ ...grpc.CallOption) (*taskpb.TaskResponse, error) { + s.lastMethod = "binance-by-date" + return &taskpb.TaskResponse{TaskId: req.GetTaskId()}, s.err +} + +func (s *stubTaskClient) CollectUniswap(ctx context.Context, req *taskpb.CollectUniswapRequest, _ ...grpc.CallOption) (*taskpb.TaskResponse, error) { + s.lastMethod = "uniswap" + s.uniswapReq = req + return &taskpb.TaskResponse{TaskId: req.GetTaskId()}, s.err +} + +func (s *stubTaskClient) ProcessPrices(ctx context.Context, req *taskpb.ProcessPricesRequest, _ ...grpc.CallOption) (*taskpb.TaskResponse, error) { + s.lastMethod = "prices" + s.pricesReq = req + return &taskpb.TaskResponse{TaskId: req.GetTaskId()}, s.err +} + +func (s *stubTaskClient) Analyse(ctx context.Context, req *taskpb.AnalyseRequest, _ ...grpc.CallOption) (*taskpb.TaskResponse, error) { + s.lastMethod = "analyse" + s.analyseReq = req + return &taskpb.TaskResponse{TaskId: req.GetTaskId()}, s.err +} + +func TestDispatcherValidation(t *testing.T) { + dispatcher := NewGRPCTaskDispatcher(nil) + require.Error(t, dispatcher.Dispatch(context.Background(), &models.Task{})) + + dispatcher = &GRPCTaskDispatcher{} + require.EqualError(t, dispatcher.Dispatch(context.Background(), nil), "task is nil") +} + +func TestDispatcherDispatchesByType(t *testing.T) { + client := &stubTaskClient{} + dispatcher := NewGRPCTaskDispatcher(client) + ctx := context.Background() + + binanceTask := &models.Task{TaskID: "bin", Type: "collect_binance", ConfigJSON: datatypes.JSONMap{"import_percentage": 0, "chunk_size": 0}} + require.NoError(t, dispatcher.Dispatch(ctx, binanceTask)) + require.Equal(t, "binance", client.lastMethod) + require.EqualValues(t, 100, client.binanceReq.GetImportPercentage()) + require.EqualValues(t, 1000000, client.binanceReq.GetChunkSize()) + + uniswapTask := &models.Task{TaskID: "uni", Type: "collect_uniswap", ConfigJSON: datatypes.JSONMap{"pool_address": "pool", "start_ts": 1, "end_ts": 2}} + require.NoError(t, dispatcher.Dispatch(ctx, uniswapTask)) + require.Equal(t, "uniswap", client.lastMethod) + require.Equal(t, "pool", client.uniswapReq.GetPoolAddress()) + + startISO := time.Unix(1, 0).UTC().Format(time.RFC3339) + endISO := "2024-01-02T03:04:05" + pricesTask := &models.Task{TaskID: "prices", Type: "process_prices", ConfigJSON: datatypes.JSONMap{ + "start_date": startISO, + "end_date": endISO, + "aggregation_interval": "1h", + "overwrite": true, + "db_overrides": map[string]interface{}{"dsn": "postgres"}, + }} + require.NoError(t, dispatcher.Dispatch(ctx, pricesTask)) + require.Equal(t, "prices", client.pricesReq.GetTaskId()) + require.Equal(t, int32(1), client.pricesReq.GetStartDate()) + expectedEnd := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC).Unix() + require.Equal(t, int32(expectedEnd), client.pricesReq.GetEndDate()) + + strategy := map[string]interface{}{"strategy": map[string]interface{}{"window": 5}, "experiment_id": 9} + analyseTask := &models.Task{TaskID: "analyse", Type: "analyse", ConfigJSON: datatypes.JSONMap(strategy)} + require.NoError(t, dispatcher.Dispatch(ctx, analyseTask)) + var payload map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(client.analyseReq.GetStrategyJson()), &payload)) + require.EqualValues(t, 9, payload["experiment_id"]) + + unsupported := &models.Task{TaskID: "x", Type: "unknown", ConfigJSON: datatypes.JSONMap{}} + require.EqualError(t, dispatcher.Dispatch(ctx, unsupported), "unsupported task type unknown") +} + +func TestHelperConversions(t *testing.T) { + require.Equal(t, int64(42), asInt64("42")) + require.Equal(t, int64(0), asInt64("bad")) + require.True(t, asBool("true")) + require.False(t, asBool(0)) + require.Equal(t, int64(1700000000), parseTimeOrInt("2023-11-14T22:13:20")) +} diff --git a/backend/service/task_manager_test.go b/backend/service/task_manager_test.go new file mode 100644 index 0000000..37c9b72 --- /dev/null +++ b/backend/service/task_manager_test.go @@ -0,0 +1,85 @@ +package service + +import ( + "context" + "errors" + "testing" + + "backend/testutil" + + "github.com/stretchr/testify/require" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +func newTaskManager(t *testing.T) *TaskManager { + t.Helper() + return NewTaskManager(testutil.NewInMemoryDB(t)) +} + +func TestTaskManagerCreateAndQuery(t *testing.T) { + ctx := context.Background() + mgr := newTaskManager(t) + + task, err := mgr.CreateTask(ctx, "collect_binance", "task-1", "trigger", datatypes.JSONMap{"import_percentage": 10}) + require.NoError(t, err) + require.Equal(t, "task-1", task.TaskID) + + fetched, err := mgr.GetTaskByExternalID(ctx, "task-1") + require.NoError(t, err) + require.Equal(t, task.ID, fetched.ID) + + tasks, total, err := mgr.ListTasks(ctx, 0, 0) + require.NoError(t, err) + require.Equal(t, int64(1), total) + require.Len(t, tasks, 1) +} + +func TestTaskManagerLogsAndCancel(t *testing.T) { + ctx := context.Background() + mgr := newTaskManager(t) + + task, err := mgr.CreateTask(ctx, "collect_binance", "task-logs", "trigger", datatypes.JSONMap{}) + require.NoError(t, err) + + require.Error(t, mgr.AddTaskLog(ctx, 0, "INFO", "boom")) + require.NoError(t, mgr.AddTaskLog(ctx, task.ID, "INFO", "ready")) + require.NoError(t, mgr.AddTaskLog(ctx, task.ID, "INFO", "go")) + + logs, err := mgr.ListTaskLogs(ctx, task.ID, -1, 0) + require.NoError(t, err) + require.Len(t, logs, 2) + + updated, err := mgr.CancelTaskByExternalID(ctx, "task-logs", "user request") + require.NoError(t, err) + require.Equal(t, "CANCELLED", updated.Status) + require.Contains(t, updated.LogSummary, "user request") + + // Calling cancel again should return the same task without error because status is terminal. + again, err := mgr.CancelTaskByExternalID(ctx, "task-logs", "ignored") + require.NoError(t, err) + require.Equal(t, updated.ID, again.ID) +} + +func TestTaskManagerCancelRequiresID(t *testing.T) { + ctx := context.Background() + mgr := newTaskManager(t) + + _, err := mgr.CancelTaskByExternalID(ctx, "", "") + require.Error(t, err) +} + +func TestEncodeMergeAndFormatHelpers(t *testing.T) { + cfg := EncodeConfig(map[string]interface{}{"key": "value"}) + require.Equal(t, "value", cfg["key"]) + + merged := MergeConfig(cfg, map[string]interface{}{"key": "new", "extra": 1}) + require.Equal(t, "new", merged["key"]) + require.Equal(t, 1, merged["extra"]) + + err := FormatDBError(gorm.ErrRecordNotFound, "missing task") + require.True(t, errors.Is(err, gorm.ErrRecordNotFound)) + + generic := FormatDBError(errors.New("boom"), "wrap") + require.Error(t, generic) +} diff --git a/backend/service/template_service_test.go b/backend/service/template_service_test.go new file mode 100644 index 0000000..8bc8249 --- /dev/null +++ b/backend/service/template_service_test.go @@ -0,0 +1,110 @@ +package service + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "backend/models" + "backend/testutil" + + "github.com/stretchr/testify/require" + "gorm.io/datatypes" +) + +type stubDispatcher struct { + lastTask *models.Task + callCount int + returnErr error +} + +func (s *stubDispatcher) Dispatch(ctx context.Context, task *models.Task) error { + s.callCount++ + s.lastTask = task + return s.returnErr +} + +func TestTemplateServiceCRUDAndRun(t *testing.T) { + db := testutil.NewInMemoryDB(t) + ctx := context.Background() + taskManager := NewTaskManager(db) + dispatcher := &stubDispatcher{} + svc := NewTemplateService(db, taskManager, dispatcher) + + tpl, err := svc.CreateTemplate(ctx, "Analyse Alpha", "analyse", datatypes.JSONMap{"foo": "bar"}) + require.NoError(t, err) + require.NotZero(t, tpl.ID) + + fetched, err := svc.GetTemplate(ctx, tpl.ID) + require.NoError(t, err) + require.Equal(t, tpl.ID, fetched.ID) + + updated, err := svc.UpdateTemplate(ctx, tpl.ID, "Updated", "", nil) + require.NoError(t, err) + require.Equal(t, "Updated", updated.Name) + + templates, err := svc.ListTemplates(ctx) + require.NoError(t, err) + require.Len(t, templates, 1) + + task, err := svc.RunTemplate(ctx, tpl.ID, map[string]interface{}{"extra": 1}, "", "unit-test") + require.NoError(t, err) + require.NotEmpty(t, task.TaskID) + require.Equal(t, "analyse", task.Type) + require.Equal(t, "unit-test", task.Trigger) + require.True(t, dispatcher.callCount > 0) + require.Contains(t, dispatcher.lastTask.ConfigJSON, "batch_id") + require.Equal(t, 1, dispatcher.lastTask.ConfigJSON["extra"]) + + dispatcher.returnErr = errors.New("dispatch failed") + _, err = svc.RunTemplate(ctx, tpl.ID, map[string]interface{}{}, "manual-id", "unit-test") + require.Error(t, err) + + require.NoError(t, svc.DeleteTemplate(ctx, tpl.ID)) +} + +func TestShouldAutoCreateBatchCases(t *testing.T) { + cases := []struct { + name string + value interface{} + expected bool + }{ + {"nil value", nil, true}, + {"empty string", " ", true}, + {"negative string", "-1", true}, + {"bad string", "abc", false}, + {"zero float", float64(0), true}, + {"positive int", 2, false}, + {"zero uint", uint(0), true}, + {"json number zero", json.Number("0"), true}, + {"json parse error", json.Number("bad"), true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, shouldAutoCreateBatch(tc.value)) + }) + } +} + +func TestBatchServiceCRUD(t *testing.T) { + db := testutil.NewInMemoryDB(t) + ctx := context.Background() + svc := NewBatchService(db) + + batch, err := svc.CreateBatch(ctx, "Batch A", "desc") + require.NoError(t, err) + + updated, err := svc.UpdateBatch(ctx, batch.ID, "Batch B", "updated", true) + require.NoError(t, err) + require.Equal(t, "Batch B", updated.Name) + require.NotNil(t, updated.LastRefreshedAt) + + batches, err := svc.ListBatches(ctx) + require.NoError(t, err) + require.Len(t, batches, 1) + + got, err := svc.GetBatch(ctx, batch.ID) + require.NoError(t, err) + require.Equal(t, batch.ID, got.ID) +} diff --git a/backend/testutil/testdb.go b/backend/testutil/testdb.go index aab98f4..e5d1ee9 100644 --- a/backend/testutil/testdb.go +++ b/backend/testutil/testdb.go @@ -1,27 +1,69 @@ package testutil import ( + "fmt" + "net" + "path/filepath" "testing" "backend/models" - "github.com/glebarez/sqlite" + embeddedpostgres "github.com/fergusstrange/embedded-postgres" + postgresdriver "gorm.io/driver/postgres" "gorm.io/gorm" ) -// NewInMemoryDB creates a transient SQLite database for tests. +// NewInMemoryDB spins up an ephemeral PostgreSQL instance for isolated tests. func NewInMemoryDB(t *testing.T) *gorm.DB { t.Helper() - db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { - t.Fatalf("failed to open in-memory database: %v", err) + t.Fatalf("failed to allocate port: %v", err) + } + port := ln.Addr().(*net.TCPAddr).Port + _ = ln.Close() + + rootDir := t.TempDir() + runtimePath := filepath.Join(rootDir, "runtime") + binariesPath := filepath.Join(rootDir, "binaries") + dataPath := filepath.Join(rootDir, "data") + + pg := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig(). + Username("postgres"). + Password("postgres"). + Database("testdb"). + Port(uint32(port)). + Locale("C"). + RuntimePath(runtimePath). + DataPath(dataPath). + BinariesPath(binariesPath), + ) + + if err := pg.Start(); err != nil { + t.Fatalf("failed to start embedded postgres: %v", err) + } + + dsn := fmt.Sprintf("host=127.0.0.1 user=postgres password=postgres dbname=testdb port=%d sslmode=disable", port) + + db, err := gorm.Open(postgresdriver.Open(dsn), &gorm.Config{}) + if err != nil { + _ = pg.Stop() + t.Fatalf("failed to open postgres database: %v", err) } if err := db.AutoMigrate( &models.ArbitrageOpportunity{}, &models.AggregatedPrice{}, + &models.Task{}, + &models.TaskLog{}, + &models.ParamTemplate{}, + &models.Batch{}, + &models.Report{}, + &models.Experiment{}, + &models.ExperimentRun{}, ); err != nil { + _ = pg.Stop() t.Fatalf("failed to migrate schema: %v", err) } @@ -30,6 +72,7 @@ func NewInMemoryDB(t *testing.T) *gorm.DB { if sqlDB != nil { _ = sqlDB.Close() } + _ = pg.Stop() }) return db diff --git a/backend/testutil/testdb_test.go b/backend/testutil/testdb_test.go new file mode 100644 index 0000000..2eb671c --- /dev/null +++ b/backend/testutil/testdb_test.go @@ -0,0 +1,25 @@ +package testutil + +import ( + "testing" + + "backend/models" + + "github.com/stretchr/testify/require" + "gorm.io/datatypes" +) + +func TestNewInMemoryDBCreatesPostgresInstance(t *testing.T) { + db := NewInMemoryDB(t) + + require.NoError(t, db.Exec("SELECT 1").Error) + + task := &models.Task{ + TaskID: "unit", + Type: "test", + Status: "RUNNING", + ConfigJSON: datatypes.JSONMap{"k": "v"}, + } + require.NoError(t, db.Create(task).Error) + require.NotZero(t, task.ID) +} diff --git a/backend/utils/response_test.go b/backend/utils/response_test.go new file mode 100644 index 0000000..a9e7fb1 --- /dev/null +++ b/backend/utils/response_test.go @@ -0,0 +1,76 @@ +package utils + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func init() { + gin.SetMode(gin.TestMode) +} + +func newTestContext() (*gin.Context, *httptest.ResponseRecorder) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + return c, w +} + +func TestSuccessWritesPayload(t *testing.T) { + c, w := newTestContext() + + Success(c, gin.H{"foo": "bar"}) + + require.Equal(t, http.StatusOK, w.Code) + var resp Response + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + require.Equal(t, http.StatusOK, resp.Code) + require.Equal(t, "成功", resp.Message) +} + +func TestSuccessWithMessageOverridesDefault(t *testing.T) { + c, w := newTestContext() + + SuccessWithMessage(c, "ok", gin.H{"foo": "bar"}) + + require.Equal(t, http.StatusOK, w.Code) + var resp Response + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + require.Equal(t, "ok", resp.Message) +} + +func TestFailHelpers(t *testing.T) { + t.Run("fail", func(t *testing.T) { + c, w := newTestContext() + Fail(c, http.StatusTeapot, "nope") + + require.Equal(t, http.StatusTeapot, w.Code) + var resp Response + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + require.Equal(t, "nope", resp.Message) + }) + + t.Run("bad request default", func(t *testing.T) { + c, w := newTestContext() + BadRequest(c, "") + + require.Equal(t, http.StatusBadRequest, w.Code) + var resp Response + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + require.Equal(t, "请求参数错误", resp.Message) + }) + + t.Run("server error custom", func(t *testing.T) { + c, w := newTestContext() + ServerError(c, "boom") + + require.Equal(t, http.StatusInternalServerError, w.Code) + var resp Response + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + require.Equal(t, "boom", resp.Message) + }) +} From c80fab4477de54545f9fadff003e03829f25564a Mon Sep 17 00:00:00 2001 From: XuHaoran <3230105281@zju.edu.cn> Date: Sun, 28 Dec 2025 03:08:30 +0800 Subject: [PATCH 2/6] test: add tests --- backend/api/handler_negative_test.go | 226 ++++++++++++++++++++++ backend/api/handler_test.go | 22 +++ backend/grpcserver/server_test.go | 18 ++ backend/pkg/taskpb/taskpb_test.go | 270 +++++++++++++++++++++++++++ 4 files changed, 536 insertions(+) create mode 100644 backend/api/handler_negative_test.go diff --git a/backend/api/handler_negative_test.go b/backend/api/handler_negative_test.go new file mode 100644 index 0000000..2043dd9 --- /dev/null +++ b/backend/api/handler_negative_test.go @@ -0,0 +1,226 @@ +package api + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "backend/service" + "backend/testutil" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func newTemplateHandlerForTest(t *testing.T) *TemplateHandler { + t.Helper() + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + templateSvc := service.NewTemplateService(db, taskManager, nil) + return NewTemplateHandler(templateSvc, taskManager) +} + +func newGinContext(method, path, payload string) (*gin.Context, *httptest.ResponseRecorder) { + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + req := httptest.NewRequest(method, path, strings.NewReader(payload)) + if payload != "" { + req.Header.Set("Content-Type", "application/json") + } + c.Request = req + return c, recorder +} + +func TestTemplateHandlerCreateTemplateValidationError(t *testing.T) { + handler := newTemplateHandlerForTest(t) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req := httptest.NewRequest(http.MethodPost, "/templates", strings.NewReader(`{"task_type":"analyse"}`)) + req.Header.Set("Content-Type", "application/json") + c.Request = req + + handler.CreateTemplate(c) + + require.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestTemplateHandlerUpdateTemplateInvalidID(t *testing.T) { + handler := newTemplateHandlerForTest(t) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req := httptest.NewRequest(http.MethodPut, "/templates/abc", strings.NewReader(`{"name":"n","task_type":"analyse"}`)) + req.Header.Set("Content-Type", "application/json") + c.Request = req + c.Params = gin.Params{{Key: "id", Value: "abc"}} + + handler.UpdateTemplate(c) + + require.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestTemplateHandlerRunTemplateBadJSON(t *testing.T) { + handler := newTemplateHandlerForTest(t) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req := httptest.NewRequest(http.MethodPost, "/templates/1/run", strings.NewReader("not-json")) + req.Header.Set("Content-Type", "application/json") + c.Request = req + c.Params = gin.Params{{Key: "id", Value: "1"}} + + handler.RunTemplate(c) + + require.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestReportHandlerDownloadRequiresReadyStatus(t *testing.T) { + db := testutil.NewInMemoryDB(t) + reportSvc := service.NewReportService(db) + handler := NewReportHandler(reportSvc) + ctx := context.Background() + report, err := reportSvc.CreateReport(ctx, 7, 1, "pdf", "") + require.NoError(t, err) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req := httptest.NewRequest(http.MethodGet, "/reports/1/download", nil) + c.Request = req + c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", report.ID)}} + + handler.DownloadReport(c) + + require.Equal(t, http.StatusBadRequest, w.Code) + + w2 := httptest.NewRecorder() + c2, _ := gin.CreateTestContext(w2) + c2.Request = httptest.NewRequest(http.MethodGet, "/reports/bad/download", nil) + c2.Params = gin.Params{{Key: "id", Value: "bad"}} + handler.DownloadReport(c2) + require.Equal(t, http.StatusBadRequest, w2.Code) +} + +func TestTaskHandlerCancelTaskBadPayload(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + handler := NewTaskHandler(taskManager) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req := httptest.NewRequest(http.MethodPost, "/tasks/task-1/cancel", strings.NewReader("{")) + req.Header.Set("Content-Type", "application/json") + c.Request = req + c.Params = gin.Params{{Key: "task_id", Value: "task-1"}} + + handler.CancelTask(c) + + require.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestBatchHandlerCreateBatchInvalidJSON(t *testing.T) { + handler := &BatchHandler{} + c, w := newGinContext(http.MethodPost, "/batches", "{") + handler.CreateBatch(c) + require.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestBatchHandlerGetBatchInvalidID(t *testing.T) { + handler := &BatchHandler{} + c, w := newGinContext(http.MethodGet, "/batches/abc", "") + c.Params = gin.Params{{Key: "id", Value: "abc"}} + handler.GetBatch(c) + require.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestBatchHandlerUpdateBatchInvalidPayload(t *testing.T) { + handler := &BatchHandler{} + c, w := newGinContext(http.MethodPut, "/batches/1", "not-json") + c.Params = gin.Params{{Key: "id", Value: "1"}} + handler.UpdateBatch(c) + require.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestHandlerGetOpportunitiesRejectsInvalidOrder(t *testing.T) { + h := &Handler{} + c, w := newGinContext(http.MethodGet, "/opportunities?order=bad", "") + h.GetOpportunities(c) + require.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestHandlerPriceComparisonValidation(t *testing.T) { + h := &Handler{} + ctxMissing, wMissing := newGinContext(http.MethodGet, "/price-comparison", "") + h.GetPriceComparisonData(ctxMissing) + require.Equal(t, http.StatusBadRequest, wMissing.Code) + + ctxInvalid, wInvalid := newGinContext(http.MethodGet, "/price-comparison?startTime=abc&endTime=1", "") + h.GetPriceComparisonData(ctxInvalid) + require.Equal(t, http.StatusBadRequest, wInvalid.Code) +} + +func TestReportHandlerListReportsInvalidBatchID(t *testing.T) { + h := &ReportHandler{} + c, w := newGinContext(http.MethodGet, "/reports?batch_id=bad", "") + h.ListReports(c) + require.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestReportHandlerDeleteReportInvalidID(t *testing.T) { + h := &ReportHandler{} + c, w := newGinContext(http.MethodDelete, "/reports/bad", "") + c.Params = gin.Params{{Key: "id", Value: "bad"}} + h.DeleteReport(c) + require.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestExperimentHandlerValidations(t *testing.T) { + h := &ExperimentHandler{} + ctxList, wList := newGinContext(http.MethodGet, "/experiments?batch_id=x", "") + h.ListExperiments(ctxList) + require.Equal(t, http.StatusBadRequest, wList.Code) + + ctxGet, wGet := newGinContext(http.MethodGet, "/experiments/abc", "") + ctxGet.Params = gin.Params{{Key: "id", Value: "abc"}} + h.GetExperiment(ctxGet) + require.Equal(t, http.StatusBadRequest, wGet.Code) + + ctxRuns, wRuns := newGinContext(http.MethodGet, "/experiments/def/runs", "") + ctxRuns.Params = gin.Params{{Key: "id", Value: "def"}} + h.ListRuns(ctxRuns) + require.Equal(t, http.StatusBadRequest, wRuns.Code) + + ctxRunTemplate, wRunTemplate := newGinContext(http.MethodPost, "/experiments/1/runs", "not-json") + ctxRunTemplate.Params = gin.Params{{Key: "id", Value: "1"}} + h.RunTemplate(ctxRunTemplate) + require.Equal(t, http.StatusBadRequest, wRunTemplate.Code) +} + +func TestTaskHandlerNotFoundBranches(t *testing.T) { + db := testutil.NewInMemoryDB(t) + manager := service.NewTaskManager(db) + h := NewTaskHandler(manager) + + ctxTask, wTask := newGinContext(http.MethodGet, "/tasks/missing", "") + ctxTask.Params = gin.Params{{Key: "task_id", Value: "missing"}} + h.GetTask(ctxTask) + require.Equal(t, http.StatusNotFound, wTask.Code) + + ctxLogs, wLogs := newGinContext(http.MethodGet, "/tasks/missing/logs", "") + ctxLogs.Params = gin.Params{{Key: "task_id", Value: "missing"}} + h.ListLogs(ctxLogs) + require.Equal(t, http.StatusNotFound, wLogs.Code) +} + +func TestParseUintParamHelpers(t *testing.T) { + ctxBad, _ := gin.CreateTestContext(httptest.NewRecorder()) + ctxBad.Params = gin.Params{{Key: "id", Value: "abc"}} + _, err := parseUintParam(ctxBad, "id") + require.Error(t, err) + + ctxGood, _ := gin.CreateTestContext(httptest.NewRecorder()) + ctxGood.Params = gin.Params{{Key: "id", Value: "42"}} + id, err := parseUintParam(ctxGood, "id") + require.NoError(t, err) + require.Equal(t, uint(42), id) +} diff --git a/backend/api/handler_test.go b/backend/api/handler_test.go index 49b24e0..ebea10a 100644 --- a/backend/api/handler_test.go +++ b/backend/api/handler_test.go @@ -97,3 +97,25 @@ func TestGetPriceComparisonDataHandlerReturnsSeries(t *testing.T) { require.Len(t, resp.Data["binance"], 1) require.Len(t, resp.Data["uniswap"], 1) } + +func TestGetOpportunitiesRejectsInvalidOrder(t *testing.T) { + h, _ := newTestHandler(t) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/opportunities?order=sideways", nil) + + h.GetOpportunities(c) + + require.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestGetPriceComparisonDataHandlerRejectsBadTimestamp(t *testing.T) { + h, _ := newTestHandler(t) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/price-comparison?startTime=abc&endTime=123", nil) + + h.GetPriceComparisonData(c) + + require.Equal(t, http.StatusBadRequest, w.Code) +} diff --git a/backend/grpcserver/server_test.go b/backend/grpcserver/server_test.go index 2f8e717..2f571f2 100644 --- a/backend/grpcserver/server_test.go +++ b/backend/grpcserver/server_test.go @@ -11,6 +11,8 @@ import ( "backend/testutil" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) func TestServerRPCsPersistTasks(t *testing.T) { @@ -106,3 +108,19 @@ func jsonNumberAsInt(v interface{}) int { } return 0 } + +func TestCreateTaskReturnsInternalOnManagerError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + srv := New(service.NewTaskManager(db), "grpc-trigger") + _, err := srv.createTask(context.Background(), "", "", map[string]interface{}{}) + require.Error(t, err) + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Internal, st.Code()) +} + +func TestRunReturnsListenError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + err := Run(Config{Port: "bad-port"}, service.NewTaskManager(db)) + require.Error(t, err) +} diff --git a/backend/pkg/taskpb/taskpb_test.go b/backend/pkg/taskpb/taskpb_test.go index 3314fd9..3dde4ff 100644 --- a/backend/pkg/taskpb/taskpb_test.go +++ b/backend/pkg/taskpb/taskpb_test.go @@ -7,6 +7,10 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" ) type fakeConn struct { @@ -41,6 +45,10 @@ func TestTaskServiceClientInvokesMethods(t *testing.T) { _, err = client.Analyse(context.Background(), &AnalyseRequest{}) require.NoError(t, err) require.Equal(t, TaskService_Analyse_FullMethodName, conn.lastMethod) + + _, err = client.CollectBinanceByDate(context.Background(), &CollectBinanceByDateRequest{}) + require.NoError(t, err) + require.Equal(t, TaskService_CollectBinanceByDate_FullMethodName, conn.lastMethod) } type noopServer struct { @@ -56,3 +64,265 @@ func TestRegisterTaskServiceServer(t *testing.T) { RegisterTaskServiceServer(srv, &noopServer{}) srv.Stop() } + +func TestUnimplementedTaskServiceServerReturnsCodes(t *testing.T) { + server := &UnimplementedTaskServiceServer{} + ctx := context.Background() + checks := []struct { + name string + run func() error + }{ + {"CollectBinance", func() error { _, err := server.CollectBinance(ctx, &CollectBinanceRequest{}); return err }}, + {"CollectBinanceByDate", func() error { _, err := server.CollectBinanceByDate(ctx, &CollectBinanceByDateRequest{}); return err }}, + {"CollectUniswap", func() error { _, err := server.CollectUniswap(ctx, &CollectUniswapRequest{}); return err }}, + {"ProcessPrices", func() error { _, err := server.ProcessPrices(ctx, &ProcessPricesRequest{}); return err }}, + {"Analyse", func() error { _, err := server.Analyse(ctx, &AnalyseRequest{}); return err }}, + } + + for _, tc := range checks { + t.Run(tc.name, func(t *testing.T) { + err := tc.run() + require.Error(t, err) + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Unimplemented, st.Code()) + }) + } +} + +type spyTaskServer struct { + UnimplementedTaskServiceServer + lastMethod string + lastReq interface{} +} + +func (s *spyTaskServer) CollectBinance(ctx context.Context, req *CollectBinanceRequest) (*TaskResponse, error) { + s.lastMethod = "CollectBinance" + s.lastReq = req + return &TaskResponse{TaskId: req.GetTaskId()}, nil +} + +func (s *spyTaskServer) CollectBinanceByDate(ctx context.Context, req *CollectBinanceByDateRequest) (*TaskResponse, error) { + s.lastMethod = "CollectBinanceByDate" + s.lastReq = req + return &TaskResponse{TaskId: req.GetTaskId()}, nil +} + +func (s *spyTaskServer) CollectUniswap(ctx context.Context, req *CollectUniswapRequest) (*TaskResponse, error) { + s.lastMethod = "CollectUniswap" + s.lastReq = req + return &TaskResponse{TaskId: req.GetTaskId()}, nil +} + +func (s *spyTaskServer) ProcessPrices(ctx context.Context, req *ProcessPricesRequest) (*TaskResponse, error) { + s.lastMethod = "ProcessPrices" + s.lastReq = req + return &TaskResponse{TaskId: req.GetTaskId()}, nil +} + +func (s *spyTaskServer) Analyse(ctx context.Context, req *AnalyseRequest) (*TaskResponse, error) { + s.lastMethod = "Analyse" + s.lastReq = req + return &TaskResponse{TaskId: req.GetTaskId()}, nil +} + +func TestCollectBinanceHandlerInvokesServer(t *testing.T) { + rec := &spyTaskServer{} + dec := func(v interface{}) error { + req := v.(*CollectBinanceRequest) + req.TaskId = "decoded" + return nil + } + called := false + interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + called = true + require.Equal(t, TaskService_CollectBinance_FullMethodName, info.FullMethod) + return handler(ctx, req) + } + + resp, err := _TaskService_CollectBinance_Handler(rec, context.Background(), dec, interceptor) + require.NoError(t, err) + require.True(t, called) + require.NotNil(t, rec.lastReq) + require.Equal(t, "decoded", rec.lastReq.(*CollectBinanceRequest).GetTaskId()) + msg, ok := resp.(*TaskResponse) + require.True(t, ok) + require.Equal(t, "decoded", msg.TaskId) +} + +func TestTaskServiceHandlersWithoutInterceptor(t *testing.T) { + srv := &spyTaskServer{} + ctx := context.Background() + testCases := []struct { + name string + handler func(interface{}, context.Context, func(interface{}) error, grpc.UnaryServerInterceptor) (interface{}, error) + prep func(interface{}) error + wantMethod string + }{ + {"CollectBinance", _TaskService_CollectBinance_Handler, func(v interface{}) error { + req := v.(*CollectBinanceRequest) + req.TaskId = "cb" + return nil + }, "CollectBinance"}, + {"CollectBinanceByDate", _TaskService_CollectBinanceByDate_Handler, func(v interface{}) error { + req := v.(*CollectBinanceByDateRequest) + req.TaskId = "cbd" + return nil + }, "CollectBinanceByDate"}, + {"CollectUniswap", _TaskService_CollectUniswap_Handler, func(v interface{}) error { + req := v.(*CollectUniswapRequest) + req.TaskId = "cu" + return nil + }, "CollectUniswap"}, + {"ProcessPrices", _TaskService_ProcessPrices_Handler, func(v interface{}) error { + req := v.(*ProcessPricesRequest) + req.TaskId = "pp" + return nil + }, "ProcessPrices"}, + {"Analyse", _TaskService_Analyse_Handler, func(v interface{}) error { + req := v.(*AnalyseRequest) + req.TaskId = "an" + return nil + }, "Analyse"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + srv.lastMethod = "" + srv.lastReq = nil + resp, err := tc.handler(srv, ctx, tc.prep, nil) + require.NoError(t, err) + require.Equal(t, tc.wantMethod, srv.lastMethod) + require.NotNil(t, srv.lastReq) + msg, ok := resp.(*TaskResponse) + require.True(t, ok) + require.Equal(t, srv.lastReq.(interface{ GetTaskId() string }).GetTaskId(), msg.TaskId) + }) + } +} + +func TestTaskServiceHandlerDecoderError(t *testing.T) { + decErr := errors.New("boom") + _, err := _TaskService_CollectBinance_Handler(&spyTaskServer{}, context.Background(), func(interface{}) error { + return decErr + }, nil) + require.ErrorIs(t, err, decErr) +} + +func TestTaskServiceServiceDescMetadata(t *testing.T) { + require.Equal(t, "task.proto", TaskService_ServiceDesc.Metadata) + var names []string + for _, m := range TaskService_ServiceDesc.Methods { + names = append(names, m.MethodName) + } + require.ElementsMatch(t, []string{ + "CollectBinance", + "CollectBinanceByDate", + "CollectUniswap", + "ProcessPrices", + "Analyse", + }, names) +} + +func TestTaskStatusEnumHelpers(t *testing.T) { + for value, name := range TaskStatus_name { + t.Run(name, func(t *testing.T) { + enum := TaskStatus(value) + require.Equal(t, name, enum.String()) + require.Equal(t, protoreflect.EnumNumber(value), enum.Number()) + require.Equal(t, value, int32(TaskStatus_value[name])) + }) + } + + enum := TaskStatus_RUNNING + ptr := enum.Enum() + require.NotNil(t, ptr) + require.Equal(t, enum, *ptr) + require.NotNil(t, enum.Descriptor()) + require.NotNil(t, enum.Type()) +} + +func TestTaskProtoMessagesRoundTrip(t *testing.T) { + testCases := []struct { + name string + msg proto.Message + }{ + {"CollectBinanceRequest", &CollectBinanceRequest{TaskId: "cb", ImportPercentage: 70, ChunkSize: 5}}, + {"CollectBinanceByDateRequest", &CollectBinanceByDateRequest{TaskId: "cbd", StartTs: 1, EndTs: 2}}, + {"CollectUniswapRequest", &CollectUniswapRequest{TaskId: "cu", PoolAddress: "pool", StartTs: 3, EndTs: 4}}, + {"ProcessPricesRequest", &ProcessPricesRequest{ + TaskId: "pp", + StartDate: 1, + EndDate: 2, + AggregationInterval: "1m", + Overwrite: true, + DbOverrides: map[string]string{"dsn": "postgres"}, + }}, + {"AnalyseRequest", &AnalyseRequest{TaskId: "ar", BatchId: 9, Overwrite: true, StrategyJson: "{}"}}, + {"TaskResponse", &TaskResponse{TaskId: "tr", Status: TaskStatus_SUCCESS}}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + data, err := proto.Marshal(tc.msg) + require.NoError(t, err) + + clone := proto.Clone(tc.msg) + require.Equal(t, messageString(tc.msg), messageString(clone)) + require.True(t, proto.Equal(tc.msg, clone)) + require.NotNil(t, tc.msg.ProtoReflect().Descriptor()) + + resetProtoMessage(tc.msg) + _ = messageString(tc.msg) + require.NotNil(t, tc.msg.ProtoReflect().Descriptor()) + + newMsg := proto.Clone(tc.msg) + require.NoError(t, proto.Unmarshal(data, newMsg)) + require.True(t, proto.Equal(clone, newMsg)) + }) + } +} + +func TestTaskProtoNilGetters(t *testing.T) { + var resp *TaskResponse + require.Equal(t, "", resp.GetTaskId()) + require.Equal(t, TaskStatus_WAIT, resp.GetStatus()) + + var analyse *AnalyseRequest + require.Equal(t, "", analyse.GetTaskId()) + require.Equal(t, int32(0), analyse.GetBatchId()) +} + +func TestTaskProtoRawDescCaching(t *testing.T) { + first := file_task_proto_rawDescGZIP() + second := file_task_proto_rawDescGZIP() + require.NotEmpty(t, first) + require.Equal(t, first, second) +} + +func resetProtoMessage(m proto.Message) { + switch v := m.(type) { + case *CollectBinanceRequest: + v.Reset() + case *CollectBinanceByDateRequest: + v.Reset() + case *CollectUniswapRequest: + v.Reset() + case *ProcessPricesRequest: + v.Reset() + case *AnalyseRequest: + v.Reset() + case *TaskResponse: + v.Reset() + default: + panic("unsupported proto message type") + } +} + +func messageString(m proto.Message) string { + type stringer interface{ String() string } + if s, ok := m.(stringer); ok { + return s.String() + } + return "" +} From 6a2cefcfb564f72bed2042a5e1c7c87ac6b7b4c3 Mon Sep 17 00:00:00 2001 From: XuHaoran <3230105281@zju.edu.cn> Date: Sun, 28 Dec 2025 14:19:17 +0800 Subject: [PATCH 3/6] test: add tests --- backend/docs/docs_test.go | 29 ++++++-- backend/service/task_manager_test.go | 62 ++++++++++++++++++ .../reports/Report_Batch1_1764410803.pdf | Bin 187178 -> 0 bytes backend/utils/response_test.go | 10 +++ 4 files changed, 94 insertions(+), 7 deletions(-) delete mode 100644 backend/storage/reports/Report_Batch1_1764410803.pdf diff --git a/backend/docs/docs_test.go b/backend/docs/docs_test.go index 9f18b79..f938e70 100644 --- a/backend/docs/docs_test.go +++ b/backend/docs/docs_test.go @@ -1,12 +1,27 @@ package docs -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/swaggo/swag" +) func TestSwaggerInfoMetadata(t *testing.T) { - if SwaggerInfo.Title == "" { - t.Fatal("swagger info title should be populated") - } - if SwaggerInfo.BasePath == "" { - t.Fatal("swagger base path should be set") - } + doc, err := swag.ReadDoc(SwaggerInfo.InstanceName()) + require.NoError(t, err) + require.Contains(t, doc, "\"swagger\": \"2.0\"") + require.Contains(t, doc, "\"/tasks\"") +} + +func TestSwaggerInfoCustomization(t *testing.T) { + original := SwaggerInfo.Title + SwaggerInfo.Title = "Coverage Title" + t.Cleanup(func() { + SwaggerInfo.Title = original + }) + + doc, err := swag.ReadDoc(SwaggerInfo.InstanceName()) + require.NoError(t, err) + require.Contains(t, doc, "Coverage Title") } diff --git a/backend/service/task_manager_test.go b/backend/service/task_manager_test.go index 37c9b72..540655f 100644 --- a/backend/service/task_manager_test.go +++ b/backend/service/task_manager_test.go @@ -3,8 +3,11 @@ package service import ( "context" "errors" + "fmt" "testing" + "time" + "backend/models" "backend/testutil" "github.com/stretchr/testify/require" @@ -83,3 +86,62 @@ func TestEncodeMergeAndFormatHelpers(t *testing.T) { generic := FormatDBError(errors.New("boom"), "wrap") require.Error(t, generic) } + +func TestTaskManagerCreateTaskRequiresType(t *testing.T) { + ctx := context.Background() + mgr := newTaskManager(t) + _, err := mgr.CreateTask(ctx, "", "", "", datatypes.JSONMap{}) + require.EqualError(t, err, "task type is required") +} + +func TestTaskManagerListTasksClampsLimit(t *testing.T) { + ctx := context.Background() + mgr := newTaskManager(t) + for i := 0; i < 30; i++ { + _, err := mgr.CreateTask(ctx, "collect", fmt.Sprintf("task-%d", i), "trigger", datatypes.JSONMap{}) + require.NoError(t, err) + } + + list, total, err := mgr.ListTasks(ctx, -5, 10_000) + require.NoError(t, err) + require.Equal(t, int64(30), total) + require.LessOrEqual(t, len(list), 20) +} + +func TestTaskManagerListTaskLogsClampsLimit(t *testing.T) { + ctx := context.Background() + mgr := newTaskManager(t) + task, err := mgr.CreateTask(ctx, "collect", "loggy", "trigger", datatypes.JSONMap{}) + require.NoError(t, err) + require.NoError(t, mgr.AddTaskLog(ctx, task.ID, "INFO", "first")) + require.NoError(t, mgr.AddTaskLog(ctx, task.ID, "INFO", "second")) + + logs, err := mgr.ListTaskLogs(ctx, task.ID, 9999, 0) + require.NoError(t, err) + require.Len(t, logs, 2) +} + +func TestCancelTaskByExternalIDSkipsTerminalStatuses(t *testing.T) { + ctx := context.Background() + mgr := newTaskManager(t) + finished := &models.Task{TaskID: "finished", Type: "collect", Status: "SUCCESS"} + require.NoError(t, mgr.db.WithContext(ctx).Create(finished).Error) + + result, err := mgr.CancelTaskByExternalID(ctx, "finished", "ignored") + require.NoError(t, err) + require.Equal(t, "SUCCESS", result.Status) +} + +func TestCancelTaskByExternalIDUsesStartedAtDuration(t *testing.T) { + ctx := context.Background() + mgr := newTaskManager(t) + started := time.Now().Add(-3 * time.Minute) + entry := &models.Task{TaskID: "inflight", Type: "collect", Status: "RUNNING", StartedAt: &started} + require.NoError(t, mgr.db.WithContext(ctx).Create(entry).Error) + + result, err := mgr.CancelTaskByExternalID(ctx, "inflight", "maintenance") + require.NoError(t, err) + require.Equal(t, "CANCELLED", result.Status) + require.Contains(t, result.LogSummary, "maintenance") + require.Greater(t, result.DurationSeconds, int64(0)) +} diff --git a/backend/storage/reports/Report_Batch1_1764410803.pdf b/backend/storage/reports/Report_Batch1_1764410803.pdf deleted file mode 100644 index 3f9ecb90f94fe394b4bd193fc47e47afe7eaa4b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 187178 zcmbTebyyVY);_F&64I%JgfIe1$AAb@DuWClt$+wh$Iu-TQi32YgM_3~qQDSChaw@; zIfIna-Ss_#y3aZ9`F+>*UhhBWk`1!gUhBT^b+7fTIb7;x*DmPVF)Hhtk5Y&|s=Shp*S0pksimc@u&VVAuB>2>*5>AVH>c*NLVkWbNm=%N zU0aFtwOCs6Q6280u#+6tn`{b!xyfYq*2%&6X7Aof-7&1ro4d>7QxH$D9&3)@n9r{FiQWG3Uejpq zhRDv&Ejb6T*@CjA4oT}sGR=qdnqNI0n<(>qoyaqgxXx#GhF>A3 zu#XGa?M2O$*~fU^;d6B-^O}dK7xa5tg{-fpWYf1#=GunSCB3FL;tUF&6)Ej)V`uO` z9Z;ac?0tnkHAM{89SPlN9yJO09G#}Rf+16DYrB3S2YtL0HFxkc7n=M1zHygJjDMX9 zOSGy^L|X?kcE?nc#2XDw3mWEcwUZ%sQ>#l&tE?k4RC;hL@3a5Tq{0=|mttI1bFZAQ z4!6X4gb!4R);Jcu|FU7}DYbO#07-H8WxS#jk63ohmp0w^1?zXn==JO7NXSyIL9Q08 z8m*+s9QmHy53fjxY@dgtUHVlgo}zs{{Z20PO#QT(e2e82h2dxc|+h*%0#Ou7@JP=spaYJ z>`R!5j%`ero*0CuwSwbJjQ5zJg9>@(9Y;#BYLS=tVw9i6P#&)re6E!}2<_m@P1fin zgG`&*^~sKpWyy!|3PYD59m1A9B@rF)8sY}kUXQ<LOLTig`R|eFJ^cZJw0nNxymkN%}Mm& zlb7IUe+G5bw2Gr=FJ+0vwpjN2(d*c&8mIwT!|^Qn&%8R&83?I}WluvyKfIdwyK1u5 z$n99;oX0MjMU4_;T5SyyS~;%0AaNo|C-gs(P=wDR{drr<~WA_z4 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 diff --git a/backend/utils/response_test.go b/backend/utils/response_test.go index a9e7fb1..41a522f 100644 --- a/backend/utils/response_test.go +++ b/backend/utils/response_test.go @@ -73,4 +73,14 @@ func TestFailHelpers(t *testing.T) { require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) require.Equal(t, "boom", resp.Message) }) + + t.Run("server error default", func(t *testing.T) { + c, w := newTestContext() + ServerError(c, "") + + require.Equal(t, http.StatusInternalServerError, w.Code) + var resp Response + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + require.Equal(t, "服务器内部错误", resp.Message) + }) } From 706bce025a7b596ae32820963a61504f77413cea Mon Sep 17 00:00:00 2001 From: XuHaoran <3230105281@zju.edu.cn> Date: Sun, 28 Dec 2025 17:14:41 +0800 Subject: [PATCH 4/6] test: add more tests --- backend/api/handler_negative_test.go | 57 ++++++++++++++ backend/api/handler_test.go | 28 +++++++ backend/pkg/taskpb/taskpb_test.go | 100 ++++++++++++++++++++++++ backend/service/service_test.go | 22 ++++++ backend/service/task_dispatcher_test.go | 17 ++++ 5 files changed, 224 insertions(+) diff --git a/backend/api/handler_negative_test.go b/backend/api/handler_negative_test.go index 2043dd9..daef736 100644 --- a/backend/api/handler_negative_test.go +++ b/backend/api/handler_negative_test.go @@ -224,3 +224,60 @@ func TestParseUintParamHelpers(t *testing.T) { require.NoError(t, err) require.Equal(t, uint(42), id) } + +func TestTemplateHandlerUpdateTemplateNotFound(t *testing.T) { + handler := newTemplateHandlerForTest(t) + c, w := newGinContext(http.MethodPut, "/templates/999", `{"name":"ghost","task_type":"analyse","config":{"pair":"ETHUSDT"}}`) + c.Params = gin.Params{{Key: "id", Value: "999"}} + + handler.UpdateTemplate(c) + + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestTemplateHandlerRunTemplateMissingTemplate(t *testing.T) { + handler := newTemplateHandlerForTest(t) + c, w := newGinContext(http.MethodPost, "/templates/999/run", `{"trigger":"manual","overrides":{"overwrite":true}}`) + c.Params = gin.Params{{Key: "id", Value: "999"}} + + handler.RunTemplate(c) + + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestBatchHandlerUpdateBatchNotFound(t *testing.T) { + db := testutil.NewInMemoryDB(t) + handler := NewBatchHandler(service.NewBatchService(db)) + c, w := newGinContext(http.MethodPut, "/batches/77", `{"name":"missing","description":"ghost","refreshed":true}`) + c.Params = gin.Params{{Key: "id", Value: "77"}} + + handler.UpdateBatch(c) + + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestReportHandlerDeleteReportNotFound(t *testing.T) { + db := testutil.NewInMemoryDB(t) + handler := NewReportHandler(service.NewReportService(db)) + c, w := newGinContext(http.MethodDelete, "/reports/33", "") + c.Params = gin.Params{{Key: "id", Value: "33"}} + + handler.DeleteReport(c) + + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestExperimentHandlerRunTemplateServiceError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + templateSvc := service.NewTemplateService(db, taskManager, nil) + experimentSvc := service.NewExperimentService(db, templateSvc, taskManager) + handler := NewExperimentHandler(experimentSvc) + + c, w := newGinContext(http.MethodPost, "/experiments/555/runs", `{"template_id":1,"trigger":"manual"}`) + c.Params = gin.Params{{Key: "id", Value: "555"}} + + handler.RunTemplate(c) + + require.Equal(t, http.StatusInternalServerError, w.Code) +} diff --git a/backend/api/handler_test.go b/backend/api/handler_test.go index ebea10a..cb6f144 100644 --- a/backend/api/handler_test.go +++ b/backend/api/handler_test.go @@ -119,3 +119,31 @@ func TestGetPriceComparisonDataHandlerRejectsBadTimestamp(t *testing.T) { require.Equal(t, http.StatusBadRequest, w.Code) } + +func TestGetOpportunitiesHandlesServiceError(t *testing.T) { + h, svc := newTestHandler(t) + sqlDB, err := svc.DB().DB() + require.NoError(t, err) + require.NoError(t, sqlDB.Close()) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/opportunities", nil) + + h.GetOpportunities(c) + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestGetPriceComparisonHandlesServiceError(t *testing.T) { + h, svc := newTestHandler(t) + sqlDB, err := svc.DB().DB() + require.NoError(t, err) + require.NoError(t, sqlDB.Close()) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/price-comparison?startTime=1&endTime=2", nil) + + h.GetPriceComparisonData(c) + require.Equal(t, http.StatusInternalServerError, w.Code) +} diff --git a/backend/pkg/taskpb/taskpb_test.go b/backend/pkg/taskpb/taskpb_test.go index 3dde4ff..2af29b4 100644 --- a/backend/pkg/taskpb/taskpb_test.go +++ b/backend/pkg/taskpb/taskpb_test.go @@ -300,6 +300,106 @@ func TestTaskProtoRawDescCaching(t *testing.T) { require.Equal(t, first, second) } +func TestTaskProtoGetterDefaultsAndValues(t *testing.T) { + t.Run("CollectBinanceRequest", func(t *testing.T) { + req := &CollectBinanceRequest{TaskId: "cb", ImportPercentage: 80, ChunkSize: 10} + require.Equal(t, "cb", req.GetTaskId()) + require.Equal(t, int32(80), req.GetImportPercentage()) + require.Equal(t, int32(10), req.GetChunkSize()) + + var nilReq *CollectBinanceRequest + require.Equal(t, "", nilReq.GetTaskId()) + require.Equal(t, int32(0), nilReq.GetImportPercentage()) + require.Equal(t, int32(0), nilReq.GetChunkSize()) + }) + + t.Run("CollectBinanceByDateRequest", func(t *testing.T) { + req := &CollectBinanceByDateRequest{TaskId: "cbd", StartTs: 11, EndTs: 22} + require.Equal(t, "cbd", req.GetTaskId()) + require.Equal(t, int32(11), req.GetStartTs()) + require.Equal(t, int32(22), req.GetEndTs()) + + var nilReq *CollectBinanceByDateRequest + require.Equal(t, "", nilReq.GetTaskId()) + require.Equal(t, int32(0), nilReq.GetStartTs()) + require.Equal(t, int32(0), nilReq.GetEndTs()) + }) + + t.Run("CollectUniswapRequest", func(t *testing.T) { + req := &CollectUniswapRequest{TaskId: "cu", PoolAddress: "pool", StartTs: 33, EndTs: 44} + require.Equal(t, "cu", req.GetTaskId()) + require.Equal(t, "pool", req.GetPoolAddress()) + require.Equal(t, int32(33), req.GetStartTs()) + require.Equal(t, int32(44), req.GetEndTs()) + + var nilReq *CollectUniswapRequest + require.Equal(t, "", nilReq.GetTaskId()) + require.Equal(t, "", nilReq.GetPoolAddress()) + require.Equal(t, int32(0), nilReq.GetStartTs()) + require.Equal(t, int32(0), nilReq.GetEndTs()) + }) + + t.Run("ProcessPricesRequest", func(t *testing.T) { + req := &ProcessPricesRequest{ + TaskId: "pp", + StartDate: 1, + EndDate: 2, + AggregationInterval: "1m", + Overwrite: true, + DbOverrides: map[string]string{"dsn": "postgres"}, + } + require.Equal(t, "pp", req.GetTaskId()) + require.Equal(t, int32(1), req.GetStartDate()) + require.Equal(t, int32(2), req.GetEndDate()) + require.Equal(t, "1m", req.GetAggregationInterval()) + require.True(t, req.GetOverwrite()) + require.Equal(t, "postgres", req.GetDbOverrides()["dsn"]) + + var nilReq *ProcessPricesRequest + require.Equal(t, "", nilReq.GetTaskId()) + require.Equal(t, int32(0), nilReq.GetStartDate()) + require.Equal(t, int32(0), nilReq.GetEndDate()) + require.Equal(t, "", nilReq.GetAggregationInterval()) + require.False(t, nilReq.GetOverwrite()) + require.Nil(t, nilReq.GetDbOverrides()) + }) + + t.Run("AnalyseRequest", func(t *testing.T) { + req := &AnalyseRequest{TaskId: "ar", BatchId: 9, Overwrite: true, StrategyJson: "{}"} + require.Equal(t, "ar", req.GetTaskId()) + require.Equal(t, int32(9), req.GetBatchId()) + require.True(t, req.GetOverwrite()) + require.Equal(t, "{}", req.GetStrategyJson()) + + var nilReq *AnalyseRequest + require.Equal(t, "", nilReq.GetTaskId()) + require.Equal(t, int32(0), nilReq.GetBatchId()) + require.False(t, nilReq.GetOverwrite()) + require.Equal(t, "", nilReq.GetStrategyJson()) + }) + + t.Run("TaskResponse", func(t *testing.T) { + req := &TaskResponse{TaskId: "resp", Status: TaskStatus_SUCCESS} + require.Equal(t, "resp", req.GetTaskId()) + require.Equal(t, TaskStatus_SUCCESS, req.GetStatus()) + + var nilReq *TaskResponse + require.Equal(t, "", nilReq.GetTaskId()) + require.Equal(t, TaskStatus_WAIT, nilReq.GetStatus()) + }) +} + +func TestTaskStatusDescriptors(t *testing.T) { + data, idx := TaskStatus_RUNNING.EnumDescriptor() + require.NotEmpty(t, data) + require.Equal(t, []int{0}, idx) + + require.NotNil(t, File_task_proto) + require.Contains(t, File_task_proto.Path(), "task.proto") + require.GreaterOrEqual(t, File_task_proto.Messages().Len(), 6) + require.Equal(t, protoreflect.FullName("task.v1.TaskStatus"), File_task_proto.Enums().Get(0).FullName()) +} + func resetProtoMessage(m proto.Message) { switch v := m.(type) { case *CollectBinanceRequest: diff --git a/backend/service/service_test.go b/backend/service/service_test.go index 2f0ffa8..7f4c073 100644 --- a/backend/service/service_test.go +++ b/backend/service/service_test.go @@ -68,3 +68,25 @@ func TestGetPriceComparisonDataFiltersWindowAndGroupsBySource(t *testing.T) { assert.Equal(t, rows[0].TimeBucket.UnixMilli(), data["binance"][0][0]) assert.Equal(t, rows[1].TimeBucket.UnixMilli(), data["uniswap"][0][0]) } + +func TestGetOpportunitiesPropagatesDBError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + svc := NewService(db) + sqlDB, err := svc.DB().DB() + require.NoError(t, err) + require.NoError(t, sqlDB.Close()) + + _, _, err = svc.GetOpportunities(1, 1, "profit_usdt", "asc") + require.Error(t, err) +} + +func TestGetPriceComparisonDataPropagatesDBError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + svc := NewService(db) + sqlDB, err := svc.DB().DB() + require.NoError(t, err) + require.NoError(t, sqlDB.Close()) + + _, err = svc.GetPriceComparisonData(0, 10) + require.Error(t, err) +} diff --git a/backend/service/task_dispatcher_test.go b/backend/service/task_dispatcher_test.go index 2f9e168..2bbb95e 100644 --- a/backend/service/task_dispatcher_test.go +++ b/backend/service/task_dispatcher_test.go @@ -102,10 +102,27 @@ func TestDispatcherDispatchesByType(t *testing.T) { require.EqualError(t, dispatcher.Dispatch(ctx, unsupported), "unsupported task type unknown") } +func TestDispatcherProcessPricesWithStringOverrides(t *testing.T) { + client := &stubTaskClient{} + dispatcher := &GRPCTaskDispatcher{client: client, timeout: time.Second} + task := &models.Task{TaskID: "pp-map", Type: "process_prices", ConfigJSON: datatypes.JSONMap{ + "start_date": int64(10), + "end_date": int64(20), + "aggregation_interval": "5m", + "db_overrides": map[string]string{"dsn": "memory"}, + }} + require.NoError(t, dispatcher.Dispatch(context.Background(), task)) + require.Equal(t, "memory", client.pricesReq.GetDbOverrides()["dsn"]) +} + func TestHelperConversions(t *testing.T) { require.Equal(t, int64(42), asInt64("42")) require.Equal(t, int64(0), asInt64("bad")) require.True(t, asBool("true")) require.False(t, asBool(0)) + require.True(t, asBool(int64(2))) + require.True(t, asBool(float32(0.5))) require.Equal(t, int64(1700000000), parseTimeOrInt("2023-11-14T22:13:20")) + require.Equal(t, int64(1700003600), parseTimeOrInt("2023-11-14T23:13:20")) + require.Equal(t, int64(99), parseTimeOrInt(int64(99))) } From c77067266369d349896bd1ac04f735701c5e1c0f Mon Sep 17 00:00:00 2001 From: XuHaoran <3230105281@zju.edu.cn> Date: Sun, 28 Dec 2025 17:47:20 +0800 Subject: [PATCH 5/6] test: more tests --- backend/api/extended_handlers_test.go | 19 ++- backend/api/handler_negative_test.go | 173 ++++++++++++++++++++++++++ backend/api/register_test.go | 95 ++++++++++++++ 3 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 backend/api/register_test.go diff --git a/backend/api/extended_handlers_test.go b/backend/api/extended_handlers_test.go index 05790ad..ba97ddc 100644 --- a/backend/api/extended_handlers_test.go +++ b/backend/api/extended_handlers_test.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" "testing" + "time" "backend/models" "backend/service" @@ -217,12 +218,28 @@ func TestReportHandlerEndpoints(t *testing.T) { db := testutil.NewInMemoryDB(t) reportSvc := service.NewReportService(db) handler := NewReportHandler(reportSvc) + ctx := context.Background() invalidPayload := strings.NewReader(`{"batch_id":0}`) _, resp := invokeJSONHandler(t, handler.CreateReport, http.MethodPost, "/reports", invalidPayload, nil) require.Equal(t, http.StatusBadRequest, resp.Code) - ctx := context.Background() + validPayload := strings.NewReader(`{"batch_id":9,"format":"pdf"}`) + _, createResp := invokeJSONHandler(t, handler.CreateReport, http.MethodPost, "/reports", validPayload, nil) + require.Equal(t, http.StatusOK, createResp.Code) + + var createdReport models.Report + decodeData(t, createResp.Data, &createdReport) + require.NotZero(t, createdReport.ID) + + require.Eventually(t, func() bool { + var refreshed models.Report + if err := db.WithContext(ctx).First(&refreshed, createdReport.ID).Error; err != nil { + return false + } + return refreshed.Status == "SUCCESS" + }, 5*time.Second, 100*time.Millisecond) + report, err := reportSvc.CreateReport(ctx, 7, 1, "pdf", "") require.NoError(t, err) diff --git a/backend/api/handler_negative_test.go b/backend/api/handler_negative_test.go index daef736..16d449d 100644 --- a/backend/api/handler_negative_test.go +++ b/backend/api/handler_negative_test.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" + "gorm.io/gorm" ) func newTemplateHandlerForTest(t *testing.T) *TemplateHandler { @@ -34,6 +35,13 @@ func newGinContext(method, path, payload string) (*gin.Context, *httptest.Respon return c, recorder } +func closeSQLDB(t *testing.T, gdb *gorm.DB) { + t.Helper() + sqlDB, err := gdb.DB() + require.NoError(t, err) + require.NoError(t, sqlDB.Close()) +} + func TestTemplateHandlerCreateTemplateValidationError(t *testing.T) { handler := newTemplateHandlerForTest(t) w := httptest.NewRecorder() @@ -281,3 +289,168 @@ func TestExperimentHandlerRunTemplateServiceError(t *testing.T) { require.Equal(t, http.StatusInternalServerError, w.Code) } + +func TestBatchHandlerListBatchesServiceError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + handler := NewBatchHandler(service.NewBatchService(db)) + closeSQLDB(t, db) + + c, w := newGinContext(http.MethodGet, "/batches", "") + handler.ListBatches(c) + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestBatchHandlerCreateBatchServiceError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + handler := NewBatchHandler(service.NewBatchService(db)) + closeSQLDB(t, db) + + c, w := newGinContext(http.MethodPost, "/batches", `{"name":"batch","description":"desc"}`) + handler.CreateBatch(c) + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestBatchHandlerUpdateBatchServiceError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + handler := NewBatchHandler(service.NewBatchService(db)) + closeSQLDB(t, db) + + c, w := newGinContext(http.MethodPut, "/batches/1", `{"name":"batch","description":"desc"}`) + c.Params = gin.Params{{Key: "id", Value: "1"}} + handler.UpdateBatch(c) + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestTemplateHandlerListTemplatesServiceError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + handler := NewTemplateHandler(service.NewTemplateService(db, taskManager, nil), taskManager) + closeSQLDB(t, db) + + c, w := newGinContext(http.MethodGet, "/templates", "") + handler.ListTemplates(c) + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestTemplateHandlerCreateTemplateServiceError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + handler := NewTemplateHandler(service.NewTemplateService(db, taskManager, nil), taskManager) + closeSQLDB(t, db) + + c, w := newGinContext(http.MethodPost, "/templates", `{"name":"n","task_type":"analyse"}`) + handler.CreateTemplate(c) + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestTemplateHandlerDeleteTemplateServiceError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + handler := NewTemplateHandler(service.NewTemplateService(db, taskManager, nil), taskManager) + closeSQLDB(t, db) + + c, w := newGinContext(http.MethodDelete, "/templates/1", "") + c.Params = gin.Params{{Key: "id", Value: "1"}} + handler.DeleteTemplate(c) + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestExperimentHandlerServiceErrors(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + templateSvc := service.NewTemplateService(db, taskManager, nil) + experimentSvc := service.NewExperimentService(db, templateSvc, taskManager) + handler := NewExperimentHandler(experimentSvc) + + _, err := experimentSvc.CreateExperiment(context.Background(), 1, "desc") + require.NoError(t, err) + + closeSQLDB(t, db) + + t.Run("ListExperiments", func(t *testing.T) { + c, w := newGinContext(http.MethodGet, "/experiments", "") + handler.ListExperiments(c) + require.Equal(t, http.StatusInternalServerError, w.Code) + }) + + t.Run("CreateExperiment", func(t *testing.T) { + c, w := newGinContext(http.MethodPost, "/experiments", `{"batch_id":1,"description":"d"}`) + handler.CreateExperiment(c) + require.Equal(t, http.StatusInternalServerError, w.Code) + }) + + t.Run("GetExperiment", func(t *testing.T) { + c, w := newGinContext(http.MethodGet, "/experiments/1", "") + c.Params = gin.Params{{Key: "id", Value: "1"}} + handler.GetExperiment(c) + require.Equal(t, http.StatusNotFound, w.Code) + }) + + t.Run("ListRuns", func(t *testing.T) { + c, w := newGinContext(http.MethodGet, "/experiments/1/runs", "") + c.Params = gin.Params{{Key: "id", Value: "1"}} + handler.ListRuns(c) + require.Equal(t, http.StatusInternalServerError, w.Code) + }) + + t.Run("RunTemplate", func(t *testing.T) { + c, w := newGinContext(http.MethodPost, "/experiments/1/runs", `{"template_id":1}`) + c.Params = gin.Params{{Key: "id", Value: "1"}} + handler.RunTemplate(c) + require.Equal(t, http.StatusInternalServerError, w.Code) + }) +} + +func TestTaskHandlerListTasksServiceError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + h := NewTaskHandler(service.NewTaskManager(db)) + closeSQLDB(t, db) + + c, w := newGinContext(http.MethodGet, "/tasks", "") + h.ListTasks(c) + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestTaskHandlerCancelTaskServiceError(t *testing.T) { + db := testutil.NewInMemoryDB(t) + manager := service.NewTaskManager(db) + h := NewTaskHandler(manager) + _, err := manager.CreateTask(context.Background(), "collect_binance", "task-cancel", "manual", service.EncodeConfig(nil)) + require.NoError(t, err) + closeSQLDB(t, db) + + c, w := newGinContext(http.MethodPost, "/tasks/task-cancel/cancel", `{"reason":"stop"}`) + c.Params = gin.Params{{Key: "task_id", Value: "task-cancel"}} + h.CancelTask(c) + require.Equal(t, http.StatusInternalServerError, w.Code) +} + +func TestReportHandlerServiceErrors(t *testing.T) { + db := testutil.NewInMemoryDB(t) + reportSvc := service.NewReportService(db) + handler := NewReportHandler(reportSvc) + + report, err := reportSvc.CreateReport(context.Background(), 1, 0, "pdf", "") + require.NoError(t, err) + + closeSQLDB(t, db) + + t.Run("ListReports", func(t *testing.T) { + c, w := newGinContext(http.MethodGet, "/reports", "") + handler.ListReports(c) + require.Equal(t, http.StatusInternalServerError, w.Code) + }) + + t.Run("CreateReport", func(t *testing.T) { + c, w := newGinContext(http.MethodPost, "/reports", `{"batch_id":1,"format":"pdf"}`) + handler.CreateReport(c) + require.Equal(t, http.StatusInternalServerError, w.Code) + }) + + t.Run("DeleteReport", func(t *testing.T) { + c, w := newGinContext(http.MethodDelete, fmt.Sprintf("/reports/%d", report.ID), "") + c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", report.ID)}} + handler.DeleteReport(c) + require.Equal(t, http.StatusInternalServerError, w.Code) + }) +} diff --git a/backend/api/register_test.go b/backend/api/register_test.go new file mode 100644 index 0000000..064950e --- /dev/null +++ b/backend/api/register_test.go @@ -0,0 +1,95 @@ +package api + +import ( + "net/http" + "testing" + + "github.com/gin-gonic/gin" +) + +func routeKey(method, path string) string { + return method + " " + path +} + +func assertRouteRegistered(t *testing.T, engine *gin.Engine, method, path string) { + t.Helper() + key := routeKey(method, path) + for _, r := range engine.Routes() { + if routeKey(r.Method, r.Path) == key { + return + } + } + t.Fatalf("route %s not found", key) +} + +func TestTemplateHandlerRegisterRoutes(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + group := router.Group("/api/v1") + handler := &TemplateHandler{} + + handler.Register(group) + + assertRouteRegistered(t, router, http.MethodGet, "/api/v1/templates") + assertRouteRegistered(t, router, http.MethodPost, "/api/v1/templates") + assertRouteRegistered(t, router, http.MethodPut, "/api/v1/templates/:id") + assertRouteRegistered(t, router, http.MethodDelete, "/api/v1/templates/:id") + assertRouteRegistered(t, router, http.MethodPost, "/api/v1/templates/:id/run") +} + +func TestBatchHandlerRegisterRoutes(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + group := router.Group("/api/v1") + handler := &BatchHandler{} + + handler.Register(group) + + assertRouteRegistered(t, router, http.MethodGet, "/api/v1/batches") + assertRouteRegistered(t, router, http.MethodPost, "/api/v1/batches") + assertRouteRegistered(t, router, http.MethodGet, "/api/v1/batches/:id") + assertRouteRegistered(t, router, http.MethodPut, "/api/v1/batches/:id") +} + +func TestTaskHandlerRegisterRoutes(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + group := router.Group("/api/v1") + handler := &TaskHandler{} + + handler.Register(group) + + assertRouteRegistered(t, router, http.MethodGet, "/api/v1/tasks") + assertRouteRegistered(t, router, http.MethodGet, "/api/v1/tasks/:task_id") + assertRouteRegistered(t, router, http.MethodGet, "/api/v1/tasks/:task_id/logs") + assertRouteRegistered(t, router, http.MethodPost, "/api/v1/tasks/:task_id/cancel") +} + +func TestReportHandlerRegisterRoutes(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + group := router.Group("/api/v1") + handler := &ReportHandler{} + + handler.Register(group) + + assertRouteRegistered(t, router, http.MethodGet, "/api/v1/reports") + assertRouteRegistered(t, router, http.MethodPost, "/api/v1/reports") + assertRouteRegistered(t, router, http.MethodGet, "/api/v1/reports/:id/download") + assertRouteRegistered(t, router, http.MethodDelete, "/api/v1/reports/:id") +} + +func TestExperimentHandlerRegisterRoutes(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + group := router.Group("/api/v1") + handler := &ExperimentHandler{} + + handler.Register(group) + + assertRouteRegistered(t, router, http.MethodGet, "/api/v1/experiments") + assertRouteRegistered(t, router, http.MethodPost, "/api/v1/experiments") + assertRouteRegistered(t, router, http.MethodGet, "/api/v1/experiments/:id") + assertRouteRegistered(t, router, http.MethodGet, "/api/v1/experiments/:id/runs") + assertRouteRegistered(t, router, http.MethodPost, "/api/v1/experiments/:id/runs") +} From cb09fa249b64befb90461de99e8c64cbd3d1ee99 Mon Sep 17 00:00:00 2001 From: XuHaoran <3230105281@zju.edu.cn> Date: Sun, 28 Dec 2025 18:32:51 +0800 Subject: [PATCH 6/6] test: refactor testutil to support more tests --- backend/api/extended_handlers_test.go | 40 +++++ backend/pkg/taskpb/taskpb_test.go | 239 +++++++++++++++++++++++++- backend/testutil/testdb.go | 58 +++++-- backend/testutil/testdb_test.go | 103 +++++++++++ 4 files changed, 425 insertions(+), 15 deletions(-) diff --git a/backend/api/extended_handlers_test.go b/backend/api/extended_handlers_test.go index ba97ddc..237967a 100644 --- a/backend/api/extended_handlers_test.go +++ b/backend/api/extended_handlers_test.go @@ -96,6 +96,27 @@ func TestTemplateHandlerCRUDAndRun(t *testing.T) { require.Len(t, templates, 0) } +func TestTemplateHandlerRunTemplateAllowsEmptyBody(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + templateSvc := service.NewTemplateService(db, taskManager, nil) + handler := NewTemplateHandler(templateSvc, taskManager) + + ctx := context.Background() + template, err := templateSvc.CreateTemplate(ctx, "Headless", "analyse", datatypes.JSONMap{"strategy": "baseline"}) + require.NoError(t, err) + + params := gin.Params{{Key: "id", Value: fmt.Sprintf("%d", template.ID)}} + emptyPayload := strings.NewReader("") + _, resp := invokeJSONHandler(t, handler.RunTemplate, http.MethodPost, fmt.Sprintf("/templates/%d/run", template.ID), emptyPayload, params) + + var detail TaskDetailResponse + decodeData(t, resp.Data, &detail) + require.Equal(t, http.StatusOK, resp.Code) + require.Equal(t, "analyse", detail.Type) + require.NotEmpty(t, detail.TaskID) +} + func TestBatchHandlerLifecycle(t *testing.T) { db := testutil.NewInMemoryDB(t) batchSvc := service.NewBatchService(db) @@ -161,6 +182,25 @@ func TestTaskHandlerEndpoints(t *testing.T) { require.Equal(t, "CANCELLED", detail.Status) } +func TestTaskHandlerCancelTaskAllowsEmptyBody(t *testing.T) { + db := testutil.NewInMemoryDB(t) + taskManager := service.NewTaskManager(db) + handler := NewTaskHandler(taskManager) + + ctx := context.Background() + task, err := taskManager.CreateTask(ctx, "collect_binance", "cancel-empty", "unit-test", service.EncodeConfig(nil)) + require.NoError(t, err) + + params := gin.Params{{Key: "task_id", Value: task.TaskID}} + emptyPayload := strings.NewReader("") + _, resp := invokeJSONHandler(t, handler.CancelTask, http.MethodPost, fmt.Sprintf("/tasks/%s/cancel", task.TaskID), emptyPayload, params) + + var detail TaskDetailResponse + decodeData(t, resp.Data, &detail) + require.Equal(t, http.StatusOK, resp.Code) + require.Equal(t, "CANCELLED", detail.Status) +} + func TestExperimentHandlerLifecycle(t *testing.T) { db := testutil.NewInMemoryDB(t) taskManager := service.NewTaskManager(db) diff --git a/backend/pkg/taskpb/taskpb_test.go b/backend/pkg/taskpb/taskpb_test.go index 2af29b4..f51f9f5 100644 --- a/backend/pkg/taskpb/taskpb_test.go +++ b/backend/pkg/taskpb/taskpb_test.go @@ -15,10 +15,16 @@ import ( type fakeConn struct { lastMethod string + lastOpts []grpc.CallOption + errors map[string]error } func (f *fakeConn) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error { f.lastMethod = method + f.lastOpts = append([]grpc.CallOption(nil), opts...) + if err, ok := f.errors[method]; ok { + return err + } return nil } @@ -51,6 +57,72 @@ func TestTaskServiceClientInvokesMethods(t *testing.T) { require.Equal(t, TaskService_CollectBinanceByDate_FullMethodName, conn.lastMethod) } +func TestTaskServiceClientPropagatesErrorsAndOptions(t *testing.T) { + boom := errors.New("invoke failure") + conn := &fakeConn{errors: map[string]error{ + TaskService_ProcessPrices_FullMethodName: boom, + }} + client := NewTaskServiceClient(conn) + opts := []grpc.CallOption{grpc.WaitForReady(true)} + resp, err := client.ProcessPrices(context.Background(), &ProcessPricesRequest{TaskId: "pp"}, opts...) + require.Nil(t, resp) + require.ErrorIs(t, err, boom) + require.Equal(t, TaskService_ProcessPrices_FullMethodName, conn.lastMethod) + require.Len(t, conn.lastOpts, len(opts)+1) +} + +func TestTaskServiceClientErrorBranches(t *testing.T) { + testCases := []struct { + name string + method string + call func(TaskServiceClient) error + }{ + { + name: "CollectBinance", + method: TaskService_CollectBinance_FullMethodName, + call: func(client TaskServiceClient) error { + _, err := client.CollectBinance(context.Background(), &CollectBinanceRequest{TaskId: "cb"}) + return err + }, + }, + { + name: "CollectBinanceByDate", + method: TaskService_CollectBinanceByDate_FullMethodName, + call: func(client TaskServiceClient) error { + _, err := client.CollectBinanceByDate(context.Background(), &CollectBinanceByDateRequest{TaskId: "cbd"}) + return err + }, + }, + { + name: "CollectUniswap", + method: TaskService_CollectUniswap_FullMethodName, + call: func(client TaskServiceClient) error { + _, err := client.CollectUniswap(context.Background(), &CollectUniswapRequest{TaskId: "cu"}) + return err + }, + }, + { + name: "Analyse", + method: TaskService_Analyse_FullMethodName, + call: func(client TaskServiceClient) error { + _, err := client.Analyse(context.Background(), &AnalyseRequest{TaskId: "an"}) + return err + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + expect := errors.New("call failed: " + tc.name) + conn := &fakeConn{errors: map[string]error{tc.method: expect}} + client := NewTaskServiceClient(conn) + err := tc.call(client) + require.ErrorIs(t, err, expect) + require.Equal(t, tc.method, conn.lastMethod) + }) + } +} + type noopServer struct { UnimplementedTaskServiceServer } @@ -201,12 +273,93 @@ func TestTaskServiceHandlersWithoutInterceptor(t *testing.T) { } } +func TestTaskServiceHandlersWithInterceptor(t *testing.T) { + srv := &spyTaskServer{} + ctx := context.Background() + testCases := []struct { + name string + handler func(interface{}, context.Context, func(interface{}) error, grpc.UnaryServerInterceptor) (interface{}, error) + prep func(interface{}) error + fullMethod string + }{ + { + name: "CollectBinanceByDate", + handler: _TaskService_CollectBinanceByDate_Handler, + prep: func(v interface{}) error { + v.(*CollectBinanceByDateRequest).TaskId = "cbd" + return nil + }, + fullMethod: TaskService_CollectBinanceByDate_FullMethodName, + }, + { + name: "CollectUniswap", + handler: _TaskService_CollectUniswap_Handler, + prep: func(v interface{}) error { + v.(*CollectUniswapRequest).TaskId = "cu" + return nil + }, + fullMethod: TaskService_CollectUniswap_FullMethodName, + }, + { + name: "ProcessPrices", + handler: _TaskService_ProcessPrices_Handler, + prep: func(v interface{}) error { + v.(*ProcessPricesRequest).TaskId = "pp" + return nil + }, + fullMethod: TaskService_ProcessPrices_FullMethodName, + }, + { + name: "Analyse", + handler: _TaskService_Analyse_Handler, + prep: func(v interface{}) error { + v.(*AnalyseRequest).TaskId = "an" + return nil + }, + fullMethod: TaskService_Analyse_FullMethodName, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + called := false + srv.lastMethod = "" + srv.lastReq = nil + resp, err := tc.handler(srv, ctx, tc.prep, func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + called = true + require.Equal(t, tc.fullMethod, info.FullMethod) + require.Equal(t, srv, info.Server) + return handler(ctx, req) + }) + require.NoError(t, err) + require.True(t, called) + require.Equal(t, tc.name, srv.lastMethod) + require.NotNil(t, resp) + }) + } +} + func TestTaskServiceHandlerDecoderError(t *testing.T) { decErr := errors.New("boom") - _, err := _TaskService_CollectBinance_Handler(&spyTaskServer{}, context.Background(), func(interface{}) error { - return decErr - }, nil) - require.ErrorIs(t, err, decErr) + handlers := []struct { + name string + handler func(interface{}, context.Context, func(interface{}) error, grpc.UnaryServerInterceptor) (interface{}, error) + }{ + {"CollectBinance", _TaskService_CollectBinance_Handler}, + {"CollectBinanceByDate", _TaskService_CollectBinanceByDate_Handler}, + {"CollectUniswap", _TaskService_CollectUniswap_Handler}, + {"ProcessPrices", _TaskService_ProcessPrices_Handler}, + {"Analyse", _TaskService_Analyse_Handler}, + } + + for _, tc := range handlers { + t.Run(tc.name, func(t *testing.T) { + _, err := tc.handler(&spyTaskServer{}, context.Background(), func(interface{}) error { + return decErr + }, nil) + require.ErrorIs(t, err, decErr) + }) + } } func TestTaskServiceServiceDescMetadata(t *testing.T) { @@ -400,6 +553,84 @@ func TestTaskStatusDescriptors(t *testing.T) { require.Equal(t, protoreflect.FullName("task.v1.TaskStatus"), File_task_proto.Enums().Get(0).FullName()) } +func TestTaskProtoResetsAndReflection(t *testing.T) { + exerciseProtoMessage(t, "CollectBinanceRequest", + func() *CollectBinanceRequest { + return &CollectBinanceRequest{TaskId: "cb", ImportPercentage: 88, ChunkSize: 7} + }, + func() *CollectBinanceRequest { return &CollectBinanceRequest{} }, + ) + + exerciseProtoMessage(t, "CollectBinanceByDateRequest", + func() *CollectBinanceByDateRequest { + return &CollectBinanceByDateRequest{TaskId: "cbd", StartTs: 11, EndTs: 22} + }, + func() *CollectBinanceByDateRequest { return &CollectBinanceByDateRequest{} }, + ) + + exerciseProtoMessage(t, "CollectUniswapRequest", + func() *CollectUniswapRequest { + return &CollectUniswapRequest{TaskId: "cu", PoolAddress: "pool", StartTs: 33, EndTs: 44} + }, + func() *CollectUniswapRequest { return &CollectUniswapRequest{} }, + ) + + exerciseProtoMessage(t, "ProcessPricesRequest", + func() *ProcessPricesRequest { + return &ProcessPricesRequest{ + TaskId: "pp", + StartDate: 1, + EndDate: 2, + AggregationInterval: "1m", + Overwrite: true, + DbOverrides: map[string]string{"dsn": "postgres"}, + } + }, + func() *ProcessPricesRequest { return &ProcessPricesRequest{} }, + ) + + exerciseProtoMessage(t, "AnalyseRequest", + func() *AnalyseRequest { + return &AnalyseRequest{TaskId: "ar", BatchId: 9, Overwrite: true, StrategyJson: "{}"} + }, + func() *AnalyseRequest { return &AnalyseRequest{} }, + ) + + exerciseProtoMessage(t, "TaskResponse", + func() *TaskResponse { + return &TaskResponse{TaskId: "resp", Status: TaskStatus_SUCCESS} + }, + func() *TaskResponse { return &TaskResponse{} }, + ) +} + +type protoTestSubject interface { + proto.Message + interface { + Reset() + String() string + Descriptor() ([]byte, []int) + ProtoMessage() + } +} + +func exerciseProtoMessage[T protoTestSubject](t *testing.T, name string, populated func() T, zero func() T) { + t.Helper() + t.Run(name, func(t *testing.T) { + msg := populated() + zeroMsg := zero() + require.NotEqual(t, zeroMsg.String(), msg.String()) + msg.Reset() + require.True(t, proto.Equal(msg, zeroMsg)) + msg.ProtoMessage() + require.NotNil(t, msg.ProtoReflect().Descriptor()) + _, idx := msg.Descriptor() + require.NotEmpty(t, idx) + var nilMsg T + require.NotNil(t, nilMsg.ProtoReflect().Descriptor()) + }) +} + func resetProtoMessage(m proto.Message) { switch v := m.(type) { case *CollectBinanceRequest: diff --git a/backend/testutil/testdb.go b/backend/testutil/testdb.go index e5d1ee9..999a20d 100644 --- a/backend/testutil/testdb.go +++ b/backend/testutil/testdb.go @@ -13,13 +13,48 @@ import ( "gorm.io/gorm" ) +type embeddedDatabase interface { + Start() error + Stop() error +} + +type embeddedWrapper struct { + inner *embeddedpostgres.EmbeddedPostgres +} + +func (w *embeddedWrapper) Start() error { + return w.inner.Start() +} + +func (w *embeddedWrapper) Stop() error { + return w.inner.Stop() +} + +var ( + listenEphemeral = func() (net.Listener, error) { + return net.Listen("tcp", "127.0.0.1:0") + } + newEmbeddedInstance = func(cfg embeddedpostgres.Config) embeddedDatabase { + return &embeddedWrapper{embeddedpostgres.NewDatabase(cfg)} + } + gormOpen = func(dialector gorm.Dialector, opts ...gorm.Option) (*gorm.DB, error) { + return gorm.Open(dialector, opts...) + } + autoMigrateFn = func(db *gorm.DB, models ...interface{}) error { + return db.AutoMigrate(models...) + } + fatalf = func(t *testing.T, format string, args ...interface{}) { + t.Fatalf(format, args...) + } +) + // NewInMemoryDB spins up an ephemeral PostgreSQL instance for isolated tests. func NewInMemoryDB(t *testing.T) *gorm.DB { t.Helper() - ln, err := net.Listen("tcp", "127.0.0.1:0") + ln, err := listenEphemeral() if err != nil { - t.Fatalf("failed to allocate port: %v", err) + fatalf(t, "failed to allocate port: %v", err) } port := ln.Addr().(*net.TCPAddr).Port _ = ln.Close() @@ -29,7 +64,7 @@ func NewInMemoryDB(t *testing.T) *gorm.DB { binariesPath := filepath.Join(rootDir, "binaries") dataPath := filepath.Join(rootDir, "data") - pg := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig(). + cfg := embeddedpostgres.DefaultConfig(). Username("postgres"). Password("postgres"). Database("testdb"). @@ -37,22 +72,22 @@ func NewInMemoryDB(t *testing.T) *gorm.DB { Locale("C"). RuntimePath(runtimePath). DataPath(dataPath). - BinariesPath(binariesPath), - ) + BinariesPath(binariesPath) + pg := newEmbeddedInstance(cfg) if err := pg.Start(); err != nil { - t.Fatalf("failed to start embedded postgres: %v", err) + fatalf(t, "failed to start embedded postgres: %v", err) } dsn := fmt.Sprintf("host=127.0.0.1 user=postgres password=postgres dbname=testdb port=%d sslmode=disable", port) - db, err := gorm.Open(postgresdriver.Open(dsn), &gorm.Config{}) + db, err := gormOpen(postgresdriver.Open(dsn), &gorm.Config{}) if err != nil { _ = pg.Stop() - t.Fatalf("failed to open postgres database: %v", err) + fatalf(t, "failed to open postgres database: %v", err) } - if err := db.AutoMigrate( + modelsToMigrate := []interface{}{ &models.ArbitrageOpportunity{}, &models.AggregatedPrice{}, &models.Task{}, @@ -62,9 +97,10 @@ func NewInMemoryDB(t *testing.T) *gorm.DB { &models.Report{}, &models.Experiment{}, &models.ExperimentRun{}, - ); err != nil { + } + if err := autoMigrateFn(db, modelsToMigrate...); err != nil { _ = pg.Stop() - t.Fatalf("failed to migrate schema: %v", err) + fatalf(t, "failed to migrate schema: %v", err) } t.Cleanup(func() { diff --git a/backend/testutil/testdb_test.go b/backend/testutil/testdb_test.go index 2eb671c..8c0af65 100644 --- a/backend/testutil/testdb_test.go +++ b/backend/testutil/testdb_test.go @@ -1,12 +1,17 @@ package testutil import ( + "errors" + "fmt" + "net" "testing" "backend/models" + embeddedpostgres "github.com/fergusstrange/embedded-postgres" "github.com/stretchr/testify/require" "gorm.io/datatypes" + "gorm.io/gorm" ) func TestNewInMemoryDBCreatesPostgresInstance(t *testing.T) { @@ -23,3 +28,101 @@ func TestNewInMemoryDBCreatesPostgresInstance(t *testing.T) { require.NoError(t, db.Create(task).Error) require.NotZero(t, task.ID) } + +func TestNewInMemoryDBFailureBranches(t *testing.T) { + expectFatal(t, "listener failure", "failed to allocate port: boom", func(st *testing.T) { + patchListener(st, errors.New("boom")) + }) + + startFake := &fakeEmbedded{startErr: errors.New("start-fail")} + expectFatal(t, "postgres start failure", "failed to start embedded postgres: start-fail", func(st *testing.T) { + patchEmbedded(st, startFake) + }) + require.Equal(t, 1, startFake.startCalls) + require.Equal(t, 0, startFake.stopCalls) + + gormFake := &fakeEmbedded{} + expectFatal(t, "gorm open failure", "failed to open postgres database: open-fail", func(st *testing.T) { + patchEmbedded(st, gormFake) + patchGormOpen(st, errors.New("open-fail")) + }) + require.Equal(t, 1, gormFake.startCalls) + require.Equal(t, 1, gormFake.stopCalls) + + expectFatal(t, "auto migrate failure", "failed to migrate schema: migrate-fail", func(st *testing.T) { + patchAutoMigrate(st, errors.New("migrate-fail")) + }) +} + +func expectFatal(t *testing.T, name, expectMsg string, setup func(st *testing.T)) { + t.Helper() + t.Run(name, func(st *testing.T) { + patchFatal(st) + setup(st) + require.PanicsWithValue(st, expectMsg, func() { + NewInMemoryDB(st) + }) + }) +} + +type fakeEmbedded struct { + startErr error + stopErr error + startCalls int + stopCalls int +} + +func (f *fakeEmbedded) Start() error { + f.startCalls++ + return f.startErr +} + +func (f *fakeEmbedded) Stop() error { + f.stopCalls++ + return f.stopErr +} + +func patchListener(t *testing.T, err error) { + t.Helper() + original := listenEphemeral + listenEphemeral = func() (net.Listener, error) { + return nil, err + } + t.Cleanup(func() { listenEphemeral = original }) +} + +func patchEmbedded(t *testing.T, fake embeddedDatabase) { + t.Helper() + original := newEmbeddedInstance + newEmbeddedInstance = func(cfg embeddedpostgres.Config) embeddedDatabase { + return fake + } + t.Cleanup(func() { newEmbeddedInstance = original }) +} + +func patchFatal(t *testing.T) { + t.Helper() + original := fatalf + fatalf = func(tb *testing.T, format string, args ...interface{}) { + panic(fmt.Sprintf(format, args...)) + } + t.Cleanup(func() { fatalf = original }) +} + +func patchGormOpen(t *testing.T, err error) { + t.Helper() + original := gormOpen + gormOpen = func(gorm.Dialector, ...gorm.Option) (*gorm.DB, error) { + return nil, err + } + t.Cleanup(func() { gormOpen = original }) +} + +func patchAutoMigrate(t *testing.T, err error) { + t.Helper() + original := autoMigrateFn + autoMigrateFn = func(*gorm.DB, ...interface{}) error { + return err + } + t.Cleanup(func() { autoMigrateFn = original }) +}