//go:build unit package handler import ( "context" "io" "net/http" "net/http/httptest" "strings" "testing" middleware "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) func TestSeedanceHandlerLifecycleAndOwnership(t *testing.T) { h, slots, bindings, upstream := newGrokMediaSlotHandler(t, false, false, service.PlatformOpenAI) var owner int64 upstream.call = func(req *http.Request, id int64) (*http.Response, error) { body := `{"id":"task-ark","status":"queued"}` if req.Method == http.MethodPost { owner = id } else { require.Equal(t, owner, id) } return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(body))}, nil } newContext := func(method string) (*gin.Context, *httptest.ResponseRecorder) { c, w := grokMediaSlotContext(context.Background(), method == http.MethodPost) key, _ := middleware.GetAPIKeyFromContext(c) key.Group.Platform = service.PlatformOpenAI body := "" if method == http.MethodPost { body = `{"model":"doubao-seedance","content":[{"type":"text","text":"waves"}]}` } c.Request = httptest.NewRequest(method, "/api/v3/contents/generations/tasks", strings.NewReader(body)) c.Params = gin.Params{{Key: "task_id", Value: "task-ark"}} return c, w } c, w := newContext(http.MethodPost) h.SeedanceTasks(c) require.Equal(t, 200, w.Code, w.Body.String()) require.Positive(t, owner) require.Len(t, bindings.pending, 1) slots.assertReleased(t) for _, method := range []string{http.MethodGet, http.MethodDelete} { c, w = newContext(method) h.SeedanceTasks(c) require.Equal(t, 200, w.Code, w.Body.String()) slots.assertReleased(t) } for _, other := range []string{"user", "key", "group", "task", "provider"} { c, w = newContext(http.MethodGet) key, _ := middleware.GetAPIKeyFromContext(c) switch other { case "user": c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{UserID: 11, Concurrency: 5}) case "key": key.ID = 21 case "group": group := int64(25) key.GroupID = &group case "task": c.Params = gin.Params{{Key: "task_id", Value: "other"}} case "provider": c.Params = gin.Params{{Key: "request_id", Value: "task-ark"}} } before := upstream.calls if other == "provider" { h.GrokVideoStatus(c) } else { h.SeedanceTasks(c) } require.Equal(t, 404, w.Code, other+": "+w.Body.String()) require.Equal(t, before, upstream.calls) slots.assertReleased(t) } c, _ = newContext(http.MethodGet) key, _ := middleware.GetAPIKeyFromContext(c) subject, _ := middleware.GetAuthSubjectFromContext(c) result := &service.OpenAIForwardResult{Usage: service.OpenAIUsage{OutputTokens: 12345}, ResponseID: "seedance:task-ark"} for i := range 20 { billed := prepareSeedanceCompletionBilling(context.Background(), h, key, subject, result.ResponseID, result) if i == 0 { require.NotNil(t, billed) require.Equal(t, "doubao-seedance", billed.BillingModel) require.Equal(t, 12345, billed.Usage.OutputTokens) require.Zero(t, billed.VideoCount) } else { require.Nil(t, billed) } } require.Len(t, bindings.billed, 1) }