chore(服务端): 初始化工程仓库
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"perler-beads-server/internal/domain"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRequestCancellationFallback(t *testing.T) {
|
||||
r := (&Server{}).Router()
|
||||
r.GET("/cancel-probe", func(c *gin.Context) {
|
||||
if c.Err() != context.Canceled {
|
||||
t.Error("Gin must propagate request cancellation")
|
||||
}
|
||||
if _, yes := c.Deadline(); !yes {
|
||||
t.Error("request operation deadline missing")
|
||||
}
|
||||
})
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
r.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/cancel-probe", nil).WithContext(ctx))
|
||||
}
|
||||
func TestPrintGeometryBounds(t *testing.T) {
|
||||
for _, pitch := range []float64{2, 5, 10} {
|
||||
code := "R"
|
||||
p := domain.Pattern{Title: strings.Repeat("标题", 45), Width: 1, Height: 1, Cells: []*string{&code}, Colors: []domain.Color{{Code: code, Hex: "#ff0000"}}}
|
||||
var b bytes.Buffer
|
||||
renderSVG(&b, p, pitch)
|
||||
d := xml.NewDecoder(&b)
|
||||
width, height := 0.0, 0.0
|
||||
gridFound := false
|
||||
for {
|
||||
token, e := d.Token()
|
||||
if e == io.EOF {
|
||||
break
|
||||
}
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
el, yes := token.(xml.StartElement)
|
||||
if !yes {
|
||||
continue
|
||||
}
|
||||
attrs := map[string]string{}
|
||||
for _, a := range el.Attr {
|
||||
attrs[a.Name.Local] = a.Value
|
||||
}
|
||||
if el.Name.Local == "svg" {
|
||||
width, _ = strconv.ParseFloat(strings.TrimSuffix(attrs["width"], "mm"), 64)
|
||||
height, _ = strconv.ParseFloat(strings.TrimSuffix(attrs["height"], "mm"), 64)
|
||||
if width < 75 {
|
||||
t.Fatal("ruler exceeds page")
|
||||
}
|
||||
}
|
||||
if el.Name.Local == "text" {
|
||||
x, _ := strconv.ParseFloat(attrs["x"], 64)
|
||||
y, _ := strconv.ParseFloat(attrs["y"], 64)
|
||||
if x >= width || y >= height {
|
||||
t.Fatalf("text outside page: %+v", attrs)
|
||||
}
|
||||
}
|
||||
if el.Name.Local == "rect" && attrs["stroke"] == "#666" {
|
||||
w, _ := strconv.ParseFloat(attrs["width"], 64)
|
||||
h, _ := strconv.ParseFloat(attrs["height"], 64)
|
||||
if w != pitch || h != pitch {
|
||||
t.Fatal("actual bead pitch changed")
|
||||
}
|
||||
gridFound = true
|
||||
}
|
||||
}
|
||||
if !gridFound {
|
||||
t.Fatal("no grid")
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestSingleConnectionHandlers(t *testing.T) {
|
||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
||||
if dsn == "" {
|
||||
t.Skip("TEST_DATABASE_URL absent; PostgreSQL handler regression not run")
|
||||
}
|
||||
cfg, e := pgxpool.ParseConfig(dsn)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if !strings.Contains(cfg.ConnConfig.Database, "test") {
|
||||
t.Fatal("requires independent test database")
|
||||
}
|
||||
cfg.MaxConns = 1
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
||||
defer cancel()
|
||||
db, e := pgxpool.NewWithConfig(ctx, cfg)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
defer db.Close()
|
||||
s := &Server{DB: db}
|
||||
if e = Migrate(ctx, db); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
id := uuid.NewString()
|
||||
store := "regression-" + id
|
||||
u := domain.User{ID: id, Role: "guest", StoreID: store}
|
||||
p := demoPatterns(store, demoColors())[0]
|
||||
p.ID = "pattern-" + id
|
||||
p.OwnerID = id
|
||||
p.Visibility = "private"
|
||||
if e = s.put(ctx, "pattern", p.ID, store, id, p); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if e = s.put(ctx, "tables", store, store, "", []domain.Table{{Code: "T1", Label: "Test", Enabled: true}}); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
v, _ := json.Marshal(domain.Visit{ID: id, StoreID: store, TableCode: "T1", Status: "active"})
|
||||
if _, e = db.Exec(ctx, `INSERT INTO visits VALUES($1,$2)`, id, v); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if _, e = db.Exec(ctx, `INSERT INTO favorites VALUES($1,$2)`, id, p.ID); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
defer func() {
|
||||
cleanup, stop := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer stop()
|
||||
db.Exec(cleanup, `DELETE FROM favorites WHERE user_id=$1`, id)
|
||||
db.Exec(cleanup, `DELETE FROM visits WHERE user_id=$1`, id)
|
||||
db.Exec(cleanup, `DELETE FROM request_keys WHERE user_id=$1`, id)
|
||||
db.Exec(cleanup, `DELETE FROM documents WHERE store_id=$1`, store)
|
||||
}()
|
||||
r := gin.New()
|
||||
r.ContextWithFallback = true
|
||||
r.Use(func(c *gin.Context) { c.Set("user", u); c.Next() })
|
||||
r.GET("/favorites", s.favoriteList)
|
||||
r.POST("/requests", s.requestCreate)
|
||||
body := `{"kind":"print","patternId":"` + p.ID + `","requestId":"` + uuid.NewString() + `"}`
|
||||
for _, call := range []struct{ method, path, body string }{{"GET", "/favorites", ""}, {"POST", "/requests", body}, {"POST", "/requests", body}} {
|
||||
w := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(call.method, call.path, strings.NewReader(call.body)).WithContext(ctx)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, request)
|
||||
var result struct{ Code int }
|
||||
if json.Unmarshal(w.Body.Bytes(), &result) != nil || result.Code != 200 {
|
||||
t.Fatalf("%s %s", call.path, w.Body)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
t.Fatal("single pool connection handler exceeded deadline")
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestWebUIStaticAndSPAFallback(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if e := os.WriteFile(dir+"/index.html", []byte("<html>SPA</html>"), 0600); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if e := os.WriteFile(dir+"/app.js", []byte("console.log('static')"), 0600); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
outside := t.TempDir()
|
||||
os.WriteFile(outside+"/secret.txt", []byte("SECRET"), 0600)
|
||||
if e := os.Symlink(outside+"/secret.txt", dir+"/leak.txt"); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
r := (&Server{WebUIDir: dir}).Router()
|
||||
for _, item := range []struct{ path, want string }{{"/patterns/editor", "SPA"}, {"/app.js", "static"}, {"/api/unknown", `"code":404`}, {"/api", `"code":404`}, {"/leak.txt", `"code":404`}, {"/../secret.txt", `"code":404`}} {
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest("GET", item.path, nil))
|
||||
if !strings.Contains(w.Body.String(), item.want) || strings.Contains(w.Body.String(), "SECRET") {
|
||||
t.Fatalf("%s: %s", item.path, w.Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestRecoveryUsesEnvelope(t *testing.T) {
|
||||
r := (&Server{}).Router()
|
||||
r.GET("/panic-probe", func(c *gin.Context) { panic("test panic") })
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest("GET", "/panic-probe", nil))
|
||||
if w.Code != 200 || !strings.Contains(w.Body.String(), `"code":500`) {
|
||||
t.Fatalf("%d %s", w.Code, w.Body)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user