| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "net/http" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | // A push subscription may only be removed by its own token — not by anyone who |
| 11 | // happens to know the endpoint. |
| 12 | func TestUnsubscribePushIsOwnerScoped(t *testing.T) { |
| 13 | ts, srv := newTestServer(t) |
| 14 | aTok := mintDeviceToken(t, srv.db, "A") |
| 15 | bTok := mintDeviceToken(t, srv.db, "B") |
| 16 | if err := srv.db.addSubscription(PushSub{Endpoint: "https://e/1", Token: aTok, P256dh: "p", Auth: "a"}); err != nil { |
| 17 | t.Fatalf("addSubscription: %v", err) |
| 18 | } |
| 19 | |
| 20 | post := func(body string) int { |
| 21 | resp, err := http.Post(ts.URL+"/api/push/unsubscribe", "application/json", strings.NewReader(body)) |
| 22 | if err != nil { |
| 23 | t.Fatal(err) |
| 24 | } |
| 25 | resp.Body.Close() |
| 26 | return resp.StatusCode |
| 27 | } |
| 28 | subCount := func() int { subs, _ := srv.db.listSubscriptions(); return len(subs) } |
| 29 | |
| 30 | // No token → refused, nothing deleted. |
| 31 | if got := post(`{"endpoint":"https://e/1"}`); got != http.StatusForbidden { |
| 32 | t.Fatalf("no token: want 403, got %d", got) |
| 33 | } |
| 34 | if subCount() != 1 { |
| 35 | t.Fatalf("no-token request deleted a sub") |
| 36 | } |
| 37 | // Valid but non-owning token → accepted, but the other user's sub survives. |
| 38 | if got := post(fmt.Sprintf(`{"token":%q,"endpoint":"https://e/1"}`, bTok)); got != http.StatusOK { |
| 39 | t.Fatalf("non-owner: want 200, got %d", got) |
| 40 | } |
| 41 | if subCount() != 1 { |
| 42 | t.Fatalf("non-owner deleted someone else's subscription") |
| 43 | } |
| 44 | // Owner token → removed. |
| 45 | if got := post(fmt.Sprintf(`{"token":%q,"endpoint":"https://e/1"}`, aTok)); got != http.StatusOK { |
| 46 | t.Fatalf("owner: want 200, got %d", got) |
| 47 | } |
| 48 | if subCount() != 0 { |
| 49 | t.Fatalf("owner unsubscribe left the sub") |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | // A second grant must not overwrite a member's existing wrapped instance key. |
| 54 | func TestGrantInstanceKeyNeverOverwrites(t *testing.T) { |
| 55 | db := newTestDB(t) |
| 56 | if err := db.grantInstanceKey("u1", "GOOD"); err != nil { |
| 57 | t.Fatalf("grant: %v", err) |
| 58 | } |
| 59 | if err := db.grantInstanceKey("u1", "JUNK"); err != nil { |
| 60 | t.Fatalf("re-grant: %v", err) |
| 61 | } |
| 62 | got, _, _ := db.instanceKeyFor("u1") |
| 63 | if got != "GOOD" { |
| 64 | t.Fatalf("instance key overwritten: got %q, want GOOD", got) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | // Re-adding an existing member must not clobber their wrapped thread key. |
| 69 | func TestAddThreadMemberNeverClobbers(t *testing.T) { |
| 70 | db := newTestDB(t) |
| 71 | if err := db.addThreadMember("t1", "m1", "GOOD"); err != nil { |
| 72 | t.Fatalf("add: %v", err) |
| 73 | } |
| 74 | if err := db.addThreadMember("t1", "m1", "JUNK"); err != nil { |
| 75 | t.Fatalf("re-add: %v", err) |
| 76 | } |
| 77 | var wrapped string |
| 78 | if err := db.sql.QueryRow( |
| 79 | "SELECT wrapped FROM thread_keys WHERE thread_id = ? AND user_id = ?", "t1", "m1", |
| 80 | ).Scan(&wrapped); err != nil { |
| 81 | t.Fatalf("read thread key: %v", err) |
| 82 | } |
| 83 | if wrapped != "GOOD" { |
| 84 | t.Fatalf("thread key clobbered: got %q, want GOOD", wrapped) |
| 85 | } |
| 86 | } |
| 87 | |