Files

294 lines
9.1 KiB
Go

package server
import (
"bytes"
"encoding/json"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config"
)
func TestUploadProjectAsset(t *testing.T) {
cfg := &config.Config{Env: "test", DataDir: t.TempDir()}
svc := bizdev.MustNewService(t, cfg)
srv := NewServer(cfg, svc)
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
var body bytes.Buffer
w := multipart.NewWriter(&body)
part, _ := w.CreateFormFile("file", "spec.txt")
_, _ = part.Write([]byte("project asset content"))
_ = w.Close()
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/projects/7/assets", &body)
req.Header.Set("Content-Type", w.FormDataContentType())
withBearerAuth(req, loginTestAdmin(t, srv))
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("upload failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status=%d", resp.StatusCode)
}
var out projectAssetResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
t.Fatalf("decode: %v", err)
}
if out.AssetID <= 0 || out.SHA256 == "" {
t.Fatalf("unexpected response: %+v", out)
}
}
func TestUploadProjectAsset_RejectsHTML(t *testing.T) {
cfg := &config.Config{Env: "test", DataDir: t.TempDir()}
srv := NewServer(cfg, bizdev.MustNewService(t, cfg))
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
var body bytes.Buffer
w := multipart.NewWriter(&body)
part, _ := w.CreateFormFile("file", "evil.txt")
_, _ = part.Write([]byte("<html><body>x</body></html>"))
_ = w.Close()
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/projects/7/assets", &body)
req.Header.Set("Content-Type", w.FormDataContentType())
withBearerAuth(req, loginTestAdmin(t, srv))
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnsupportedMediaType {
t.Fatalf("expected 415, got %d", resp.StatusCode)
}
}
func TestUploadProjectAsset_RejectsOversizedFilename(t *testing.T) {
cfg := &config.Config{Env: "test", DataDir: t.TempDir()}
srv := NewServer(cfg, bizdev.MustNewService(t, cfg))
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
longName := strings.Repeat("a", 201) + ".txt"
var body bytes.Buffer
w := multipart.NewWriter(&body)
part, _ := w.CreateFormFile("file", longName)
_, _ = part.Write([]byte("ok"))
_ = w.Close()
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/projects/7/assets", &body)
req.Header.Set("Content-Type", w.FormDataContentType())
withBearerAuth(req, loginTestAdmin(t, srv))
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnsupportedMediaType {
t.Fatalf("expected 415 for long filename, got %d", resp.StatusCode)
}
}
func TestGetProjectAssetRaw_RoundTrip(t *testing.T) {
cfg := &config.Config{Env: "test", DataDir: t.TempDir()}
svc := bizdev.MustNewService(t, cfg)
srv := NewServer(cfg, svc)
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
payload := []byte("raw asset preview payload")
var body bytes.Buffer
w := multipart.NewWriter(&body)
part, _ := w.CreateFormFile("file", "preview.txt")
_, _ = part.Write(payload)
_ = w.Close()
token := loginTestAdmin(t, srv)
upReq, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/projects/7/assets", &body)
upReq.Header.Set("Content-Type", w.FormDataContentType())
withBearerAuth(upReq, token)
upResp, err := http.DefaultClient.Do(upReq)
if err != nil {
t.Fatalf("upload: %v", err)
}
defer upResp.Body.Close()
if upResp.StatusCode != http.StatusOK {
t.Fatalf("upload status=%d", upResp.StatusCode)
}
var uploaded projectAssetResponse
if err := json.NewDecoder(upResp.Body).Decode(&uploaded); err != nil {
t.Fatalf("decode upload: %v", err)
}
rawReq, _ := http.NewRequest(
http.MethodGet,
ts.URL+"/api/v1/projects/7/assets/"+strconv.FormatInt(uploaded.AssetID, 10)+"/raw",
nil,
)
withBearerAuth(rawReq, token)
rawResp, err := http.DefaultClient.Do(rawReq)
if err != nil {
t.Fatalf("raw: %v", err)
}
defer rawResp.Body.Close()
if rawResp.StatusCode != http.StatusOK {
t.Fatalf("raw status=%d", rawResp.StatusCode)
}
got, _ := io.ReadAll(rawResp.Body)
if !bytes.Equal(got, payload) {
t.Fatalf("raw body mismatch: got %q want %q", got, payload)
}
if ct := rawResp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") {
t.Fatalf("unexpected content-type: %s", ct)
}
// Wrong project id must not leak the blob.
wrongReq, _ := http.NewRequest(
http.MethodGet,
ts.URL+"/api/v1/projects/999/assets/"+strconv.FormatInt(uploaded.AssetID, 10)+"/raw",
nil,
)
withBearerAuth(wrongReq, token)
wrongResp, err := http.DefaultClient.Do(wrongReq)
if err != nil {
t.Fatalf("wrong project raw: %v", err)
}
defer wrongResp.Body.Close()
if wrongResp.StatusCode == http.StatusOK {
t.Fatalf("expected non-200 for mismatched project, got %d", wrongResp.StatusCode)
}
}
func uploadAssetBytes(t *testing.T, url, token string, filename string, payload []byte) projectAssetResponse {
t.Helper()
var body bytes.Buffer
w := multipart.NewWriter(&body)
declared := "application/octet-stream"
if strings.HasSuffix(strings.ToLower(filename), ".pdf") {
declared = "application/pdf"
}
part, _ := w.CreatePart(map[string][]string{
"Content-Disposition": {`form-data; name="file"; filename="` + filename + `"`},
"Content-Type": {declared},
})
_, _ = part.Write(payload)
_ = w.Close()
req, _ := http.NewRequest(http.MethodPost, url, &body)
req.Header.Set("Content-Type", w.FormDataContentType())
withBearerAuth(req, token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("upload %s: %v", filename, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
raw, _ := io.ReadAll(resp.Body)
t.Fatalf("upload %s status=%d body=%s", filename, resp.StatusCode, raw)
}
var out projectAssetResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
t.Fatalf("decode upload: %v", err)
}
return out
}
// 相同内容第二次上传:响应标注 deduplicated,列表接口同样标注非首条引用。
func TestUploadProjectAsset_DedupHint(t *testing.T) {
cfg := &config.Config{Env: "test", DataDir: t.TempDir()}
svc := bizdev.MustNewService(t, cfg)
srv := NewServer(cfg, svc)
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
token := loginTestAdmin(t, srv)
payload := []byte("%PDF-1.4 dedup hint shared content")
first := uploadAssetBytes(t, ts.URL+"/api/v1/projects/7/assets", token, "a.pdf", payload)
if first.Deduplicated {
t.Fatalf("first upload must not be deduplicated")
}
second := uploadAssetBytes(t, ts.URL+"/api/v1/projects/7/assets", token, "b.pdf", payload)
if !second.Deduplicated {
t.Fatalf("second identical upload must carry deduplicated=true, got %+v", second)
}
if second.SHA256 != first.SHA256 {
t.Fatalf("dedup must share the same blob sha")
}
listReq, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/v1/projects/7/assets", nil)
withBearerAuth(listReq, token)
listResp, err := http.DefaultClient.Do(listReq)
if err != nil {
t.Fatal(err)
}
defer listResp.Body.Close()
var assets []projectAssetResponse
if err := json.NewDecoder(listResp.Body).Decode(&assets); err != nil {
t.Fatal(err)
}
var flagged int
for _, a := range assets {
if a.Deduplicated {
flagged++
}
if a.AssetID == first.AssetID && a.Deduplicated {
t.Fatalf("first reference must stay unflagged")
}
}
if flagged != 1 {
t.Fatalf("expected exactly one dedup-flagged row in list, got %+v", assets)
}
}
// 重新解析:内存模式(无 PG 状态存储)下必须 503,且不触发任何解析。
func TestReparseProjectAsset_NoStore503(t *testing.T) {
cfg := &config.Config{Env: "test", DataDir: t.TempDir()}
svc := bizdev.MustNewService(t, cfg)
srv := NewServer(cfg, svc)
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
token := loginTestAdmin(t, srv)
up := uploadAssetBytes(t, ts.URL+"/api/v1/projects/7/assets", token, "doc.pdf", []byte("%PDF-1.4 fake"))
req, _ := http.NewRequest(http.MethodPost,
ts.URL+"/api/v1/projects/7/assets/"+strconv.FormatInt(up.AssetID, 10)+"/reparse", nil)
withBearerAuth(req, token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("reparse without store want 503 got %d", resp.StatusCode)
}
}
// 不存在的资产 reparse 必须 404。
func TestReparseProjectAsset_UnknownAsset404(t *testing.T) {
cfg := &config.Config{Env: "test", DataDir: t.TempDir()}
srv := NewServer(cfg, bizdev.MustNewService(t, cfg))
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
token := loginTestAdmin(t, srv)
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/projects/7/assets/999999/reparse", nil)
withBearerAuth(req, token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
t.Fatalf("reparse unknown asset want 404 got %d", resp.StatusCode)
}
}