mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-16 21:54:14 +00:00
chore(version): bump to 0.122.47
- refactor(turn): extract decodeTURNConfig for testability - feat(turn): add stealth domain fields to config - fix(apns): nest custom data under "body" for expo-notifications compatibility
This commit is contained in:
parent
f4c58db710
commit
cd8c717363
@ -39,19 +39,6 @@ func parseTURNConfig(logger *logging.ColoredLogger) *turn.Config {
|
||||
}
|
||||
}
|
||||
|
||||
type yamlCfg struct {
|
||||
ListenAddr string `yaml:"listen_addr"`
|
||||
TURNSListenAddr string `yaml:"turns_listen_addr"`
|
||||
PublicIP string `yaml:"public_ip"`
|
||||
Realm string `yaml:"realm"`
|
||||
AuthSecret string `yaml:"auth_secret"`
|
||||
RelayPortStart int `yaml:"relay_port_start"`
|
||||
RelayPortEnd int `yaml:"relay_port_end"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
TLSCertPath string `yaml:"tls_cert_path"`
|
||||
TLSKeyPath string `yaml:"tls_key_path"`
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentTURN, "Config file not found",
|
||||
@ -60,26 +47,13 @@ func parseTURNConfig(logger *logging.ColoredLogger) *turn.Config {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var y yamlCfg
|
||||
if err := config.DecodeStrict(strings.NewReader(string(data)), &y); err != nil {
|
||||
cfg, err := decodeTURNConfig(data)
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentTURN, "Failed to parse TURN config", zap.Error(err))
|
||||
fmt.Fprintf(os.Stderr, "Configuration parse error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cfg := &turn.Config{
|
||||
ListenAddr: y.ListenAddr,
|
||||
TURNSListenAddr: y.TURNSListenAddr,
|
||||
PublicIP: y.PublicIP,
|
||||
Realm: y.Realm,
|
||||
AuthSecret: y.AuthSecret,
|
||||
RelayPortStart: y.RelayPortStart,
|
||||
RelayPortEnd: y.RelayPortEnd,
|
||||
Namespace: y.Namespace,
|
||||
TLSCertPath: y.TLSCertPath,
|
||||
TLSKeyPath: y.TLSKeyPath,
|
||||
}
|
||||
|
||||
if errs := cfg.Validate(); len(errs) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\nTURN configuration errors (%d):\n", len(errs))
|
||||
for _, e := range errs {
|
||||
@ -98,3 +72,50 @@ func parseTURNConfig(logger *logging.ColoredLogger) *turn.Config {
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// decodeTURNConfig strictly decodes the TURN YAML the namespace spawner writes
|
||||
// (yaml.Marshal of turn.Config) into a turn.Config. The yamlCfg struct MUST
|
||||
// carry every yaml-tagged field turn.Config marshals — DecodeStrict rejects
|
||||
// unknown keys, so a missing field crashes the TURN binary at startup.
|
||||
// Extracted (no os.Exit) so the spawner-output ↔ parser contract is unit-
|
||||
// testable (see config_test.go).
|
||||
func decodeTURNConfig(data []byte) (*turn.Config, error) {
|
||||
type yamlCfg struct {
|
||||
ListenAddr string `yaml:"listen_addr"`
|
||||
TURNSListenAddr string `yaml:"turns_listen_addr"`
|
||||
PublicIP string `yaml:"public_ip"`
|
||||
Realm string `yaml:"realm"`
|
||||
AuthSecret string `yaml:"auth_secret"`
|
||||
RelayPortStart int `yaml:"relay_port_start"`
|
||||
RelayPortEnd int `yaml:"relay_port_end"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
TLSCertPath string `yaml:"tls_cert_path"`
|
||||
TLSKeyPath string `yaml:"tls_key_path"`
|
||||
// feat-124 stealth TURNS-over-:443: second cert served by SNI.
|
||||
StealthDomain string `yaml:"stealth_domain"`
|
||||
TLSStealthCertPath string `yaml:"tls_stealth_cert_path"`
|
||||
TLSStealthKeyPath string `yaml:"tls_stealth_key_path"`
|
||||
}
|
||||
|
||||
var y yamlCfg
|
||||
if err := config.DecodeStrict(strings.NewReader(string(data)), &y); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &turn.Config{
|
||||
ListenAddr: y.ListenAddr,
|
||||
TURNSListenAddr: y.TURNSListenAddr,
|
||||
PublicIP: y.PublicIP,
|
||||
Realm: y.Realm,
|
||||
AuthSecret: y.AuthSecret,
|
||||
RelayPortStart: y.RelayPortStart,
|
||||
RelayPortEnd: y.RelayPortEnd,
|
||||
Namespace: y.Namespace,
|
||||
TLSCertPath: y.TLSCertPath,
|
||||
TLSKeyPath: y.TLSKeyPath,
|
||||
|
||||
StealthDomain: y.StealthDomain,
|
||||
TLSStealthCertPath: y.TLSStealthCertPath,
|
||||
TLSStealthKeyPath: y.TLSStealthKeyPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
60
core/cmd/turn/config_test.go
Normal file
60
core/cmd/turn/config_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/turn"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// TestDecodeTURNConfig_acceptsSpawnerOutput is the regression guard for the
|
||||
// feat-124 crash: the namespace spawner writes the TURN config via
|
||||
// yaml.Marshal(turn.Config), and the TURN binary parses it with a STRICT
|
||||
// decoder. If turn.Config gains a yaml field the parser doesn't know, strict
|
||||
// decode rejects it and TURN crash-loops at startup. This pins that the
|
||||
// spawner's exact output round-trips through the parser, including the stealth
|
||||
// fields.
|
||||
func TestDecodeTURNConfig_acceptsSpawnerOutput(t *testing.T) {
|
||||
src := turn.Config{
|
||||
ListenAddr: "0.0.0.0:3478",
|
||||
TURNSListenAddr: "0.0.0.0:5349",
|
||||
PublicIP: "203.0.113.7",
|
||||
Realm: "orama-devnet.network",
|
||||
AuthSecret: "secret",
|
||||
RelayPortStart: 49152,
|
||||
RelayPortEnd: 49951,
|
||||
Namespace: "anchat-test",
|
||||
TLSCertPath: "/x/turn-cert.pem",
|
||||
TLSKeyPath: "/x/turn-key.pem",
|
||||
StealthDomain: "cdn-3259254d4d3e.orama-devnet.network",
|
||||
TLSStealthCertPath: "/var/lib/caddy/caddy/certificates/.../wildcard_.orama-devnet.network.crt",
|
||||
TLSStealthKeyPath: "/var/lib/caddy/caddy/certificates/.../wildcard_.orama-devnet.network.key",
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(src)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
got, err := decodeTURNConfig(data)
|
||||
if err != nil {
|
||||
t.Fatalf("strict decode of spawner output failed — TURN would crash-loop at startup: %v\n---\n%s", err, data)
|
||||
}
|
||||
|
||||
if got.StealthDomain != src.StealthDomain ||
|
||||
got.TLSStealthCertPath != src.TLSStealthCertPath ||
|
||||
got.TLSStealthKeyPath != src.TLSStealthKeyPath {
|
||||
t.Errorf("stealth fields did not round-trip: got %+v", got)
|
||||
}
|
||||
if got.AuthSecret != src.AuthSecret || got.TURNSListenAddr != src.TURNSListenAddr {
|
||||
t.Errorf("core fields did not round-trip: got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecodeTURNConfig_rejectsUnknownField confirms the strict decoder still
|
||||
// rejects genuinely-unknown keys (so the contract above is meaningful).
|
||||
func TestDecodeTURNConfig_rejectsUnknownField(t *testing.T) {
|
||||
if _, err := decodeTURNConfig([]byte("listen_addr: \"0.0.0.0:3478\"\nbogus_field: 1\n")); err == nil {
|
||||
t.Fatal("expected strict decode to reject an unknown field")
|
||||
}
|
||||
}
|
||||
@ -149,7 +149,7 @@ func (p *Provider) Send(ctx context.Context, msg push.PushMessage) error {
|
||||
if p.kind != KindVoIP && !hasVisibleContent(msg) {
|
||||
return push.ErrEmptyContent
|
||||
}
|
||||
payload, err := buildAPSPayload(msg)
|
||||
payload, err := buildAPSPayload(msg, p.kind)
|
||||
if err != nil {
|
||||
return fmt.Errorf("apns: build payload: %w", err)
|
||||
}
|
||||
@ -281,12 +281,25 @@ func tokenPrefix(token string) string {
|
||||
return token[:8] + "..."
|
||||
}
|
||||
|
||||
// buildAPSPayload assembles the APNs JSON payload from a generic
|
||||
// PushMessage. The `aps` dictionary is the Apple-required wrapper;
|
||||
// custom fields (`data`) go alongside at the top level.
|
||||
// buildAPSPayload assembles the APNs JSON payload from a generic PushMessage.
|
||||
// The `aps` dictionary is the Apple-required wrapper; custom `Data` placement
|
||||
// depends on the kind:
|
||||
//
|
||||
// - KindAlert: custom data is nested under a top-level "body" object.
|
||||
// expo-notifications' iOS serializer sets content.data ONLY from
|
||||
// userInfo["body"] for remote notifications (NotificationRecords.swift:
|
||||
// `if isRemote { return userInfo["body"] }`) — top-level sibling keys of
|
||||
// `aps` are IGNORED, so spreading them there yields content.data=null on
|
||||
// iOS. This was bugboard #38 (Data never reached the JS client despite
|
||||
// correct wire serialization). Note: "body" here is the data envelope
|
||||
// expo expects; it is distinct from the human-readable alert body, which
|
||||
// lives at aps.alert.body.
|
||||
// - KindVoIP: custom data stays at the top level. PushKit/CallKit pushes are
|
||||
// handled by the app's native pushRegistry (not expo-notifications), which
|
||||
// reads payload.dictionaryPayload directly.
|
||||
//
|
||||
// Reference: https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification
|
||||
func buildAPSPayload(msg push.PushMessage) ([]byte, error) {
|
||||
func buildAPSPayload(msg push.PushMessage, kind Kind) ([]byte, error) {
|
||||
alert := map[string]string{}
|
||||
if msg.Title != "" {
|
||||
alert["title"] = msg.Title
|
||||
@ -338,13 +351,28 @@ func buildAPSPayload(msg push.PushMessage) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
root := map[string]interface{}{"aps": aps}
|
||||
|
||||
// Collect tenant custom data, excluding reserved keys: `aps` (must not be
|
||||
// clobbered) and `content_available` (already mapped into aps above).
|
||||
data := map[string]interface{}{}
|
||||
for k, v := range msg.Data {
|
||||
// Don't allow tenant data to clobber `aps`, and skip the
|
||||
// content_available marker since we mapped it to aps above.
|
||||
if k == "aps" || k == "content_available" {
|
||||
continue
|
||||
}
|
||||
root[k] = v
|
||||
data[k] = v
|
||||
}
|
||||
|
||||
if len(data) > 0 {
|
||||
if kind == KindVoIP {
|
||||
// Native PushKit reads the dictionary payload directly — top-level.
|
||||
for k, v := range data {
|
||||
root[k] = v
|
||||
}
|
||||
} else {
|
||||
// expo-notifications surfaces content.data from userInfo["body"]
|
||||
// only (bugboard #38) — nest the data envelope there.
|
||||
root["body"] = data
|
||||
}
|
||||
}
|
||||
return json.Marshal(root)
|
||||
}
|
||||
|
||||
@ -155,7 +155,7 @@ func TestValidator_RedactNeverEchoesP8Key(t *testing.T) {
|
||||
|
||||
func TestBuildAPSPayload_basicAlert(t *testing.T) {
|
||||
msg := push.PushMessage{Title: "hi", Body: "from orama"}
|
||||
raw, err := buildAPSPayload(msg)
|
||||
raw, err := buildAPSPayload(msg, KindAlert)
|
||||
if err != nil {
|
||||
t.Fatalf("build: %v", err)
|
||||
}
|
||||
@ -174,23 +174,58 @@ func TestBuildAPSPayload_basicAlert(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAPSPayload_dataAlongsideAPS(t *testing.T) {
|
||||
// Bugboard #38: for an ALERT push, custom data must be nested under a
|
||||
// top-level "body" object — expo-notifications' iOS serializer reads
|
||||
// content.data from userInfo["body"] only, ignoring top-level sibling keys.
|
||||
func TestBuildAPSPayload_alertNestsDataUnderBody(t *testing.T) {
|
||||
msg := push.PushMessage{
|
||||
Title: "x",
|
||||
Body: "y",
|
||||
Data: map[string]interface{}{"thread": "abc", "deeplink": "anchat://room/42"},
|
||||
}
|
||||
raw, _ := buildAPSPayload(msg)
|
||||
raw, _ := buildAPSPayload(msg, KindAlert)
|
||||
var out map[string]interface{}
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
t.Fatalf("payload not valid JSON: %v", err)
|
||||
}
|
||||
if _, hasAPS := out["aps"]; !hasAPS {
|
||||
t.Error("payload missing aps")
|
||||
}
|
||||
if out["thread"] != "abc" {
|
||||
t.Errorf("data.thread missing; got %v", out)
|
||||
// Must NOT be at the top level (expo would ignore it there).
|
||||
if _, leaked := out["thread"]; leaked {
|
||||
t.Errorf("data leaked to top level; expo-notifications would drop it: %v", out)
|
||||
}
|
||||
if out["deeplink"] != "anchat://room/42" {
|
||||
t.Errorf("data.deeplink missing; got %v", out)
|
||||
body, ok := out["body"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("alert data not nested under top-level \"body\" object; got %v", out)
|
||||
}
|
||||
if body["thread"] != "abc" || body["deeplink"] != "anchat://room/42" {
|
||||
t.Errorf("body envelope missing data; got %v", body)
|
||||
}
|
||||
// The human-readable alert body stays under aps.alert.body, distinct from
|
||||
// the data envelope key.
|
||||
aps := out["aps"].(map[string]interface{})
|
||||
if alert, ok := aps["alert"].(map[string]interface{}); !ok || alert["body"] != "y" {
|
||||
t.Errorf("aps.alert.body should be the human-readable body; got %v", aps["alert"])
|
||||
}
|
||||
}
|
||||
|
||||
// VoIP pushes are handled by native PushKit (not expo-notifications), so
|
||||
// custom data stays at the top level of the dictionary payload.
|
||||
func TestBuildAPSPayload_voipKeepsDataTopLevel(t *testing.T) {
|
||||
msg := push.PushMessage{
|
||||
Data: map[string]interface{}{"callId": "c-1", "callerName": "Alice"},
|
||||
}
|
||||
raw, _ := buildAPSPayload(msg, KindVoIP)
|
||||
var out map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
t.Fatalf("payload not valid JSON: %v", err)
|
||||
}
|
||||
if out["callId"] != "c-1" || out["callerName"] != "Alice" {
|
||||
t.Errorf("voip data must stay top-level for PushKit; got %v", out)
|
||||
}
|
||||
if _, nested := out["body"]; nested {
|
||||
t.Errorf("voip data must NOT be nested under body; got %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,7 +234,7 @@ func TestBuildAPSPayload_dataCannotClobberAPS(t *testing.T) {
|
||||
Title: "x",
|
||||
Data: map[string]interface{}{"aps": "evil"},
|
||||
}
|
||||
raw, _ := buildAPSPayload(msg)
|
||||
raw, _ := buildAPSPayload(msg, KindAlert)
|
||||
var out map[string]interface{}
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
apsField, ok := out["aps"]
|
||||
@ -215,7 +250,7 @@ func TestBuildAPSPayload_badgeAndSound(t *testing.T) {
|
||||
msg := push.PushMessage{
|
||||
Title: "x", Badge: 3, Sound: "ding.caf",
|
||||
}
|
||||
raw, _ := buildAPSPayload(msg)
|
||||
raw, _ := buildAPSPayload(msg, KindAlert)
|
||||
if !strings.Contains(string(raw), `"badge":3`) {
|
||||
t.Errorf("badge not in payload: %s", raw)
|
||||
}
|
||||
@ -226,7 +261,7 @@ func TestBuildAPSPayload_badgeAndSound(t *testing.T) {
|
||||
|
||||
func TestBuildAPSPayload_channelMapsToThreadID(t *testing.T) {
|
||||
msg := push.PushMessage{Title: "x", Channel: "messages"}
|
||||
raw, _ := buildAPSPayload(msg)
|
||||
raw, _ := buildAPSPayload(msg, KindAlert)
|
||||
if !strings.Contains(string(raw), `"thread-id":"messages"`) {
|
||||
t.Errorf("channel not mapped to thread-id: %s", raw)
|
||||
}
|
||||
|
||||
@ -1,18 +1,28 @@
|
||||
// Package ntfy implements a push.PushProvider backed by an ntfy server.
|
||||
//
|
||||
// ntfy delivers notifications via plain HTTP POST to <baseURL>/<topic>.
|
||||
// We map PushMessage fields to ntfy headers:
|
||||
// - Title -> "Title"
|
||||
// - Priority -> "Priority"
|
||||
// - Channel -> "Tags"
|
||||
// - Data -> base64-encoded JSON in "X-Data"
|
||||
// We map PushMessage fields to the ntfy publish surface:
|
||||
// - Title -> "Title" header
|
||||
// - Priority -> "Priority" header
|
||||
// - Channel -> "Tags" header
|
||||
// - Body -> the POST body (ntfy's "message", relayed verbatim)
|
||||
// - Data -> the POST body as JSON, ONLY when Body is empty
|
||||
//
|
||||
// See https://docs.ntfy.sh/publish/#publish-as-json for details.
|
||||
// IMPORTANT (bugboard #126): ntfy does NOT relay arbitrary `X-*` request
|
||||
// headers into the subscriber stream — only its recognized publish headers
|
||||
// (Title, Priority, Tags, Click, Actions, Attach, …) and the message body
|
||||
// reach the client. So structured Data and a numeric Badge cannot be carried
|
||||
// as custom headers; the only field a subscriber reliably receives besides
|
||||
// title/priority/tags is the message BODY. We therefore deliver Data through
|
||||
// the body (UnifiedPush convention: the body IS the payload). A caller that
|
||||
// sets an explicit Body owns it — to ship structured data alongside a
|
||||
// human-readable body, encode both into the Body envelope.
|
||||
//
|
||||
// See https://docs.ntfy.sh/publish/ for the recognized header set.
|
||||
package ntfy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -79,7 +89,20 @@ func (p *Provider) Send(ctx context.Context, msg push.PushMessage) error {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpointURL, strings.NewReader(msg.Body))
|
||||
// Determine the POST body — the only structured payload ntfy relays to
|
||||
// subscribers (bugboard #126). A caller-supplied Body wins; otherwise, if
|
||||
// there's structured Data, serialize it as the body so a data-only push
|
||||
// still reaches the client (UnifiedPush convention: body == payload).
|
||||
body := msg.Body
|
||||
if body == "" && len(msg.Data) > 0 {
|
||||
b, err := json.Marshal(msg.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntfy: marshal data: %w", err)
|
||||
}
|
||||
body = string(b)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpointURL, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntfy: build request: %w", err)
|
||||
}
|
||||
@ -96,16 +119,10 @@ func (p *Provider) Send(ctx context.Context, msg push.PushMessage) error {
|
||||
// ntfy uses "Tags" for both visual emoji and operator-defined tags.
|
||||
req.Header.Set("Tags", msg.Channel)
|
||||
}
|
||||
if msg.Badge > 0 {
|
||||
req.Header.Set("X-Badge", fmt.Sprintf("%d", msg.Badge))
|
||||
}
|
||||
if len(msg.Data) > 0 {
|
||||
b, err := json.Marshal(msg.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntfy: marshal data: %w", err)
|
||||
}
|
||||
req.Header.Set("X-Data", base64.StdEncoding.EncodeToString(b))
|
||||
}
|
||||
// NOTE: Badge and arbitrary Data are intentionally NOT sent as custom
|
||||
// headers — ntfy does not relay `X-*` headers to subscribers (#126), so
|
||||
// doing so silently drops them. Data rides the body (above); a badge
|
||||
// count, if needed, must be encoded into the body by the caller.
|
||||
if p.authToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+p.authToken)
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package ntfy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -17,11 +16,11 @@ import (
|
||||
|
||||
func TestSend_happy_path(t *testing.T) {
|
||||
var (
|
||||
gotPath string
|
||||
gotBody string
|
||||
gotTitle string
|
||||
gotPath string
|
||||
gotBody string
|
||||
gotTitle string
|
||||
gotPriority string
|
||||
gotAuth string
|
||||
gotAuth string
|
||||
)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
@ -61,9 +60,14 @@ func TestSend_happy_path(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_includes_data_header_when_data_set(t *testing.T) {
|
||||
var gotData string
|
||||
// Bugboard #126: ntfy does not relay X-* headers to subscribers, so Data must
|
||||
// ride the body. With no explicit Body, a data-only push serializes Data as
|
||||
// the JSON body — and must NOT set the dead X-Data header.
|
||||
func TestSend_dataOnly_ridesBody_noXDataHeader(t *testing.T) {
|
||||
var gotBody, gotData string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
gotBody = string(b)
|
||||
gotData = r.Header.Get("X-Data")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
@ -72,39 +76,49 @@ func TestSend_includes_data_header_when_data_set(t *testing.T) {
|
||||
p := New(Config{BaseURL: srv.URL}, nil)
|
||||
err := p.Send(context.Background(), push.PushMessage{
|
||||
DeviceToken: "topic",
|
||||
Body: "x",
|
||||
Data: map[string]interface{}{"call_id": "abc-123"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Send: %v", err)
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(gotData)
|
||||
if err != nil {
|
||||
t.Fatalf("X-Data not valid base64: %v", err)
|
||||
if gotData != "" {
|
||||
t.Errorf("X-Data header must not be set (ntfy drops it); got %q", gotData)
|
||||
}
|
||||
var got map[string]interface{}
|
||||
if err := json.Unmarshal(decoded, &got); err != nil {
|
||||
t.Fatalf("X-Data not valid JSON: %v", err)
|
||||
if err := json.Unmarshal([]byte(gotBody), &got); err != nil {
|
||||
t.Fatalf("data-only body not valid JSON: %v (body=%q)", err, gotBody)
|
||||
}
|
||||
if got["call_id"] != "abc-123" {
|
||||
t.Errorf("data round-trip failed: got %v", got)
|
||||
t.Errorf("data did not ride the body: got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_no_data_no_data_header(t *testing.T) {
|
||||
var gotData string
|
||||
// An explicit Body wins — Data does NOT clobber a caller-supplied body (the
|
||||
// caller owns the envelope; this is anchat's call-push pattern).
|
||||
func TestSend_explicitBody_winsOverData(t *testing.T) {
|
||||
var gotBody, gotData string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
gotBody = string(b)
|
||||
gotData = r.Header.Get("X-Data")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := New(Config{BaseURL: srv.URL}, nil)
|
||||
if err := p.Send(context.Background(), push.PushMessage{DeviceToken: "t", Body: "x"}); err != nil {
|
||||
t.Fatal(err)
|
||||
err := p.Send(context.Background(), push.PushMessage{
|
||||
DeviceToken: "topic",
|
||||
Body: `{"type":"call.invite","callId":"c1"}`,
|
||||
Data: map[string]interface{}{"ignored": "yes"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Send: %v", err)
|
||||
}
|
||||
if gotBody != `{"type":"call.invite","callId":"c1"}` {
|
||||
t.Errorf("explicit body not preserved; got %q", gotBody)
|
||||
}
|
||||
if gotData != "" {
|
||||
t.Errorf("expected no X-Data header, got %q", gotData)
|
||||
t.Errorf("X-Data header must not be set; got %q", gotData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@debros/orama",
|
||||
"version": "0.122.46",
|
||||
"version": "0.122.47",
|
||||
"description": "TypeScript SDK for Orama Network - Database, PubSub, Cache, Storage, Vault, and more",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user