feat(webhook): ensure webhook source badge displays for triage thoughts and tasks
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m0s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m0s
This commit is contained in:
1 parent
baac20ea10
commit
0e06f6761d
6 files changed
+233
-27
No files matched your search
+55
-6
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/smtp"
|
||||
@@ -586,7 +587,25 @@ func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
|
||||
taskListID = req.TaskListID2
|
||||
}
|
||||
|
||||
thought, err := s.store.CreateThoughtWithDetails(userID, req.Content, req.Priority, req.Source, dueDate, taskListID, "todo", "")
|
||||
source := strings.ToLower(strings.TrimSpace(req.Source))
|
||||
if source == "" {
|
||||
source = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("source")))
|
||||
}
|
||||
if source == "" {
|
||||
source = strings.ToLower(strings.TrimSpace(r.Header.Get("X-Source")))
|
||||
}
|
||||
if source == "" {
|
||||
// When called via API key or webhook token, automatically default source to "webhook"
|
||||
if r.Header.Get("X-API-Key") != "" ||
|
||||
strings.HasPrefix(r.Header.Get("Authorization"), "Bearer mindsift_sec_") ||
|
||||
strings.HasPrefix(r.URL.Query().Get("token"), "mindsift_sec_") {
|
||||
source = "webhook"
|
||||
} else {
|
||||
source = "manual"
|
||||
}
|
||||
}
|
||||
|
||||
thought, err := s.store.CreateThoughtWithDetails(userID, req.Content, req.Priority, source, dueDate, taskListID, "todo", "")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
@@ -1689,13 +1708,36 @@ func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Text string `json:"text"`
|
||||
Message string `json:"message"`
|
||||
Priority string `json:"priority"`
|
||||
Source string `json:"source"`
|
||||
Tags []string `json:"tags"`
|
||||
DueDate string `json:"dueDate"`
|
||||
DueDate2 string `json:"due_date"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON-Payload")
|
||||
return
|
||||
|
||||
bodyBytes, _ := io.ReadAll(r.Body)
|
||||
if len(bodyBytes) > 0 {
|
||||
if jsonErr := json.Unmarshal(bodyBytes, &req); jsonErr != nil {
|
||||
plain := strings.TrimSpace(string(bodyBytes))
|
||||
if plain != "" {
|
||||
req.Content = plain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Content == "" {
|
||||
if req.Text != "" {
|
||||
req.Content = req.Text
|
||||
} else if req.Message != "" {
|
||||
req.Content = req.Message
|
||||
}
|
||||
}
|
||||
|
||||
dueDate := req.DueDate
|
||||
if dueDate == "" {
|
||||
dueDate = req.DueDate2
|
||||
}
|
||||
|
||||
bodyText := strings.TrimSpace(req.Title)
|
||||
@@ -1707,7 +1749,7 @@ func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if bodyText == "" {
|
||||
writeError(w, http.StatusBadRequest, "Feld 'title' oder 'content' ist erforderlich")
|
||||
writeError(w, http.StatusBadRequest, "Feld 'title', 'content', 'text' oder 'message' ist erforderlich")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1722,12 +1764,19 @@ func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
|
||||
req.Priority = "high"
|
||||
}
|
||||
|
||||
thought, err := s.store.CreateThought(user.ID, bodyText, req.Priority, "webhook", req.DueDate)
|
||||
source := strings.ToLower(strings.TrimSpace(req.Source))
|
||||
if source == "" {
|
||||
source = "webhook"
|
||||
}
|
||||
|
||||
thought, err := s.store.CreateThought(user.ID, bodyText, req.Priority, source, dueDate)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.wsHub.BroadcastTaskEvent(thought.UserID, "TASK_CREATED", thought)
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"status": "ingested",
|
||||
"thought": thought,
|
||||
|
||||
@@ -747,3 +747,94 @@ func TestWorkspaceTagsAPI(t *testing.T) {
|
||||
t.Fatalf("expected 200 OK from DELETE /api/v1/folders, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookTriageBadgeAndIngestion(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "test_webhook.db")
|
||||
store, err := storage.NewStore(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to init store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
cfg := &config.Config{
|
||||
JWTSecret: "test-jwt-secret-key",
|
||||
AllowRegistration: false,
|
||||
}
|
||||
srv := NewServer(cfg, store, ws.NewHub(), nil)
|
||||
router := srv.Router()
|
||||
|
||||
// 1. Setup user
|
||||
setupBody := []byte(`{
|
||||
"email": "smarthome_owner@example.com",
|
||||
"password": "secretpassword",
|
||||
"displayName": "Home User",
|
||||
"instanceName": "MindSift Home",
|
||||
"allowRegistration": false,
|
||||
"seedSampleData": false
|
||||
}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", bytes.NewReader(setupBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("setup failed: %s", rec.Body.String())
|
||||
}
|
||||
var authResp struct {
|
||||
Token string `json:"token"`
|
||||
User *domain.User `json:"user"`
|
||||
}
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &authResp)
|
||||
|
||||
// 2. Create API key for user
|
||||
apiKey, err := store.CreateAPIKey(authResp.User.ID, "Home Assistant Webhook")
|
||||
if err != nil || apiKey == nil || apiKey.RawKey == "" {
|
||||
t.Fatalf("failed to create API key: %v", err)
|
||||
}
|
||||
plainKey := apiKey.RawKey
|
||||
|
||||
// 3. Post to /api/v1/inbox using X-API-Key without passing "source"
|
||||
inboxBody := []byte(`{
|
||||
"content": "Kellerfeuchtigkeit über 68% gestiegen – Entfeuchter prüfen #smarthome #alarm",
|
||||
"priority": "urgent"
|
||||
}`)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/inbox", bytes.NewReader(inboxBody))
|
||||
req.Header.Set("X-API-Key", plainKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201 Created from /api/v1/inbox with API key, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var createdThought domain.Thought
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &createdThought)
|
||||
if createdThought.Source != "webhook" {
|
||||
t.Fatalf("expected created thought to have source='webhook' when called with API key, got '%s'", createdThought.Source)
|
||||
}
|
||||
|
||||
// 4. Post to /api/v1/webhooks/ingest using ?token=... with message alias
|
||||
webhookBody := []byte(`{
|
||||
"message": "Waschmaschine im Keller fertig",
|
||||
"priority": "high",
|
||||
"tags": ["automation"]
|
||||
}`)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/webhooks/ingest?token="+plainKey, bytes.NewReader(webhookBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201 Created from /api/v1/webhooks/ingest, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var ingestResp struct {
|
||||
Status string `json:"status"`
|
||||
Thought domain.Thought `json:"thought"`
|
||||
}
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &ingestResp)
|
||||
if ingestResp.Thought.Source != "webhook" {
|
||||
t.Fatalf("expected ingested webhook thought to have source='webhook', got '%s'", ingestResp.Thought.Source)
|
||||
}
|
||||
if !strings.Contains(ingestResp.Thought.Content, "Waschmaschine") {
|
||||
t.Fatalf("expected thought content to contain 'Waschmaschine', got '%s'", ingestResp.Thought.Content)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user