Files
ds2api/internal/httpapi/openai/files_route_test.go

270 lines
8.6 KiB
Go

package openai
import (
"bytes"
"context"
"encoding/json"
"errors"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"ds2api/internal/auth"
dsclient "ds2api/internal/deepseek/client"
)
type managedFilesAuthStub struct{}
func (managedFilesAuthStub) Determine(_ *http.Request) (*auth.RequestAuth, error) {
return &auth.RequestAuth{
UseConfigToken: true,
DeepSeekToken: "managed-token",
CallerID: "caller:test",
AccountID: "acct-123",
TriedAccounts: map[string]bool{},
}, nil
}
func (managedFilesAuthStub) DetermineCaller(_ *http.Request) (*auth.RequestAuth, error) {
return &auth.RequestAuth{
UseConfigToken: true,
DeepSeekToken: "managed-token",
CallerID: "caller:test",
AccountID: "acct-123",
TriedAccounts: map[string]bool{},
}, nil
}
func (managedFilesAuthStub) Release(_ *auth.RequestAuth) {}
type filesRouteDSStub struct {
lastReq dsclient.UploadFileRequest
upload *dsclient.UploadFileResult
fetched *dsclient.UploadFileResult
err error
}
func (m *filesRouteDSStub) CreateSession(_ context.Context, _ *auth.RequestAuth, _ int) (string, error) {
return "", nil
}
func (m *filesRouteDSStub) GetPow(_ context.Context, _ *auth.RequestAuth, _ int) (string, error) {
return "", nil
}
func (m *filesRouteDSStub) UploadFile(_ context.Context, _ *auth.RequestAuth, req dsclient.UploadFileRequest, _ int) (*dsclient.UploadFileResult, error) {
m.lastReq = req
if m.err != nil {
return nil, m.err
}
if m.upload != nil {
return m.upload, nil
}
return &dsclient.UploadFileResult{ID: "file-123", Filename: req.Filename, Bytes: int64(len(req.Data)), Purpose: req.Purpose, Status: "uploaded"}, nil
}
func (m *filesRouteDSStub) FetchUploadedFile(_ context.Context, _ *auth.RequestAuth, fileID string) (*dsclient.UploadFileResult, error) {
if m.err != nil {
return nil, m.err
}
if m.fetched != nil {
return m.fetched, nil
}
return &dsclient.UploadFileResult{ID: fileID, Filename: "notes.txt", Bytes: 11, Purpose: "assistants", Status: "processed"}, nil
}
func (m *filesRouteDSStub) CallCompletion(_ context.Context, _ *auth.RequestAuth, _ map[string]any, _ string, _ int) (*http.Response, error) {
return nil, errors.New("not implemented")
}
func (m *filesRouteDSStub) DeleteSessionForToken(_ context.Context, _ string, _ string) (*dsclient.DeleteSessionResult, error) {
return &dsclient.DeleteSessionResult{Success: true}, nil
}
func (m *filesRouteDSStub) DeleteAllSessionsForToken(_ context.Context, _ string) error {
return nil
}
func newMultipartUploadRequest(t *testing.T, purpose string, filename string, data []byte, model string) *http.Request {
t.Helper()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if purpose != "" {
if err := writer.WriteField("purpose", purpose); err != nil {
t.Fatalf("write purpose failed: %v", err)
}
}
if model != "" {
if err := writer.WriteField("model", model); err != nil {
t.Fatalf("write model failed: %v", err)
}
}
part, err := writer.CreateFormFile("file", filename)
if err != nil {
t.Fatalf("create form file failed: %v", err)
}
if _, err := part.Write(data); err != nil {
t.Fatalf("write file failed: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("close writer failed: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/v1/files", &body)
req.Header.Set("Authorization", "Bearer direct-token")
req.Header.Set("Content-Type", writer.FormDataContentType())
return req
}
func TestFilesRouteUploadSuccess(t *testing.T) {
ds := &filesRouteDSStub{}
h := &openAITestSurface{Store: mockOpenAIConfig{}, Auth: streamStatusAuthStub{}, DS: ds}
r := chi.NewRouter()
registerOpenAITestRoutes(r, h)
req := newMultipartUploadRequest(t, "assistants", "notes.txt", []byte("hello world"), "deepseek-v4-vision")
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}
if ds.lastReq.Filename != "notes.txt" {
t.Fatalf("expected filename notes.txt, got %q", ds.lastReq.Filename)
}
if ds.lastReq.Purpose != "assistants" {
t.Fatalf("expected purpose assistants, got %q", ds.lastReq.Purpose)
}
if ds.lastReq.ModelType != "vision" {
t.Fatalf("expected vision model type, got %q", ds.lastReq.ModelType)
}
if string(ds.lastReq.Data) != "hello world" {
t.Fatalf("unexpected uploaded data: %q", string(ds.lastReq.Data))
}
var out map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatalf("decode response failed: %v body=%s", err, rec.Body.String())
}
if out["object"] != "file" {
t.Fatalf("expected file object, got %#v", out)
}
if out["id"] != "file-123" {
t.Fatalf("expected file id file-123, got %#v", out["id"])
}
if out["filename"] != "notes.txt" {
t.Fatalf("expected filename notes.txt, got %#v", out["filename"])
}
}
func TestFilesRouteUploadIncludesAccountIDForManagedAccount(t *testing.T) {
ds := &filesRouteDSStub{}
h := &openAITestSurface{Store: mockOpenAIConfig{}, Auth: managedFilesAuthStub{}, DS: ds}
r := chi.NewRouter()
registerOpenAITestRoutes(r, h)
req := newMultipartUploadRequest(t, "assistants", "notes.txt", []byte("hello world"), "deepseek-v4-vision")
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}
var out map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatalf("decode response failed: %v body=%s", err, rec.Body.String())
}
if out["account_id"] != "acct-123" {
t.Fatalf("expected account_id acct-123, got %#v", out["account_id"])
}
}
func TestFilesRouteRetrieveSuccess(t *testing.T) {
ds := &filesRouteDSStub{fetched: &dsclient.UploadFileResult{
ID: "file-123",
Filename: "notes.txt",
Bytes: 11,
Purpose: "assistants",
Status: "processed",
}}
h := &openAITestSurface{Store: mockOpenAIConfig{}, Auth: managedFilesAuthStub{}, DS: ds}
r := chi.NewRouter()
registerOpenAITestRoutes(r, h)
req := httptest.NewRequest(http.MethodGet, "/v1/files/file-123", nil)
req.Header.Set("Authorization", "Bearer direct-token")
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
}
var out map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatalf("decode response failed: %v body=%s", err, rec.Body.String())
}
if out["id"] != "file-123" || out["filename"] != "notes.txt" || out["status"] != "processed" {
t.Fatalf("unexpected file object: %#v", out)
}
if out["account_id"] != "acct-123" {
t.Fatalf("expected account_id acct-123, got %#v", out["account_id"])
}
}
func TestFilesRouteRetrieveNotFound(t *testing.T) {
ds := &filesRouteDSStub{err: dsclient.ErrUploadFileNotFound}
h := &openAITestSurface{Store: mockOpenAIConfig{}, Auth: streamStatusAuthStub{}, DS: ds}
r := chi.NewRouter()
registerOpenAITestRoutes(r, h)
req := httptest.NewRequest(http.MethodGet, "/v1/files/missing-file", nil)
req.Header.Set("Authorization", "Bearer direct-token")
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected 404, got %d body=%s", rec.Code, rec.Body.String())
}
}
func TestFilesRouteRejectsNonMultipart(t *testing.T) {
h := &openAITestSurface{Store: mockOpenAIConfig{}, Auth: streamStatusAuthStub{}, DS: &filesRouteDSStub{}}
r := chi.NewRouter()
registerOpenAITestRoutes(r, h)
req := httptest.NewRequest(http.MethodPost, "/v1/files", bytes.NewBufferString(`{"purpose":"assistants"}`))
req.Header.Set("Authorization", "Bearer direct-token")
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String())
}
}
func TestFilesRouteRequiresFileField(t *testing.T) {
h := &openAITestSurface{Store: mockOpenAIConfig{}, Auth: streamStatusAuthStub{}, DS: &filesRouteDSStub{}}
r := chi.NewRouter()
registerOpenAITestRoutes(r, h)
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if err := writer.WriteField("purpose", "assistants"); err != nil {
t.Fatalf("write field failed: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("close writer failed: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/v1/files", &body)
req.Header.Set("Authorization", "Bearer direct-token")
req.Header.Set("Content-Type", writer.FormDataContentType())
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String())
}
}