-
Notifications
You must be signed in to change notification settings - Fork 1k
Expand file tree
/
Copy pathclient_custom_test.go
More file actions
186 lines (150 loc) · 6.86 KB
/
Copy pathclient_custom_test.go
File metadata and controls
186 lines (150 loc) · 6.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
package azwebpubsub_test
import (
"context"
"io"
"net/http"
"net/url"
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/messaging/azwebpubsub"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
)
type stubCredential struct{}
func (stubCredential) GetToken(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error) {
return azcore.AccessToken{Token: "stub-aad-token"}, nil
}
// stubTransport records the last request and returns a canned generateToken response.
type stubTransport struct{ lastRequest *http.Request }
func (s *stubTransport) Do(req *http.Request) (*http.Response, error) {
s.lastRequest = req
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"token":"server-issued-token"}`)),
Request: req,
}, nil
}
func newTokenCredentialClient(t *testing.T, endpoint string) (*azwebpubsub.Client, *stubTransport) {
transport := &stubTransport{}
options := &azwebpubsub.ClientOptions{}
options.Transport = transport
client, err := azwebpubsub.NewClient(endpoint, stubCredential{}, options)
require.NoError(t, err)
return client, transport
}
// The endpoint passed to NewClient is not required to have a trailing slash, and the
// audience and client URL must be well formed either way.
func TestClient_GenerateClientAccessURLEndpointTrailingSlash(t *testing.T) {
hub := "chat"
for _, endpoint := range []string{
"https://host.webpubsub.azure.com",
"https://host.webpubsub.azure.com/",
} {
t.Run(endpoint, func(t *testing.T) {
client, _ := newTokenCredentialClient(t, endpoint)
resp, err := client.GenerateClientAccessURL(context.Background(), hub, nil)
require.NoError(t, err)
require.Equal(t, "wss://host.webpubsub.azure.com/client/hubs/chat", resp.BaseURL)
require.Equal(t, "wss://host.webpubsub.azure.com/client/hubs/chat?access_token=server-issued-token", resp.URL)
})
}
}
// The same normalization applies to the audience embedded in a key-signed token.
func TestClient_GenerateClientAccessURLAudienceTrailingSlash(t *testing.T) {
client, err := azwebpubsub.NewClientFromConnectionString("Endpoint=https://host.webpubsub.azure.com;AccessKey=ABC;", nil)
require.NoError(t, err)
resp, err := client.GenerateClientAccessURL(context.Background(), "chat", nil)
require.NoError(t, err)
claims := jwt.MapClaims{}
_, _, err = jwt.NewParser().ParseUnverified(resp.Token, claims)
require.NoError(t, err)
// The service validates the host and path of the audience, so a missing separator
// between the endpoint and "client/hubs" makes the token unusable.
require.Equal(t, "https://host.webpubsub.azure.com/client/hubs/chat", claims["aud"])
require.Equal(t, "wss://host.webpubsub.azure.com/client/hubs/chat", resp.BaseURL)
}
// nil options must be treated the same as empty options on both credential paths.
func TestClient_GenerateClientAccessURLNilOptions(t *testing.T) {
t.Run("TokenCredential", func(t *testing.T) {
client, transport := newTokenCredentialClient(t, "https://host.webpubsub.azure.com/")
require.NotPanics(t, func() {
resp, err := client.GenerateClientAccessURL(context.Background(), "chat", nil)
require.NoError(t, err)
require.Equal(t, "server-issued-token", resp.Token)
})
require.NotNil(t, transport.lastRequest)
})
t.Run("ConnectionString", func(t *testing.T) {
client, err := azwebpubsub.NewClientFromConnectionString("Endpoint=https://host.webpubsub.azure.com;AccessKey=ABC;", nil)
require.NoError(t, err)
require.NotPanics(t, func() {
resp, err := client.GenerateClientAccessURL(context.Background(), "chat", nil)
require.NoError(t, err)
require.NotEmpty(t, resp.Token)
})
})
}
// The service rejects minutesToExpire=0, so an unset expiration must fall back to the
// default rather than being forwarded as a zero value.
func TestClient_GenerateClientAccessURLMinutesToExpire(t *testing.T) {
minutesToExpire := func(t *testing.T, transport *stubTransport) string {
t.Helper()
require.NotNil(t, transport.lastRequest)
query, err := url.ParseQuery(transport.lastRequest.URL.RawQuery)
require.NoError(t, err)
return query.Get("minutesToExpire")
}
t.Run("unset uses the default", func(t *testing.T) {
client, transport := newTokenCredentialClient(t, "https://host.webpubsub.azure.com/")
_, err := client.GenerateClientAccessURL(context.Background(), "chat", &azwebpubsub.GenerateClientAccessURLOptions{})
require.NoError(t, err)
require.Equal(t, "60", minutesToExpire(t, transport))
})
t.Run("nil options uses the default", func(t *testing.T) {
client, transport := newTokenCredentialClient(t, "https://host.webpubsub.azure.com/")
_, err := client.GenerateClientAccessURL(context.Background(), "chat", nil)
require.NoError(t, err)
require.Equal(t, "60", minutesToExpire(t, transport))
})
t.Run("explicit value is honored", func(t *testing.T) {
client, transport := newTokenCredentialClient(t, "https://host.webpubsub.azure.com/")
_, err := client.GenerateClientAccessURL(context.Background(), "chat", &azwebpubsub.GenerateClientAccessURLOptions{
ExpirationTimeInMinutes: 5,
})
require.NoError(t, err)
require.Equal(t, "5", minutesToExpire(t, transport))
})
}
// A negative expiration is rejected on both credential paths.
func TestClient_GenerateClientAccessURLNegativeExpiration(t *testing.T) {
options := &azwebpubsub.GenerateClientAccessURLOptions{ExpirationTimeInMinutes: -1}
client, _ := newTokenCredentialClient(t, "https://host.webpubsub.azure.com/")
_, err := client.GenerateClientAccessURL(context.Background(), "chat", options)
require.ErrorContains(t, err, "out of range")
keyClient, err := azwebpubsub.NewClientFromConnectionString("Endpoint=https://host.webpubsub.azure.com;AccessKey=ABC;", nil)
require.NoError(t, err)
_, err = keyClient.GenerateClientAccessURL(context.Background(), "chat", options)
require.ErrorContains(t, err, "out of range")
}
// Options are forwarded to the service on the TokenCredential path.
func TestClient_GenerateClientAccessURLForwardsOptions(t *testing.T) {
client, transport := newTokenCredentialClient(t, "https://host.webpubsub.azure.com/")
_, err := client.GenerateClientAccessURL(context.Background(), "chat", &azwebpubsub.GenerateClientAccessURLOptions{
UserID: "user1",
Roles: []string{"admin"},
Groups: []string{"group1"},
})
require.NoError(t, err)
query, err := url.ParseQuery(transport.lastRequest.URL.RawQuery)
require.NoError(t, err)
require.Equal(t, "user1", query.Get("userId"))
require.Equal(t, []string{"admin"}, query["role"])
require.Equal(t, []string{"group1"}, query["group"])
}