-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmsgraph_test.go
More file actions
264 lines (235 loc) · 11.6 KB
/
Copy pathmsgraph_test.go
File metadata and controls
264 lines (235 loc) · 11.6 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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
package msgraph
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newTestClient wires a graphClient at the given token + graph servers.
func newTestClient(tokenURL, baseURL string) Client {
return New(
Config{TenantID: "t", ClientID: "c", ClientSecret: "s"},
WithTokenURL(tokenURL),
WithBaseURL(baseURL),
)
}
// newTestDirectory wires a DirectoryReader at the given token + graph servers.
func newTestDirectory(tokenURL, baseURL string) DirectoryReader {
return NewDirectoryClient(
Config{TenantID: "t", ClientID: "c", ClientSecret: "s"},
WithTokenURL(tokenURL),
WithBaseURL(baseURL),
)
}
func TestCreateOnlineMeeting_Success(t *testing.T) {
var tokenCalls, meetingCalls int
tokenSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenCalls++
require.NoError(t, r.ParseForm())
assert.Equal(t, "client_credentials", r.Form.Get("grant_type"))
assert.Equal(t, graphScope, r.Form.Get("scope"))
_ = json.NewEncoder(w).Encode(tokenResponse{AccessToken: "tok-123", ExpiresIn: 3600}) // #nosec G117 -- test mock encodes a fake OAuth token response; dummy value, not a real secret
}))
defer tokenSrv.Close()
graphSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
meetingCalls++
assert.Equal(t, "Bearer tok-123", r.Header.Get("Authorization"))
// Idempotent endpoint: the organizer-scoped createOrGet path.
assert.True(t, strings.Contains(r.URL.Path, "/users/alice%40corp.com/onlineMeetings/createOrGet") ||
strings.Contains(r.URL.Path, "/users/alice@corp.com/onlineMeetings/createOrGet"),
"organizer-scoped createOrGet path expected, got %s", r.URL.Path)
// externalId is the per-room idempotency key and must be sent.
var body onlineMeetingPayload
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
assert.Equal(t, "room-key-1", body.ExternalID, "externalId must be sent to createOrGet")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(OnlineMeeting{ID: "m1", JoinURL: "https://join/1"})
}))
defer graphSrv.Close()
c := newTestClient(tokenSrv.URL, graphSrv.URL)
m, err := c.CreateOnlineMeeting(context.Background(), CreateOnlineMeetingRequest{
ExternalID: "room-key-1", Subject: "Standup", OrganizerEmail: "alice@corp.com", AttendeeEmails: []string{"bob@corp.com"},
})
require.NoError(t, err)
assert.Equal(t, "m1", m.ID)
assert.Equal(t, "https://join/1", m.JoinURL)
// Second call reuses the cached token (no second token fetch).
_, err = c.CreateOnlineMeeting(context.Background(), CreateOnlineMeetingRequest{ExternalID: "room-key-1", OrganizerEmail: "alice@corp.com"})
require.NoError(t, err)
assert.Equal(t, 1, tokenCalls, "token should be cached across calls")
assert.Equal(t, 2, meetingCalls)
}
// TestCreateOnlineMeeting_Idempotent_SameExternalID asserts the client hits
// createOrGet and that a repeat call with the same externalId returns the same
// meeting Graph already holds for that key (Graph is the idempotency source of
// truth — the server returns the existing meeting on the second createOrGet).
func TestCreateOnlineMeeting_Idempotent_SameExternalID(t *testing.T) {
tokenSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(tokenResponse{AccessToken: "tok", ExpiresIn: 3600}) // #nosec G117 -- test mock encodes a fake OAuth token response; dummy value, not a real secret
}))
defer tokenSrv.Close()
// Server mimics Graph createOrGet: one meeting per externalId, returned on
// every call with that key.
byExternalID := map[string]OnlineMeeting{}
graphSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Contains(t, r.URL.Path, "/onlineMeetings/createOrGet", "must use createOrGet endpoint")
var body onlineMeetingPayload
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
require.NotEmpty(t, body.ExternalID, "externalId required")
m, ok := byExternalID[body.ExternalID]
if !ok {
m = OnlineMeeting{ID: "mtg-" + body.ExternalID, JoinURL: "https://join/" + body.ExternalID}
byExternalID[body.ExternalID] = m
w.WriteHeader(http.StatusCreated)
} else {
w.WriteHeader(http.StatusOK) // existing meeting returned
}
_ = json.NewEncoder(w).Encode(m)
}))
defer graphSrv.Close()
c := newTestClient(tokenSrv.URL, graphSrv.URL)
first, err := c.CreateOnlineMeeting(context.Background(), CreateOnlineMeetingRequest{
ExternalID: "k", OrganizerEmail: "a@b.com",
})
require.NoError(t, err)
second, err := c.CreateOnlineMeeting(context.Background(), CreateOnlineMeetingRequest{
ExternalID: "k", OrganizerEmail: "a@b.com",
})
require.NoError(t, err)
assert.Equal(t, first.ID, second.ID, "same externalId returns the same meeting")
assert.Equal(t, first.JoinURL, second.JoinURL)
assert.Len(t, byExternalID, 1, "only one meeting created for one externalId")
}
// TestCreateOnlineMeeting_RequiresExternalID guards the createOrGet contract:
// an empty externalId is rejected before any network call.
func TestCreateOnlineMeeting_RequiresExternalID(t *testing.T) {
c := newTestClient("http://unused", "http://unused")
_, err := c.CreateOnlineMeeting(context.Background(), CreateOnlineMeetingRequest{OrganizerEmail: "a@b.com"}) // no ExternalID
require.Error(t, err)
assert.Contains(t, err.Error(), "externalId")
}
func TestCreateOnlineMeeting_TokenError(t *testing.T) {
tokenSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(tokenResponse{Error: "invalid_client", ErrorDesc: "bad secret"}) // #nosec G117 -- test mock encodes a fake OAuth token response; dummy value, not a real secret
}))
defer tokenSrv.Close()
c := New(
Config{TenantID: "t", ClientID: "c", ClientSecret: "super-secret-value"},
WithTokenURL(tokenSrv.URL), WithBaseURL("http://unused"),
)
_, err := c.CreateOnlineMeeting(context.Background(), CreateOnlineMeetingRequest{ExternalID: "k", OrganizerEmail: "a@b.com"})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid_client")
// Never leak the secret in the error.
assert.NotContains(t, err.Error(), "super-secret-value")
}
func TestCreateOnlineMeeting_GraphError(t *testing.T) {
tokenSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(tokenResponse{AccessToken: "tok", ExpiresIn: 3600}) // #nosec G117 -- test mock encodes a fake OAuth token response; dummy value, not a real secret
}))
defer tokenSrv.Close()
graphSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusForbidden)
// The message carries sensitive-looking detail that must NOT leak into the error.
_, _ = w.Write([]byte(`{"error":{"code":"Forbidden","message":"secret-internal-detail-xyz"}}`))
}))
defer graphSrv.Close()
c := newTestClient(tokenSrv.URL, graphSrv.URL)
_, err := c.CreateOnlineMeeting(context.Background(), CreateOnlineMeetingRequest{ExternalID: "k", OrganizerEmail: "a@b.com"})
require.Error(t, err)
assert.Contains(t, err.Error(), "403")
assert.Contains(t, err.Error(), "Forbidden", "sanitized error code should be surfaced")
assert.NotContains(t, err.Error(), "secret-internal-detail-xyz", "raw response message must not leak")
}
func TestCreateOnlineMeeting_MissingJoinURL(t *testing.T) {
tokenSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(tokenResponse{AccessToken: "tok", ExpiresIn: 3600}) // #nosec G117 -- test mock encodes a fake OAuth token response; dummy value, not a real secret
}))
defer tokenSrv.Close()
graphSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(OnlineMeeting{ID: "m1"}) // no joinWebUrl
}))
defer graphSrv.Close()
c := newTestClient(tokenSrv.URL, graphSrv.URL)
_, err := c.CreateOnlineMeeting(context.Background(), CreateOnlineMeetingRequest{ExternalID: "k", OrganizerEmail: "a@b.com"})
require.Error(t, err)
assert.Contains(t, err.Error(), "joinWebUrl")
}
func TestNew_TLSInsecureSkipVerify(t *testing.T) {
// Default: no custom transport, so the stdlib default (verifying) is used.
def := New(Config{TenantID: "t"}).(*graphClient)
assert.Nil(t, def.httpClient.Transport, "default client must keep TLS verification")
// Enabled: transport carries InsecureSkipVerify.
ins := New(Config{TenantID: "t", TLSInsecureSkipVerify: true}).(*graphClient)
tr, ok := ins.httpClient.Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, tr.TLSClientConfig)
assert.True(t, tr.TLSClientConfig.InsecureSkipVerify)
}
func TestGetUsersByAccounts_BatchesAndReturns(t *testing.T) {
tokenSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(tokenResponse{AccessToken: "tok", ExpiresIn: 3600}) // #nosec G117 -- test mock OAuth token
}))
defer tokenSrv.Close()
graphSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer tok", r.Header.Get("Authorization"))
assert.Equal(t, "eventual", r.Header.Get("ConsistencyLevel"), "startsWith needs advanced query")
filter := r.URL.Query().Get("$filter")
// Domain-agnostic prefix match; both lower- and upper-cased variants of
// each account OR'd into one query.
assert.Contains(t, filter, "startsWith(userPrincipalName,'alice@')")
assert.Contains(t, filter, "startsWith(userPrincipalName,'ALICE@')")
assert.Contains(t, filter, "startsWith(userPrincipalName,'bob@')")
assert.Contains(t, filter, "startsWith(userPrincipalName,'BOB@')")
assert.Contains(t, filter, " or ")
_ = json.NewEncoder(w).Encode(map[string]any{"value": []GraphUser{
{ID: "ida", UserPrincipalName: "alice@corp.com"},
{ID: "idb", UserPrincipalName: "bob@partner.io"}, // different domain
}})
}))
defer graphSrv.Close()
c := newTestDirectory(tokenSrv.URL, graphSrv.URL)
users, err := c.GetUsersByAccounts(context.Background(), []string{"alice", "bob"})
require.NoError(t, err)
require.Len(t, users, 2)
assert.Equal(t, "ida", users[0].ID)
assert.Equal(t, "bob@partner.io", users[1].UserPrincipalName)
}
func TestGetUsersByAccounts_ChunksLargeInput(t *testing.T) {
tokenSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(tokenResponse{AccessToken: "tok", ExpiresIn: 3600}) // #nosec G117 -- test mock OAuth token
}))
defer tokenSrv.Close()
var calls int
graphSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
calls++
_ = json.NewEncoder(w).Encode(map[string]any{"value": []GraphUser{}})
}))
defer graphSrv.Close()
accounts := make([]string, maxAccountsPerQuery+1) // one over a chunk -> 2 requests
for i := range accounts {
accounts[i] = fmt.Sprintf("u%d", i)
}
c := newTestDirectory(tokenSrv.URL, graphSrv.URL)
_, err := c.GetUsersByAccounts(context.Background(), accounts)
require.NoError(t, err)
assert.Equal(t, 2, calls, "accounts beyond one chunk trigger a second query")
}
func TestCasedVariants(t *testing.T) {
assert.Equal(t, []string{"alice", "ALICE"}, casedVariants("alice"))
assert.Equal(t, []string{"alice", "ALICE"}, casedVariants("Alice"))
assert.Equal(t, []string{"123"}, casedVariants("123"), "caseless value -> single clause")
}
func TestGetUsersByAccounts_Empty(t *testing.T) {
c := NewDirectoryClient(Config{TenantID: "t"})
users, err := c.GetUsersByAccounts(context.Background(), nil)
require.NoError(t, err)
assert.Empty(t, users)
}