code.oscarkilo.com/okg

Hash:
9e495d4e8c3e0f3399af24039f67692023a70a9f
Author:
Igor Naverniouk <[email protected]>
Date:
Sat Jun 6 15:14:48 2026 -0400
Message:
//okg/who: SetAuthzMulti + SetAuthz wraps it The new wire format for POST /authz/set is a JSON array of AuthzSetRequest. The server processes all entries atomically (Pebble batch) — either every write succeeds or none do. Add SetAuthzMulti(reqs []AuthzSetRequest) as the primary method. SetAuthz(req) becomes a one-element wrapper. Tests updated to verify the array body on the wire and cover the multi case.
diff --git a/who/who.go b/who/who.go
index d3999c7..72d5069 100644
--- a/who/who.go
+++ b/who/who.go
@@ -205,10 +205,20 @@ type AuthzSetRequest struct {
ReaderUsername string `json:"reader_username"`
}

-// SetAuthz writes an authz entry. The caller must be an owner
-// of the URI (or a //who admin).
+// SetAuthzMulti writes one or more authz entries atomically.
+// Either every entry's write succeeds or none do (server-side
+// Pebble batch). The caller must be an owner of every URI in
+// the batch (or a //who admin).
+func (c *HTTPClient) SetAuthzMulti(
+ reqs []AuthzSetRequest,
+) error {
+ return c.PostJSON("/authz/set", reqs, nil)
+}
+
+// SetAuthz writes one authz entry. Syntactic sugar over
+// SetAuthzMulti for the single-entry case.
func (c *HTTPClient) SetAuthz(req AuthzSetRequest) error {
- return c.PostJSON("/authz/set", req, nil)
+ return c.SetAuthzMulti([]AuthzSetRequest{req})
}

// AuthzDeleteRequest is the body for POST /authz/delete.
diff --git a/who/who_test.go b/who/who_test.go
index 090550c..1e379cc 100644
--- a/who/who_test.go
+++ b/who/who_test.go
@@ -254,7 +254,7 @@ func TestListAuthz(t *testing.T) {
}

