package main import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) // fakeCloud stands in for the cloud provisioner and captures what home sends. func fakeCloud(t *testing.T, status int, body map[string]any) (*httptest.Server, *http.Request, *[]byte) { t.Helper() var captured http.Request var capturedBody []byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { captured = *r buf := new(bytes.Buffer) buf.ReadFrom(r.Body) capturedBody = buf.Bytes() writeJSON(w, status, body) })) t.Cleanup(srv.Close) return srv, &captured, &capturedBody } func postSpaces(t *testing.T, ts *httptest.Server, token string) *http.Response { t.Helper() req, _ := http.NewRequest("POST", ts.URL+"/api/spaces", nil) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } t.Cleanup(func() { resp.Body.Close() }) return resp } // seedMember is a seedUser who already has a saved space (a backup blob) — the // state the create gate requires, and the realistic state of any signed-in user. func seedMember(t *testing.T, s *Server) (userID, token string) { t.Helper() userID, token = seedUser(t, s) if _, _, err := s.store.putBlob(userID, []byte("vault"), "0"); err != nil { t.Fatalf("putBlob: %v", err) } return userID, token } func TestFeaturesReflectsProvisioner(t *testing.T) { ts, s := newTestServer(t) // Unconfigured: spaces off (and the endpoint needs no auth). resp, err := http.Get(ts.URL + "/api/features") if err != nil { t.Fatal(err) } defer resp.Body.Close() var f map[string]any json.NewDecoder(resp.Body).Decode(&f) if f["spaces"] != false { t.Errorf("unconfigured: spaces = %v, want false", f["spaces"]) } // Configured: spaces on. s.cloud = newCloudProvisioner("https://cloud.example.com", "tok") resp2, err := http.Get(ts.URL + "/api/features") if err != nil { t.Fatal(err) } defer resp2.Body.Close() var f2 map[string]any json.NewDecoder(resp2.Body).Decode(&f2) if f2["spaces"] != true { t.Errorf("configured: spaces = %v, want true", f2["spaces"]) } } // The doors block tells native clients which web-flow doors are lit — and // carries GitHub's per-instance client id, the one value they can't compile // in. Dark doors read false / absent; never an error. func TestFeaturesAdvertisesSigninDoors(t *testing.T) { ts, s := newTestServer(t) get := func() map[string]any { t.Helper() resp, err := http.Get(ts.URL + "/api/features") if err != nil { t.Fatal(err) } defer resp.Body.Close() var f struct { Doors map[string]any `json:"doors"` } json.NewDecoder(resp.Body).Decode(&f) if f.Doors == nil { t.Fatal("features: no doors block") } return f.Doors } dark := get() if dark["google"] != false || dark["microsoft"] != false { t.Errorf("dark: google=%v microsoft=%v, want false/false", dark["google"], dark["microsoft"]) } if _, ok := dark["github_client_id"]; ok { t.Errorf("dark: github_client_id present, want absent") } s.oidc = map[string]*oidcProvider{ "google": newOIDCProvider("google", []string{"cid"}, []byte("pepper")), } s.github = &githubDoor{clientID: "Ov23liTESTID", secret: "sec", pepper: []byte("p")} lit := get() if lit["google"] != true || lit["microsoft"] != false { t.Errorf("lit: google=%v microsoft=%v, want true/false", lit["google"], lit["microsoft"]) } if lit["github_client_id"] != "Ov23liTESTID" { t.Errorf("lit: github_client_id = %v, want Ov23liTESTID", lit["github_client_id"]) } } func TestCreateSpaceRequiresAuth(t *testing.T) { ts, _ := newTestServer(t) if resp := postSpaces(t, ts, ""); resp.StatusCode != http.StatusUnauthorized { t.Errorf("no token: status = %d, want 401", resp.StatusCode) } } func TestCreateSpaceUnconfigured(t *testing.T) { ts, s := newTestServer(t) _, token := seedUser(t, s) resp := postSpaces(t, ts, token) if resp.StatusCode != http.StatusNotImplemented { t.Fatalf("status = %d, want 501", resp.StatusCode) } var m map[string]any json.NewDecoder(resp.Body).Decode(&m) if m["error"] != "unavailable" { t.Errorf("error = %v, want unavailable", m["error"]) } } func TestCreateSpaceRelaysSuccess(t *testing.T) { ts, s := newTestServer(t) uid, token := seedMember(t, s) cloud, captured, capturedBody := fakeCloud(t, http.StatusCreated, map[string]any{ "host": "amber-fox-42.example.com", "url": "https://amber-fox-42.example.com", "setup_link": "https://amber-fox-42.example.com/join/setup#S", }) s.cloud = newCloudProvisioner(cloud.URL, "caller-token") resp := postSpaces(t, ts, token) if resp.StatusCode != http.StatusCreated { t.Fatalf("status = %d, want 201", resp.StatusCode) } var m map[string]any json.NewDecoder(resp.Body).Decode(&m) if m["host"] != "amber-fox-42.example.com" || m["setup_link"] != "https://amber-fox-42.example.com/join/setup#S" { t.Errorf("relayed body = %v", m) } // Home authenticated with its caller token and forwarded the user id (and // nothing else) as the requester. if got := captured.Header.Get("Authorization"); got != "Bearer caller-token" { t.Errorf("cloud saw Authorization %q", got) } var sent map[string]string json.Unmarshal(*capturedBody, &sent) if sent["requester"] != uid || len(sent) != 1 { t.Errorf("cloud saw body %v, want just requester=%s", sent, uid) } } func TestCreateSpaceRequiresExistingSpace(t *testing.T) { ts, s := newTestServer(t) uid, token := seedUser(t, s) // a signed-in user with NO saved space (no blob) cloud, captured, _ := fakeCloud(t, http.StatusCreated, map[string]any{ "host": "h.example.com", "url": "https://h.example.com", "setup_link": "https://h.example.com/join/setup#S", }) s.cloud = newCloudProvisioner(cloud.URL, "caller-token") // No space yet → gated with 403 not_eligible, and the provisioner is never called. resp := postSpaces(t, ts, token) if resp.StatusCode != http.StatusForbidden { t.Fatalf("no-space create = %d, want 403", resp.StatusCode) } var m map[string]any json.NewDecoder(resp.Body).Decode(&m) if m["error"] != "not_eligible" { t.Errorf("error = %v, want not_eligible", m["error"]) } if captured.Method != "" { t.Error("provisioner must not be called when the caller is ineligible") } // Once they have a saved space (a blob), creation goes through. if _, _, err := s.store.putBlob(uid, []byte("vault"), "0"); err != nil { t.Fatal(err) } if resp2 := postSpaces(t, ts, token); resp2.StatusCode != http.StatusCreated { t.Fatalf("with-space create = %d, want 201", resp2.StatusCode) } } func TestCreateSpaceRelaysQuotaAndCapacity(t *testing.T) { ts, s := newTestServer(t) _, token := seedMember(t, s) quota, _, _ := fakeCloud(t, http.StatusConflict, map[string]any{ "error": "quota_exceeded", "message": "this account already has 3 spaces (the limit)", }) s.cloud = newCloudProvisioner(quota.URL, "caller-token") resp := postSpaces(t, ts, token) if resp.StatusCode != http.StatusConflict { t.Fatalf("quota: status = %d, want 409", resp.StatusCode) } var m map[string]any json.NewDecoder(resp.Body).Decode(&m) if m["error"] != "quota_exceeded" { t.Errorf("quota: error = %v", m["error"]) } full, _, _ := fakeCloud(t, http.StatusServiceUnavailable, map[string]any{ "error": "at_capacity", "message": "We can't create a new Eleven space right now — please try again later.", }) s.cloud = newCloudProvisioner(full.URL, "caller-token") resp = postSpaces(t, ts, token) if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("capacity: status = %d, want 503", resp.StatusCode) } var m2 map[string]any json.NewDecoder(resp.Body).Decode(&m2) if m2["error"] != "at_capacity" || m2["message"] == "" { t.Errorf("capacity: body = %v", m2) } } func TestCreateSpaceFlattensUnexpectedStatus(t *testing.T) { ts, s := newTestServer(t) _, token := seedMember(t, s) weird, _, _ := fakeCloud(t, http.StatusTeapot, map[string]any{"error": "internal-detail"}) s.cloud = newCloudProvisioner(weird.URL, "caller-token") resp := postSpaces(t, ts, token) if resp.StatusCode != http.StatusBadGateway { t.Fatalf("status = %d, want 502", resp.StatusCode) } var m map[string]any json.NewDecoder(resp.Body).Decode(&m) if m["error"] != "create_failed" { t.Errorf("error = %v, want create_failed", m["error"]) } } func TestCheckoutSpaceProxies(t *testing.T) { ts, s := newTestServer(t) uid, token := seedUser(t, s) post := func(tok string) *http.Response { req, _ := http.NewRequest("POST", ts.URL+"/api/spaces/a4f.example.com/checkout", strings.NewReader(`{"tier":"friends","band":"reduced"}`)) if tok != "" { req.Header.Set("Authorization", "Bearer "+tok) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } t.Cleanup(func() { resp.Body.Close() }) return resp } // No auth → 401; no provisioner → 501. if r := post(""); r.StatusCode != http.StatusUnauthorized { t.Fatalf("no auth: %d, want 401", r.StatusCode) } if r := post(token); r.StatusCode != http.StatusNotImplemented { t.Fatalf("unconfigured: %d, want 501", r.StatusCode) } cloud, captured, capturedBody := fakeCloud(t, http.StatusOK, map[string]any{ "url": "https://checkout.stripe.com/c/pay/cs_x", }) s.cloud = newCloudProvisioner(cloud.URL, "caller-token") resp := post(token) if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } var m map[string]any json.NewDecoder(resp.Body).Decode(&m) if !strings.Contains(m["url"].(string), "checkout.stripe.com") { t.Errorf("relayed url = %v", m["url"]) } // Home forwarded the uid as requester and return URLs on its own origin — // success carries the host back so the page can thank inline. if captured.URL.Path != "/api/instances/a4f.example.com/checkout" { t.Errorf("cloud saw path %s", captured.URL.Path) } var sent map[string]string json.Unmarshal(*capturedBody, &sent) if sent["requester"] != uid { t.Errorf("requester = %q, want %s", sent["requester"], uid) } if sent["tier"] != "friends" { t.Errorf("tier = %q, want friends", sent["tier"]) } // The honor band rides through verbatim — the provisioner's book validates. if sent["band"] != "reduced" { t.Errorf("band = %q, want reduced", sent["band"]) } // A missing tier is rejected before the provisioner is ever called. noTier, _ := http.NewRequest("POST", ts.URL+"/api/spaces/a4f.example.com/checkout", nil) noTier.Header.Set("Authorization", "Bearer "+token) ntResp, err := http.DefaultClient.Do(noTier) if err != nil { t.Fatal(err) } ntResp.Body.Close() if ntResp.StatusCode != http.StatusBadRequest { t.Errorf("no tier: %d, want 400", ntResp.StatusCode) } if !strings.HasPrefix(sent["success_url"], "http://localhost:1748/?upgraded=") || !strings.Contains(sent["success_url"], "a4f.example.com") { t.Errorf("success_url = %q", sent["success_url"]) } if sent["cancel_url"] != "http://localhost:1748/" { t.Errorf("cancel_url = %q", sent["cancel_url"]) } // Stable downstream codes relay as-is (409 already upgraded)… conflict, _, _ := fakeCloud(t, http.StatusConflict, map[string]any{"error": "already_upgraded"}) s.cloud = newCloudProvisioner(conflict.URL, "caller-token") if r := post(token); r.StatusCode != http.StatusConflict { t.Errorf("conflict relay: %d, want 409", r.StatusCode) } // …anything unexpected flattens to 502. weird, _, _ := fakeCloud(t, http.StatusTeapot, map[string]any{"error": "internal-detail"}) s.cloud = newCloudProvisioner(weird.URL, "caller-token") if r := post(token); r.StatusCode != http.StatusBadGateway { t.Errorf("weird relay: %d, want 502", r.StatusCode) } } func TestDeleteSpaceRequiresFreshCeremony(t *testing.T) { ts, s := newTestServer(t) uid, linkToken := seedUser(t, s) cloud, captured, _ := fakeCloud(t, http.StatusOK, map[string]any{"ok": true}) s.cloud = newCloudProvisioner(cloud.URL, "caller-token") del := func(headers map[string]string) *http.Response { req, _ := http.NewRequest("DELETE", ts.URL+"/api/spaces/a4f.example.com", nil) for k, v := range headers { req.Header.Set(k, v) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } t.Cleanup(func() { resp.Body.Close() }) return resp } // No auth → 401. A LINK TOKEN → still 401: destroying a space demands a // fresh passkey ceremony, never the bearer a device holds all day. if r := del(nil); r.StatusCode != http.StatusUnauthorized { t.Fatalf("no auth: %d, want 401", r.StatusCode) } if r := del(map[string]string{"Authorization": "Bearer " + linkToken}); r.StatusCode != http.StatusUnauthorized { t.Fatalf("link token: %d, want 401", r.StatusCode) } // A fresh ceremony proof authorises it, and home forwards the requester uid. proof := s.keys.mintCeremonyProof(uid) if r := del(map[string]string{"X-Ceremony-Proof": proof}); r.StatusCode != http.StatusOK { t.Fatalf("with ceremony: %d, want 200", r.StatusCode) } if captured.Method != "DELETE" || !strings.Contains(captured.URL.RawQuery, "requester="+uid) { t.Errorf("cloud saw %s %s?%s, want DELETE with requester=%s", captured.Method, captured.URL.Path, captured.URL.RawQuery, uid) } } func TestListSpacesProxiesLedger(t *testing.T) { ts, s := newTestServer(t) uid, linkToken := seedUser(t, s) cloud, captured, _ := fakeCloud(t, http.StatusOK, map[string]any{ "spaces": []map[string]any{{"host": "a4f.example.com", "url": "https://a4f.example.com"}}, }) s.cloud = newCloudProvisioner(cloud.URL, "caller-token") req, _ := http.NewRequest("GET", ts.URL+"/api/spaces", nil) req.Header.Set("Authorization", "Bearer "+linkToken) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status %d, want 200", resp.StatusCode) } var m struct{ Spaces []struct{ Host string } } json.NewDecoder(resp.Body).Decode(&m) if len(m.Spaces) != 1 || m.Spaces[0].Host != "a4f.example.com" { t.Errorf("spaces = %+v", m.Spaces) } if !strings.Contains(captured.URL.RawQuery, "requester="+uid) { t.Errorf("cloud saw ?%s, want requester=%s", captured.URL.RawQuery, uid) } } // The optional space name rides the create body through to the provisioner; // an oversized one is refused before any dial. func TestCreateSpaceForwardsName(t *testing.T) { ts, s := newTestServer(t) _, token := seedMember(t, s) cloud, _, capturedBody := fakeCloud(t, http.StatusCreated, map[string]any{ "host": "h.example.com", "url": "https://h.example.com", }) s.cloud = newCloudProvisioner(cloud.URL, "caller-token") post := func(body string) *http.Response { req, _ := http.NewRequest("POST", ts.URL+"/api/spaces", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } t.Cleanup(func() { resp.Body.Close() }) return resp } if r := post(`{"name":" Book Club "}`); r.StatusCode != http.StatusCreated { t.Fatalf("create with name: %d, want 201", r.StatusCode) } var sent map[string]string json.Unmarshal(*capturedBody, &sent) if sent["name"] != "Book Club" { t.Errorf("provisioner saw name %q, want %q", sent["name"], "Book Club") } if r := post(`{"name":"` + strings.Repeat("x", 61) + `"}`); r.StatusCode != http.StatusBadRequest { t.Errorf("61-char name: %d, want 400", r.StatusCode) } } // The pay-first pair forwards with the session-id template in the redirect and // relays the provisioner's answers (including the claim's retriable message). func TestCheckoutCreateAndClaimForward(t *testing.T) { ts, s := newTestServer(t) uid, token := seedMember(t, s) cloud, captured, capturedBody := fakeCloud(t, http.StatusOK, map[string]any{ "url": "https://checkout.stripe.com/c/pay/cs_1", }) s.cloud = newCloudProvisioner(cloud.URL, "caller-token") post := func(path, body string) *http.Response { req, _ := http.NewRequest("POST", ts.URL+path, strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } t.Cleanup(func() { resp.Body.Close() }) return resp } if r := post("/api/spaces/checkout-create", `{"tier":"family","band":"base","name":"Book Club"}`); r.StatusCode != http.StatusOK { t.Fatalf("checkout-create: %d, want 200", r.StatusCode) } if captured.URL.Path != "/api/instances/checkout-create" { t.Errorf("forwarded to %s", captured.URL.Path) } var sent map[string]string json.Unmarshal(*capturedBody, &sent) if sent["requester"] != uid || sent["tier"] != "family" || sent["name"] != "Book Club" { t.Errorf("forwarded body = %v", sent) } if !strings.Contains(sent["success_url"], "?paidcreate={CHECKOUT_SESSION_ID}") { t.Errorf("success_url = %q, want the session-id template", sent["success_url"]) } // Missing tier → 400 before any dial; claim without a session likewise. if r := post("/api/spaces/checkout-create", `{"name":"X"}`); r.StatusCode != http.StatusBadRequest { t.Errorf("no tier: %d, want 400", r.StatusCode) } if r := post("/api/spaces/claim-create", `{}`); r.StatusCode != http.StatusBadRequest { t.Errorf("no session: %d, want 400", r.StatusCode) } if r := post("/api/spaces/claim-create", `{"session_id":"cs_1"}`); r.StatusCode != http.StatusOK { t.Fatalf("claim relay: %d, want 200", r.StatusCode) } json.Unmarshal(*capturedBody, &sent) if sent["requester"] != uid || sent["session_id"] != "cs_1" { t.Errorf("claim body = %v", sent) } }