ntfy/server/server_firebase_test.go
Karmanyaah Malhotra c2382d29a1 refactor visitor IPs and allow exempting IP Ranges
Use netip.Addr instead of storing addresses as strings. This requires
conversions at the database level and in tests, but is more memory
efficient otherwise, and facilitates the following.

Parse rate limit exemptions as netip.Prefix. This allows storing IP
ranges in the exemption list. Regular IP addresses (entered explicitly
or resolved from hostnames) are IPV4/32, denoting a range of one
address.
2022-10-05 16:04:42 -05:00

341 lines
10 KiB
Go

package server
import (
"encoding/json"
"errors"
"fmt"
"net/netip"
"strings"
"sync"
"testing"
"firebase.google.com/go/v4/messaging"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/auth"
)
type testAuther struct {
Allow bool
}
func (t testAuther) Authenticate(_, _ string) (*auth.User, error) {
return nil, errors.New("not used")
}
func (t testAuther) Authorize(_ *auth.User, _ string, _ auth.Permission) error {
if t.Allow {
return nil
}
return errors.New("unauthorized")
}
type testFirebaseSender struct {
allowed int
messages []*messaging.Message
mu sync.Mutex
}
func newTestFirebaseSender(allowed int) *testFirebaseSender {
return &testFirebaseSender{
allowed: allowed,
messages: make([]*messaging.Message, 0),
}
}
func (s *testFirebaseSender) Send(m *messaging.Message) error {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.messages)+1 > s.allowed {
return errFirebaseQuotaExceeded
}
s.messages = append(s.messages, m)
return nil
}
func (s *testFirebaseSender) Messages() []*messaging.Message {
s.mu.Lock()
defer s.mu.Unlock()
return append(make([]*messaging.Message, 0), s.messages...)
}
func TestToFirebaseMessage_Keepalive(t *testing.T) {
m := newKeepaliveMessage("mytopic")
fbm, err := toFirebaseMessage(m, nil)
require.Nil(t, err)
require.Equal(t, "mytopic", fbm.Topic)
require.Nil(t, fbm.Android)
require.Equal(t, &messaging.APNSConfig{
Headers: map[string]string{
"apns-push-type": "background",
"apns-priority": "5",
},
Payload: &messaging.APNSPayload{
Aps: &messaging.Aps{
ContentAvailable: true,
},
CustomData: map[string]any{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
},
},
}, fbm.APNS)
require.Equal(t, map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
}, fbm.Data)
}
func TestToFirebaseMessage_Open(t *testing.T) {
m := newOpenMessage("mytopic")
fbm, err := toFirebaseMessage(m, nil)
require.Nil(t, err)
require.Equal(t, "mytopic", fbm.Topic)
require.Nil(t, fbm.Android)
require.Equal(t, &messaging.APNSConfig{
Headers: map[string]string{
"apns-push-type": "background",
"apns-priority": "5",
},
Payload: &messaging.APNSPayload{
Aps: &messaging.Aps{
ContentAvailable: true,
},
CustomData: map[string]any{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
},
},
}, fbm.APNS)
require.Equal(t, map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
}, fbm.Data)
}
func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
m := newDefaultMessage("mytopic", "this is a message")
m.Priority = 4
m.Tags = []string{"tag 1", "tag2"}
m.Click = "https://google.com"
m.Icon = "https://ntfy.sh/static/img/ntfy.png"
m.Title = "some title"
m.Actions = []*action{
{
ID: "123",
Action: "view",
Label: "Open page",
Clear: true,
URL: "https://ntfy.sh",
},
{
ID: "456",
Action: "http",
Label: "Close door",
URL: "https://door.com/close",
Method: "PUT",
Headers: map[string]string{
"really": "yes",
},
},
}
m.Attachment = &attachment{
Name: "some file.jpg",
Type: "image/jpeg",
Size: 12345,
Expires: 98765543,
URL: "https://example.com/file.jpg",
}
fbm, err := toFirebaseMessage(m, &testAuther{Allow: true})
require.Nil(t, err)
require.Equal(t, "mytopic", fbm.Topic)
require.Equal(t, &messaging.AndroidConfig{
Priority: "high",
}, fbm.Android)
require.Equal(t, &messaging.APNSConfig{
Payload: &messaging.APNSPayload{
Aps: &messaging.Aps{
MutableContent: true,
Alert: &messaging.ApsAlert{
Title: "some title",
Body: "this is a message",
},
},
CustomData: map[string]any{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "message",
"topic": "mytopic",
"priority": "4",
"tags": strings.Join(m.Tags, ","),
"click": "https://google.com",
"icon": "https://ntfy.sh/static/img/ntfy.png",
"title": "some title",
"message": "this is a message",
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
"encoding": "",
"attachment_name": "some file.jpg",
"attachment_type": "image/jpeg",
"attachment_size": "12345",
"attachment_expires": "98765543",
"attachment_url": "https://example.com/file.jpg",
},
},
}, fbm.APNS)
require.Equal(t, map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "message",
"topic": "mytopic",
"priority": "4",
"tags": strings.Join(m.Tags, ","),
"click": "https://google.com",
"icon": "https://ntfy.sh/static/img/ntfy.png",
"title": "some title",
"message": "this is a message",
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
"encoding": "",
"attachment_name": "some file.jpg",
"attachment_type": "image/jpeg",
"attachment_size": "12345",
"attachment_expires": "98765543",
"attachment_url": "https://example.com/file.jpg",
}, fbm.Data)
}
func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) {
m := newDefaultMessage("mytopic", "this is a message")
m.Priority = 5
fbm, err := toFirebaseMessage(m, &testAuther{Allow: false}) // Not allowed!
require.Nil(t, err)
require.Equal(t, "mytopic", fbm.Topic)
require.Equal(t, &messaging.AndroidConfig{
Priority: "high",
}, fbm.Android)
require.Equal(t, "", fbm.Data["message"])
require.Equal(t, "", fbm.Data["priority"])
require.Equal(t, map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "poll_request",
"topic": "mytopic",
}, fbm.Data)
}
func TestToFirebaseMessage_PollRequest(t *testing.T) {
m := newPollRequestMessage("mytopic", "fOv6k1QbCzo6")
fbm, err := toFirebaseMessage(m, nil)
require.Nil(t, err)
require.Equal(t, "mytopic", fbm.Topic)
require.Nil(t, fbm.Android)
require.Equal(t, &messaging.APNSConfig{
Payload: &messaging.APNSPayload{
Aps: &messaging.Aps{
MutableContent: true,
Alert: &messaging.ApsAlert{
Title: "",
Body: "New message",
},
},
CustomData: map[string]any{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "poll_request",
"topic": "mytopic",
"message": "New message",
"poll_id": "fOv6k1QbCzo6",
},
},
}, fbm.APNS)
require.Equal(t, map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "poll_request",
"topic": "mytopic",
"message": "New message",
"poll_id": "fOv6k1QbCzo6",
}, fbm.Data)
}
func TestMaybeTruncateFCMMessage(t *testing.T) {
origMessage := strings.Repeat("this is a long string", 300)
origFCMMessage := &messaging.Message{
Topic: "mytopic",
Data: map[string]string{
"id": "abcdefg",
"time": "1641324761",
"event": "message",
"topic": "mytopic",
"priority": "0",
"tags": "",
"title": "",
"message": origMessage,
},
Android: &messaging.AndroidConfig{
Priority: "high",
},
}
origMessageLength := len(origFCMMessage.Data["message"])
serializedOrigFCMMessage, _ := json.Marshal(origFCMMessage)
require.Greater(t, len(serializedOrigFCMMessage), fcmMessageLimit) // Pre-condition
truncatedFCMMessage := maybeTruncateFCMMessage(origFCMMessage)
truncatedMessageLength := len(truncatedFCMMessage.Data["message"])
serializedTruncatedFCMMessage, _ := json.Marshal(truncatedFCMMessage)
require.Equal(t, fcmMessageLimit, len(serializedTruncatedFCMMessage))
require.Equal(t, "1", truncatedFCMMessage.Data["truncated"])
require.NotEqual(t, origMessageLength, truncatedMessageLength)
}
func TestMaybeTruncateFCMMessage_NotTooLong(t *testing.T) {
origMessage := "not really a long string"
origFCMMessage := &messaging.Message{
Topic: "mytopic",
Data: map[string]string{
"id": "abcdefg",
"time": "1641324761",
"event": "message",
"topic": "mytopic",
"priority": "0",
"tags": "",
"title": "",
"message": origMessage,
},
}
origMessageLength := len(origFCMMessage.Data["message"])
serializedOrigFCMMessage, _ := json.Marshal(origFCMMessage)
require.LessOrEqual(t, len(serializedOrigFCMMessage), fcmMessageLimit) // Pre-condition
notTruncatedFCMMessage := maybeTruncateFCMMessage(origFCMMessage)
notTruncatedMessageLength := len(notTruncatedFCMMessage.Data["message"])
serializedNotTruncatedFCMMessage, _ := json.Marshal(notTruncatedFCMMessage)
require.Equal(t, origMessageLength, notTruncatedMessageLength)
require.Equal(t, len(serializedOrigFCMMessage), len(serializedNotTruncatedFCMMessage))
require.Equal(t, "", notTruncatedFCMMessage.Data["truncated"])
}
func TestToFirebaseSender_Abuse(t *testing.T) {
sender := &testFirebaseSender{allowed: 2}
client := newFirebaseClient(sender, &testAuther{})
visitor := newVisitor(newTestConfig(t), newMemTestCache(t), netip.MustParseAddr("1.2.3.4"))
require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"}))
require.Equal(t, 1, len(sender.Messages()))
require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"}))
require.Equal(t, 2, len(sender.Messages()))
require.Equal(t, errFirebaseQuotaExceeded, client.Send(visitor, &message{Topic: "mytopic"}))
require.Equal(t, 2, len(sender.Messages()))
sender.messages = make([]*messaging.Message, 0) // Reset to test that time limit is working
require.Equal(t, errFirebaseTemporarilyBanned, client.Send(visitor, &message{Topic: "mytopic"}))
require.Equal(t, 0, len(sender.Messages()))
}