package oltp
import (
"path/filepath"
"testing"
)
func pushDB(t *testing.T) *DB {
t.Helper()
db, err := Open(filepath.Join(t.TempDir(), "oltp.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
return db
}
// A device is remembered by the HASH of its endpoint, never the
// endpoint itself: a push endpoint is a bearer capability to notify
// somebody's phone, and quarry has no reason to hold one — notify does.
func TestPushDeviceRoundTrip(t *testing.T) {
db := pushDB(t)
const ep = "https://push.example/abcdef"
if err := db.AddPushDevice("dan", ep, "Firefox on Linux"); err != nil {
t.Fatal(err)
}
devs, err := db.PushDevices("dan")
if err != nil {
t.Fatal(err)
}
if len(devs) != 1 {
t.Fatalf("devices = %+v", devs)
}
if devs[0].UA != "Firefox on Linux" || devs[0].At == "" {
t.Fatalf("device = %+v", devs[0])
}
if devs[0].EndpointHash == ep || len(devs[0].EndpointHash) != 64 {
t.Fatalf("endpoint must be stored as a sha-256 hex hash, got %q", devs[0].EndpointHash)
}
if devs[0].EndpointHash != PushEndpointHash(ep) {
t.Fatalf("hash is not derivable from the endpoint")
}
}
// Re-registering the same browser refreshes the row rather than adding
// a second one: one endpoint is one device however often the page
// re-subscribes.
func TestPushDeviceIsIdempotentPerEndpoint(t *testing.T) {
db := pushDB(t)
for i := 0; i < 3; i++ {
if err := db.AddPushDevice("dan", "https://push.example/a", "UA"); err != nil {
t.Fatal(err)
}
}
devs, _ := db.PushDevices("dan")
if len(devs) != 1 {
t.Fatalf("devices = %+v", devs)
}
}
func TestPushDeviceDelete(t *testing.T) {
db := pushDB(t)
db.AddPushDevice("dan", "https://push.example/a", "one")
db.AddPushDevice("dan", "https://push.example/b", "two")
db.AddPushDevice("alice", "https://push.example/c", "hers")
if err := db.DeletePushDevice("dan", "https://push.example/a"); err != nil {
t.Fatal(err)
}
devs, _ := db.PushDevices("dan")
if len(devs) != 1 || devs[0].UA != "two" {
t.Fatalf("devices = %+v", devs)
}
// Another account's device is untouched by a namesake delete.
if devs, _ := db.PushDevices("alice"); len(devs) != 1 {
t.Fatalf("alice's devices = %+v", devs)
}
}
// An unsubscribe notify would not confirm must leave the device row in
// place, and must leave quarry able to ask again — which needs the
// endpoint, not just its hash. Forgetting the row instead is how a
// browser keeps receiving pushes with nothing left in the interface to
// stop it.
func TestPushDevicePendingDeleteSurvivesAndRetries(t *testing.T) {
db := pushDB(t)
const endpoint = "https://push.example.test/abc"
if err := db.AddPushDevice("dan", endpoint, "Firefox"); err != nil {
t.Fatal(err)
}
if pending, err := db.PendingPushDeletes("dan"); err != nil || len(pending) != 0 {
t.Fatalf("a fresh device is not pending: %+v %v", pending, err)
}
if err := db.MarkPushDevicePendingDelete("dan", endpoint); err != nil {
t.Fatal(err)
}
devs, err := db.PushDevices("dan")
if err != nil {
t.Fatal(err)
}
if len(devs) != 1 || !devs[0].PendingDelete {
t.Fatalf("the row must stay, marked: %+v", devs)
}
pending, err := db.PendingPushDeletes("dan")
if err != nil {
t.Fatal(err)
}
if len(pending) != 1 || pending[0] != endpoint {
t.Fatalf("the retry needs the endpoint back: %+v", pending)
}
// Confirmed at last: the row goes, and with it the endpoint quarry
// was only holding to finish the job.
if err := db.DeletePushDevice("dan", endpoint); err != nil {
t.Fatal(err)
}
if devs, _ := db.PushDevices("dan"); len(devs) != 0 {
t.Fatalf("devices = %+v", devs)
}
if pending, _ := db.PendingPushDeletes("dan"); len(pending) != 0 {
t.Fatalf("pending = %+v", pending)
}
}
// Re-subscribing the same endpoint clears a stale pending mark: the
// device is wanted again, so nothing should be retrying its removal.
func TestAddPushDeviceClearsAPendingDelete(t *testing.T) {
db := pushDB(t)
const endpoint = "https://push.example.test/abc"
if err := db.AddPushDevice("dan", endpoint, "Firefox"); err != nil {
t.Fatal(err)
}
if err := db.MarkPushDevicePendingDelete("dan", endpoint); err != nil {
t.Fatal(err)
}
if err := db.AddPushDevice("dan", endpoint, "Firefox"); err != nil {
t.Fatal(err)
}
if pending, _ := db.PendingPushDeletes("dan"); len(pending) != 0 {
t.Fatalf("re-subscribing must clear the mark: %+v", pending)
}
}
// A row can be dropped by its hash, which is all notify's outcome carries.
func TestDeletePushDeviceByHash(t *testing.T) {
db := pushDB(t)
for _, ep := range []string{"https://push.example/a", "https://push.example/b"} {
if err := db.AddPushDevice("dan", ep, "x"); err != nil {
t.Fatal(err)
}
}
if err := db.DeletePushDeviceByHash("dan", PushEndpointHash("https://push.example/a")); err != nil {
t.Fatal(err)
}
devs, _ := db.PushDevices("dan")
if len(devs) != 1 || devs[0].EndpointHash != PushEndpointHash("https://push.example/b") {
t.Errorf("FAIL: %+v", devs)
}
}