Checks / verify (push) Failing after 9m52s
统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
124 lines
4.2 KiB
Go
124 lines
4.2 KiB
Go
package integration
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
|
|
"git.sundynix.cn/Blizzard/Veyra/internal/model"
|
|
"git.sundynix.cn/Blizzard/Veyra/internal/repository"
|
|
"git.sundynix.cn/Blizzard/Veyra/internal/service"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func testNotifications(t *testing.T, ctx context.Context, pool *pgxpool.Pool, user model.User, candidate model.CandidateRevision, account string) {
|
|
t.Helper()
|
|
repo := repository.NewNotificationRepository(pool)
|
|
svc := service.NewNotificationService(repo, true)
|
|
var admin model.User
|
|
if err := pool.QueryRow(ctx, `SELECT id,role,status FROM veyra_user WHERE role='ADMIN' LIMIT 1`).Scan(&admin.ID, &admin.Role, &admin.Status); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
process := func() {
|
|
for i := 0; i < 50; i++ {
|
|
worked, err := repo.ProcessNext(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !worked {
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("notification queue did not drain")
|
|
}
|
|
var cashBefore, fillBefore int64
|
|
if err := pool.QueryRow(ctx, `SELECT COALESCE(sum(amount_minor),0)::bigint FROM veyra_cash_entry WHERE account_id=$1`, account).Scan(&cashBefore); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM veyra_trade_fill WHERE account_id=$1`, account).Scan(&fillBefore); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
quote := service.SimulationQuoteInput{Symbol: candidate.Snapshot.Symbol, Exchange: candidate.Snapshot.Exchange, Price: "10.50", ObservedAt: time.Now().UTC()}
|
|
if _, err := svc.SaveQuote(ctx, user, quote, uuid.NewString()); !errors.Is(err, apperror.ErrForbidden) {
|
|
t.Fatal("user wrote quote")
|
|
}
|
|
q, err := svc.SaveQuote(ctx, admin, quote, uuid.NewString())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
process()
|
|
notices, err := svc.List(ctx, user)
|
|
if err != nil || len(notices) != 1 {
|
|
t.Fatalf("plan reminder missing: %d %v", len(notices), err)
|
|
}
|
|
quote.ObservedAt = time.Now().UTC()
|
|
if _, err := svc.SaveQuote(ctx, admin, quote, uuid.NewString()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
process()
|
|
notices, err = svc.List(ctx, user)
|
|
if err != nil || len(notices) != 1 {
|
|
t.Fatal("repeated price produced duplicate reminder")
|
|
}
|
|
var cashAfter, fillAfter int64
|
|
if err := pool.QueryRow(ctx, `SELECT COALESCE(sum(amount_minor),0)::bigint FROM veyra_cash_entry WHERE account_id=$1`, account).Scan(&cashAfter); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM veyra_trade_fill WHERE account_id=$1`, account).Scan(&fillAfter); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cashBefore != cashAfter || fillBefore != fillAfter {
|
|
t.Fatal("reminder altered ledger")
|
|
}
|
|
stranger := user
|
|
stranger.ID = uuid.NewString()
|
|
if err := svc.MarkRead(ctx, stranger, notices[0].ID); !errors.Is(err, apperror.ErrNotFound) {
|
|
t.Fatal("cross user notification write")
|
|
}
|
|
if err := svc.MarkRead(ctx, user, notices[0].ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tasks, err := svc.Tasks(ctx, admin)
|
|
if err != nil || len(tasks) == 0 {
|
|
t.Fatal("task audit missing")
|
|
}
|
|
for _, task := range tasks {
|
|
if task.ProcessedAt == nil {
|
|
t.Fatal("queue event did not commit processed flag")
|
|
}
|
|
}
|
|
// A second plan must not trigger on an expired or superseded quote event.
|
|
plans := service.NewTradePlanService(repository.NewTradePlanRepository(pool), true)
|
|
preview, err := plans.Preview(ctx, user, service.PlanPreviewInput{AccountID: account, CandidateRevisionID: candidate.ID, ExpectedCandidateVersion: candidate.Version, Quantity: "100", FeeBudget: "0"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := plans.Confirm(ctx, user, preview.ID, uuid.NewString()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
quote.ObservedAt = time.Now().UTC()
|
|
q, err = svc.SaveQuote(ctx, admin, quote, uuid.NewString())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `UPDATE veyra_quote_snapshot SET observed_at=now()-interval '2 minutes' WHERE id=$1`, q.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
process()
|
|
notices, err = svc.List(ctx, user)
|
|
if err != nil || len(notices) != 1 {
|
|
t.Fatal("stale quote triggered plan")
|
|
}
|
|
quote.ObservedAt = time.Now().UTC()
|
|
if _, err := svc.SaveQuote(ctx, admin, quote, uuid.NewString()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
process()
|
|
notices, err = svc.List(ctx, user)
|
|
if err != nil || len(notices) != 2 {
|
|
t.Fatal("fresh second-plan reminder failed")
|
|
}
|
|
}
|