From 533143766434ed448a8688c709933018a255a4a6 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 16 Feb 2026 12:38:00 -0500 Subject: [PATCH] Unify webpush store tests across SQLite and PostgreSQL backends Share test logic in store_test.go with thin per-backend wrappers. Add SetSubscriptionUpdatedAt to both stores, removing the need for raw SQL and the DB() accessor in tests. --- server/server_webpush_test.go | 9 +- webpush/postgres.go | 12 ++- webpush/postgres_test.go | 186 ++++++---------------------------- webpush/sqlite.go | 12 ++- webpush/sqlite_test.go | 178 +++----------------------------- webpush/store_test.go | 169 ++++++++++++++++++++++++++++++ 6 files changed, 233 insertions(+), 333 deletions(-) create mode 100644 webpush/store_test.go diff --git a/server/server_webpush_test.go b/server/server_webpush_test.go index fe77787e..03101578 100644 --- a/server/server_webpush_test.go +++ b/server/server_webpush_test.go @@ -237,11 +237,11 @@ func TestServer_WebPush_Expiry(t *testing.T) { })) defer pushService.Close() - addSubscription(t, s, pushService.URL+"/push-receive", "test-topic") + endpoint := pushService.URL + "/push-receive" + addSubscription(t, s, endpoint, "test-topic") requireSubscriptionCount(t, s, "test-topic", 1) - _, err := s.webPush.(*wpush.SQLiteStore).DB().Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-55*24*time.Hour).Unix()) - require.Nil(t, err) + require.Nil(t, s.webPush.(*wpush.SQLiteStore).SetSubscriptionUpdatedAt(endpoint, time.Now().Add(-55*24*time.Hour).Unix())) s.pruneAndNotifyWebPushSubscriptions() requireSubscriptionCount(t, s, "test-topic", 1) @@ -250,8 +250,7 @@ func TestServer_WebPush_Expiry(t *testing.T) { return received.Load() }) - _, err = s.webPush.(*wpush.SQLiteStore).DB().Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-60*24*time.Hour).Unix()) - require.Nil(t, err) + require.Nil(t, s.webPush.(*wpush.SQLiteStore).SetSubscriptionUpdatedAt(endpoint, time.Now().Add(-60*24*time.Hour).Unix())) s.pruneAndNotifyWebPushSubscriptions() waitFor(t, func() bool { diff --git a/webpush/postgres.go b/webpush/postgres.go index bc6a984e..6d4a1027 100644 --- a/webpush/postgres.go +++ b/webpush/postgres.go @@ -94,11 +94,6 @@ func NewPostgresStore(dsn string) (*PostgresStore, error) { }, nil } -// DB returns the underlying database connection. This is exported for testing purposes. -func (c *PostgresStore) DB() *sql.DB { - return c.db -} - func setupPostgresDB(db *sql.DB) error { // If 'wp_schema_version' table does not exist, this must be a new database rows, err := db.Query(pgSelectSchemaVersionQuery) @@ -218,6 +213,13 @@ func (c *PostgresStore) RemoveExpiredSubscriptions(expireAfter time.Duration) er return err } +// SetSubscriptionUpdatedAt updates the updated_at timestamp for a subscription by endpoint. This is +// exported for testing purposes and is not part of the Store interface. +func (c *PostgresStore) SetSubscriptionUpdatedAt(endpoint string, updatedAt int64) error { + _, err := c.db.Exec("UPDATE webpush_subscription SET updated_at = $1 WHERE endpoint = $2", updatedAt, endpoint) + return err +} + // Close closes the underlying database connection. func (c *PostgresStore) Close() error { return c.db.Close() diff --git a/webpush/postgres_test.go b/webpush/postgres_test.go index 2cb77bc8..77334839 100644 --- a/webpush/postgres_test.go +++ b/webpush/postgres_test.go @@ -1,13 +1,14 @@ package webpush_test import ( + "database/sql" "fmt" - "net/netip" + "net/url" "os" "testing" - "time" "github.com/stretchr/testify/require" + "heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/webpush" ) @@ -16,192 +17,67 @@ func newTestPostgresStore(t *testing.T) *webpush.PostgresStore { if dsn == "" { t.Skip("NTFY_TEST_DATABASE_URL not set, skipping PostgreSQL tests") } - store, err := webpush.NewPostgresStore(dsn) + // Create a unique schema for this test + schema := fmt.Sprintf("test_%s", util.RandomString(10)) + setupDB, err := sql.Open("pgx", dsn) + require.Nil(t, err) + _, err = setupDB.Exec(fmt.Sprintf("CREATE SCHEMA %s", schema)) + require.Nil(t, err) + require.Nil(t, setupDB.Close()) + // Open store with search_path set to the new schema + u, err := url.Parse(dsn) + require.Nil(t, err) + q := u.Query() + q.Set("search_path", schema) + u.RawQuery = q.Encode() + store, err := webpush.NewPostgresStore(u.String()) require.Nil(t, err) t.Cleanup(func() { - // Clean up tables after each test - db := store.DB() - db.Exec("DELETE FROM webpush_subscription_topic") - db.Exec("DELETE FROM webpush_subscription") store.Close() + cleanDB, err := sql.Open("pgx", dsn) + if err == nil { + cleanDB.Exec(fmt.Sprintf("DROP SCHEMA %s CASCADE", schema)) + cleanDB.Close() + } }) - // Clean up tables before test - db := store.DB() - db.Exec("DELETE FROM webpush_subscription_topic") - db.Exec("DELETE FROM webpush_subscription") return store } func TestPostgresStore_UpsertSubscription_SubscriptionsForTopic(t *testing.T) { - store := newTestPostgresStore(t) - - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - - subs, err := store.SubscriptionsForTopic("test-topic") - require.Nil(t, err) - require.Len(t, subs, 1) - require.Equal(t, subs[0].Endpoint, testWebPushEndpoint) - require.Equal(t, subs[0].P256dh, "p256dh-key") - require.Equal(t, subs[0].Auth, "auth-key") - require.Equal(t, subs[0].UserID, "u_1234") - - subs2, err := store.SubscriptionsForTopic("mytopic") - require.Nil(t, err) - require.Len(t, subs2, 1) - require.Equal(t, subs[0].Endpoint, subs2[0].Endpoint) + testStoreUpsertSubscription_SubscriptionsForTopic(t, newTestPostgresStore(t)) } func TestPostgresStore_UpsertSubscription_SubscriberIPLimitReached(t *testing.T) { - store := newTestPostgresStore(t) - - // Insert 10 subscriptions with the same IP address - for i := 0; i < 10; i++ { - endpoint := fmt.Sprintf(testWebPushEndpoint+"%d", i) - require.Nil(t, store.UpsertSubscription(endpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - } - - // Another one for the same endpoint should be fine - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - - // But with a different endpoint it should fail - require.Equal(t, webpush.ErrWebPushTooManySubscriptions, store.UpsertSubscription(testWebPushEndpoint+"11", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - - // But with a different IP address it should be fine again - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"99", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("9.9.9.9"), []string{"test-topic", "mytopic"})) + testStoreUpsertSubscription_SubscriberIPLimitReached(t, newTestPostgresStore(t)) } func TestPostgresStore_UpsertSubscription_UpdateTopics(t *testing.T) { - store := newTestPostgresStore(t) - - // Insert subscription with two topics, and another with one topic - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"1", "auth-key", "p256dh-key", "", netip.MustParseAddr("9.9.9.9"), []string{"topic1"})) - - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 2) - require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) - require.Equal(t, testWebPushEndpoint+"1", subs[1].Endpoint) - - subs, err = store.SubscriptionsForTopic("topic2") - require.Nil(t, err) - require.Len(t, subs, 1) - require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) - - // Update the first subscription to have only one topic - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1"})) - - subs, err = store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 2) - require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) - - subs, err = store.SubscriptionsForTopic("topic2") - require.Nil(t, err) - require.Len(t, subs, 0) + testStoreUpsertSubscription_UpdateTopics(t, newTestPostgresStore(t)) } func TestPostgresStore_RemoveSubscriptionsByEndpoint(t *testing.T) { - store := newTestPostgresStore(t) - - // Insert subscription with two topics - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // And remove it again - require.Nil(t, store.RemoveSubscriptionsByEndpoint(testWebPushEndpoint)) - subs, err = store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 0) + testStoreRemoveSubscriptionsByEndpoint(t, newTestPostgresStore(t)) } func TestPostgresStore_RemoveSubscriptionsByUserID(t *testing.T) { - store := newTestPostgresStore(t) - - // Insert subscription with two topics - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // And remove it again - require.Nil(t, store.RemoveSubscriptionsByUserID("u_1234")) - subs, err = store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 0) + testStoreRemoveSubscriptionsByUserID(t, newTestPostgresStore(t)) } func TestPostgresStore_RemoveSubscriptionsByUserID_Empty(t *testing.T) { - store := newTestPostgresStore(t) - require.Equal(t, webpush.ErrWebPushUserIDCannotBeEmpty, store.RemoveSubscriptionsByUserID("")) + testStoreRemoveSubscriptionsByUserID_Empty(t, newTestPostgresStore(t)) } func TestPostgresStore_MarkExpiryWarningSent(t *testing.T) { store := newTestPostgresStore(t) - - // Insert subscription with two topics - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // Mark them as warning sent - require.Nil(t, store.MarkExpiryWarningSent(subs)) - - rows, err := store.DB().Query("SELECT endpoint FROM webpush_subscription WHERE warned_at > 0") - require.Nil(t, err) - defer rows.Close() - var endpoint string - require.True(t, rows.Next()) - require.Nil(t, rows.Scan(&endpoint)) - require.Nil(t, err) - require.Equal(t, testWebPushEndpoint, endpoint) - require.False(t, rows.Next()) + testStoreMarkExpiryWarningSent(t, store, store.SetSubscriptionUpdatedAt) } func TestPostgresStore_SubscriptionsExpiring(t *testing.T) { store := newTestPostgresStore(t) - - // Insert subscription with two topics - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // Fake-mark them as soon-to-expire - _, err = store.DB().Exec("UPDATE webpush_subscription SET updated_at = $1 WHERE endpoint = $2", time.Now().Add(-8*24*time.Hour).Unix(), testWebPushEndpoint) - require.Nil(t, err) - - // Should not be cleaned up yet - require.Nil(t, store.RemoveExpiredSubscriptions(9*24*time.Hour)) - - // Run expiration - subs, err = store.SubscriptionsExpiring(7 * 24 * time.Hour) - require.Nil(t, err) - require.Len(t, subs, 1) - require.Equal(t, testWebPushEndpoint, subs[0].Endpoint) + testStoreSubscriptionsExpiring(t, store, store.SetSubscriptionUpdatedAt) } func TestPostgresStore_RemoveExpiredSubscriptions(t *testing.T) { store := newTestPostgresStore(t) - - // Insert subscription with two topics - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // Fake-mark them as expired - _, err = store.DB().Exec("UPDATE webpush_subscription SET updated_at = $1 WHERE endpoint = $2", time.Now().Add(-10*24*time.Hour).Unix(), testWebPushEndpoint) - require.Nil(t, err) - - // Run expiration - require.Nil(t, store.RemoveExpiredSubscriptions(9*24*time.Hour)) - - // List again, should be 0 - subs, err = store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 0) + testStoreRemoveExpiredSubscriptions(t, store, store.SetSubscriptionUpdatedAt) } diff --git a/webpush/sqlite.go b/webpush/sqlite.go index a7871f97..2483fa93 100644 --- a/webpush/sqlite.go +++ b/webpush/sqlite.go @@ -101,11 +101,6 @@ func NewSQLiteStore(filename, startupQueries string) (*SQLiteStore, error) { }, nil } -// DB returns the underlying database connection. This is exported for testing purposes. -func (c *SQLiteStore) DB() *sql.DB { - return c.db -} - func setupSQLiteWebPushDB(db *sql.DB) error { // If 'schemaVersion' table does not exist, this must be a new database rows, err := db.Query(sqliteSelectWebPushSchemaVersionQuery) @@ -256,6 +251,13 @@ func (c *SQLiteStore) RemoveExpiredSubscriptions(expireAfter time.Duration) erro return err } +// SetSubscriptionUpdatedAt updates the updated_at timestamp for a subscription by endpoint. This is +// exported for testing purposes and is not part of the Store interface. +func (c *SQLiteStore) SetSubscriptionUpdatedAt(endpoint string, updatedAt int64) error { + _, err := c.db.Exec("UPDATE subscription SET updated_at = ? WHERE endpoint = ?", updatedAt, endpoint) + return err +} + // Close closes the underlying database connection. func (c *SQLiteStore) Close() error { return c.db.Close() diff --git a/webpush/sqlite_test.go b/webpush/sqlite_test.go index c43eca86..b64a93b9 100644 --- a/webpush/sqlite_test.go +++ b/webpush/sqlite_test.go @@ -1,203 +1,55 @@ package webpush_test import ( - "fmt" - "net/netip" "path/filepath" "testing" - "time" "github.com/stretchr/testify/require" "heckel.io/ntfy/v2/webpush" ) -const testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF" +func newTestSQLiteStore(t *testing.T) *webpush.SQLiteStore { + store, err := webpush.NewSQLiteStore(filepath.Join(t.TempDir(), "webpush.db"), "") + require.Nil(t, err) + t.Cleanup(func() { store.Close() }) + return store +} func TestSQLiteStore_UpsertSubscription_SubscriptionsForTopic(t *testing.T) { - store := newTestSQLiteStore(t) - defer store.Close() - - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - - subs, err := store.SubscriptionsForTopic("test-topic") - require.Nil(t, err) - require.Len(t, subs, 1) - require.Equal(t, subs[0].Endpoint, testWebPushEndpoint) - require.Equal(t, subs[0].P256dh, "p256dh-key") - require.Equal(t, subs[0].Auth, "auth-key") - require.Equal(t, subs[0].UserID, "u_1234") - - subs2, err := store.SubscriptionsForTopic("mytopic") - require.Nil(t, err) - require.Len(t, subs2, 1) - require.Equal(t, subs[0].Endpoint, subs2[0].Endpoint) + testStoreUpsertSubscription_SubscriptionsForTopic(t, newTestSQLiteStore(t)) } func TestSQLiteStore_UpsertSubscription_SubscriberIPLimitReached(t *testing.T) { - store := newTestSQLiteStore(t) - defer store.Close() - - // Insert 10 subscriptions with the same IP address - for i := 0; i < 10; i++ { - endpoint := fmt.Sprintf(testWebPushEndpoint+"%d", i) - require.Nil(t, store.UpsertSubscription(endpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - } - - // Another one for the same endpoint should be fine - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - - // But with a different endpoint it should fail - require.Equal(t, webpush.ErrWebPushTooManySubscriptions, store.UpsertSubscription(testWebPushEndpoint+"11", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - - // But with a different IP address it should be fine again - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"99", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("9.9.9.9"), []string{"test-topic", "mytopic"})) + testStoreUpsertSubscription_SubscriberIPLimitReached(t, newTestSQLiteStore(t)) } func TestSQLiteStore_UpsertSubscription_UpdateTopics(t *testing.T) { - store := newTestSQLiteStore(t) - defer store.Close() - - // Insert subscription with two topics, and another with one topic - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"1", "auth-key", "p256dh-key", "", netip.MustParseAddr("9.9.9.9"), []string{"topic1"})) - - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 2) - require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) - require.Equal(t, testWebPushEndpoint+"1", subs[1].Endpoint) - - subs, err = store.SubscriptionsForTopic("topic2") - require.Nil(t, err) - require.Len(t, subs, 1) - require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) - - // Update the first subscription to have only one topic - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1"})) - - subs, err = store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 2) - require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) - - subs, err = store.SubscriptionsForTopic("topic2") - require.Nil(t, err) - require.Len(t, subs, 0) + testStoreUpsertSubscription_UpdateTopics(t, newTestSQLiteStore(t)) } func TestSQLiteStore_RemoveSubscriptionsByEndpoint(t *testing.T) { - store := newTestSQLiteStore(t) - defer store.Close() - - // Insert subscription with two topics - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // And remove it again - require.Nil(t, store.RemoveSubscriptionsByEndpoint(testWebPushEndpoint)) - subs, err = store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 0) + testStoreRemoveSubscriptionsByEndpoint(t, newTestSQLiteStore(t)) } func TestSQLiteStore_RemoveSubscriptionsByUserID(t *testing.T) { - store := newTestSQLiteStore(t) - defer store.Close() - - // Insert subscription with two topics - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // And remove it again - require.Nil(t, store.RemoveSubscriptionsByUserID("u_1234")) - subs, err = store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 0) + testStoreRemoveSubscriptionsByUserID(t, newTestSQLiteStore(t)) } func TestSQLiteStore_RemoveSubscriptionsByUserID_Empty(t *testing.T) { - store := newTestSQLiteStore(t) - defer store.Close() - require.Equal(t, webpush.ErrWebPushUserIDCannotBeEmpty, store.RemoveSubscriptionsByUserID("")) + testStoreRemoveSubscriptionsByUserID_Empty(t, newTestSQLiteStore(t)) } func TestSQLiteStore_MarkExpiryWarningSent(t *testing.T) { store := newTestSQLiteStore(t) - defer store.Close() - - // Insert subscription with two topics - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // Mark them as warning sent - require.Nil(t, store.MarkExpiryWarningSent(subs)) - - rows, err := store.DB().Query("SELECT endpoint FROM subscription WHERE warned_at > 0") - require.Nil(t, err) - defer rows.Close() - var endpoint string - require.True(t, rows.Next()) - require.Nil(t, rows.Scan(&endpoint)) - require.Nil(t, err) - require.Equal(t, testWebPushEndpoint, endpoint) - require.False(t, rows.Next()) + testStoreMarkExpiryWarningSent(t, store, store.SetSubscriptionUpdatedAt) } func TestSQLiteStore_SubscriptionsExpiring(t *testing.T) { store := newTestSQLiteStore(t) - defer store.Close() - - // Insert subscription with two topics - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // Fake-mark them as soon-to-expire - _, err = store.DB().Exec("UPDATE subscription SET updated_at = ? WHERE endpoint = ?", time.Now().Add(-8*24*time.Hour).Unix(), testWebPushEndpoint) - require.Nil(t, err) - - // Should not be cleaned up yet - require.Nil(t, store.RemoveExpiredSubscriptions(9*24*time.Hour)) - - // Run expiration - subs, err = store.SubscriptionsExpiring(7 * 24 * time.Hour) - require.Nil(t, err) - require.Len(t, subs, 1) - require.Equal(t, testWebPushEndpoint, subs[0].Endpoint) + testStoreSubscriptionsExpiring(t, store, store.SetSubscriptionUpdatedAt) } func TestSQLiteStore_RemoveExpiredSubscriptions(t *testing.T) { store := newTestSQLiteStore(t) - defer store.Close() - - // Insert subscription with two topics - require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // Fake-mark them as expired - _, err = store.DB().Exec("UPDATE subscription SET updated_at = ? WHERE endpoint = ?", time.Now().Add(-10*24*time.Hour).Unix(), testWebPushEndpoint) - require.Nil(t, err) - - // Run expiration - require.Nil(t, store.RemoveExpiredSubscriptions(9*24*time.Hour)) - - // List again, should be 0 - subs, err = store.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 0) -} - -func newTestSQLiteStore(t *testing.T) *webpush.SQLiteStore { - store, err := webpush.NewSQLiteStore(filepath.Join(t.TempDir(), "webpush.db"), "") - require.Nil(t, err) - return store + testStoreRemoveExpiredSubscriptions(t, store, store.SetSubscriptionUpdatedAt) } diff --git a/webpush/store_test.go b/webpush/store_test.go new file mode 100644 index 00000000..9f1c7adc --- /dev/null +++ b/webpush/store_test.go @@ -0,0 +1,169 @@ +package webpush_test + +import ( + "fmt" + "net/netip" + "testing" + "time" + + "github.com/stretchr/testify/require" + "heckel.io/ntfy/v2/webpush" +) + +const testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF" + +func testStoreUpsertSubscription_SubscriptionsForTopic(t *testing.T, store webpush.Store) { + require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) + + subs, err := store.SubscriptionsForTopic("test-topic") + require.Nil(t, err) + require.Len(t, subs, 1) + require.Equal(t, subs[0].Endpoint, testWebPushEndpoint) + require.Equal(t, subs[0].P256dh, "p256dh-key") + require.Equal(t, subs[0].Auth, "auth-key") + require.Equal(t, subs[0].UserID, "u_1234") + + subs2, err := store.SubscriptionsForTopic("mytopic") + require.Nil(t, err) + require.Len(t, subs2, 1) + require.Equal(t, subs[0].Endpoint, subs2[0].Endpoint) +} + +func testStoreUpsertSubscription_SubscriberIPLimitReached(t *testing.T, store webpush.Store) { + // Insert 10 subscriptions with the same IP address + for i := 0; i < 10; i++ { + endpoint := fmt.Sprintf(testWebPushEndpoint+"%d", i) + require.Nil(t, store.UpsertSubscription(endpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) + } + + // Another one for the same endpoint should be fine + require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) + + // But with a different endpoint it should fail + require.Equal(t, webpush.ErrWebPushTooManySubscriptions, store.UpsertSubscription(testWebPushEndpoint+"11", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) + + // But with a different IP address it should be fine again + require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"99", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("9.9.9.9"), []string{"test-topic", "mytopic"})) +} + +func testStoreUpsertSubscription_UpdateTopics(t *testing.T, store webpush.Store) { + // Insert subscription with two topics, and another with one topic + require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"1", "auth-key", "p256dh-key", "", netip.MustParseAddr("9.9.9.9"), []string{"topic1"})) + + subs, err := store.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 2) + require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) + require.Equal(t, testWebPushEndpoint+"1", subs[1].Endpoint) + + subs, err = store.SubscriptionsForTopic("topic2") + require.Nil(t, err) + require.Len(t, subs, 1) + require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) + + // Update the first subscription to have only one topic + require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1"})) + + subs, err = store.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 2) + require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) + + subs, err = store.SubscriptionsForTopic("topic2") + require.Nil(t, err) + require.Len(t, subs, 0) +} + +func testStoreRemoveSubscriptionsByEndpoint(t *testing.T, store webpush.Store) { + // Insert subscription with two topics + require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + subs, err := store.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 1) + + // And remove it again + require.Nil(t, store.RemoveSubscriptionsByEndpoint(testWebPushEndpoint)) + subs, err = store.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 0) +} + +func testStoreRemoveSubscriptionsByUserID(t *testing.T, store webpush.Store) { + // Insert subscription with two topics + require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + subs, err := store.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 1) + + // And remove it again + require.Nil(t, store.RemoveSubscriptionsByUserID("u_1234")) + subs, err = store.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 0) +} + +func testStoreRemoveSubscriptionsByUserID_Empty(t *testing.T, store webpush.Store) { + require.Equal(t, webpush.ErrWebPushUserIDCannotBeEmpty, store.RemoveSubscriptionsByUserID("")) +} + +func testStoreMarkExpiryWarningSent(t *testing.T, store webpush.Store, setUpdatedAt func(endpoint string, updatedAt int64) error) { + // Insert subscription with two topics + require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + + // Set updated_at to the past so it shows up as expiring + require.Nil(t, setUpdatedAt(testWebPushEndpoint, time.Now().Add(-8*24*time.Hour).Unix())) + + // Verify subscription appears in expiring list (warned_at == 0) + subs, err := store.SubscriptionsExpiring(7 * 24 * time.Hour) + require.Nil(t, err) + require.Len(t, subs, 1) + require.Equal(t, testWebPushEndpoint, subs[0].Endpoint) + + // Mark them as warning sent + require.Nil(t, store.MarkExpiryWarningSent(subs)) + + // Verify subscription no longer appears in expiring list (warned_at > 0) + subs, err = store.SubscriptionsExpiring(7 * 24 * time.Hour) + require.Nil(t, err) + require.Len(t, subs, 0) +} + +func testStoreSubscriptionsExpiring(t *testing.T, store webpush.Store, setUpdatedAt func(endpoint string, updatedAt int64) error) { + // Insert subscription with two topics + require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + subs, err := store.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 1) + + // Fake-mark them as soon-to-expire + require.Nil(t, setUpdatedAt(testWebPushEndpoint, time.Now().Add(-8*24*time.Hour).Unix())) + + // Should not be cleaned up yet + require.Nil(t, store.RemoveExpiredSubscriptions(9*24*time.Hour)) + + // Run expiration + subs, err = store.SubscriptionsExpiring(7 * 24 * time.Hour) + require.Nil(t, err) + require.Len(t, subs, 1) + require.Equal(t, testWebPushEndpoint, subs[0].Endpoint) +} + +func testStoreRemoveExpiredSubscriptions(t *testing.T, store webpush.Store, setUpdatedAt func(endpoint string, updatedAt int64) error) { + // Insert subscription with two topics + require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + subs, err := store.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 1) + + // Fake-mark them as expired + require.Nil(t, setUpdatedAt(testWebPushEndpoint, time.Now().Add(-10*24*time.Hour).Unix())) + + // Run expiration + require.Nil(t, store.RemoveExpiredSubscriptions(9*24*time.Hour)) + + // List again, should be 0 + subs, err = store.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 0) +}