2023-05-29 17:57:21 +02:00
package server
import (
2023-06-02 13:22:54 +02:00
"encoding/json"
"fmt"
2023-06-09 05:09:38 +02:00
"github.com/stretchr/testify/require"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
2023-05-29 17:57:21 +02:00
"io"
"net/http"
"net/http/httptest"
2023-06-17 03:59:07 +02:00
"net/netip"
2023-05-30 20:23:03 +02:00
"strings"
2023-05-29 17:57:21 +02:00
"sync/atomic"
"testing"
2023-06-10 05:17:48 +02:00
"time"
2023-05-29 17:57:21 +02:00
)
2023-06-02 16:52:35 +02:00
const (
2023-06-09 05:09:38 +02:00
testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF"
2023-06-02 16:52:35 +02:00
)
2023-06-17 21:40:08 +02:00
func TestServer_WebPush_Disabled ( t * testing . T ) {
s := newTestServer ( t , newTestConfig ( t ) )
response := request ( t , s , "POST" , "/v1/webpush" , payloadForTopics ( t , [ ] string { "test-topic" } , testWebPushEndpoint ) , nil )
require . Equal ( t , 404 , response . Code )
}
2023-06-02 13:22:54 +02:00
func TestServer_WebPush_TopicAdd ( t * testing . T ) {
2023-05-29 17:57:21 +02:00
s := newTestServer ( t , newTestConfigWithWebPush ( t ) )
2023-06-17 20:44:55 +02:00
response := request ( t , s , "POST" , "/v1/webpush" , payloadForTopics ( t , [ ] string { "test-topic" } , testWebPushEndpoint ) , nil )
2023-05-29 17:57:21 +02:00
require . Equal ( t , 200 , response . Code )
require . Equal ( t , ` { "success":true} ` + "\n" , response . Body . String ( ) )
2023-05-30 20:23:03 +02:00
subs , err := s . webPush . SubscriptionsForTopic ( "test-topic" )
2023-05-31 18:02:04 +02:00
require . Nil ( t , err )
2023-05-29 17:57:21 +02:00
require . Len ( t , subs , 1 )
2023-06-09 05:09:38 +02:00
require . Equal ( t , subs [ 0 ] . Endpoint , testWebPushEndpoint )
require . Equal ( t , subs [ 0 ] . P256dh , "p256dh-key" )
require . Equal ( t , subs [ 0 ] . Auth , "auth-key" )
2023-05-30 20:23:03 +02:00
require . Equal ( t , subs [ 0 ] . UserID , "" )
2023-05-29 17:57:21 +02:00
}
2023-06-02 16:52:35 +02:00
func TestServer_WebPush_TopicAdd_InvalidEndpoint ( t * testing . T ) {
s := newTestServer ( t , newTestConfigWithWebPush ( t ) )
2023-06-17 20:44:55 +02:00
response := request ( t , s , "POST" , "/v1/webpush" , payloadForTopics ( t , [ ] string { "test-topic" } , "https://ddos-target.example.com/webpush" ) , nil )
2023-06-02 16:52:35 +02:00
require . Equal ( t , 400 , response . Code )
require . Equal ( t , ` { "code":40039,"http":400,"error":"invalid request: web push endpoint unknown"} ` + "\n" , response . Body . String ( ) )
}
func TestServer_WebPush_TopicAdd_TooManyTopics ( t * testing . T ) {
s := newTestServer ( t , newTestConfigWithWebPush ( t ) )
topicList := make ( [ ] string , 51 )
for i := range topicList {
topicList [ i ] = util . RandomString ( 5 )
}
2023-06-17 20:44:55 +02:00
response := request ( t , s , "POST" , "/v1/webpush" , payloadForTopics ( t , topicList , testWebPushEndpoint ) , nil )
2023-06-02 16:52:35 +02:00
require . Equal ( t , 400 , response . Code )
require . Equal ( t , ` { "code":40040,"http":400,"error":"invalid request: too many web push topic subscriptions"} ` + "\n" , response . Body . String ( ) )
}
2023-06-02 13:22:54 +02:00
func TestServer_WebPush_TopicUnsubscribe ( t * testing . T ) {
s := newTestServer ( t , newTestConfigWithWebPush ( t ) )
2023-06-09 05:09:38 +02:00
addSubscription ( t , s , testWebPushEndpoint , "test-topic" )
2023-06-02 13:22:54 +02:00
requireSubscriptionCount ( t , s , "test-topic" , 1 )
2023-06-17 20:44:55 +02:00
response := request ( t , s , "POST" , "/v1/webpush" , payloadForTopics ( t , [ ] string { } , testWebPushEndpoint ) , nil )
2023-06-02 13:22:54 +02:00
require . Equal ( t , 200 , response . Code )
require . Equal ( t , ` { "success":true} ` + "\n" , response . Body . String ( ) )
requireSubscriptionCount ( t , s , "test-topic" , 0 )
}
2023-06-17 21:44:21 +02:00
func TestServer_WebPush_Delete ( t * testing . T ) {
s := newTestServer ( t , newTestConfigWithWebPush ( t ) )
addSubscription ( t , s , testWebPushEndpoint , "test-topic" )
requireSubscriptionCount ( t , s , "test-topic" , 1 )
response := request ( t , s , "DELETE" , "/v1/webpush" , fmt . Sprintf ( ` { "endpoint":"%s"} ` , testWebPushEndpoint ) , nil )
require . Equal ( t , 200 , response . Code )
require . Equal ( t , ` { "success":true} ` + "\n" , r esponse . Body . String ( ) )
requireSubscriptionCount ( t , s , "test-topic" , 0 )
}
2023-05-29 17:57:21 +02:00
func TestServer_WebPush_TopicSubscribeProtected_Allowed ( t * testing . T ) {
config := configureAuth ( t , newTestConfigWithWebPush ( t ) )
config . AuthDefault = user . PermissionDenyAll
s := newTestServer ( t , config )
require . Nil ( t , s . userManager . AddUser ( "ben" , "ben" , user . RoleUser ) )
require . Nil ( t , s . userManager . AllowAccess ( "ben" , "test-topic" , user . PermissionReadWrite ) )
2023-06-17 20:44:55 +02:00
response := request ( t , s , "POST" , "/v1/webpush" , payloadForTopics ( t , [ ] string { "test-topic" } , testWebPushEndpoint ) , map [ string ] string {
2023-05-29 17:57:21 +02:00
"Authorization" : util . BasicAuth ( "ben" , "ben" ) ,
} )
require . Equal ( t , 200 , response . Code )
require . Equal ( t , ` { "success":true} ` + "\n" , response . Body . String ( ) )
2023-05-30 20:23:03 +02:00
subs , err := s . webPush . SubscriptionsForTopic ( "test-topic" )
require . Nil ( t , err )
2023-05-29 17:57:21 +02:00
require . Len ( t , subs , 1 )
2023-05-30 20:23:03 +02:00
require . True ( t , strings . HasPrefix ( subs [ 0 ] . UserID , "u_" ) )
2023-05-29 17:57:21 +02:00
}
func TestServer_WebPush_TopicSubscribeProtected_Denied ( t * testing . T ) {
config := configureAuth ( t , newTestConfigWithWebPush ( t ) )
config . AuthDefault = user . PermissionDenyAll
s := newTestServer ( t , config )
2023-06-17 20:44:55 +02:00
response := request ( t , s , "POST" , "/v1/webpush" , payloadForTopics ( t , [ ] string { "test-topic" } , testWebPushEndpoint ) , nil )
2023-05-29 17:57:21 +02:00
require . Equal ( t , 403 , response . Code )
requireSubscriptionCount ( t , s , "test-topic" , 0 )
}
func TestServer_WebPush_DeleteAccountUnsubscribe ( t * testing . T ) {
config := configureAuth ( t , newTestConfigWithWebPush ( t ) )
s := newTestServer ( t , config )
require . Nil ( t , s . userManager . AddUser ( "ben" , "ben" , user . RoleUser ) )
require . Nil ( t , s . userManager . AllowAccess ( "ben" , "test-topic" , user . PermissionReadWrite ) )
2023-06-17 20:44:55 +02:00
response := request ( t , s , "POST" , "/v1/webpush" , payloadForTopics ( t , [ ] string { "test-topic" } , testWebPushEndpoint ) , map [ string ] string {
2023-05-29 17:57:21 +02:00
"Authorization" : util . BasicAuth ( "ben" , "ben" ) ,
} )
require . Equal ( t , 200 , response . Code )
require . Equal ( t , ` { "success":true} ` + "\n" , response . Body . String ( ) )
requireSubscriptionCount ( t , s , "test-topic" , 1 )
request ( t , s , "DELETE" , "/v1/account" , ` { "password":"ben"} ` , map [ string ] string {
"Authorization" : util . BasicAuth ( "ben" , "ben" ) ,
} )
// should've been deleted with the account
requireSubscriptionCount ( t , s , "test-topic" , 0 )
}
func TestServer_WebPush_Publish ( t * testing . T ) {
s := newTestServer ( t , newTestConfigWithWebPush ( t ) )
var received atomic . Bool
2023-06-02 16:52:35 +02:00
pushService := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2023-05-29 17:57:21 +02:00
_ , err := io . ReadAll ( r . Body )
require . Nil ( t , err )
require . Equal ( t , "/push-receive" , r . URL . Path )
require . Equal ( t , "high" , r . Header . Get ( "Urgency" ) )
require . Equal ( t , "" , r . Header . Get ( "Topic" ) )
received . Store ( true )
} ) )
2023-06-02 16:52:35 +02:00
defer pushService . Close ( )
2023-05-29 17:57:21 +02:00
2023-06-09 05:09:38 +02:00
addSubscription ( t , s , pushService . URL + "/push-receive" , "test-topic" )
2023-06-11 03:09:01 +02:00
request ( t , s , "POST" , "/test-topic" , "web push test" , nil )
2023-05-29 17:57:21 +02:00
waitFor ( t , func ( ) bool {
return received . Load ( )
} )
}
2023-06-09 03:45:52 +02:00
func TestServer_WebPush_Publish_RemoveOnError ( t * testing . T ) {
2023-05-29 17:57:21 +02:00
s := newTestServer ( t , newTestConfigWithWebPush ( t ) )
var received atomic . Bool
2023-06-02 16:52:35 +02:00
pushService := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2023-05-29 17:57:21 +02:00
_ , err := io . ReadAll ( r . Body )
require . Nil ( t , err )
2023-06-09 05:09:38 +02:00
w . WriteHeader ( http . StatusGone )
2023-05-29 17:57:21 +02:00
received . Store ( true )
} ) )
2023-06-02 16:52:35 +02:00
defer pushService . Close ( )
2023-05-29 17:57:21 +02:00
2023-06-09 05:09:38 +02:00
addSubscription ( t , s , pushService . URL + "/push-receive" , "test-topic" , "test-topic-abc" )
2023-05-29 17:57:21 +02:00
requireSubscriptionCount ( t , s , "test-topic" , 1 )
requireSubscriptionCount ( t , s , "test-topic-abc" , 1 )
2023-06-11 03:09:01 +02:00
request ( t , s , "POST" , "/test-topic" , "web push test" , nil )
2023-05-29 17:57:21 +02:00
waitFor ( t , func ( ) bool {
return received . Load ( )
} )
// Receiving the 410 should've caused the publisher to expire all subscriptions on the endpoint
requireSubscriptionCount ( t , s , "test-topic" , 0 )
requireSubscriptionCount ( t , s , "test-topic-abc" , 0 )
}
2023-06-02 14:45:05 +02:00
func TestServer_WebPush_Expiry ( t * testing . T ) {
s := newTestServer ( t , newTestConfigWithWebPush ( t ) )
var received atomic . Bool
2023-06-02 16:52:35 +02:00
pushService := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2023-06-02 14:45:05 +02:00
_ , err := io . ReadAll ( r . Body )
require . Nil ( t , err )
w . WriteHeader ( 200 )
w . Write ( [ ] byte ( ` ` ) )
received . Store ( true )
} ) )
2023-06-02 16:52:35 +02:00
defer pushService . Close ( )
2023-06-02 14:45:05 +02:00
2023-06-09 05:09:38 +02:00
addSubscription ( t , s , pushService . URL + "/push-receive" , "test-topic" )
2023-06-02 14:45:05 +02:00
requireSubscriptionCount ( t , s , "test-topic" , 1 )
2023-06-10 05:17:48 +02:00
_ , err := s . webPush . db . Exec ( "UPDATE subscription SET updated_at = ?" , time . Now ( ) . Add ( - 7 * 24 * time . Hour ) . Unix ( ) )
2023-06-02 14:45:05 +02:00
require . Nil ( t , err )
2023-06-10 05:17:48 +02:00
s . pruneAndNotifyWebPushSubscriptions ( )
2023-06-02 14:45:05 +02:00
requireSubscriptionCount ( t , s , "test-topic" , 1 )
waitFor ( t , func ( ) bool {
return received . Load ( )
} )
2023-06-10 05:17:48 +02:00
_ , err = s . webPush . db . Exec ( "UPDATE subscription SET updated_at = ?" , time . Now ( ) . Add ( - 9 * 24 * time . Hour ) . Unix ( ) )
2023-06-02 14:45:05 +02:00
require . Nil ( t , err )
2023-06-10 05:17:48 +02:00
s . pruneAndNotifyWebPushSubscriptions ( )
2023-06-09 03:45:52 +02:00
waitFor ( t , func ( ) bool {
subs , err := s . webPush . SubscriptionsForTopic ( "test-topic" )
require . Nil ( t , err )
return len ( subs ) == 0
} )
2023-06-02 14:45:05 +02:00
}
2023-06-02 16:52:35 +02:00
func payloadForTopics ( t * testing . T , topics [ ] string , endpoint string ) string {
2023-06-02 14:45:05 +02:00
topicsJSON , err := json . Marshal ( topics )
2023-06-02 13:22:54 +02:00
require . Nil ( t , err )
return fmt . Sprintf ( ` {
"topics" : % s ,
2023-06-09 05:09:38 +02:00
"endpoint" : "%s" ,
"p256dh" : "p256dh-key" ,
"auth" : "auth-key"
2023-06-02 16:52:35 +02:00
} ` , topicsJSON , endpoint )
2023-06-02 13:22:54 +02:00
}
2023-06-09 05:09:38 +02:00
func addSubscription ( t * testing . T , s * Server , endpoint string , topics ... string ) {
2023-06-17 03:59:07 +02:00
require . Nil ( t , s . webPush . UpsertSubscription ( endpoint , "kSC3T8aN1JCQxxPdrFLrZg" , "BMKKbxdUU_xLS7G1Wh5AN8PvWOjCzkCuKZYb8apcqYrDxjOF_2piggBnoJLQYx9IeSD70fNuwawI3e9Y8m3S3PE" , "u_123" , netip . MustParseAddr ( "1.2.3.4" ) , topics ) ) // Test auth and p256dh
2023-05-29 17:57:21 +02:00
}
func requireSubscriptionCount ( t * testing . T , s * Server , topic string , expectedLength int ) {
2023-06-09 05:09:38 +02:00
subs , err := s . webPush . SubscriptionsForTopic ( topic )
2023-05-31 18:02:04 +02:00
require . Nil ( t , err )
2023-05-29 17:57:21 +02:00
require . Len ( t , subs , expectedLength )
}