func TestSetAuthz(t *testing.T) {
- var seen AuthzSetRequest
+ var seen []AuthzSetRequest
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/authz/set" {
@@ -274,11 +274,46 @@ func TestSetAuthz(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if seen.Uri != "chat://team#post" {
- t.Errorf("Uri: got %q", seen.Uri)
+ if len(seen) != 1 {
+ t.Fatalf("len: got %d, want 1", len(seen))
+ }
+ if seen[0].Uri != "chat://team#post" {
+ t.Errorf("Uri: got %q", seen[0].Uri)
+ }
+ if seen[0].OwnerUsername != "team" {
+ t.Errorf("OwnerUsername: got %q", seen[0].OwnerUsername)
+ }
+}
+
+// TestSetAuthzMulti covers the multi-entry wire shape: array on
+// the wire, all entries seen by the server.
+func TestSetAuthzMulti(t *testing.T) {
+ var seen []AuthzSetRequest
+ srv := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ json.NewDecoder(r.Body).Decode(&seen)
+ w.WriteHeader(204)
+ }))
+ defer srv.Close()
+
+ c := NewHTTPClient(srv.URL, "test-key")
+ err := c.SetAuthzMulti([]AuthzSetRequest{
+ {
+ Uri: "chat://team#post",
+ OwnerUsername: "team",
+ ReaderUsername: "team",
+ },
+ {
+ Uri: "chat://team#read",
+ OwnerUsername: "team",
+ ReaderUsername: "team",
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
}
- if seen.OwnerUsername != "team" {
- t.Errorf("OwnerUsername: got %q", seen.OwnerUsername)
+ if len(seen) != 2 {
+ t.Fatalf("len: got %d, want 2", len(seen))
}
}

a/who/who.go
b/who/who.go
1
// Package who is the public HTTP client for OscarKilo's //who
1
// Package who is the public HTTP client for OscarKilo's //who
2
// service. It is used by `okg` and any other tool that talks
2
// service. It is used by `okg` and any other tool that talks
3
// to oscarkilo.com over the network.
3
// to oscarkilo.com over the network.
4
//
4
//
5
// The struct here is named HTTPClient (not just Client) so
5
// The struct here is named HTTPClient (not just Client) so
6
// applications can wrap it in their own Client abstractions
6
// applications can wrap it in their own Client abstractions
7
// without name collision.
7
// without name collision.
8
package who
8
package who
9
9
10
import "time"
10
import "time"
11
11
12
import "oscarkilo.com/okg/internal/rest"
12
import "oscarkilo.com/okg/internal/rest"
13
13
14
// HTTPClient talks to a //who server over HTTPS. Embeds the
14
// HTTPClient talks to a //who server over HTTPS. Embeds the
15
// shared rest helpers; who-specific methods live below.
15
// shared rest helpers; who-specific methods live below.
16
type HTTPClient struct {
16
type HTTPClient struct {
17
*rest.Client
17
*rest.Client
18
}
18
}
19
19
20
func NewHTTPClient(host, apiKey string) *HTTPClient {
20
func NewHTTPClient(host, apiKey string) *HTTPClient {
21
return &HTTPClient{Client: rest.NewClient(host, apiKey)}
21
return &HTTPClient{Client: rest.NewClient(host, apiKey)}
22
}
22
}
23
23
24
// ---- Common types ----
24
// ---- Common types ----
25
25
26
// AppDataEntry is one user-scoped app-data record served by
26
// AppDataEntry is one user-scoped app-data record served by
27
// //who (per-user key/value, namespaced by app).
27
// //who (per-user key/value, namespaced by app).
28
type AppDataEntry struct {
28
type AppDataEntry struct {
29
App string `json:"app"`
29
App string `json:"app"`
30
Key string `json:"key"`
30
Key string `json:"key"`
31
Value string `json:"value"`
31
Value string `json:"value"`
32
Timestamp time.Time `json:"timestamp"`
32
Timestamp time.Time `json:"timestamp"`
33
}
33
}
34
34
35
// ---- Caller identity ----
35
// ---- Caller identity ----
36
36
37
// Profile is the caller's //who profile. Mirrors the
37
// Profile is the caller's //who profile. Mirrors the
38
// JSON-serialized subset of //who.Profile that GET
38
// JSON-serialized subset of //who.Profile that GET
39
// /login/profile/get returns. ApiKeys, Logins, and timestamps
39
// /login/profile/get returns. ApiKeys, Logins, and timestamps
40
// are not exposed here yet — add when okg has a use.
40
// are not exposed here yet — add when okg has a use.
41
type Profile struct {
41
type Profile struct {
42
Owid string `json:"owid"`
42
Owid string `json:"owid"`
43
Username string `json:"username"`
43
Username string `json:"username"`
44
Name string `json:"name"`
44
Name string `json:"name"`
45
Email string `json:"email"`
45
Email string `json:"email"`
46
PortraitUrl string `json:"portrait_url"`
46
PortraitUrl string `json:"portrait_url"`
47
OwnerOwid string `json:"owner_owid"`
47
OwnerOwid string `json:"owner_owid"`
48
Groups []string `json:"groups"`
48
Groups []string `json:"groups"`
49
AppData []AppDataEntry `json:"app_data,omitempty"`
49
AppData []AppDataEntry `json:"app_data,omitempty"`
50
}
50
}
51
51
52
// GetProfile returns the caller's own profile, identified by
52
// GetProfile returns the caller's own profile, identified by
53
// the Bearer key.
53
// the Bearer key.
54
func (c *HTTPClient) GetProfile() (*Profile, error) {
54
func (c *HTTPClient) GetProfile() (*Profile, error) {
55
var p Profile
55
var p Profile
56
if err := c.GetJSON("/login/profile/get", &p); err != nil {
56
if err := c.GetJSON("/login/profile/get", &p); err != nil {
57
return nil, err
57
return nil, err
58
}
58
}
59
return &p, nil
59
return &p, nil
60
}
60
}
61
61
62
// ---- Groups ----
62
// ---- Groups ----
63
63
64
// Group describes a //who group (a named entity with members).
64
// Group describes a //who group (a named entity with members).
65
// Matches the row shape in /groups/list's response.
65
// Matches the row shape in /groups/list's response.
66
type Group struct {
66
type Group struct {
67
Owid string `json:"owid"`
67
Owid string `json:"owid"`
68
Username string `json:"username"`
68
Username string `json:"username"`
69
Name string `json:"name"`
69
Name string `json:"name"`
70
OwnerOwid string `json:"owner_owid"`
70
OwnerOwid string `json:"owner_owid"`
71
OwnerUsername string `json:"owner_username"`
71
OwnerUsername string `json:"owner_username"`
72
}
72
}
73
73
74
type listGroupsResponse struct {
74
type listGroupsResponse struct {
75
Groups []Group `json:"groups"`
75
Groups []Group `json:"groups"`
76
}
76
}
77
77
78
// ListGroups returns the groups visible to the caller.
78
// ListGroups returns the groups visible to the caller.
79
func (c *HTTPClient) ListGroups() ([]Group, error) {
79
func (c *HTTPClient) ListGroups() ([]Group, error) {
80
var res listGroupsResponse
80
var res listGroupsResponse
81
if err := c.GetJSON("/groups/list", &res); err != nil {
81
if err := c.GetJSON("/groups/list", &res); err != nil {
82
return nil, err
82
return nil, err
83
}
83
}
84
return res.Groups, nil
84
return res.Groups, nil
85
}
85
}
86
86
87
// CreateGroupRequest is the body for POST /groups/add. Username
87
// CreateGroupRequest is the body for POST /groups/add. Username
88
// is the group's //who username (e.g. "team"); Name is the
88
// is the group's //who username (e.g. "team"); Name is the
89
// human-facing display name; OwnerUsername is the user who'll
89
// human-facing display name; OwnerUsername is the user who'll
90
// own the new group.
90
// own the new group.
91
type CreateGroupRequest struct {
91
type CreateGroupRequest struct {
92
Username string `json:"username"`
92
Username string `json:"username"`
93
Name string `json:"name"`
93
Name string `json:"name"`
94
OwnerUsername string `json:"owner_username"`
94
OwnerUsername string `json:"owner_username"`
95
}
95
}
96
96
97
// CreateGroup creates a new //who group.
97
// CreateGroup creates a new //who group.
98
func (c *HTTPClient) CreateGroup(req CreateGroupRequest) error {
98
func (c *HTTPClient) CreateGroup(req CreateGroupRequest) error {
99
return c.PostJSON("/groups/add", req, nil)
99
return c.PostJSON("/groups/add", req, nil)
100
}
100
}
101
101
102
// JoinGroupsRequest is the body for POST /groups/join. The
102
// JoinGroupsRequest is the body for POST /groups/join. The
103
// server adds every (GroupUsernames[i], MemberUsernames[j])
103
// server adds every (GroupUsernames[i], MemberUsernames[j])
104
// pair, requiring the caller to be an owner of each group.
104
// pair, requiring the caller to be an owner of each group.
105
type JoinGroupsRequest struct {
105
type JoinGroupsRequest struct {
106
GroupUsernames []string `json:"group_usernames"`
106
GroupUsernames []string `json:"group_usernames"`
107
MemberUsernames []string `json:"member_usernames"`
107
MemberUsernames []string `json:"member_usernames"`
108
}
108
}
109
109
110
// JoinGroups adds members to groups (all pairwise combinations).
110
// JoinGroups adds members to groups (all pairwise combinations).
111
func (c *HTTPClient) JoinGroups(req JoinGroupsRequest) error {
111
func (c *HTTPClient) JoinGroups(req JoinGroupsRequest) error {
112
return c.PostJSON("/groups/join", req, nil)
112
return c.PostJSON("/groups/join", req, nil)
113
}
113
}
114
114
115
// GroupMembersResponse is the response from POST
115
// GroupMembersResponse is the response from POST
116
// /groups/members. Up lists the groups that contain the queried
116
// /groups/members. Up lists the groups that contain the queried
117
// entity (each level as a slice of owids). Down lists its
117
// entity (each level as a slice of owids). Down lists its
118
// members the same way. Usernames maps every owid mentioned to
118
// members the same way. Usernames maps every owid mentioned to
119
// its //who username.
119
// its //who username.
120
type GroupMembersResponse struct {
120
type GroupMembersResponse struct {
121
Up [][]string `json:"up"`
121
Up [][]string `json:"up"`
122
Down [][]string `json:"down"`
122
Down [][]string `json:"down"`
123
Usernames map[string]string `json:"usernames"`
123
Usernames map[string]string `json:"usernames"`
124
}
124
}
125
125
126
// GroupMembers returns membership DAG navigation for the given
126
// GroupMembers returns membership DAG navigation for the given
127
// entity owid. For a group, Down enumerates its members.
127
// entity owid. For a group, Down enumerates its members.
128
func (c *HTTPClient) GroupMembers(
128
func (c *HTTPClient) GroupMembers(
129
ownerOwid string,
129
ownerOwid string,
130
) (*GroupMembersResponse, error) {
130
) (*GroupMembersResponse, error) {
131
var res GroupMembersResponse
131
var res GroupMembersResponse
132
err := c.PostJSON("/groups/members",
132
err := c.PostJSON("/groups/members",
133
map[string]string{"owid": ownerOwid}, &res)
133
map[string]string{"owid": ownerOwid}, &res)
134
if err != nil {
134
if err != nil {
135
return nil, err
135
return nil, err
136
}
136
}
137
return &res, nil
137
return &res, nil
138
}
138
}
139
139
140
// LeaveGroupRequest is the body for POST /groups/leave. Both
140
// LeaveGroupRequest is the body for POST /groups/leave. Both
141
// IDs are owids (obfuscated wids); to translate from usernames,
141
// IDs are owids (obfuscated wids); to translate from usernames,
142
// call ListGroups + GroupMembers first.
142
// call ListGroups + GroupMembers first.
143
type LeaveGroupRequest struct {
143
type LeaveGroupRequest struct {
144
GroupOwid string `json:"group_owid"`
144
GroupOwid string `json:"group_owid"`
145
MemberOwid string `json:"member_owid"`
145
MemberOwid string `json:"member_owid"`
146
}
146
}
147
147
148
// LeaveGroup removes a member from a group. The caller must own
148
// LeaveGroup removes a member from a group. The caller must own
149
// the group.
149
// the group.
150
func (c *HTTPClient) LeaveGroup(req LeaveGroupRequest) error {
150
func (c *HTTPClient) LeaveGroup(req LeaveGroupRequest) error {
151
return c.PostJSON("/groups/leave", req, nil)
151
return c.PostJSON("/groups/leave", req, nil)
152
}
152
}
153
153
154
// DeleteGroupRequest is the body for POST /groups/delete. Owid
154
// DeleteGroupRequest is the body for POST /groups/delete. Owid
155
// is the group's obfuscated wid.
155
// is the group's obfuscated wid.
156
type DeleteGroupRequest struct {
156
type DeleteGroupRequest struct {
157
Owid string `json:"owid"`
157
Owid string `json:"owid"`
158
}
158
}
159
159
160
// DeleteGroup destroys a group. The caller must own it (or be a
160
// DeleteGroup destroys a group. The caller must own it (or be a
161
// //who admin).
161
// //who admin).
162
func (c *HTTPClient) DeleteGroup(req DeleteGroupRequest) error {
162
func (c *HTTPClient) DeleteGroup(req DeleteGroupRequest) error {
163
return c.PostJSON("/groups/delete", req, nil)
163
return c.PostJSON("/groups/delete", req, nil)
164
}
164
}
165
165
166
// ---- Authz ----
166
// ---- Authz ----
167
167
168
// User is the public //who-user identifier. It carries only
168
// User is the public //who-user identifier. It carries only
169
// the fields safe to expose to external callers.
169
// the fields safe to expose to external callers.
170
type User struct {
170
type User struct {
171
Owid string `json:"owid"`
171
Owid string `json:"owid"`
172
Username string `json:"username"`
172
Username string `json:"username"`
173
AppData []AppDataEntry `json:"appdata,omitempty"`
173
AppData []AppDataEntry `json:"appdata,omitempty"`
174
}
174
}
175
175
176
// AuthzEntry is one row of GET /authz/list: the URI, its owner
176
// AuthzEntry is one row of GET /authz/list: the URI, its owner
177
// and reader, and the caller's effective rights on it.
177
// and reader, and the caller's effective rights on it.
178
type AuthzEntry struct {
178
type AuthzEntry struct {
179
Uri string `json:"uri"`
179
Uri string `json:"uri"`
180
Owner *User `json:"owner"`
180
Owner *User `json:"owner"`
181
Reader *User `json:"reader"`
181
Reader *User `json:"reader"`
182
IsOwner bool `json:"is_owner"`
182
IsOwner bool `json:"is_owner"`
183
IsReader bool `json:"is_reader"`
183
IsReader bool `json:"is_reader"`
184
}
184
}
185
185
186
type listAuthzResponse struct {
186
type listAuthzResponse struct {
187
Uris []AuthzEntry `json:"uris"`
187
Uris []AuthzEntry `json:"uris"`
188
}
188
}
189
189
190
// ListAuthz returns all authz URIs visible to the caller.
190
// ListAuthz returns all authz URIs visible to the caller.
191
func (c *HTTPClient) ListAuthz() ([]AuthzEntry, error) {
191
func (c *HTTPClient) ListAuthz() ([]AuthzEntry, error) {
192
var res listAuthzResponse
192
var res listAuthzResponse
193
if err := c.GetJSON("/authz/list", &res); err != nil {
193
if err := c.GetJSON("/authz/list", &res); err != nil {
194
return nil, err
194
return nil, err
195
}
195
}
196
return res.Uris, nil
196
return res.Uris, nil
197
}
197
}
198
198
199
// AuthzSetRequest is the body for POST /authz/set. Both
199
// AuthzSetRequest is the body for POST /authz/set. Both
200
// usernames must be non-empty and known to //who; the public
200
// usernames must be non-empty and known to //who; the public
201
// endpoint does not honor the "anyone" sentinel.
201
// endpoint does not honor the "anyone" sentinel.
202
type AuthzSetRequest struct {
202
type AuthzSetRequest struct {
203
Uri string `json:"uri"`
203
Uri string `json:"uri"`
204
OwnerUsername string `json:"owner_username"`
204
OwnerUsername string `json:"owner_username"`
205
ReaderUsername string `json:"reader_username"`
205
ReaderUsername string `json:"reader_username"`
206
}
206
}
207
207
208
// SetAuthz writes an authz entry. The caller must be an owner
208
// SetAuthzMulti writes one or more authz entries atomically.
209
// of the URI (or a //who admin).
209
// Either every entry's write succeeds or none do (server-side
210
// Pebble batch). The caller must be an owner of every URI in
211
// the batch (or a //who admin).
212
func (c *HTTPClient) SetAuthzMulti(
213
reqs []AuthzSetRequest,
214
) error {
215
return c.PostJSON("/authz/set", reqs, nil)
216
}
217
218
// SetAuthz writes one authz entry. Syntactic sugar over
219
// SetAuthzMulti for the single-entry case.
210
func (c *HTTPClient) SetAuthz(req AuthzSetRequest) error {
220
func (c *HTTPClient) SetAuthz(req AuthzSetRequest) error {
211
return c.PostJSON("/authz/set", req, nil)
221
return c.SetAuthzMulti([]AuthzSetRequest{req})
212
}
222
}
213
223
214
// AuthzDeleteRequest is the body for POST /authz/delete.
224
// AuthzDeleteRequest is the body for POST /authz/delete.
215
type AuthzDeleteRequest struct {
225
type AuthzDeleteRequest struct {
216
Uri string `json:"uri"`
226
Uri string `json:"uri"`
217
}
227
}
218
228
219
// DeleteAuthz removes an authz entry. The caller must own the URI.
229
// DeleteAuthz removes an authz entry. The caller must own the URI.
220
func (c *HTTPClient) DeleteAuthz(req AuthzDeleteRequest) error {
230
func (c *HTTPClient) DeleteAuthz(req AuthzDeleteRequest) error {
221
return c.PostJSON("/authz/delete", req, nil)
231
return c.PostJSON("/authz/delete", req, nil)
222
}
232
}
a/who/who_test.go
b/who/who_test.go
1
package who
1
package who
2
2
3
import "encoding/json"
3
import "encoding/json"
4
import "net/http"
4
import "net/http"
5
import "net/http/httptest"
5
import "net/http/httptest"
6
import "testing"
6
import "testing"
7
7
8
func TestListGroups(t *testing.T) {
8
func TestListGroups(t *testing.T) {
9
srv := httptest.NewServer(http.HandlerFunc(
9
srv := httptest.NewServer(http.HandlerFunc(
10
func(w http.ResponseWriter, r *http.Request) {
10
func(w http.ResponseWriter, r *http.Request) {
11
if r.URL.Path != "/groups/list" {
11
if r.URL.Path != "/groups/list" {
12
t.Errorf("path: got %q, want /groups/list",
12
t.Errorf("path: got %q, want /groups/list",
13
r.URL.Path)
13
r.URL.Path)
14
}
14
}
15
if r.Header.Get("Authorization") !=
15
if r.Header.Get("Authorization") !=
16
"Bearer test-key" {
16
"Bearer test-key" {
17
t.Errorf("Authorization: got %q",
17
t.Errorf("Authorization: got %q",
18
r.Header.Get("Authorization"))
18
r.Header.Get("Authorization"))
19
}
19
}
20
w.Header().Set(
20
w.Header().Set(
21
"Content-Type", "application/json")
21
"Content-Type", "application/json")
22
json.NewEncoder(w).Encode(listGroupsResponse{
22
json.NewEncoder(w).Encode(listGroupsResponse{
23
Groups: []Group{
23
Groups: []Group{
24
{
24
{
25
Username: "team",
25
Username: "team",
26
Name: "Team",
26
Name: "Team",
27
OwnerUsername: "alice",
27
OwnerUsername: "alice",
28
},
28
},
29
{
29
{
30
Username: "admins",
30
Username: "admins",
31
Name: "Admins",
31
Name: "Admins",
32
OwnerUsername: "alice",
32
OwnerUsername: "alice",
33
},
33
},
34
},
34
},
35
})
35
})
36
}))
36
}))
37
defer srv.Close()
37
defer srv.Close()
38
38
39
c := NewHTTPClient(srv.URL, "test-key")
39
c := NewHTTPClient(srv.URL, "test-key")
40
groups, err := c.ListGroups()
40
groups, err := c.ListGroups()
41
if err != nil {
41
if err != nil {
42
t.Fatal(err)
42
t.Fatal(err)
43
}
43
}
44
if len(groups) != 2 {
44
if len(groups) != 2 {
45
t.Fatalf("len: got %d, want 2", len(groups))
45
t.Fatalf("len: got %d, want 2", len(groups))
46
}
46
}
47
if groups[0].Username != "team" {
47
if groups[0].Username != "team" {
48
t.Errorf(
48
t.Errorf(
49
"groups[0].Username: got %q, want team",
49
"groups[0].Username: got %q, want team",
50
groups[0].Username)
50
groups[0].Username)
51
}
51
}
52
if groups[1].OwnerUsername != "alice" {
52
if groups[1].OwnerUsername != "alice" {
53
t.Errorf(
53
t.Errorf(
54
"groups[1].OwnerUsername: got %q, want alice",
54
"groups[1].OwnerUsername: got %q, want alice",
55
groups[1].OwnerUsername)
55
groups[1].OwnerUsername)
56
}
56
}
57
}
57
}
58
58
59
func TestListGroupsHTTPError(t *testing.T) {
59
func TestListGroupsHTTPError(t *testing.T) {
60
srv := httptest.NewServer(http.HandlerFunc(
60
srv := httptest.NewServer(http.HandlerFunc(
61
func(w http.ResponseWriter, r *http.Request) {
61
func(w http.ResponseWriter, r *http.Request) {
62
w.WriteHeader(401)
62
w.WriteHeader(401)
63
w.Write([]byte("not authenticated"))
63
w.Write([]byte("not authenticated"))
64
}))
64
}))
65
defer srv.Close()
65
defer srv.Close()
66
66
67
c := NewHTTPClient(srv.URL, "bad-key")
67
c := NewHTTPClient(srv.URL, "bad-key")
68
_, err := c.ListGroups()
68
_, err := c.ListGroups()
69
if err == nil {
69
if err == nil {
70
t.Fatal("want error on 401, got nil")
70
t.Fatal("want error on 401, got nil")
71
}
71
}
72
}
72
}
73
73
74
func TestGetProfile(t *testing.T) {
74
func TestGetProfile(t *testing.T) {
75
srv := httptest.NewServer(http.HandlerFunc(
75
srv := httptest.NewServer(http.HandlerFunc(
76
func(w http.ResponseWriter, r *http.Request) {
76
func(w http.ResponseWriter, r *http.Request) {
77
if r.URL.Path != "/login/profile/get" {
77
if r.URL.Path != "/login/profile/get" {
78
t.Errorf(
78
t.Errorf(
79
"path: got %q, want /login/profile/get",
79
"path: got %q, want /login/profile/get",
80
r.URL.Path)
80
r.URL.Path)
81
}
81
}
82
w.Header().Set(
82
w.Header().Set(
83
"Content-Type", "application/json")
83
"Content-Type", "application/json")
84
json.NewEncoder(w).Encode(Profile{
84
json.NewEncoder(w).Encode(Profile{
85
Username: "alice",
85
Username: "alice",
86
Name: "Alice Liddell",
86
Name: "Alice Liddell",
87
87
88
Groups: []string{"team", "admins"},
88
Groups: []string{"team", "admins"},
89
})
89
})
90
}))
90
}))
91
defer srv.Close()
91
defer srv.Close()
92
92
93
c := NewHTTPClient(srv.URL, "test-key")
93
c := NewHTTPClient(srv.URL, "test-key")
94
p, err := c.GetProfile()
94
p, err := c.GetProfile()
95
if err != nil {
95
if err != nil {
96
t.Fatal(err)
96
t.Fatal(err)
97
}
97
}
98
if p.Username != "alice" {
98
if p.Username != "alice" {
99
t.Errorf(
99
t.Errorf(
100
"Username: got %q, want alice", p.Username)
100
"Username: got %q, want alice", p.Username)
101
}
101
}
102
if len(p.Groups) != 2 {
102
if len(p.Groups) != 2 {
103
t.Errorf(
103
t.Errorf(
104
"Groups: got %d, want 2", len(p.Groups))
104
"Groups: got %d, want 2", len(p.Groups))
105
}
105
}
106
}
106
}
107
107
108
func TestCreateGroup(t *testing.T) {
108
func TestCreateGroup(t *testing.T) {
109
var seen CreateGroupRequest
109
var seen CreateGroupRequest
110
srv := httptest.NewServer(http.HandlerFunc(
110
srv := httptest.NewServer(http.HandlerFunc(
111
func(w http.ResponseWriter, r *http.Request) {
111
func(w http.ResponseWriter, r *http.Request) {
112
if r.URL.Path != "/groups/add" {
112
if r.URL.Path != "/groups/add" {
113
t.Errorf(
113
t.Errorf(
114
"path: got %q, want /groups/add",
114
"path: got %q, want /groups/add",
115
r.URL.Path)
115
r.URL.Path)
116
}
116
}
117
if r.Method != "POST" {
117
if r.Method != "POST" {
118
t.Errorf("method: got %q, want POST", r.Method)
118
t.Errorf("method: got %q, want POST", r.Method)
119
}
119
}
120
json.NewDecoder(r.Body).Decode(&seen)
120
json.NewDecoder(r.Body).Decode(&seen)
121
w.WriteHeader(200)
121
w.WriteHeader(200)
122
}))
122
}))
123
defer srv.Close()
123
defer srv.Close()
124
124
125
c := NewHTTPClient(srv.URL, "test-key")
125
c := NewHTTPClient(srv.URL, "test-key")
126
err := c.CreateGroup(CreateGroupRequest{
126
err := c.CreateGroup(CreateGroupRequest{
127
Username: "team",
127
Username: "team",
128
Name: "Team",
128
Name: "Team",
129
OwnerUsername: "alice",
129
OwnerUsername: "alice",
130
})
130
})
131
if err != nil {
131
if err != nil {
132
t.Fatal(err)
132
t.Fatal(err)
133
}
133
}
134
if seen.Username != "team" {
134
if seen.Username != "team" {
135
t.Errorf(
135
t.Errorf(
136
"Username: got %q, want team", seen.Username)
136
"Username: got %q, want team", seen.Username)
137
}
137
}
138
if seen.OwnerUsername != "alice" {
138
if seen.OwnerUsername != "alice" {
139
t.Errorf(
139
t.Errorf(
140
"OwnerUsername: got %q, want alice",
140
"OwnerUsername: got %q, want alice",
141
seen.OwnerUsername)
141
seen.OwnerUsername)
142
}
142
}
143
}
143
}
144
144
145
func TestGroupMembers(t *testing.T) {
145
func TestGroupMembers(t *testing.T) {
146
srv := httptest.NewServer(http.HandlerFunc(
146
srv := httptest.NewServer(http.HandlerFunc(
147
func(w http.ResponseWriter, r *http.Request) {
147
func(w http.ResponseWriter, r *http.Request) {
148
if r.URL.Path != "/groups/members" {
148
if r.URL.Path != "/groups/members" {
149
t.Errorf(
149
t.Errorf(
150
"path: got %q, want /groups/members",
150
"path: got %q, want /groups/members",
151
r.URL.Path)
151
r.URL.Path)
152
}
152
}
153
var body map[string]string
153
var body map[string]string
154
json.NewDecoder(r.Body).Decode(&body)
154
json.NewDecoder(r.Body).Decode(&body)
155
if body["owid"] != "team-owid" {
155
if body["owid"] != "team-owid" {
156
t.Errorf(
156
t.Errorf(
157
"owid: got %q, want team-owid", body["owid"])
157
"owid: got %q, want team-owid", body["owid"])
158
}
158
}
159
json.NewEncoder(w).Encode(GroupMembersResponse{
159
json.NewEncoder(w).Encode(GroupMembersResponse{
160
Down: [][]string{
160
Down: [][]string{
161
{"alice-owid", "bob-owid"},
161
{"alice-owid", "bob-owid"},
162
},
162
},
163
Usernames: map[string]string{
163
Usernames: map[string]string{
164
"alice-owid": "alice",
164
"alice-owid": "alice",
165
"bob-owid": "bob",
165
"bob-owid": "bob",
166
},
166
},
167
})
167
})
168
}))
168
}))
169
defer srv.Close()
169
defer srv.Close()
170
170
171
c := NewHTTPClient(srv.URL, "test-key")
171
c := NewHTTPClient(srv.URL, "test-key")
172
res, err := c.GroupMembers("team-owid")
172
res, err := c.GroupMembers("team-owid")
173
if err != nil {
173
if err != nil {
174
t.Fatal(err)
174
t.Fatal(err)
175
}
175
}
176
if res.Usernames["alice-owid"] != "alice" {
176
if res.Usernames["alice-owid"] != "alice" {
177
t.Errorf(
177
t.Errorf(
178
"Usernames[alice-owid]: got %q",
178
"Usernames[alice-owid]: got %q",
179
res.Usernames["alice-owid"])
179
res.Usernames["alice-owid"])
180
}
180
}
181
}
181
}
182
182
183
func TestLeaveGroup(t *testing.T) {
183
func TestLeaveGroup(t *testing.T) {
184
var seen LeaveGroupRequest
184
var seen LeaveGroupRequest
185
srv := httptest.NewServer(http.HandlerFunc(
185
srv := httptest.NewServer(http.HandlerFunc(
186
func(w http.ResponseWriter, r *http.Request) {
186
func(w http.ResponseWriter, r *http.Request) {
187
if r.URL.Path != "/groups/leave" {
187
if r.URL.Path != "/groups/leave" {
188
t.Errorf(
188
t.Errorf(
189
"path: got %q, want /groups/leave",
189
"path: got %q, want /groups/leave",
190
r.URL.Path)
190
r.URL.Path)
191
}
191
}
192
json.NewDecoder(r.Body).Decode(&seen)
192
json.NewDecoder(r.Body).Decode(&seen)
193
w.WriteHeader(204)
193
w.WriteHeader(204)
194
}))
194
}))
195
defer srv.Close()
195
defer srv.Close()
196
196
197
c := NewHTTPClient(srv.URL, "test-key")
197
c := NewHTTPClient(srv.URL, "test-key")
198
err := c.LeaveGroup(LeaveGroupRequest{
198
err := c.LeaveGroup(LeaveGroupRequest{
199
GroupOwid: "team-owid",
199
GroupOwid: "team-owid",
200
MemberOwid: "alice-owid",
200
MemberOwid: "alice-owid",
201
})
201
})
202
if err != nil {
202
if err != nil {
203
t.Fatal(err)
203
t.Fatal(err)
204
}
204
}
205
if seen.GroupOwid != "team-owid" {
205
if seen.GroupOwid != "team-owid" {
206
t.Errorf(
206
t.Errorf(
207
"GroupOwid: got %q", seen.GroupOwid)
207
"GroupOwid: got %q", seen.GroupOwid)
208
}
208
}
209
if seen.MemberOwid != "alice-owid" {
209
if seen.MemberOwid != "alice-owid" {
210
t.Errorf(
210
t.Errorf(
211
"MemberOwid: got %q", seen.MemberOwid)
211
"MemberOwid: got %q", seen.MemberOwid)
212
}
212
}
213
}
213
}
214
214
215
func TestListAuthz(t *testing.T) {
215
func TestListAuthz(t *testing.T) {
216
srv := httptest.NewServer(http.HandlerFunc(
216
srv := httptest.NewServer(http.HandlerFunc(
217
func(w http.ResponseWriter, r *http.Request) {
217
func(w http.ResponseWriter, r *http.Request) {
218
if r.URL.Path != "/authz/list" {
218
if r.URL.Path != "/authz/list" {
219
t.Errorf(
219
t.Errorf(
220
"path: got %q, want /authz/list", r.URL.Path)
220
"path: got %q, want /authz/list", r.URL.Path)
221
}
221
}
222
json.NewEncoder(w).Encode(listAuthzResponse{
222
json.NewEncoder(w).Encode(listAuthzResponse{
223
Uris: []AuthzEntry{
223
Uris: []AuthzEntry{
224
{
224
{
225
Uri: "chat://team#post",
225
Uri: "chat://team#post",
226
Owner: &User{
226
Owner: &User{
227
Owid: "team-owid", Username: "team"},
227
Owid: "team-owid", Username: "team"},
228
Reader: &User{
228
Reader: &User{
229
Owid: "team-owid", Username: "team"},
229
Owid: "team-owid", Username: "team"},
230
IsOwner: true,
230
IsOwner: true,
231
IsReader: true,
231
IsReader: true,
232
},
232
},
233
},
233
},
234
})
234
})
235
}))
235
}))
236
defer srv.Close()
236
defer srv.Close()
237
237
238
c := NewHTTPClient(srv.URL, "test-key")
238
c := NewHTTPClient(srv.URL, "test-key")
239
uris, err := c.ListAuthz()
239
uris, err := c.ListAuthz()
240
if err != nil {
240
if err != nil {
241
t.Fatal(err)
241
t.Fatal(err)
242
}
242
}
243
if len(uris) != 1 {
243
if len(uris) != 1 {
244
t.Fatalf("len: got %d, want 1", len(uris))
244
t.Fatalf("len: got %d, want 1", len(uris))
245
}
245
}
246
if uris[0].Uri != "chat://team#post" {
246
if uris[0].Uri != "chat://team#post" {
247
t.Errorf(
247
t.Errorf(
248
"Uri: got %q, want chat://team#post", uris[0].Uri)
248
"Uri: got %q, want chat://team#post", uris[0].Uri)
249
}
249
}
250
if uris[0].Owner.Username != "team" {
250
if uris[0].Owner.Username != "team" {
251
t.Errorf(
251
t.Errorf(
252
"Owner.Username: got %q", uris[0].Owner.Username)
252
"Owner.Username: got %q", uris[0].Owner.Username)
253
}
253
}
254
}
254
}
255
255
256
func TestSetAuthz(t *testing.T) {
256
func TestSetAuthz(t *testing.T) {
257
var seen AuthzSetRequest
257
var seen []AuthzSetRequest
258
srv := httptest.NewServer(http.HandlerFunc(
258
srv := httptest.NewServer(http.HandlerFunc(
259
func(w http.ResponseWriter, r *http.Request) {
259
func(w http.ResponseWriter, r *http.Request) {
260
if r.URL.Path != "/authz/set" {
260
if r.URL.Path != "/authz/set" {
261
t.Errorf("path: got %q", r.URL.Path)
261
t.Errorf("path: got %q", r.URL.Path)
262
}
262
}
263
json.NewDecoder(r.Body).Decode(&seen)
263
json.NewDecoder(r.Body).Decode(&seen)
264
w.WriteHeader(204)
264
w.WriteHeader(204)
265
}))
265
}))
266
defer srv.Close()
266
defer srv.Close()
267
267
268
c := NewHTTPClient(srv.URL, "test-key")
268
c := NewHTTPClient(srv.URL, "test-key")
269
err := c.SetAuthz(AuthzSetRequest{
269
err := c.SetAuthz(AuthzSetRequest{
270
Uri: "chat://team#post",
270
Uri: "chat://team#post",
271
OwnerUsername: "team",
271
OwnerUsername: "team",
272
ReaderUsername: "team",
272
ReaderUsername: "team",
273
})
273
})
274
if err != nil {
274
if err != nil {
275
t.Fatal(err)
275
t.Fatal(err)
276
}
276
}
277
if seen.Uri != "chat://team#post" {
277
if len(seen) != 1 {
278
t.Errorf("Uri: got %q", seen.Uri)
278
t.Fatalf("len: got %d, want 1", len(seen))
279
}
280
if seen[0].Uri != "chat://team#post" {
281
t.Errorf("Uri: got %q", seen[0].Uri)
282
}
283
if seen[0].OwnerUsername != "team" {
284
t.Errorf("OwnerUsername: got %q", seen[0].OwnerUsername)
285
}
286
}
287
288
// TestSetAuthzMulti covers the multi-entry wire shape: array on
289
// the wire, all entries seen by the server.
290
func TestSetAuthzMulti(t *testing.T) {
291
var seen []AuthzSetRequest
292
srv := httptest.NewServer(http.HandlerFunc(
293
func(w http.ResponseWriter, r *http.Request) {
294
json.NewDecoder(r.Body).Decode(&seen)
295
w.WriteHeader(204)
296
}))
297
defer srv.Close()
298
299
c := NewHTTPClient(srv.URL, "test-key")
300
err := c.SetAuthzMulti([]AuthzSetRequest{
301
{
302
Uri: "chat://team#post",
303
OwnerUsername: "team",
304
ReaderUsername: "team",
305
},
306
{
307
Uri: "chat://team#read",
308
OwnerUsername: "team",
309
ReaderUsername: "team",
310
},
311
})
312
if err != nil {
313
t.Fatal(err)
279
}
314
}
280
if seen.OwnerUsername != "team" {
315
if len(seen) != 2 {
281
t.Errorf("OwnerUsername: got %q", seen.OwnerUsername)
316
t.Fatalf("len: got %d, want 2", len(seen))
282
}
317
}
283
}
318
}
284
319
285
func TestDeleteAuthz(t *testing.T) {
320
func TestDeleteAuthz(t *testing.T) {
286
var seen AuthzDeleteRequest
321
var seen AuthzDeleteRequest
287
srv := httptest.NewServer(http.HandlerFunc(
322
srv := httptest.NewServer(http.HandlerFunc(
288
func(w http.ResponseWriter, r *http.Request) {
323
func(w http.ResponseWriter, r *http.Request) {
289
if r.URL.Path != "/authz/delete" {
324
if r.URL.Path != "/authz/delete" {
290
t.Errorf("path: got %q", r.URL.Path)
325
t.Errorf("path: got %q", r.URL.Path)
291
}
326
}
292
json.NewDecoder(r.Body).Decode(&seen)
327
json.NewDecoder(r.Body).Decode(&seen)
293
w.WriteHeader(204)
328
w.WriteHeader(204)
294
}))
329
}))
295
defer srv.Close()
330
defer srv.Close()
296
331
297
c := NewHTTPClient(srv.URL, "test-key")
332
c := NewHTTPClient(srv.URL, "test-key")
298
err := c.DeleteAuthz(AuthzDeleteRequest{
333
err := c.DeleteAuthz(AuthzDeleteRequest{
299
Uri: "chat://team#post",
334
Uri: "chat://team#post",
300
})
335
})
301
if err != nil {
336
if err != nil {
302
t.Fatal(err)
337
t.Fatal(err)
303
}
338
}
304
if seen.Uri != "chat://team#post" {
339
if seen.Uri != "chat://team#post" {
305
t.Errorf("Uri: got %q", seen.Uri)
340
t.Errorf("Uri: got %q", seen.Uri)
306
}
341
}
307
}
342
}
308
343
309
func TestDeleteGroup(t *testing.T) {
344
func TestDeleteGroup(t *testing.T) {
310
var seen DeleteGroupRequest
345
var seen DeleteGroupRequest
311
srv := httptest.NewServer(http.HandlerFunc(
346
srv := httptest.NewServer(http.HandlerFunc(
312
func(w http.ResponseWriter, r *http.Request) {
347
func(w http.ResponseWriter, r *http.Request) {
313
if r.URL.Path != "/groups/delete" {
348
if r.URL.Path != "/groups/delete" {
314
t.Errorf(
349
t.Errorf(
315
"path: got %q, want /groups/delete",
350
"path: got %q, want /groups/delete",
316
r.URL.Path)
351
r.URL.Path)
317
}
352
}
318
json.NewDecoder(r.Body).Decode(&seen)
353
json.NewDecoder(r.Body).Decode(&seen)
319
w.WriteHeader(200)
354
w.WriteHeader(200)
320
}))
355
}))
321
defer srv.Close()
356
defer srv.Close()
322
357
323
c := NewHTTPClient(srv.URL, "test-key")
358
c := NewHTTPClient(srv.URL, "test-key")
324
err := c.DeleteGroup(DeleteGroupRequest{
359
err := c.DeleteGroup(DeleteGroupRequest{
325
Owid: "team-owid",
360
Owid: "team-owid",
326
})
361
})
327
if err != nil {
362
if err != nil {
328
t.Fatal(err)
363
t.Fatal(err)
329
}
364
}
330
if seen.Owid != "team-owid" {
365
if seen.Owid != "team-owid" {
331
t.Errorf("Owid: got %q, want team-owid", seen.Owid)
366
t.Errorf("Owid: got %q, want team-owid", seen.Owid)
332
}
367
}
333
}
368
}
334
369
335
func TestJoinGroups(t *testing.T) {
370
func TestJoinGroups(t *testing.T) {
336
var seen JoinGroupsRequest
371
var seen JoinGroupsRequest
337
srv := httptest.NewServer(http.HandlerFunc(
372
srv := httptest.NewServer(http.HandlerFunc(
338
func(w http.ResponseWriter, r *http.Request) {
373
func(w http.ResponseWriter, r *http.Request) {
339
if r.URL.Path != "/groups/join" {
374
if r.URL.Path != "/groups/join" {
340
t.Errorf(
375
t.Errorf(
341
"path: got %q, want /groups/join",
376
"path: got %q, want /groups/join",
342
r.URL.Path)
377
r.URL.Path)
343
}
378
}
344
if r.Method != "POST" {
379
if r.Method != "POST" {
345
t.Errorf("method: got %q, want POST", r.Method)
380
t.Errorf("method: got %q, want POST", r.Method)
346
}
381
}
347
json.NewDecoder(r.Body).Decode(&seen)
382
json.NewDecoder(r.Body).Decode(&seen)
348
w.WriteHeader(200)
383
w.WriteHeader(200)
349
}))
384
}))
350
defer srv.Close()
385
defer srv.Close()
351
386
352
c := NewHTTPClient(srv.URL, "test-key")
387
c := NewHTTPClient(srv.URL, "test-key")
353
err := c.JoinGroups(JoinGroupsRequest{
388
err := c.JoinGroups(JoinGroupsRequest{
354
GroupUsernames: []string{"team"},
389
GroupUsernames: []string{"team"},
355
MemberUsernames: []string{"alice", "bob"},
390
MemberUsernames: []string{"alice", "bob"},
356
})
391
})
357
if err != nil {
392
if err != nil {
358
t.Fatal(err)
393
t.Fatal(err)
359
}
394
}
360
if len(seen.GroupUsernames) != 1 ||
395
if len(seen.GroupUsernames) != 1 ||
361
seen.GroupUsernames[0] != "team" {
396
seen.GroupUsernames[0] != "team" {
362
t.Errorf(
397
t.Errorf(
363
"GroupUsernames: got %v, want [team]",
398
"GroupUsernames: got %v, want [team]",
364
seen.GroupUsernames)
399
seen.GroupUsernames)
365
}
400
}
366
if len(seen.MemberUsernames) != 2 {
401
if len(seen.MemberUsernames) != 2 {
367
t.Errorf(
402
t.Errorf(
368
"MemberUsernames: got %v, want 2 members",
403
"MemberUsernames: got %v, want 2 members",
369
seen.MemberUsernames)
404
seen.MemberUsernames)
370
}
405
}
371
}
406
}