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
@@ -264,6 +264,11 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
|||||||
const activeKeyPlaceholder =
|
const activeKeyPlaceholder =
|
||||||
newlyCreatedKey || (apiKeys[0]?.keyPrefix ? apiKeys[0].keyPrefix + '••••••••' : 'DEIN_API_KEY');
|
newlyCreatedKey || (apiKeys[0]?.keyPrefix ? apiKeys[0].keyPrefix + '••••••••' : 'DEIN_API_KEY');
|
||||||
|
|
||||||
|
const curlTriageExample = `curl -X POST ${baseUrl}/api/v1/inbox \\
|
||||||
|
-H "X-API-Key: ${activeKeyPlaceholder}" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{"content": "Kellerfeuchtigkeit über 68% gestiegen – Entfeuchter prüfen #smarthome #alarm", "priority": "urgent"}'`;
|
||||||
|
|
||||||
const curlTaskExample = `curl -X POST ${baseUrl}/api/v1/inbox \\
|
const curlTaskExample = `curl -X POST ${baseUrl}/api/v1/inbox \\
|
||||||
-H "X-API-Key: ${activeKeyPlaceholder}" \\
|
-H "X-API-Key: ${activeKeyPlaceholder}" \\
|
||||||
-H "Content-Type: application/json" \\
|
-H "Content-Type: application/json" \\
|
||||||
@@ -920,23 +925,43 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Curl Example */}
|
{/* Curl Examples */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-1.5">
|
||||||
<span className="font-semibold text-muted flex items-center gap-1.5">
|
<div className="flex items-center justify-between">
|
||||||
<Terminal className="w-3.5 h-3.5 text-accent" />
|
<span className="font-semibold text-muted flex items-center gap-1.5">
|
||||||
<span>Curl-Beispiel: Direkt als Aufgabe einspeisen</span>
|
<Terminal className="w-3.5 h-3.5 text-accent" />
|
||||||
</span>
|
<span>Curl: In Triage-Deck einspeisen (erhält Webhook-Badge)</span>
|
||||||
<button
|
</span>
|
||||||
onClick={() => handleCopy(curlTaskExample, 'curl')}
|
<button
|
||||||
className="btn-action h-7 px-2.5 text-[11px]"
|
onClick={() => handleCopy(curlTriageExample, 'curl-triage')}
|
||||||
>
|
className="btn-action h-7 px-2.5 text-[11px]"
|
||||||
{copiedLabel === 'curl' ? 'Kopiert!' : 'Befehl kopieren'}
|
>
|
||||||
</button>
|
{copiedLabel === 'curl-triage' ? 'Kopiert!' : 'Befehl kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="p-3 code-block text-[11px] font-mono overflow-x-auto whitespace-pre">
|
||||||
|
{curlTriageExample}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-semibold text-muted flex items-center gap-1.5">
|
||||||
|
<Terminal className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>Curl: Direkt als Aufgabe anlegen</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(curlTaskExample, 'curl-task')}
|
||||||
|
className="btn-action h-7 px-2.5 text-[11px]"
|
||||||
|
>
|
||||||
|
{copiedLabel === 'curl-task' ? 'Kopiert!' : 'Befehl kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="p-3 code-block text-[11px] font-mono overflow-x-auto whitespace-pre">
|
||||||
|
{curlTaskExample}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<pre className="p-3 code-block text-[11px] font-mono overflow-x-auto whitespace-pre">
|
|
||||||
{curlTaskExample}
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Full Data Backup */}
|
{/* Full Data Backup */}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Flame,
|
Flame,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Folder,
|
Folder,
|
||||||
|
Webhook,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Thought, SubTask, TaskList, KanbanColumn } from '../../lib/types';
|
import { Thought, SubTask, TaskList, KanbanColumn } from '../../lib/types';
|
||||||
import { triggerHaptic } from '../../lib/api';
|
import { triggerHaptic } from '../../lib/api';
|
||||||
@@ -192,6 +193,12 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
|||||||
<span className="text-xs font-semibold text-main truncate">
|
<span className="text-xs font-semibold text-main truncate">
|
||||||
{task.isCompleted ? 'Erledigte Aufgabe' : 'Aufgabe bearbeiten'}
|
{task.isCompleted ? 'Erledigte Aufgabe' : 'Aufgabe bearbeiten'}
|
||||||
</span>
|
</span>
|
||||||
|
{task.source?.toLowerCase() === 'webhook' && (
|
||||||
|
<span className="px-1.5 py-0.5 border border-border bg-canvas text-muted rounded-btn text-[10px] font-medium flex items-center gap-1 flex-shrink-0">
|
||||||
|
<Webhook className="w-2.5 h-2.5 text-accent" />
|
||||||
|
<span>Webhook</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
Folder,
|
Folder,
|
||||||
Filter,
|
Filter,
|
||||||
|
Webhook,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn, PriorityLevel } from '../../lib/types';
|
import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn, PriorityLevel } from '../../lib/types';
|
||||||
import { api, triggerHaptic } from '../../lib/api';
|
import { api, triggerHaptic } from '../../lib/api';
|
||||||
@@ -1170,6 +1171,16 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{task.source?.toLowerCase() === 'webhook' && (
|
||||||
|
<span
|
||||||
|
className="px-1.5 py-0.5 border border-border bg-canvas text-muted rounded-btn text-[10px] font-medium flex items-center gap-1 flex-shrink-0"
|
||||||
|
title="Erstellt über Webhook"
|
||||||
|
>
|
||||||
|
<Webhook className="w-2.5 h-2.5 text-accent" />
|
||||||
|
<span>Webhook</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Note indicator */}
|
{/* Note indicator */}
|
||||||
{task.notes && (
|
{task.notes && (
|
||||||
<button
|
<button
|
||||||
@@ -2174,6 +2185,15 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
⚠️ Wichtig
|
⚠️ Wichtig
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{task.source?.toLowerCase() === 'webhook' && (
|
||||||
|
<span
|
||||||
|
className="px-1.5 py-0.2 border border-border bg-canvas text-muted rounded-btn text-[9px] font-medium flex items-center gap-1"
|
||||||
|
title="Erstellt über Webhook"
|
||||||
|
>
|
||||||
|
<Webhook className="w-2.5 h-2.5 text-accent" />
|
||||||
|
<span>Webhook</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{task.dueDate && (
|
{task.dueDate && (
|
||||||
<span className="inline-flex items-center gap-0.5 text-[9px] text-muted">
|
<span className="inline-flex items-center gap-0.5 text-[9px] text-muted">
|
||||||
<Calendar className="w-2.5 h-2.5" />
|
<Calendar className="w-2.5 h-2.5" />
|
||||||
|
|||||||
@@ -139,11 +139,21 @@ const SwipeableQueueItem: React.FC<SwipeableQueueItemProps> = ({
|
|||||||
}}
|
}}
|
||||||
className="relative bg-panel px-4 py-3.5 sm:px-3.5 sm:py-2.5 min-h-[56px] sm:min-h-[44px] flex items-center justify-between gap-3 hover:bg-panel-hover transition-colors group cursor-pointer"
|
className="relative bg-panel px-4 py-3.5 sm:px-3.5 sm:py-2.5 min-h-[56px] sm:min-h-[44px] flex items-center justify-between gap-3 hover:bg-panel-hover transition-colors group cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className="flex-1 min-w-0 flex items-center gap-3">
|
<div className="flex-1 min-w-0 flex items-center gap-2">
|
||||||
<span className="w-2 h-2 rounded-full bg-accent flex-shrink-0" />
|
<span className="w-2 h-2 rounded-full bg-accent flex-shrink-0" />
|
||||||
<span className="text-sm sm:text-xs font-medium text-main line-clamp-2 sm:truncate">
|
<span className="text-sm sm:text-xs font-medium text-main line-clamp-2 sm:truncate">
|
||||||
{item.content}
|
{item.content}
|
||||||
</span>
|
</span>
|
||||||
|
{item.source?.toLowerCase() === 'webhook' && (
|
||||||
|
<span className="px-1.5 py-0.5 text-[10px] bg-canvas text-muted border border-border rounded-btn flex items-center gap-1 flex-shrink-0">
|
||||||
|
<Webhook className="w-2.5 h-2.5 text-accent" /> Webhook
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{item.source?.toLowerCase() === 'voice' && (
|
||||||
|
<span className="px-1.5 py-0.5 text-[10px] bg-canvas text-accent border border-border rounded-btn flex items-center gap-1 flex-shrink-0">
|
||||||
|
<Mic className="w-2.5 h-2.5" /> Diktat
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop quick buttons (hidden on mobile so queue items are clean & easy to tap/swipe) */}
|
{/* Desktop quick buttons (hidden on mobile so queue items are clean & easy to tap/swipe) */}
|
||||||
@@ -413,15 +423,16 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sourceBadge = (source: string) => {
|
const sourceBadge = (source?: string) => {
|
||||||
if (source === 'voice') {
|
const s = (source || '').toLowerCase().trim();
|
||||||
|
if (s === 'voice') {
|
||||||
return (
|
return (
|
||||||
<span className="px-2 py-0.5 text-[11px] bg-canvas text-accent border border-border rounded-btn flex items-center gap-1">
|
<span className="px-2 py-0.5 text-[11px] bg-canvas text-accent border border-border rounded-btn flex items-center gap-1">
|
||||||
<Mic className="w-3 h-3" /> Diktat
|
<Mic className="w-3 h-3" /> Diktat
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (source === 'webhook') {
|
if (s === 'webhook') {
|
||||||
return (
|
return (
|
||||||
<span className="px-2 py-0.5 text-[11px] bg-canvas text-muted border border-border rounded-btn flex items-center gap-1">
|
<span className="px-2 py-0.5 text-[11px] bg-canvas text-muted border border-border rounded-btn flex items-center gap-1">
|
||||||
<Webhook className="w-3 h-3 text-accent" /> Webhook
|
<Webhook className="w-3 h-3 text-accent" /> Webhook
|
||||||
@@ -500,7 +511,10 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
|||||||
key={nextCard.id + '-preview'}
|
key={nextCard.id + '-preview'}
|
||||||
className="absolute inset-x-3 top-3 bottom-0 panel-surface p-4 opacity-40 scale-[0.97] pointer-events-none flex flex-col justify-between"
|
className="absolute inset-x-3 top-3 bottom-0 panel-surface p-4 opacity-40 scale-[0.97] pointer-events-none flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">{priorityBadge(nextCard.priority)}</div>
|
<div className="flex items-center gap-1.5">
|
||||||
|
{priorityBadge(nextCard.priority)}
|
||||||
|
{sourceBadge(nextCard.source)}
|
||||||
|
</div>
|
||||||
<p className="text-sm text-muted line-clamp-2">{nextCard.content}</p>
|
<p className="text-sm text-muted line-clamp-2">{nextCard.content}</p>
|
||||||
<div className="text-[11px] text-faint">Nächster Quick Dump…</div>
|
<div className="text-[11px] text-faint">Nächster Quick Dump…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+55
-6
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
@@ -586,7 +587,25 @@ func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
|
|||||||
taskListID = req.TaskListID2
|
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 {
|
if err != nil {
|
||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
@@ -1689,13 +1708,36 @@ func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
|
|||||||
var req struct {
|
var req struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Message string `json:"message"`
|
||||||
Priority string `json:"priority"`
|
Priority string `json:"priority"`
|
||||||
|
Source string `json:"source"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
DueDate string `json:"dueDate"`
|
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")
|
bodyBytes, _ := io.ReadAll(r.Body)
|
||||||
return
|
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)
|
bodyText := strings.TrimSpace(req.Title)
|
||||||
@@ -1707,7 +1749,7 @@ func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if bodyText == "" {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1722,12 +1764,19 @@ func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
|
|||||||
req.Priority = "high"
|
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 {
|
if err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.wsHub.BroadcastTaskEvent(thought.UserID, "TASK_CREATED", thought)
|
||||||
|
|
||||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||||
"status": "ingested",
|
"status": "ingested",
|
||||||
"thought": thought,
|
"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)
|
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