Refactor tests again: forEachBackend

This commit is contained in:
binwiederhier
2026-02-21 21:02:41 -05:00
parent b1eb90addc
commit 459c80ef9b
9 changed files with 1562 additions and 2096 deletions

View File

@@ -1,93 +0,0 @@
package message_test
import (
"testing"
dbtest "heckel.io/ntfy/v2/db/test"
"heckel.io/ntfy/v2/message"
"github.com/stretchr/testify/require"
)
func newTestPostgresStore(t *testing.T) message.Store {
testDB := dbtest.CreateTestPostgres(t)
store, err := message.NewPostgresStore(testDB, 0, 0)
require.Nil(t, err)
return store
}
func TestPostgresStore_Messages(t *testing.T) {
testCacheMessages(t, newTestPostgresStore(t))
}
func TestPostgresStore_MessagesLock(t *testing.T) {
testCacheMessagesLock(t, newTestPostgresStore(t))
}
func TestPostgresStore_MessagesScheduled(t *testing.T) {
testCacheMessagesScheduled(t, newTestPostgresStore(t))
}
func TestPostgresStore_Topics(t *testing.T) {
testCacheTopics(t, newTestPostgresStore(t))
}
func TestPostgresStore_MessagesTagsPrioAndTitle(t *testing.T) {
testCacheMessagesTagsPrioAndTitle(t, newTestPostgresStore(t))
}
func TestPostgresStore_MessagesSinceID(t *testing.T) {
testCacheMessagesSinceID(t, newTestPostgresStore(t))
}
func TestPostgresStore_Prune(t *testing.T) {
testCachePrune(t, newTestPostgresStore(t))
}
func TestPostgresStore_Attachments(t *testing.T) {
testCacheAttachments(t, newTestPostgresStore(t))
}
func TestPostgresStore_AttachmentsExpired(t *testing.T) {
testCacheAttachmentsExpired(t, newTestPostgresStore(t))
}
func TestPostgresStore_Sender(t *testing.T) {
testSender(t, newTestPostgresStore(t))
}
func TestPostgresStore_DeleteScheduledBySequenceID(t *testing.T) {
testDeleteScheduledBySequenceID(t, newTestPostgresStore(t))
}
func TestPostgresStore_MessageByID(t *testing.T) {
testMessageByID(t, newTestPostgresStore(t))
}
func TestPostgresStore_MarkPublished(t *testing.T) {
testMarkPublished(t, newTestPostgresStore(t))
}
func TestPostgresStore_ExpireMessages(t *testing.T) {
testExpireMessages(t, newTestPostgresStore(t))
}
func TestPostgresStore_MarkAttachmentsDeleted(t *testing.T) {
testMarkAttachmentsDeleted(t, newTestPostgresStore(t))
}
func TestPostgresStore_Stats(t *testing.T) {
testStats(t, newTestPostgresStore(t))
}
func TestPostgresStore_AddMessages(t *testing.T) {
testAddMessages(t, newTestPostgresStore(t))
}
func TestPostgresStore_MessagesDue(t *testing.T) {
testMessagesDue(t, newTestPostgresStore(t))
}
func TestPostgresStore_MessageFieldRoundTrip(t *testing.T) {
testMessageFieldRoundTrip(t, newTestPostgresStore(t))
}

View File

