• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DigitalTolk / ex / 25621015252

10 May 2026 05:41AM UTC coverage: 92.558% (-0.03%) from 92.585%
25621015252

Pull #98

github

web-flow
Merge a9ef85ba0 into bf75b00a8
Pull Request #98: Incoming webhooks

1428 of 1503 branches covered (95.01%)

Branch coverage included in aggregate %.

489 of 536 new or added lines in 11 files covered. (91.23%)

2 existing lines in 1 file now uncovered.

12526 of 13573 relevant lines covered (92.29%)

34.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.58
/internal/handler/webhook.go
1
package handler
2

3
import (
4
        "encoding/json"
5
        "io"
6
        "net/http"
7
        "net/url"
8
        "strings"
9

10
        "github.com/DigitalTolk/ex/internal/middleware"
11
        "github.com/DigitalTolk/ex/internal/model"
12
        "github.com/DigitalTolk/ex/internal/service"
13
        "github.com/DigitalTolk/ex/internal/store"
14
)
15

16
type WebhookHandler struct {
17
        svc *service.IncomingWebhookService
18
}
19

20
func NewWebhookHandler(svc *service.IncomingWebhookService) *WebhookHandler {
3✔
21
        return &WebhookHandler{svc: svc}
3✔
22
}
3✔
23

24
type webhookResponse struct {
25
        *model.IncomingWebhook
26
        URL string `json:"url,omitempty"`
27
}
28

29
func (h *WebhookHandler) List(w http.ResponseWriter, r *http.Request) {
3✔
30
        if !requireAdmin(w, r) {
4✔
31
                return
1✔
32
        }
1✔
33
        items, err := h.svc.List(r.Context())
2✔
34
        if err != nil {
3✔
35
                writeError(w, http.StatusInternalServerError, "list_error", err.Error())
1✔
36
                return
1✔
37
        }
1✔
38
        out := make([]webhookResponse, 0, len(items))
1✔
39
        for _, wh := range items {
2✔
40
                out = append(out, webhookResponse{IncomingWebhook: wh, URL: h.svc.URL(wh)})
1✔
41
        }
1✔
42
        writeJSON(w, http.StatusOK, out)
1✔
43
}
44

45
func (h *WebhookHandler) Create(w http.ResponseWriter, r *http.Request) {
3✔
46
        if !requireAdmin(w, r) {
3✔
NEW
47
                return
×
NEW
48
        }
×
49
        var body model.IncomingWebhook
3✔
50
        if err := readJSON(r, &body); err != nil {
4✔
51
                writeError(w, http.StatusBadRequest, "invalid_body", err.Error())
1✔
52
                return
1✔
53
        }
1✔
54
        wh, err := h.svc.Create(r.Context(), middleware.UserIDFromContext(r.Context()), &body)
2✔
55
        if err != nil {
3✔
56
                writeError(w, http.StatusBadRequest, "create_error", err.Error())
1✔
57
                return
1✔
58
        }
1✔
59
        writeJSON(w, http.StatusCreated, webhookResponse{IncomingWebhook: wh, URL: h.svc.URL(wh)})
1✔
60
}
61

62
func (h *WebhookHandler) Delete(w http.ResponseWriter, r *http.Request) {
3✔
63
        if !requireAdmin(w, r) {
3✔
NEW
64
                return
×
NEW
65
        }
×
66
        id := pathParam(r, "id")
3✔
67
        if err := h.svc.Delete(r.Context(), id); err != nil {
5✔
68
                writeError(w, http.StatusBadRequest, "delete_error", err.Error())
2✔
69
                return
2✔
70
        }
2✔
71
        w.WriteHeader(http.StatusNoContent)
1✔
72
}
73

74
func (h *WebhookHandler) Execute(w http.ResponseWriter, r *http.Request) {
4✔
75
        id := pathParam(r, "id")
4✔
76
        payload, err := readWebhookPayload(r)
4✔
77
        if err != nil {
5✔
78
                writeError(w, http.StatusBadRequest, "invalid_payload", err.Error())
1✔
79
                return
1✔
80
        }
1✔
81
        if err := h.svc.Execute(r.Context(), id, payload); err != nil {
5✔
82
                if strings.Contains(err.Error(), "direct-message") {
3✔
83
                        writeError(w, http.StatusBadRequest, "unsupported_channel", err.Error())
1✔
84
                        return
1✔
85
                }
1✔
86
                if strings.Contains(err.Error(), store.ErrNotFound.Error()) {
2✔
87
                        writeError(w, http.StatusNotFound, "not_found", "webhook or channel not found")
1✔
88
                        return
1✔
89
                }
1✔
NEW
90
                writeError(w, http.StatusBadRequest, "webhook_error", err.Error())
×
NEW
91
                return
×
92
        }
93
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
1✔
94
        _, _ = w.Write([]byte("ok"))
1✔
95
}
96

97
func readWebhookPayload(r *http.Request) (service.IncomingWebhookPayload, error) {
8✔
98
        var payload service.IncomingWebhookPayload
8✔
99
        ct := strings.ToLower(r.Header.Get("Content-Type"))
8✔
100
        if strings.Contains(ct, "application/x-www-form-urlencoded") || strings.Contains(ct, "multipart/form-data") {
10✔
101
                if err := r.ParseForm(); err != nil {
2✔
NEW
102
                        return payload, err
×
NEW
103
                }
×
104
                raw := r.FormValue("payload")
2✔
105
                if raw == "" {
3✔
106
                        payload.Text = r.FormValue("text")
1✔
107
                        payload.Channel = r.FormValue("channel")
1✔
108
                        payload.Username = r.FormValue("username")
1✔
109
                        payload.IconURL = r.FormValue("icon_url")
1✔
110
                        return payload, nil
1✔
111
                }
1✔
112
                raw = strings.TrimPrefix(raw, "payload=")
1✔
113
                err := json.Unmarshal([]byte(raw), &payload)
1✔
114
                return payload, err
1✔
115
        }
116
        raw, err := readBodyString(r)
6✔
117
        if err != nil {
6✔
NEW
118
                return payload, err
×
NEW
119
        }
×
120
        raw = strings.TrimSpace(raw)
6✔
121
        if strings.HasPrefix(raw, "payload=") {
7✔
122
                raw = strings.TrimPrefix(raw, "payload=")
1✔
123
                if decoded, err := url.QueryUnescape(raw); err == nil {
2✔
124
                        raw = decoded
1✔
125
                }
1✔
126
        }
127
        err = json.Unmarshal([]byte(raw), &payload)
6✔
128
        return payload, err
6✔
129
}
130

131
func readBodyString(r *http.Request) (string, error) {
6✔
132
        body, err := io.ReadAll(r.Body)
6✔
133
        return string(body), err
6✔
134
}
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc