Skip to main content

max / makenotwork

5.2 KB · 159 lines History Blame Raw
1 //! HTTP tests: a group's writes bill to the *admin's* slot.
2 //!
3 //! Group push is gated on the admin's entitlement, not the pushing member's, so a
4 //! member with no subscription of their own can still contribute to a group whose
5 //! admin pays (the Groups billing decision, 2026-07-23). Reuses the paid-sync
6 //! harness helpers. Design: wiki synckit-groups-design.
7
8 use serde_json::json;
9
10 use super::synckit_paid_sync::{
11 auth_as, create_internal_app, harness_with_blobs, seed_subscription,
12 };
13 use crate::harness::TestHarness;
14
15 const GIB: i64 = 1024 * 1024 * 1024;
16
17 async fn create_group(h: &mut TestHarness, name: &str) -> String {
18 let resp = h
19 .client
20 .post_json(
21 "/api/sync/groups",
22 &json!({ "id": uuid::Uuid::new_v4().to_string(), "name": name, "admin_sealed_gck": "sealed_admin", "admin_pubkey": "pk_admin" }).to_string(),
23 )
24 .await;
25 assert_eq!(resp.status, 200, "create group: {}", resp.text);
26 let body: serde_json::Value = resp.json();
27 body["id"].as_str().expect("group id").to_string()
28 }
29
30 async fn register_device(h: &mut TestHarness, name: &str) -> String {
31 let resp = h
32 .client
33 .post_json(
34 "/api/sync/devices",
35 &json!({ "device_name": name, "platform": "macos" }).to_string(),
36 )
37 .await;
38 assert_eq!(resp.status, 200, "register device: {}", resp.text);
39 let body: serde_json::Value = resp.json();
40 body["id"].as_str().expect("device id").to_string()
41 }
42
43 fn push_body(device_id: &str) -> String {
44 json!({
45 "device_id": device_id,
46 "batch_id": uuid::Uuid::new_v4(),
47 "changes": [{
48 "table": "tasks",
49 "op": "INSERT",
50 "row_id": "r1",
51 "timestamp": "2026-01-01T00:00:00Z",
52 "data": { "v": 1 }
53 }]
54 })
55 .to_string()
56 }
57
58 #[tokio::test]
59 async fn group_push_gated_on_admin_subscription() {
60 let (mut h, _blobs) = harness_with_blobs().await;
61 let admin = h
62 .signup("gb_admin", "gb_admin@example.com", "Password1!")
63 .await;
64 let (app, _key) = create_internal_app(&h.db, admin).await;
65 auth_as(&mut h, admin, app, "admin-key");
66
67 let group_id = create_group(&mut h, "Team").await;
68 let admin_device = register_device(&mut h, "admin-dev").await;
69
70 // No subscription yet: the admin's own group push is refused (402).
71 let resp = h
72 .client
73 .post_json(
74 &format!("/api/sync/groups/{group_id}/push"),
75 &push_body(&admin_device),
76 )
77 .await;
78 assert_eq!(resp.status, 402, "unsubscribed admin push: {}", resp.text);
79 let body: serde_json::Value = resp.json();
80 assert_eq!(body["reason"], "no_subscription");
81
82 // Give the admin an active subscription: group push now succeeds.
83 seed_subscription(&h.db, admin, app, "active", 10 * GIB).await;
84 let resp = h
85 .client
86 .post_json(
87 &format!("/api/sync/groups/{group_id}/push"),
88 &push_body(&admin_device),
89 )
90 .await;
91 assert_eq!(resp.status, 200, "subscribed admin push: {}", resp.text);
92 }
93
94 #[tokio::test]
95 async fn member_without_own_subscription_writes_on_admin_slot() {
96 let (mut h, _blobs) = harness_with_blobs().await;
97 let admin = h
98 .signup("gb2_admin", "gb2_admin@example.com", "Password1!")
99 .await;
100 let (app, _key) = create_internal_app(&h.db, admin).await;
101 auth_as(&mut h, admin, app, "admin-key");
102 seed_subscription(&h.db, admin, app, "active", 10 * GIB).await;
103 let group_id = create_group(&mut h, "Team").await;
104
105 // A verified member with NO subscription of their own.
106 let bob = h
107 .signup("gb2_bob", "gb2_bob@example.com", "Password1!")
108 .await;
109 sqlx::query("UPDATE users SET email_verified = true WHERE id = $1")
110 .bind(bob)
111 .execute(&h.db)
112 .await
113 .expect("verify bob");
114 let resp = h
115 .client
116 .post_json(
117 &format!("/api/sync/groups/{group_id}/members"),
118 &json!({ "member_email": "gb2_bob@example.com", "sealed_gck": "sealed_bob", "member_pubkey": "pk_bob" })
119 .to_string(),
120 )
121 .await;
122 assert_eq!(resp.status, 204, "add member: {}", resp.text);
123
124 // Bob has no subscription, but the group's admin does: his push succeeds.
125 auth_as(&mut h, bob, app, "bob-key");
126 let bob_device = register_device(&mut h, "bob-dev").await;
127 let resp = h
128 .client
129 .post_json(
130 &format!("/api/sync/groups/{group_id}/push"),
131 &push_body(&bob_device),
132 )
133 .await;
134 assert_eq!(
135 resp.status, 200,
136 "member writes on admin's slot: {}",
137 resp.text
138 );
139
140 // A verified non-member is refused before the billing gate (403).
141 let carol = h
142 .signup("gb2_carol", "gb2_carol@example.com", "Password1!")
143 .await;
144 auth_as(&mut h, carol, app, "carol-key");
145 let carol_device = register_device(&mut h, "carol-dev").await;
146 let resp = h
147 .client
148 .post_json(
149 &format!("/api/sync/groups/{group_id}/push"),
150 &push_body(&carol_device),
151 )
152 .await;
153 assert_eq!(
154 resp.status, 403,
155 "non-member push must be forbidden: {}",
156 resp.text
157 );
158 }
159