| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- package requestmodel
- import (
- "bytes"
- "mime/multipart"
- "net/http"
- "net/http/httptest"
- "strconv"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/pkg/httputil"
- )
- func TestFromBodyJSONModel(t *testing.T) {
- if got := FromBody("application/json", []byte(`{"model":"gpt-5.4"}`)); got != "gpt-5.4" {
- t.Fatalf("got %q", got)
- }
- }
- func TestFromBodyJSONSessionModel(t *testing.T) {
- if got := FromBody("application/json", []byte(`{"session":{"model":"gpt-live"},"sdp":"v=0"}`)); got != "gpt-live" {
- t.Fatalf("got %q", got)
- }
- }
- func TestFromBodyJSONModelWinsOverSession(t *testing.T) {
- body := []byte(`{"model":"top-level","session":{"model":"nested"}}`)
- if got := FromBody("application/json", body); got != "top-level" {
- t.Fatalf("got %q", got)
- }
- }
- func TestFromBodyJSONBlankModelFallsThrough(t *testing.T) {
- if got := FromBody("application/json", []byte(`{"model":" "}`)); got != "" {
- t.Fatalf("got %q", got)
- }
- }
- func TestFromBodyMultipartModelField(t *testing.T) {
- var body bytes.Buffer
- writer := multipart.NewWriter(&body)
- _ = writer.WriteField("prompt", "draw")
- _ = writer.WriteField("model", "gpt-image-1")
- _ = writer.Close()
- if got := FromBody(writer.FormDataContentType(), body.Bytes()); got != "gpt-image-1" {
- t.Fatalf("got %q", got)
- }
- }
- func TestFromBodyMultipartSessionField(t *testing.T) {
- var body bytes.Buffer
- writer := multipart.NewWriter(&body)
- _ = writer.WriteField("sdp", "v=0")
- _ = writer.WriteField("session", `{"model":"live-alias"}`)
- _ = writer.WriteField("media", "file-part") // 字段名不匹配,应被跳过
- _ = writer.Close()
- if got := FromBody(writer.FormDataContentType(), body.Bytes()); got != "live-alias" {
- t.Fatalf("got %q", got)
- }
- }
- func TestFromBodySkipsFilePartsBeforeModelField(t *testing.T) {
- var body bytes.Buffer
- writer := multipart.NewWriter(&body)
- part, err := writer.CreateFormFile("file", "input.png")
- if err != nil {
- t.Fatalf("create form file: %v", err)
- }
- _, _ = part.Write(bytes.Repeat([]byte{0}, 4096))
- _ = writer.WriteField("model", "gpt-image-1")
- _ = writer.Close()
- if got := FromBody(writer.FormDataContentType(), body.Bytes()); got != "gpt-image-1" {
- t.Fatalf("got %q", got)
- }
- }
- func TestFromBodyNoModelReturnsEmpty(t *testing.T) {
- if got := FromBody("application/json", []byte(`{"input":"hi"}`)); got != "" {
- t.Fatalf("got %q", got)
- }
- if got := FromBody("text/plain", []byte("plain")); got != "" {
- t.Fatalf("got %q", got)
- }
- }
- func TestResetRequestBodyAllowsRereadWithoutCopy(t *testing.T) {
- body := []byte(`{"model":"gpt-5.4","input":"hi"}`)
- req := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(body))
- ResetRequestBody(req, body)
- got, err := httputil.ReadRequestBodyWithPrealloc(req)
- if err != nil {
- t.Fatalf("reread: %v", err)
- }
- if &got[0] != &body[0] {
- t.Fatalf("expected zero-copy reread of the same slice")
- }
- if req.ContentLength != int64(len(body)) {
- t.Fatalf("content length mismatch: %d", req.ContentLength)
- }
- if req.Header.Get("Content-Length") != strconv.Itoa(len(body)) {
- t.Fatalf("content-length header mismatch: %q", req.Header.Get("Content-Length"))
- }
- }
- func TestResetRequestBodyNilRequestIsNoop(t *testing.T) {
- ResetRequestBody(nil, []byte("x"))
- }
- func TestFromBodyForRouteLivePrefersSessionModel(t *testing.T) {
- body := []byte(`{"model":"top-level","session":{"model":"session-model"},"sdp":"v=0"}`)
- if got := FromBodyForRoute("/v1/live", "application/json", body); got != "session-model" {
- t.Fatalf("live route should prefer session.model, got %q", got)
- }
- if got := FromBodyForRoute("/backend-api/codex/realtime/calls", "application/json", body); got != "session-model" {
- t.Fatalf("codex realtime route should prefer session.model, got %q", got)
- }
- if got := FromBodyForRoute("/live", "application/json", body); got != "session-model" {
- t.Fatalf("root live alias should prefer session.model, got %q", got)
- }
- }
- func TestFromBodyForRouteNonLivePrefersTopLevelModel(t *testing.T) {
- body := []byte(`{"model":"top-level","session":{"model":"session-model"}}`)
- if got := FromBodyForRoute("/v1/messages", "application/json", body); got != "top-level" {
- t.Fatalf("non-live route should prefer top-level model, got %q", got)
- }
- if got := FromBodyForRoute("/v1/responses", "application/json", body); got != "top-level" {
- t.Fatalf("responses route should prefer top-level model, got %q", got)
- }
- }
- func TestFromBodyForRouteFallsBackWhenPreferredFieldMissing(t *testing.T) {
- if got := FromBodyForRoute("/v1/live", "application/json", []byte(`{"model":"top-level"}`)); got != "top-level" {
- t.Fatalf("live without session.model should fall back to model, got %q", got)
- }
- if got := FromBodyForRoute("/v1/messages", "application/json", []byte(`{"session":{"model":"session-model"}}`)); got != "session-model" {
- t.Fatalf("non-live without model should fall back to session.model, got %q", got)
- }
- }
- func TestFromBodyForRouteMultipartSessionFirstForLive(t *testing.T) {
- var body bytes.Buffer
- writer := multipart.NewWriter(&body)
- _ = writer.WriteField("model", "top-level")
- _ = writer.WriteField("session", `{"model":"session-model"}`)
- _ = writer.Close()
- contentType := writer.FormDataContentType()
- if got := FromBodyForRoute("/v1/live", contentType, body.Bytes()); got != "session-model" {
- t.Fatalf("live multipart should prefer session field, got %q", got)
- }
- if got := FromBodyForRoute("/v1/messages", contentType, body.Bytes()); got != "top-level" {
- t.Fatalf("non-live multipart should prefer model field, got %q", got)
- }
- }
- func TestIsLiveRequestRoute(t *testing.T) {
- for _, path := range []string{"/v1/live", "/live", "/backend-api/codex/realtime/calls"} {
- if !IsLiveRequestRoute(path) {
- t.Fatalf("%s should be a live route", path)
- }
- }
- for _, path := range []string{"/v1/messages", "/v1/responses", "/realtime", ""} {
- if IsLiveRequestRoute(path) {
- t.Fatalf("%s should not be a live route", path)
- }
- }
- }
- // gjson 的部分扫描会在 multipart 字节流里误匹配 session JSON 内的 model;
- // 声明为 multipart 的请求体必须走 multipart 解析。
- func TestFromBodyMultipartContentTypeSkipsJSONScan(t *testing.T) {
- var body bytes.Buffer
- writer := multipart.NewWriter(&body)
- _ = writer.WriteField("model", "top-level")
- _ = writer.WriteField("session", `{"model":"session-model"}`)
- _ = writer.Close()
- if got := FromBody(writer.FormDataContentType(), body.Bytes()); got != "top-level" {
- t.Fatalf("multipart body should use the model field, got %q", got)
- }
- if got := FromBodyForRoute("/v1/live", writer.FormDataContentType(), body.Bytes()); got != "session-model" {
- t.Fatalf("live multipart should use the session field, got %q", got)
- }
- }
- func TestFromBodyCandidatesIncludesCaseVariantsAndDuplicates(t *testing.T) {
- // encoding/json 绑定对键名大小写不敏感、重复键取末值;gjson 取首个。
- // 候选集必须包含全部出现。
- got := FromBodyCandidates("", "application/json", []byte(`{"Model":"caps-model"}`))
- if len(got) != 1 || got[0] != "caps-model" {
- t.Fatalf("case-variant key must be a candidate, got %#v", got)
- }
- got = FromBodyCandidates("", "application/json", []byte(`{"model":"first","model":"last"}`))
- if len(got) != 2 || got[0] != "first" || got[1] != "last" {
- t.Fatalf("duplicate keys must both be candidates, got %#v", got)
- }
- }
- func TestFromBodyCandidatesSessionVariants(t *testing.T) {
- body := []byte(`{"Session":{"Model":"s-caps"},"session":{"model":"s-lower"},"model":"top"}`)
- // 非 Live 入口:顶层候选。
- got := FromBodyCandidates("", "application/json", body)
- if len(got) != 1 || got[0] != "top" {
- t.Fatalf("non-live should use top-level candidates, got %#v", got)
- }
- // Live 入口:session 候选(含大小写变体与重复)。
- got = FromBodyCandidates("/v1/live", "application/json", body)
- if len(got) != 2 || got[0] != "s-caps" || got[1] != "s-lower" {
- t.Fatalf("live should collect all session candidates, got %#v", got)
- }
- }
- func TestFromBodyCandidatesMultipartDuplicateFields(t *testing.T) {
- var body bytes.Buffer
- writer := multipart.NewWriter(&body)
- _ = writer.WriteField("model", "gpt-image-1")
- _ = writer.WriteField("model", "gpt-image-1.5")
- _ = writer.WriteField("session", `{"model":"live-a"}`)
- _ = writer.WriteField("session", `{"model":"live-b"}`)
- _ = writer.Close()
- models, sessions := multipartModelCandidates(writer.FormDataContentType(), body.Bytes())
- if len(models) != 2 || models[0] != "gpt-image-1" || models[1] != "gpt-image-1.5" {
- t.Fatalf("duplicate model fields must all be candidates, got %#v", models)
- }
- if len(sessions) != 2 {
- t.Fatalf("duplicate session fields must all be captured, got %#v", sessions)
- }
- got := FromBodyCandidates("", writer.FormDataContentType(), body.Bytes())
- if len(got) != 2 || got[0] != "gpt-image-1" || got[1] != "gpt-image-1.5" {
- t.Fatalf("non-live multipart candidates mismatch, got %#v", got)
- }
- got = FromBodyCandidates("/v1/live", writer.FormDataContentType(), body.Bytes())
- if len(got) != 2 || got[0] != "live-a" || got[1] != "live-b" {
- t.Fatalf("live multipart session candidates mismatch, got %#v", got)
- }
- }
- func TestFromBodyForRouteReturnsFirstCandidate(t *testing.T) {
- got := FromBodyForRoute("", "application/json", []byte(`{"model":"first","model":"last"}`))
- if got != "first" {
- t.Fatalf("dispatch helper keeps gjson-first semantics, got %q", got)
- }
- }
|