mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-02 15:47:59 +00:00
feat: add VerifySignature + webhook decode/signature tests
Co-authored-by: coolaj86 <122831+coolaj86@users.noreply.github.com>
This commit is contained in:
parent
60097fd661
commit
97e7b9d319
18
http/androidsmsgateway/signature.go
Normal file
18
http/androidsmsgateway/signature.go
Normal file
@ -0,0 +1,18 @@
|
||||
package androidsmsgateway
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// VerifySignature verifies the HMAC-SHA256 signature of a webhook payload.
|
||||
// The message is payload concatenated with timestamp (the X-Timestamp header value).
|
||||
// The signature is the hex-encoded HMAC-SHA256 of that message using secretKey.
|
||||
func VerifySignature(secretKey, payload, timestamp, signature string) bool {
|
||||
message := payload + timestamp
|
||||
mac := hmac.New(sha256.New, []byte(secretKey))
|
||||
mac.Write([]byte(message))
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
return hmac.Equal([]byte(expected), []byte(signature))
|
||||
}
|
||||
276
http/androidsmsgateway/webhooks_test.go
Normal file
276
http/androidsmsgateway/webhooks_test.go
Normal file
@ -0,0 +1,276 @@
|
||||
package androidsmsgateway_test
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/therootcompany/golib/http/androidsmsgateway"
|
||||
)
|
||||
|
||||
// mustTime parses an RFC3339Nano timestamp and panics on error.
|
||||
func mustTime(s string) time.Time {
|
||||
t, err := time.Parse(time.RFC3339Nano, s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// buildRaw marshals a Webhook JSON body from individual fields.
|
||||
func buildRaw(t *testing.T, deviceID, event, id, webhookID string, payload any) []byte {
|
||||
t.Helper()
|
||||
rawPayload, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal payload: %v", err)
|
||||
}
|
||||
w := struct {
|
||||
DeviceID string `json:"deviceId"`
|
||||
Event string `json:"event"`
|
||||
ID string `json:"id"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
WebhookID string `json:"webhookId"`
|
||||
}{
|
||||
DeviceID: deviceID,
|
||||
Event: event,
|
||||
ID: id,
|
||||
Payload: rawPayload,
|
||||
WebhookID: webhookID,
|
||||
}
|
||||
b, err := json.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal webhook: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// decodeRaw unmarshals raw JSON into a Webhook then calls Decode.
|
||||
func decodeRaw(t *testing.T, raw []byte) androidsmsgateway.WebhookEvent {
|
||||
t.Helper()
|
||||
var wh androidsmsgateway.Webhook
|
||||
if err := json.Unmarshal(raw, &wh); err != nil {
|
||||
t.Fatalf("unmarshal Webhook: %v", err)
|
||||
}
|
||||
ev, err := androidsmsgateway.Decode(&wh)
|
||||
if err != nil {
|
||||
t.Fatalf("Decode: %v", err)
|
||||
}
|
||||
return ev
|
||||
}
|
||||
|
||||
func TestDecode_SMSReceived(t *testing.T) {
|
||||
raw := buildRaw(t,
|
||||
"ffffffffceb0b1db0000018e937c815b", "sms:received",
|
||||
"Ey6ECgOkVVFjz3CL48B8C", "LreFUt-Z3sSq0JufY9uWB",
|
||||
map[string]any{
|
||||
"messageId": "abc123",
|
||||
"message": "Android is always a sweet treat!",
|
||||
"phoneNumber": "6505551212",
|
||||
"simNumber": 1,
|
||||
"receivedAt": "2024-06-22T15:46:11.000+07:00",
|
||||
},
|
||||
)
|
||||
ev := decodeRaw(t, raw)
|
||||
got, ok := ev.(*androidsmsgateway.WebhookReceived)
|
||||
if !ok {
|
||||
t.Fatalf("expected *WebhookReceived, got %T", ev)
|
||||
}
|
||||
if got.Event != "sms:received" {
|
||||
t.Errorf("Event = %q, want sms:received", got.Event)
|
||||
}
|
||||
if got.Payload.MessageID != "abc123" {
|
||||
t.Errorf("MessageID = %q, want abc123", got.Payload.MessageID)
|
||||
}
|
||||
if got.Payload.Message != "Android is always a sweet treat!" {
|
||||
t.Errorf("Message = %q", got.Payload.Message)
|
||||
}
|
||||
if got.Payload.PhoneNumber != "6505551212" {
|
||||
t.Errorf("PhoneNumber = %q, want 6505551212", got.Payload.PhoneNumber)
|
||||
}
|
||||
if got.Payload.SimNumber != 1 {
|
||||
t.Errorf("SimNumber = %d, want 1", got.Payload.SimNumber)
|
||||
}
|
||||
want := mustTime("2024-06-22T15:46:11.000+07:00")
|
||||
if !got.Payload.ReceivedAt.Equal(want) {
|
||||
t.Errorf("ReceivedAt = %v, want %v", got.Payload.ReceivedAt, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecode_SMSSent(t *testing.T) {
|
||||
raw := buildRaw(t,
|
||||
"ffffffffceb0b1db0000018e937c815b", "sms:sent",
|
||||
"Ey6ECgOkVVFjz3CL48B8C", "LreFUt-Z3sSq0JufY9uWB",
|
||||
map[string]any{
|
||||
"messageId": "msg-456",
|
||||
"phoneNumber": "+9998887777",
|
||||
"simNumber": 1,
|
||||
"partsCount": 1,
|
||||
"sentAt": "2026-02-18T02:05:00.000+07:00",
|
||||
},
|
||||
)
|
||||
ev := decodeRaw(t, raw)
|
||||
got, ok := ev.(*androidsmsgateway.WebhookSent)
|
||||
if !ok {
|
||||
t.Fatalf("expected *WebhookSent, got %T", ev)
|
||||
}
|
||||
if got.Event != "sms:sent" {
|
||||
t.Errorf("Event = %q, want sms:sent", got.Event)
|
||||
}
|
||||
if got.Payload.MessageID != "msg-456" {
|
||||
t.Errorf("MessageID = %q, want msg-456", got.Payload.MessageID)
|
||||
}
|
||||
if got.Payload.PhoneNumber != "+9998887777" {
|
||||
t.Errorf("PhoneNumber = %q, want +9998887777", got.Payload.PhoneNumber)
|
||||
}
|
||||
if got.Payload.PartsCount != 1 {
|
||||
t.Errorf("PartsCount = %d, want 1", got.Payload.PartsCount)
|
||||
}
|
||||
want := mustTime("2026-02-18T02:05:00.000+07:00")
|
||||
if !got.Payload.SentAt.Equal(want) {
|
||||
t.Errorf("SentAt = %v, want %v", got.Payload.SentAt, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecode_SMSDelivered(t *testing.T) {
|
||||
raw := buildRaw(t,
|
||||
"ffffffffceb0b1db0000018e937c815b", "sms:delivered",
|
||||
"Ey6ECgOkVVFjz3CL48B8C", "LreFUt-Z3sSq0JufY9uWB",
|
||||
map[string]any{
|
||||
"messageId": "msg-789",
|
||||
"phoneNumber": "+9998887777",
|
||||
"deliveredAt": "2026-02-18T02:10:00.000+07:00",
|
||||
},
|
||||
)
|
||||
ev := decodeRaw(t, raw)
|
||||
got, ok := ev.(*androidsmsgateway.WebhookDelivered)
|
||||
if !ok {
|
||||
t.Fatalf("expected *WebhookDelivered, got %T", ev)
|
||||
}
|
||||
if got.Event != "sms:delivered" {
|
||||
t.Errorf("Event = %q, want sms:delivered", got.Event)
|
||||
}
|
||||
if got.Payload.MessageID != "msg-789" {
|
||||
t.Errorf("MessageID = %q, want msg-789", got.Payload.MessageID)
|
||||
}
|
||||
want := mustTime("2026-02-18T02:10:00.000+07:00")
|
||||
if !got.Payload.DeliveredAt.Equal(want) {
|
||||
t.Errorf("DeliveredAt = %v, want %v", got.Payload.DeliveredAt, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecode_SystemPing(t *testing.T) {
|
||||
raw := buildRaw(t,
|
||||
"ffffffffceb0b1db00000192672f2204", "system:ping",
|
||||
"mjDoocQLCsOIDra_GthuI", "LreFUt-Z3sSq0JufY9uWB",
|
||||
map[string]any{
|
||||
"health": map[string]any{
|
||||
"checks": map[string]any{
|
||||
"messages:failed": map[string]any{
|
||||
"description": "Failed messages for last hour",
|
||||
"observedUnit": "messages", "observedValue": 0, "status": "pass",
|
||||
},
|
||||
"connection:status": map[string]any{
|
||||
"description": "Internet connection status",
|
||||
"observedUnit": "boolean", "observedValue": 1, "status": "pass",
|
||||
},
|
||||
"connection:transport": map[string]any{
|
||||
"description": "Network transport type",
|
||||
"observedUnit": "flags", "observedValue": 4, "status": "pass",
|
||||
},
|
||||
"connection:cellular": map[string]any{
|
||||
"description": "Cellular network type",
|
||||
"observedUnit": "index", "observedValue": 0, "status": "pass",
|
||||
},
|
||||
"battery:level": map[string]any{
|
||||
"description": "Battery level in percent",
|
||||
"observedUnit": "percent", "observedValue": 94, "status": "pass",
|
||||
},
|
||||
"battery:charging": map[string]any{
|
||||
"description": "Is the phone charging?",
|
||||
"observedUnit": "flags", "observedValue": 4, "status": "pass",
|
||||
},
|
||||
},
|
||||
"releaseId": 1,
|
||||
"status": "pass",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
},
|
||||
)
|
||||
ev := decodeRaw(t, raw)
|
||||
got, ok := ev.(*androidsmsgateway.WebhookPing)
|
||||
if !ok {
|
||||
t.Fatalf("expected *WebhookPing, got %T", ev)
|
||||
}
|
||||
if got.Event != "system:ping" {
|
||||
t.Errorf("Event = %q, want system:ping", got.Event)
|
||||
}
|
||||
h := got.Payload.Health
|
||||
if h.Status != "pass" {
|
||||
t.Errorf("Health.Status = %q, want pass", h.Status)
|
||||
}
|
||||
if h.Version != "1.0.0" {
|
||||
t.Errorf("Health.Version = %q, want 1.0.0", h.Version)
|
||||
}
|
||||
if h.ReleaseID != 1 {
|
||||
t.Errorf("Health.ReleaseID = %d, want 1", h.ReleaseID)
|
||||
}
|
||||
c := h.Checks
|
||||
if c.BatteryLevel.ObservedValue != 94 {
|
||||
t.Errorf("BatteryLevel.ObservedValue = %v, want 94", c.BatteryLevel.ObservedValue)
|
||||
}
|
||||
if c.BatteryLevel.Status != "pass" {
|
||||
t.Errorf("BatteryLevel.Status = %q, want pass", c.BatteryLevel.Status)
|
||||
}
|
||||
if c.BatteryCharging.ObservedValue != 4 {
|
||||
t.Errorf("BatteryCharging.ObservedValue = %v, want 4", c.BatteryCharging.ObservedValue)
|
||||
}
|
||||
if c.ConnectionStatus.ObservedValue != 1 {
|
||||
t.Errorf("ConnectionStatus.ObservedValue = %v, want 1", c.ConnectionStatus.ObservedValue)
|
||||
}
|
||||
if c.ConnectionTransport.ObservedValue != 4 {
|
||||
t.Errorf("ConnectionTransport.ObservedValue = %v, want 4", c.ConnectionTransport.ObservedValue)
|
||||
}
|
||||
if c.ConnectionCellular.ObservedValue != 0 {
|
||||
t.Errorf("ConnectionCellular.ObservedValue = %v, want 0", c.ConnectionCellular.ObservedValue)
|
||||
}
|
||||
if c.MessagesFailed.ObservedValue != 0 {
|
||||
t.Errorf("MessagesFailed.ObservedValue = %v, want 0", c.MessagesFailed.ObservedValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecode_UnknownEvent(t *testing.T) {
|
||||
raw := buildRaw(t, "dev1", "unknown:event", "id1", "wh1", map[string]any{})
|
||||
var wh androidsmsgateway.Webhook
|
||||
if err := json.Unmarshal(raw, &wh); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if _, err := androidsmsgateway.Decode(&wh); err == nil {
|
||||
t.Fatal("expected error for unknown event, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifySignature(t *testing.T) {
|
||||
const (
|
||||
secret = "mysecretkey"
|
||||
payload = `{"event":"sms:received"}`
|
||||
timestamp = "1700000000"
|
||||
)
|
||||
|
||||
// Compute the expected signature.
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(payload + timestamp))
|
||||
sig := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !androidsmsgateway.VerifySignature(secret, payload, timestamp, sig) {
|
||||
t.Error("VerifySignature returned false for valid signature")
|
||||
}
|
||||
if androidsmsgateway.VerifySignature(secret, payload, timestamp, "badsignature") {
|
||||
t.Error("VerifySignature returned true for invalid signature")
|
||||
}
|
||||
if androidsmsgateway.VerifySignature("wrongkey", payload, timestamp, sig) {
|
||||
t.Error("VerifySignature returned true for wrong key")
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user