eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "testing"
7)
8
9// The connected-services store (connected.go) persists an opaque, client-encrypted
10// blob per (member, service), token-gated, and never reads it. Round-trip + the
11// per-member isolation that keeps one member's credential blob out of another's.
12func TestConnectedServicesStore(t *testing.T) {
13 ts, srv := newTestServer(t)
14 tokA, _, _ := joinedMember(t, srv, "Ann")
15 tokB, _, _ := joinedMember(t, srv, "Bo")
16
17 blob := "sealed-ciphertext-blob"
18 resp := doReq(t, "PUT", ts.URL+"/api/connected/dnsimple", `{"token":"`+tokA+`","blob":"`+blob+`"}`)
19 if resp.StatusCode != http.StatusOK {
20 t.Fatalf("PUT store: got %d", resp.StatusCode)
21 }
22 resp.Body.Close()
23
24 // Ann reads her blob back verbatim.
25 var got struct {
26 Service string `json:"service"`
27 Blob string `json:"blob"`
28 Connected bool `json:"connected"`
29 }
30 resp = doReq(t, "GET", ts.URL+"/api/connected/dnsimple?token="+tokA, "")
31 decodeJSON(t, resp, &got)
32 if !got.Connected || got.Blob != blob {
33 t.Fatalf("GET after store: connected=%v blob=%q", got.Connected, got.Blob)
34 }
35
36 // It's listed.
37 var list struct {
38 Services []string `json:"services"`
39 }
40 resp = doReq(t, "GET", ts.URL+"/api/connected?token="+tokA, "")
41 decodeJSON(t, resp, &list)
42 if len(list.Services) != 1 || list.Services[0] != "dnsimple" {
43 t.Fatalf("list: %v", list.Services)
44 }
45
46 // Per-member isolation: Bo sees nothing for the same service.
47 var bo struct {
48 Connected bool `json:"connected"`
49 }
50 resp = doReq(t, "GET", ts.URL+"/api/connected/dnsimple?token="+tokB, "")
51 decodeJSON(t, resp, &bo)
52 if bo.Connected {
53 t.Fatalf("member isolation broken: Bo can see Ann's credential")
54 }
55
56 // Disconnect removes it.
57 resp = doReq(t, "DELETE", ts.URL+"/api/connected/dnsimple?token="+tokA, "")
58 if resp.StatusCode != http.StatusOK {
59 t.Fatalf("DELETE: %d", resp.StatusCode)
60 }
61 resp.Body.Close()
62 resp = doReq(t, "GET", ts.URL+"/api/connected/dnsimple?token="+tokA, "")
63 decodeJSON(t, resp, &got)
64 if got.Connected {
65 t.Fatalf("still connected after DELETE")
66 }
67}
68
69// Every connected-services endpoint is token-gated.
70func TestConnectedServicesAuth(t *testing.T) {
71 ts, _ := newTestServer(t)
72
73 resp := doReq(t, "GET", ts.URL+"/api/connected/dnsimple", "")
74 if resp.StatusCode != http.StatusForbidden {
75 t.Fatalf("GET no token: got %d want 403", resp.StatusCode)
76 }
77 resp.Body.Close()
78
79 resp = doReq(t, "PUT", ts.URL+"/api/connected/dnsimple", `{"token":"nope","blob":"x"}`)
80 if resp.StatusCode != http.StatusForbidden {
81 t.Fatalf("PUT bad token: got %d want 403", resp.StatusCode)
82 }
83 resp.Body.Close()
84}
85
86// The instance's /api/oauth/config is a per-service pass-through to the connect
87// broker: a service the broker has an app for comes back with its public
88// params; one it doesn't (or a malformed name) is an empty {} — the catalog
89// then offers paste-a-token alone.
90func TestOAuthConfigPerService(t *testing.T) {
91 broker := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
92 if r.URL.Path != "/config" {
93 http.NotFound(w, r)
94 return
95 }
96 switch r.URL.Query().Get("service") {
97 case "github":
98 w.Header().Set("Content-Type", "application/json")
99 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"}`))
100 default:
101 http.NotFound(w, r)
102 }
103 }))
104 defer broker.Close()
105 t.Setenv("LCHAT_CONNECT_BROKER_URL", broker.URL)
106
107 ts, srv := newTestServer(t)
108 tok, _, _ := joinedMember(t, srv, "Ann")
109
110 var cfg map[string]string
111 resp := doReq(t, "GET", ts.URL+"/api/oauth/config?service=github&token="+tok, "")
112 decodeJSON(t, resp, &cfg)
113 if cfg["client_id"] != "gh-cid" || cfg["redirect_uri"] == "" {
114 t.Fatalf("github config = %v", cfg)
115 }
116
117 // A service the broker has no app for → empty config (never an error).
118 var none map[string]string
119 resp = doReq(t, "GET", ts.URL+"/api/oauth/config?service=stripe&token="+tok, "")
120 decodeJSON(t, resp, &none)
121 if len(none) != 0 {
122 t.Fatalf("unoffered service config = %v", none)
123 }
124
125 // A malformed service name never reaches the broker.
126 var bad map[string]string
127 resp = doReq(t, "GET", ts.URL+"/api/oauth/config?service=Not%20A%20Slug&token="+tok, "")
128 decodeJSON(t, resp, &bad)
129 if len(bad) != 0 {
130 t.Fatalf("malformed service config = %v", bad)
131 }
132
133 // No token → refused.
134 resp = doReq(t, "GET", ts.URL+"/api/oauth/config?service=github", "")
135 if resp.StatusCode != http.StatusForbidden {
136 t.Fatalf("tokenless: got %d want 403", resp.StatusCode)
137 }
138 resp.Body.Close()
139}
140