@@ -13,158 +13,6 @@ import (
"heckel.io/ntfy/v2/model"
)
func TestSqliteStore_Messages(t *testing.T) {
testCacheMessages(t, newSqliteTestStore(t))
}
func TestMemStore_Messages(t *testing.T) {
testCacheMessages(t, newMemTestStore(t))
}
func TestSqliteStore_MessagesLock(t *testing.T) {
testCacheMessagesLock(t, newSqliteTestStore(t))
}
func TestMemStore_MessagesLock(t *testing.T) {
testCacheMessagesLock(t, newMemTestStore(t))
}
func TestSqliteStore_MessagesScheduled(t *testing.T) {
testCacheMessagesScheduled(t, newSqliteTestStore(t))
}
func TestMemStore_MessagesScheduled(t *testing.T) {
testCacheMessagesScheduled(t, newMemTestStore(t))
}
func TestSqliteStore_Topics(t *testing.T) {
testCacheTopics(t, newSqliteTestStore(t))
}
func TestMemStore_Topics(t *testing.T) {
testCacheTopics(t, newMemTestStore(t))
}
func TestSqliteStore_MessagesTagsPrioAndTitle(t *testing.T) {
testCacheMessagesTagsPrioAndTitle(t, newSqliteTestStore(t))
}
func TestMemStore_MessagesTagsPrioAndTitle(t *testing.T) {
testCacheMessagesTagsPrioAndTitle(t, newMemTestStore(t))
}
func TestSqliteStore_MessagesSinceID(t *testing.T) {
testCacheMessagesSinceID(t, newSqliteTestStore(t))
}
func TestMemStore_MessagesSinceID(t *testing.T) {
testCacheMessagesSinceID(t, newMemTestStore(t))
}
func TestSqliteStore_Prune(t *testing.T) {
testCachePrune(t, newSqliteTestStore(t))
}
func TestMemStore_Prune(t *testing.T) {
testCachePrune(t, newMemTestStore(t))
}
func TestSqliteStore_Attachments(t *testing.T) {
testCacheAttachments(t, newSqliteTestStore(t))
}
func TestMemStore_Attachments(t *testing.T) {
testCacheAttachments(t, newMemTestStore(t))
}
func TestSqliteStore_AttachmentsExpired(t *testing.T) {
testCacheAttachmentsExpired(t, newSqliteTestStore(t))
}
func TestMemStore_AttachmentsExpired(t *testing.T) {
testCacheAttachmentsExpired(t, newMemTestStore(t))
}
func TestSqliteStore_Sender(t *testing.T) {
testSender(t, newSqliteTestStore(t))
}
func TestMemStore_Sender(t *testing.T) {
testSender(t, newMemTestStore(t))
}
func TestSqliteStore_DeleteScheduledBySequenceID(t *testing.T) {
testDeleteScheduledBySequenceID(t, newSqliteTestStore(t))
}
func TestMemStore_DeleteScheduledBySequenceID(t *testing.T) {
testDeleteScheduledBySequenceID(t, newMemTestStore(t))
}
func TestSqliteStore_MessageByID(t *testing.T) {
testMessageByID(t, newSqliteTestStore(t))
}
func TestMemStore_MessageByID(t *testing.T) {
testMessageByID(t, newMemTestStore(t))
}
func TestSqliteStore_MarkPublished(t *testing.T) {
testMarkPublished(t, newSqliteTestStore(t))
}
func TestMemStore_MarkPublished(t *testing.T) {
testMarkPublished(t, newMemTestStore(t))
}
func TestSqliteStore_ExpireMessages(t *testing.T) {
testExpireMessages(t, newSqliteTestStore(t))
}
func TestMemStore_ExpireMessages(t *testing.T) {
testExpireMessages(t, newMemTestStore(t))
}
func TestSqliteStore_MarkAttachmentsDeleted(t *testing.T) {
testMarkAttachmentsDeleted(t, newSqliteTestStore(t))
}
func TestMemStore_MarkAttachmentsDeleted(t *testing.T) {
testMarkAttachmentsDeleted(t, newMemTestStore(t))
}
func TestSqliteStore_Stats(t *testing.T) {
testStats(t, newSqliteTestStore(t))
}
func TestMemStore_Stats(t *testing.T) {
testStats(t, newMemTestStore(t))
}
func TestSqliteStore_AddMessages(t *testing.T) {
testAddMessages(t, newSqliteTestStore(t))
}
func TestMemStore_AddMessages(t *testing.T) {
testAddMessages(t, newMemTestStore(t))
}
func TestSqliteStore_MessagesDue(t *testing.T) {
testMessagesDue(t, newSqliteTestStore(t))
}
func TestMemStore_MessagesDue(t *testing.T) {
testMessagesDue(t, newMemTestStore(t))
}
func TestSqliteStore_MessageFieldRoundTrip(t *testing.T) {
testMessageFieldRoundTrip(t, newSqliteTestStore(t))
}
func TestMemStore_MessageFieldRoundTrip(t *testing.T) {
testMessageFieldRoundTrip(t, newMemTestStore(t))
}
func TestSqliteStore_Migration_From0(t *testing.T) {
filename := newSqliteTestStoreFile(t)
db, err := sql.Open("sqlite3", filename)
@@ -419,14 +267,6 @@ func TestNopStore(t *testing.T) {
require.Empty(t, topics)
}
func newSqliteTestStore(t *testing.T) message.Store {
filename := filepath.Join(t.TempDir(), "cache.db")
s, err := message.NewSQLiteStore(filename, "", time.Hour, 0, 0, false)
require.Nil(t, err)
t.Cleanup(func() { s.Close() })
return s
}
func newSqliteTestStoreFile(t *testing.T) string {
return filepath.Join(t.TempDir(), "cache.db")
}
@@ -438,13 +278,6 @@ func newSqliteTestStoreFromFile(t *testing.T, filename, startupQueries string) m
return s
}
func newMemTestStore(t *testing.T) message.Store {
s, err := message.NewMemStore()
require.Nil(t, err)
t.Cleanup(func() { s.Close() })
return s
}
func checkSqliteSchemaVersion(t *testing.T, filename string) {
db, err := sql.Open("sqlite3", filename)
require.Nil(t, err)

File diff suppressed because it is too large Load Diff

View File

@@ -1,180 +0,0 @@
package user_test
import (
"testing"
"github.com/stretchr/testify/require"
dbtest "heckel.io/ntfy/v2/db/test"
"heckel.io/ntfy/v2/user"
)
func newTestPostgresStore(t *testing.T) user.Store {
testDB := dbtest.CreateTestPostgres(t)
store, err := user.NewPostgresStore(testDB)
require.Nil(t, err)
return store
}
func TestPostgresStoreAddUser(t *testing.T) {
testStoreAddUser(t, newTestPostgresStore(t))
}
func TestPostgresStoreAddUserAlreadyExists(t *testing.T) {
testStoreAddUserAlreadyExists(t, newTestPostgresStore(t))
}
func TestPostgresStoreRemoveUser(t *testing.T) {
testStoreRemoveUser(t, newTestPostgresStore(t))
}
func TestPostgresStoreUserByID(t *testing.T) {
testStoreUserByID(t, newTestPostgresStore(t))
}
func TestPostgresStoreUserByToken(t *testing.T) {
testStoreUserByToken(t, newTestPostgresStore(t))
}
func TestPostgresStoreUserByStripeCustomer(t *testing.T) {
testStoreUserByStripeCustomer(t, newTestPostgresStore(t))
}
func TestPostgresStoreUsers(t *testing.T) {
testStoreUsers(t, newTestPostgresStore(t))
}
func TestPostgresStoreUsersCount(t *testing.T) {
testStoreUsersCount(t, newTestPostgresStore(t))
}
func TestPostgresStoreChangePassword(t *testing.T) {
testStoreChangePassword(t, newTestPostgresStore(t))
}
func TestPostgresStoreChangeRole(t *testing.T) {
testStoreChangeRole(t, newTestPostgresStore(t))
}
func TestPostgresStoreTokens(t *testing.T) {
testStoreTokens(t, newTestPostgresStore(t))
}
func TestPostgresStoreTokenChangeLabel(t *testing.T) {
testStoreTokenChangeLabel(t, newTestPostgresStore(t))
}
func TestPostgresStoreTokenRemove(t *testing.T) {
testStoreTokenRemove(t, newTestPostgresStore(t))
}
func TestPostgresStoreTokenRemoveExpired(t *testing.T) {
testStoreTokenRemoveExpired(t, newTestPostgresStore(t))
}
func TestPostgresStoreTokenRemoveExcess(t *testing.T) {
testStoreTokenRemoveExcess(t, newTestPostgresStore(t))
}
func TestPostgresStoreTokenUpdateLastAccess(t *testing.T) {
testStoreTokenUpdateLastAccess(t, newTestPostgresStore(t))
}
func TestPostgresStoreAllowAccess(t *testing.T) {
testStoreAllowAccess(t, newTestPostgresStore(t))
}
func TestPostgresStoreAllowAccessReadOnly(t *testing.T) {
testStoreAllowAccessReadOnly(t, newTestPostgresStore(t))
}
func TestPostgresStoreResetAccess(t *testing.T) {
testStoreResetAccess(t, newTestPostgresStore(t))
}
func TestPostgresStoreResetAccessAll(t *testing.T) {
testStoreResetAccessAll(t, newTestPostgresStore(t))
}
func TestPostgresStoreAuthorizeTopicAccess(t *testing.T) {
testStoreAuthorizeTopicAccess(t, newTestPostgresStore(t))
}
func TestPostgresStoreAuthorizeTopicAccessNotFound(t *testing.T) {
testStoreAuthorizeTopicAccessNotFound(t, newTestPostgresStore(t))
}
func TestPostgresStoreAuthorizeTopicAccessDenyAll(t *testing.T) {
testStoreAuthorizeTopicAccessDenyAll(t, newTestPostgresStore(t))
}
func TestPostgresStoreReservations(t *testing.T) {
testStoreReservations(t, newTestPostgresStore(t))
}
func TestPostgresStoreReservationsCount(t *testing.T) {
testStoreReservationsCount(t, newTestPostgresStore(t))
}
func TestPostgresStoreHasReservation(t *testing.T) {
testStoreHasReservation(t, newTestPostgresStore(t))
}
func TestPostgresStoreReservationOwner(t *testing.T) {
testStoreReservationOwner(t, newTestPostgresStore(t))
}
func TestPostgresStoreTiers(t *testing.T) {
testStoreTiers(t, newTestPostgresStore(t))
}
func TestPostgresStoreTierUpdate(t *testing.T) {
testStoreTierUpdate(t, newTestPostgresStore(t))
}
func TestPostgresStoreTierRemove(t *testing.T) {
testStoreTierRemove(t, newTestPostgresStore(t))
}
func TestPostgresStoreTierByStripePrice(t *testing.T) {
testStoreTierByStripePrice(t, newTestPostgresStore(t))
}
func TestPostgresStoreChangeTier(t *testing.T) {
testStoreChangeTier(t, newTestPostgresStore(t))
}
func TestPostgresStorePhoneNumbers(t *testing.T) {
testStorePhoneNumbers(t, newTestPostgresStore(t))
}
func TestPostgresStoreChangeSettings(t *testing.T) {
testStoreChangeSettings(t, newTestPostgresStore(t))
}
func TestPostgresStoreChangeBilling(t *testing.T) {
testStoreChangeBilling(t, newTestPostgresStore(t))
}
func TestPostgresStoreUpdateStats(t *testing.T) {
testStoreUpdateStats(t, newTestPostgresStore(t))
}
func TestPostgresStoreResetStats(t *testing.T) {
testStoreResetStats(t, newTestPostgresStore(t))
}
func TestPostgresStoreMarkUserRemoved(t *testing.T) {
testStoreMarkUserRemoved(t, newTestPostgresStore(t))
}
func TestPostgresStoreRemoveDeletedUsers(t *testing.T) {
testStoreRemoveDeletedUsers(t, newTestPostgresStore(t))
}
func TestPostgresStoreAllGrants(t *testing.T) {
testStoreAllGrants(t, newTestPostgresStore(t))
}
func TestPostgresStoreOtherAccessCount(t *testing.T) {
testStoreOtherAccessCount(t, newTestPostgresStore(t))
}

View File

@@ -1,180 +0,0 @@
package user_test
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/user"
)
func newTestSQLiteStore(t *testing.T) user.Store {
store, err := user.NewSQLiteStore(filepath.Join(t.TempDir(), "user.db"), "")
require.Nil(t, err)
t.Cleanup(func() { store.Close() })
return store
}
func TestSQLiteStoreAddUser(t *testing.T) {
testStoreAddUser(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreAddUserAlreadyExists(t *testing.T) {
testStoreAddUserAlreadyExists(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreRemoveUser(t *testing.T) {
testStoreRemoveUser(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreUserByID(t *testing.T) {
testStoreUserByID(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreUserByToken(t *testing.T) {
testStoreUserByToken(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreUserByStripeCustomer(t *testing.T) {
testStoreUserByStripeCustomer(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreUsers(t *testing.T) {
testStoreUsers(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreUsersCount(t *testing.T) {
testStoreUsersCount(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreChangePassword(t *testing.T) {
testStoreChangePassword(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreChangeRole(t *testing.T) {
testStoreChangeRole(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreTokens(t *testing.T) {
testStoreTokens(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreTokenChangeLabel(t *testing.T) {
testStoreTokenChangeLabel(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreTokenRemove(t *testing.T) {
testStoreTokenRemove(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreTokenRemoveExpired(t *testing.T) {
testStoreTokenRemoveExpired(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreTokenRemoveExcess(t *testing.T) {
testStoreTokenRemoveExcess(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreTokenUpdateLastAccess(t *testing.T) {
testStoreTokenUpdateLastAccess(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreAllowAccess(t *testing.T) {
testStoreAllowAccess(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreAllowAccessReadOnly(t *testing.T) {
testStoreAllowAccessReadOnly(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreResetAccess(t *testing.T) {
testStoreResetAccess(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreResetAccessAll(t *testing.T) {
testStoreResetAccessAll(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreAuthorizeTopicAccess(t *testing.T) {
testStoreAuthorizeTopicAccess(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreAuthorizeTopicAccessNotFound(t *testing.T) {
testStoreAuthorizeTopicAccessNotFound(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreAuthorizeTopicAccessDenyAll(t *testing.T) {
testStoreAuthorizeTopicAccessDenyAll(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreReservations(t *testing.T) {
testStoreReservations(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreReservationsCount(t *testing.T) {
testStoreReservationsCount(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreHasReservation(t *testing.T) {
testStoreHasReservation(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreReservationOwner(t *testing.T) {
testStoreReservationOwner(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreTiers(t *testing.T) {
testStoreTiers(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreTierUpdate(t *testing.T) {
testStoreTierUpdate(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreTierRemove(t *testing.T) {
testStoreTierRemove(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreTierByStripePrice(t *testing.T) {
testStoreTierByStripePrice(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreChangeTier(t *testing.T) {
testStoreChangeTier(t, newTestSQLiteStore(t))
}
func TestSQLiteStorePhoneNumbers(t *testing.T) {
testStorePhoneNumbers(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreChangeSettings(t *testing.T) {
testStoreChangeSettings(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreChangeBilling(t *testing.T) {
testStoreChangeBilling(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreUpdateStats(t *testing.T) {
testStoreUpdateStats(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreResetStats(t *testing.T) {
testStoreResetStats(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreMarkUserRemoved(t *testing.T) {
testStoreMarkUserRemoved(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreRemoveDeletedUsers(t *testing.T) {
testStoreRemoveDeletedUsers(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreAllGrants(t *testing.T) {
testStoreAllGrants(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreOtherAccessCount(t *testing.T) {
testStoreOtherAccessCount(t, newTestSQLiteStore(t))
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +0,0 @@
package webpush_test
import (
"testing"
"github.com/stretchr/testify/require"
dbtest "heckel.io/ntfy/v2/db/test"
"heckel.io/ntfy/v2/webpush"
)
func newTestPostgresStore(t *testing.T) webpush.Store {
testDB := dbtest.CreateTestPostgres(t)
store, err := webpush.NewPostgresStore(testDB)
require.Nil(t, err)
return store
}
func TestPostgresStoreUpsertSubscriptionSubscriptionsForTopic(t *testing.T) {
testStoreUpsertSubscriptionSubscriptionsForTopic(t, newTestPostgresStore(t))
}
func TestPostgresStoreUpsertSubscriptionSubscriberIPLimitReached(t *testing.T) {
testStoreUpsertSubscriptionSubscriberIPLimitReached(t, newTestPostgresStore(t))
}
func TestPostgresStoreUpsertSubscriptionUpdateTopics(t *testing.T) {
testStoreUpsertSubscriptionUpdateTopics(t, newTestPostgresStore(t))
}
func TestPostgresStoreUpsertSubscriptionUpdateFields(t *testing.T) {
testStoreUpsertSubscriptionUpdateFields(t, newTestPostgresStore(t))
}
func TestPostgresStoreRemoveByUserIDMultiple(t *testing.T) {
testStoreRemoveByUserIDMultiple(t, newTestPostgresStore(t))
}
func TestPostgresStoreRemoveByEndpoint(t *testing.T) {
testStoreRemoveByEndpoint(t, newTestPostgresStore(t))
}
func TestPostgresStoreRemoveByUserID(t *testing.T) {
testStoreRemoveByUserID(t, newTestPostgresStore(t))
}
func TestPostgresStoreRemoveByUserIDEmpty(t *testing.T) {
testStoreRemoveByUserIDEmpty(t, newTestPostgresStore(t))
}
func TestPostgresStoreExpiryWarningSent(t *testing.T) {
store := newTestPostgresStore(t)
testStoreExpiryWarningSent(t, store, store.SetSubscriptionUpdatedAt)
}
func TestPostgresStoreExpiring(t *testing.T) {
store := newTestPostgresStore(t)
testStoreExpiring(t, store, store.SetSubscriptionUpdatedAt)
}
func TestPostgresStoreRemoveExpired(t *testing.T) {
store := newTestPostgresStore(t)
testStoreRemoveExpired(t, store, store.SetSubscriptionUpdatedAt)
}

View File

@@ -1,63 +0,0 @@
package webpush_test
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/webpush"
)
func newTestSQLiteStore(t *testing.T) webpush.Store {
store, err := webpush.NewSQLiteStore(filepath.Join(t.TempDir(), "webpush.db"), "")
require.Nil(t, err)
t.Cleanup(func() { store.Close() })
return store
}
func TestSQLiteStoreUpsertSubscriptionSubscriptionsForTopic(t *testing.T) {
testStoreUpsertSubscriptionSubscriptionsForTopic(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreUpsertSubscriptionSubscriberIPLimitReached(t *testing.T) {
testStoreUpsertSubscriptionSubscriberIPLimitReached(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreUpsertSubscriptionUpdateTopics(t *testing.T) {
testStoreUpsertSubscriptionUpdateTopics(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreUpsertSubscriptionUpdateFields(t *testing.T) {
testStoreUpsertSubscriptionUpdateFields(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreRemoveByUserIDMultiple(t *testing.T) {
testStoreRemoveByUserIDMultiple(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreRemoveByEndpoint(t *testing.T) {
testStoreRemoveByEndpoint(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreRemoveByUserID(t *testing.T) {
testStoreRemoveByUserID(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreRemoveByUserIDEmpty(t *testing.T) {
testStoreRemoveByUserIDEmpty(t, newTestSQLiteStore(t))
}
func TestSQLiteStoreExpiryWarningSent(t *testing.T) {
store := newTestSQLiteStore(t)
testStoreExpiryWarningSent(t, store, store.SetSubscriptionUpdatedAt)
}
func TestSQLiteStoreExpiring(t *testing.T) {
store := newTestSQLiteStore(t)
testStoreExpiring(t, store, store.SetSubscriptionUpdatedAt)
}
func TestSQLiteStoreRemoveExpired(t *testing.T) {
store := newTestSQLiteStore(t)
testStoreRemoveExpired(t, store, store.SetSubscriptionUpdatedAt)
}

View File

@@ -3,211 +3,250 @@ package webpush_test
import (
"fmt"
"net/netip"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
dbtest "heckel.io/ntfy/v2/db/test"
"heckel.io/ntfy/v2/webpush"
)
const testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF"
func testStoreUpsertSubscriptionSubscriptionsForTopic(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 forEachBackend(t *testing.T, f func(t *testing.T, store webpush.Store)) {
t.Run("sqlite", func(t *testing.T) {
store, err := webpush.NewSQLiteStore(filepath.Join(t.TempDir(), "webpush.db"), "")
require.Nil(t, err)
t.Cleanup(func() { store.Close() })
f(t, store)
})
t.Run("postgres", func(t *testing.T) {
testDB := dbtest.CreateTestPostgres(t)
store, err := webpush.NewPostgresStore(testDB)
require.Nil(t, err)
f(t, store)
})
}
func testStoreUpsertSubscriptionSubscriberIPLimitReached(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"}))
}
func TestStoreUpsertSubscriptionSubscriptionsForTopic(t *testing.T) {
forEachBackend(t, func(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"}))
// 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"}))
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")
// 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"}))
subs2, err := store.SubscriptionsForTopic("mytopic")
require.Nil(t, err)
require.Len(t, subs2, 1)
require.Equal(t, subs[0].Endpoint, subs2[0].Endpoint)
})
}
func testStoreUpsertSubscriptionUpdateTopics(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"}))
func TestStoreUpsertSubscriptionSubscriberIPLimitReached(t *testing.T) {
forEachBackend(t, func(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"}))
}
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)
// 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"}))
subs, err = store.SubscriptionsForTopic("topic2")
require.Nil(t, err)
require.Len(t, subs, 1)
require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint)
// 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"}))
// 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)
// 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 testStoreUpsertSubscriptionUpdateFields(t *testing.T, store webpush.Store) {
// Insert a subscription
require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1"}))
func TestStoreUpsertSubscriptionUpdateTopics(t *testing.T) {
forEachBackend(t, func(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, 1)
require.Equal(t, "auth-key", subs[0].Auth)
require.Equal(t, "p256dh-key", subs[0].P256dh)
require.Equal(t, "u_1234", subs[0].UserID)
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)
// Re-upsert the same endpoint with different auth, p256dh, and userID
require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "new-auth", "new-p256dh", "u_5678", netip.MustParseAddr("1.2.3.4"), []string{"topic1"}))
subs, err = store.SubscriptionsForTopic("topic2")
require.Nil(t, err)
require.Len(t, subs, 1)
require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint)
subs, err = store.SubscriptionsForTopic("topic1")
require.Nil(t, err)
require.Len(t, subs, 1)
require.Equal(t, testWebPushEndpoint, subs[0].Endpoint)
require.Equal(t, "new-auth", subs[0].Auth)
require.Equal(t, "new-p256dh", subs[0].P256dh)
require.Equal(t, "u_5678", subs[0].UserID)
// 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 testStoreRemoveByUserIDMultiple(t *testing.T, store webpush.Store) {
// Insert two subscriptions for u_1234 and one for u_5678
require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1"}))
require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"1", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1"}))
require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"2", "auth-key", "p256dh-key", "u_5678", netip.MustParseAddr("9.9.9.9"), []string{"topic1"}))
func TestStoreUpsertSubscriptionUpdateFields(t *testing.T) {
forEachBackend(t, func(t *testing.T, store webpush.Store) {
// Insert a subscription
require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "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, 3)
subs, err := store.SubscriptionsForTopic("topic1")
require.Nil(t, err)
require.Len(t, subs, 1)
require.Equal(t, "auth-key", subs[0].Auth)
require.Equal(t, "p256dh-key", subs[0].P256dh)
require.Equal(t, "u_1234", subs[0].UserID)
// Remove all subscriptions for u_1234
require.Nil(t, store.RemoveSubscriptionsByUserID("u_1234"))
// Re-upsert the same endpoint with different auth, p256dh, and userID
require.Nil(t, store.UpsertSubscription(testWebPushEndpoint, "new-auth", "new-p256dh", "u_5678", netip.MustParseAddr("1.2.3.4"), []string{"topic1"}))
// Only u_5678's subscription should remain
subs, err = store.SubscriptionsForTopic("topic1")
require.Nil(t, err)
require.Len(t, subs, 1)
require.Equal(t, testWebPushEndpoint+"2", subs[0].Endpoint)
require.Equal(t, "u_5678", subs[0].UserID)
subs, err = store.SubscriptionsForTopic("topic1")
require.Nil(t, err)
require.Len(t, subs, 1)
require.Equal(t, testWebPushEndpoint, subs[0].Endpoint)
require.Equal(t, "new-auth", subs[0].Auth)
require.Equal(t, "new-p256dh", subs[0].P256dh)
require.Equal(t, "u_5678", subs[0].UserID)
})
}
func testStoreRemoveByEndpoint(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)
func TestStoreRemoveByUserIDMultiple(t *testing.T) {
forEachBackend(t, func(t *testing.T, store webpush.Store) {
// Insert two subscriptions for u_1234 and one for u_5678
require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1"}))
require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"1", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1"}))
require.Nil(t, store.UpsertSubscription(testWebPushEndpoint+"2", "auth-key", "p256dh-key", "u_5678", netip.MustParseAddr("9.9.9.9"), []string{"topic1"}))
// And remove it again
require.Nil(t, store.RemoveSubscriptionsByEndpoint(testWebPushEndpoint))
subs, err = store.SubscriptionsForTopic("topic1")
require.Nil(t, err)
require.Len(t, subs, 0)
subs, err := store.SubscriptionsForTopic("topic1")
require.Nil(t, err)
require.Len(t, subs, 3)
// Remove all subscriptions for u_1234
require.Nil(t, store.RemoveSubscriptionsByUserID("u_1234"))
// Only u_5678's subscription should remain
subs, err = store.SubscriptionsForTopic("topic1")
require.Nil(t, err)
require.Len(t, subs, 1)
require.Equal(t, testWebPushEndpoint+"2", subs[0].Endpoint)
require.Equal(t, "u_5678", subs[0].UserID)
})
}
func testStoreRemoveByUserID(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)
func TestStoreRemoveByEndpoint(t *testing.T) {
forEachBackend(t, func(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)
// 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 testStoreRemoveByUserIDEmpty(t *testing.T, store webpush.Store) {
require.Equal(t, webpush.ErrWebPushUserIDCannotBeEmpty, store.RemoveSubscriptionsByUserID(""))
func TestStoreRemoveByUserID(t *testing.T) {
forEachBackend(t, func(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 testStoreExpiryWarningSent(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 TestStoreRemoveByUserIDEmpty(t *testing.T) {
forEachBackend(t, func(t *testing.T, store webpush.Store) {
require.Equal(t, webpush.ErrWebPushUserIDCannotBeEmpty, store.RemoveSubscriptionsByUserID(""))
})
}
func testStoreExpiring(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)
func TestStoreExpiryWarningSent(t *testing.T) {
forEachBackend(t, func(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"}))
// Fake-mark them as soon-to-expire
require.Nil(t, setUpdatedAt(testWebPushEndpoint, time.Now().Add(-8*24*time.Hour).Unix()))
// Set updated_at to the past so it shows up as expiring
require.Nil(t, store.SetSubscriptionUpdatedAt(testWebPushEndpoint, time.Now().Add(-8*24*time.Hour).Unix()))
// Should not be cleaned up yet
require.Nil(t, store.RemoveExpiredSubscriptions(9*24*time.Hour))
// 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)
// 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)
// 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 testStoreRemoveExpired(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)
func TestStoreExpiring(t *testing.T) {
forEachBackend(t, func(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)
// Fake-mark them as expired
require.Nil(t, setUpdatedAt(testWebPushEndpoint, time.Now().Add(-10*24*time.Hour).Unix()))
// Fake-mark them as soon-to-expire
require.Nil(t, store.SetSubscriptionUpdatedAt(testWebPushEndpoint, time.Now().Add(-8*24*time.Hour).Unix()))
// Run expiration
require.Nil(t, store.RemoveExpiredSubscriptions(9*24*time.Hour))
// Should not be cleaned up yet
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)
// 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 TestStoreRemoveExpired(t *testing.T) {
forEachBackend(t, func(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)
// Fake-mark them as expired
require.Nil(t, store.SetSubscriptionUpdatedAt(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)
})
}