294 lines
9.1 KiB
Go
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)
|
|
}
|
|
}
|