package main import ( "net/http" "net/http/httptest" "testing" ) // The connected-services store (connected.go) persists an opaque, client-encrypted // blob per (member, service), token-gated, and never reads it. Round-trip + the // per-member isolation that keeps one member's credential blob out of another's. func TestConnectedServicesStore(t *testing.T) { ts, srv := newTestServer(t) tokA, _, _ := joinedMember(t, srv, "Ann") tokB, _, _ := joinedMember(t, srv, "Bo") blob := "sealed-ciphertext-blob" resp := doReq(t, "PUT", ts.URL+"/api/connected/dnsimple", `{"token":"`+tokA+`","blob":"`+blob+`"}`) if resp.StatusCode != http.StatusOK { t.Fatalf("PUT store: got %d", resp.StatusCode) } resp.Body.Close() // Ann reads her blob back verbatim. var got struct { Service string `json:"service"` Blob string `json:"blob"` Connected bool `json:"connected"` } resp = doReq(t, "GET", ts.URL+"/api/connected/dnsimple?token="+tokA, "") decodeJSON(t, resp, &got) if !got.Connected || got.Blob != blob { t.Fatalf("GET after store: connected=%v blob=%q", got.Connected, got.Blob) } // It's listed. var list struct { Services []string `json:"services"` } resp = doReq(t, "GET", ts.URL+"/api/connected?token="+tokA, "") decodeJSON(t, resp, &list) if len(list.Services) != 1 || list.Services[0] != "dnsimple" { t.Fatalf("list: %v", list.Services) } // Per-member isolation: Bo sees nothing for the same service. var bo struct { Connected bool `json:"connected"` } resp = doReq(t, "GET", ts.URL+"/api/connected/dnsimple?token="+tokB, "") decodeJSON(t, resp, &bo) if bo.Connected { t.Fatalf("member isolation broken: Bo can see Ann's credential") } // Disconnect removes it. resp = doReq(t, "DELETE", ts.URL+"/api/connected/dnsimple?token="+tokA, "") if resp.StatusCode != http.StatusOK { t.Fatalf("DELETE: %d", resp.StatusCode) } resp.Body.Close() resp = doReq(t, "GET", ts.URL+"/api/connected/dnsimple?token="+tokA, "") decodeJSON(t, resp, &got) if got.Connected { t.Fatalf("still connected after DELETE") } } // Every connected-services endpoint is token-gated. func TestConnectedServicesAuth(t *testing.T) { ts, _ := newTestServer(t) resp := doReq(t, "GET", ts.URL+"/api/connected/dnsimple", "") if resp.StatusCode != http.StatusForbidden { t.Fatalf("GET no token: got %d want 403", resp.StatusCode) } resp.Body.Close() resp = doReq(t, "PUT", ts.URL+"/api/connected/dnsimple", `{"token":"nope","blob":"x"}`) if resp.StatusCode != http.StatusForbidden { t.Fatalf("PUT bad token: got %d want 403", resp.StatusCode) } resp.Body.Close() } // The instance's /api/oauth/config is a per-service pass-through to the connect // broker: a service the broker has an app for comes back with its public // params; one it doesn't (or a malformed name) is an empty {} — the catalog // then offers paste-a-token alone. func TestOAuthConfigPerService(t *testing.T) { broker := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/config" { http.NotFound(w, r) return } switch r.URL.Query().Get("service") { case "github": w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"service":"github","client_id":"gh-cid","authorize_url":"https://github.com/login/oauth/authorize","redirect_uri":"https://broker.example/oauth/github/callback"}`)) default: http.NotFound(w, r) } })) defer broker.Close() t.Setenv("LCHAT_CONNECT_BROKER_URL", broker.URL) ts, srv := newTestServer(t) tok, _, _ := joinedMember(t, srv, "Ann") var cfg map[string]string resp := doReq(t, "GET", ts.URL+"/api/oauth/config?service=github&token="+tok, "") decodeJSON(t, resp, &cfg) if cfg["client_id"] != "gh-cid" || cfg["redirect_uri"] == "" { t.Fatalf("github config = %v", cfg) } // A service the broker has no app for → empty config (never an error). var none map[string]string resp = doReq(t, "GET", ts.URL+"/api/oauth/config?service=stripe&token="+tok, "") decodeJSON(t, resp, &none) if len(none) != 0 { t.Fatalf("unoffered service config = %v", none) } // A malformed service name never reaches the broker. var bad map[string]string resp = doReq(t, "GET", ts.URL+"/api/oauth/config?service=Not%20A%20Slug&token="+tok, "") decodeJSON(t, resp, &bad) if len(bad) != 0 { t.Fatalf("malformed service config = %v", bad) } // No token → refused. resp = doReq(t, "GET", ts.URL+"/api/oauth/config?service=github", "") if resp.StatusCode != http.StatusForbidden { t.Fatalf("tokenless: got %d want 403", resp.StatusCode) } resp.Body.Close() }