mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
- core.integrations (migration 00005) with AES-256-GCM-encrypted credentials (PRIMORA_ENCRYPTION_KEY, hex/base64, dev fallback); has_credentials serialization only — secrets never leave the server - /projects/:id/integrations list/create/delete/test; health check hits the stored base URL with a 5s timeout and records status - Rybbit connector: GET …/integrations/:id/analytics proxies /api/organizations + /api/sites/* server-side into a normalized shape - core.webhooks + core.webhook_deliveries; CRUD, deliveries, test, and POST …/deploy-markers. Events: issue.created (new fingerprint on ingest), deploy.marker, webhook.test. X-Primora-Signature HMAC-SHA256 over the raw body; in-process dispatcher, 3 attempts, quadratic backoff, 10s timeout, pending-scan on boot - HTTPS enforced for public targets; HTTP allowed for loopback/private/ self-hosted sinks - Integrations page (connectors + webhooks tabs, inline analytics, delivery log, secret-shown-once), demo-mode stubs - 12 new primora_* MCP tools - workspace 0.4.0 Authored By: TDvorak <[email protected]>
108 lines
2.8 KiB
Go
108 lines
2.8 KiB
Go
package services
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
db "github.com/tdvorak/primora/apps/backend/internal/database/db"
|
|
)
|
|
|
|
func TestSignBodyMatchesReceiverVerification(t *testing.T) {
|
|
secret := "whsec_test_123"
|
|
body := []byte(`{"event":"issue.created","data":{"fingerprint":"abc"}}`)
|
|
|
|
got := signBody(secret, body)
|
|
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write(body)
|
|
want := hex.EncodeToString(mac.Sum(nil))
|
|
if got != want {
|
|
t.Fatalf("signature mismatch: got %s want %s", got, want)
|
|
}
|
|
if len(got) != 64 {
|
|
t.Fatalf("expected 64-char hex digest, got %d", len(got))
|
|
}
|
|
if signBody(secret, []byte(`{"event":"issue.created"}`)) == got {
|
|
t.Error("different body must produce a different signature")
|
|
}
|
|
if signBody("other-secret", body) == got {
|
|
t.Error("different secret must produce a different signature")
|
|
}
|
|
}
|
|
|
|
func TestIntegrationSummaryNeverExposesCredentials(t *testing.T) {
|
|
ciphertext := []byte("nonce|aes-gcm-ciphertext|contains|api_key=super-secret")
|
|
row := db.CoreIntegration{
|
|
ID: uuid.New(),
|
|
ProjectID: uuid.New(),
|
|
Type: "rybbit",
|
|
Name: "prod",
|
|
BaseUrl: "https://analytics.example.com",
|
|
Config: []byte(`{"site_id":"s1"}`),
|
|
Credentials: ciphertext,
|
|
Status: "ok",
|
|
}
|
|
summary := toIntegrationSummary(row)
|
|
|
|
if !summary.HasCredentials {
|
|
t.Error("expected has_credentials=true")
|
|
}
|
|
raw, err := json.Marshal(summary)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
for _, leak := range []string{"super-secret", "ciphertext", "api_key", "\"credentials\"", "aes-gcm"} {
|
|
if strings.Contains(string(raw), leak) {
|
|
t.Errorf("serialized summary leaks %q: %s", leak, raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizeWebhookURL(t *testing.T) {
|
|
ok := []string{
|
|
"https://hooks.example.com/x",
|
|
"http://localhost:9000/sink",
|
|
"http://127.0.0.1:9000/sink",
|
|
"http://primora-sink:9000/",
|
|
"http://192.168.1.40:8080/hook",
|
|
"http://nas.internal/events",
|
|
}
|
|
for _, u := range ok {
|
|
if _, err := normalizeWebhookURL(u); err != nil {
|
|
t.Errorf("expected %q accepted: %v", u, err)
|
|
}
|
|
}
|
|
bad := []string{
|
|
"",
|
|
"not-a-url",
|
|
"ftp://example.com",
|
|
"http://example.com/plain-remote",
|
|
"http://8.8.8.8/x",
|
|
"https://",
|
|
}
|
|
for _, u := range bad {
|
|
if _, err := normalizeWebhookURL(u); err == nil {
|
|
t.Errorf("expected %q rejected", u)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateWebhookEvents(t *testing.T) {
|
|
got, err := validateWebhookEvents([]string{"issue.created", "deploy.marker", "issue.created", " "})
|
|
if err != nil {
|
|
t.Fatalf("validate: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected deduped 2 events, got %v", got)
|
|
}
|
|
if _, err := validateWebhookEvents([]string{"bogus.event"}); err == nil {
|
|
t.Error("expected unknown event rejected")
|
|
}
|
|
}
|