Files
YMhut-box-C-/server/unified-management/internal/feedback/feedback_test.go
T
QWQLwToo 079ee4eaeb
build-winui / winui (push) Has been cancelled
Add server components
2026-06-26 13:28:09 +08:00

159 lines
4.7 KiB
Go

package feedback
import (
"archive/zip"
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/hex"
"io"
"mime/multipart"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"ymhut-box/server/unified-management/internal/config"
"ymhut-box/server/unified-management/internal/db"
)
func TestSignedMultipartSubmission(t *testing.T) {
root := t.TempDir()
cfg := testConfig(root)
store, err := db.Open(cfg)
if err != nil {
t.Fatal(err)
}
defer store.Close()
service := NewService(cfg, store)
plain := zipBytes(t, map[string]string{
"feedback.json": `{"request":{"title":"Crash on launch","type":"issue","severity":"major","contact":"user@example.com","body":"It crashes."}}`,
"summary.txt": "launch failure",
})
encrypted := encryptPackageForTest(t, plain, cfg.PackageEncryptionKey)
encryptedHash := sha256HexTest(encrypted)
plainHash := sha256HexTest(plain)
timestamp := itoa(int(time.Now().Unix()))
payload := `{"feedbackCode":"FB-20260625-ABCDEF","title":"Crash on launch","type":"issue","severity":"major","contact":"user@example.com","bodyLength":11,"packageEncrypted":true,"encryption":"YMHUTFB1","packageBytes":` + itoa(len(encrypted)) + `,"packageSha256":"` + encryptedHash + `","plainPackageBytes":` + itoa(len(plain)) + `,"plainPackageSha256":"` + plainHash + `","createdAt":"2026-06-25T00:00:00Z"}`
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
_ = writer.WriteField("payload", payload)
_ = writer.WriteField("timestamp", timestamp)
_ = writer.WriteField("nonce", "abc123")
_ = writer.WriteField("packageSha256", encryptedHash)
_ = writer.WriteField("signature", SignWithKey(cfg.ClientSignatureKey, timestamp, "abc123", encryptedHash, payload))
part, err := writer.CreateFormFile("package", "feedback.ymfb")
if err != nil {
t.Fatal(err)
}
_, _ = io.Copy(part, bytes.NewReader(encrypted))
_ = writer.Close()
req := httptest.NewRequest("POST", "/", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
item, err := service.Submit(req)
if err != nil {
t.Fatal(err)
}
if item.Code != "FB-20260625-ABCDEF" || !strings.Contains(item.IncludedFiles, "feedback.json") {
t.Fatalf("unexpected item: %#v", item)
}
if _, err := store.GetFeedback(item.Code); err != nil {
t.Fatal(err)
}
}
func TestZipPathEscapeRejected(t *testing.T) {
root := t.TempDir()
cfg := testConfig(root)
store, err := db.Open(cfg)
if err != nil {
t.Fatal(err)
}
defer store.Close()
plain := zipBytes(t, map[string]string{"../escape.txt": "bad", "feedback.json": `{}`})
if _, err := ReadFeedbackPackageWithGuard(plain, cfg.UploadGuard); err == nil {
t.Fatal("expected unsafe zip entry error")
}
}
func testConfig(root string) *config.Config {
return &config.Config{
StorageDir: filepath.Join(root, "storage"),
ClientSignatureKey: "ymhut-box-feedback-client-v1",
PackageEncryptionKey: "ymhut-box-feedback-package-v1",
TimestampWindowSeconds: 600,
MaxRequestBytes: 12 << 20,
MaxPackageBytes: 10 << 20,
Database: config.DatabaseConfig{
Provider: "sqlite",
SQLitePath: filepath.Join(root, "storage", "unified.sqlite"),
FailoverEnabled: true,
HealthIntervalSec: 3600,
MaxOpenConns: 1,
MaxIdleConns: 1,
ConnMaxLifetimeSeconds: 60,
},
UploadGuard: config.UploadGuardConfig{MaxZipFiles: 80, MaxDecompressedBytes: 30 << 20, MaxSingleFileBytes: 8 << 20, MaxCompressionRatio: 120, MaxReadableTextBytes: 256 << 10, AllowUnexpectedZipFiles: true},
}
}
func zipBytes(t *testing.T, files map[string]string) []byte {
t.Helper()
var buf bytes.Buffer
writer := zip.NewWriter(&buf)
for name, body := range files {
entry, err := writer.Create(name)
if err != nil {
t.Fatal(err)
}
_, _ = entry.Write([]byte(body))
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
return buf.Bytes()
}
func encryptPackageForTest(t *testing.T, plain []byte, keyMaterial string) []byte {
t.Helper()
key := sha256.Sum256([]byte(keyMaterial))
block, err := aes.NewCipher(key[:])
if err != nil {
t.Fatal(err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
t.Fatal(err)
}
nonce := []byte("123456789012")
sealed := gcm.Seal(nil, nonce, plain, []byte(PackageMagic))
ciphertext := sealed[:len(sealed)-gcm.Overhead()]
tag := sealed[len(sealed)-gcm.Overhead():]
out := []byte(PackageMagic)
out = append(out, nonce...)
out = append(out, tag...)
out = append(out, ciphertext...)
return out
}
func sha256HexTest(data []byte) string {
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
func itoa(value int) string {
if value == 0 {
return "0"
}
var buf [20]byte
i := len(buf)
for value > 0 {
i--
buf[i] = byte('0' + value%10)
value /= 10
}
return string(buf[i:])
}