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:]) }