1package adal
2
3// Copyright 2017 Microsoft Corporation
4//
5//  Licensed under the Apache License, Version 2.0 (the "License");
6//  you may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at
8//
9//      http://www.apache.org/licenses/LICENSE-2.0
10//
11//  Unless required by applicable law or agreed to in writing, software
12//  distributed under the License is distributed on an "AS IS" BASIS,
13//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14//  See the License for the specific language governing permissions and
15//  limitations under the License.
16
17import (
18	"context"
19	"encoding/json"
20	"fmt"
21	"net/http"
22	"strings"
23	"testing"
24	"time"
25
26	"github.com/Azure/go-autorest/autorest/mocks"
27)
28
29const (
30	TestResource                = "SomeResource"
31	TestClientID                = "SomeClientID"
32	TestTenantID                = "SomeTenantID"
33	TestActiveDirectoryEndpoint = "https://login.test.com/"
34)
35
36var (
37	testOAuthConfig, _ = NewOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID)
38	TestOAuthConfig    = *testOAuthConfig
39)
40
41const MockDeviceCodeResponse = `
42{
43	"device_code": "10000-40-1234567890",
44	"user_code": "ABCDEF",
45	"verification_url": "http://aka.ms/deviceauth",
46	"expires_in": "900",
47	"interval": "0"
48}
49`
50
51const MockDeviceTokenResponse = `{
52	"access_token": "accessToken",
53	"refresh_token": "refreshToken",
54	"expires_in": "1000",
55	"expires_on": "2000",
56	"not_before": "3000",
57	"resource": "resource",
58	"token_type": "type"
59}
60`
61
62func TestDeviceCodeIncludesResource(t *testing.T) {
63	sender := mocks.NewSender()
64	sender.AppendResponse(mocks.NewResponseWithContent(MockDeviceCodeResponse))
65
66	code, err := InitiateDeviceAuth(sender, TestOAuthConfig, TestClientID, TestResource)
67	if err != nil {
68		t.Fatalf("adal: unexpected error initiating device auth")
69	}
70
71	if code.Resource != TestResource {
72		t.Fatalf("adal: InitiateDeviceAuth failed to stash the resource in the DeviceCode struct")
73	}
74}
75
76func TestDeviceCodeReturnsErrorIfSendingFails(t *testing.T) {
77	sender := mocks.NewSender()
78	sender.SetError(fmt.Errorf("this is an error"))
79
80	_, err := InitiateDeviceAuth(sender, TestOAuthConfig, TestClientID, TestResource)
81	if err == nil || !strings.Contains(err.Error(), errCodeSendingFails) {
82		t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errCodeSendingFails, err.Error())
83	}
84}
85
86func TestDeviceCodeReturnsErrorIfBadRequest(t *testing.T) {
87	sender := mocks.NewSender()
88	body := mocks.NewBody("doesn't matter")
89	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
90
91	_, err := InitiateDeviceAuth(sender, TestOAuthConfig, TestClientID, TestResource)
92	if err == nil || !strings.Contains(err.Error(), errCodeHandlingFails) {
93		t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errCodeHandlingFails, err.Error())
94	}
95
96	if body.IsOpen() {
97		t.Fatalf("response body was left open!")
98	}
99}
100
101func TestDeviceCodeReturnsErrorIfCannotDeserializeDeviceCode(t *testing.T) {
102	gibberishJSON := strings.Replace(MockDeviceCodeResponse, "expires_in", "\":, :gibberish", -1)
103	sender := mocks.NewSender()
104	body := mocks.NewBody(gibberishJSON)
105	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK"))
106
107	_, err := InitiateDeviceAuth(sender, TestOAuthConfig, TestClientID, TestResource)
108	if err == nil || !strings.Contains(err.Error(), errCodeHandlingFails) {
109		t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errCodeHandlingFails, err.Error())
110	}
111
112	if body.IsOpen() {
113		t.Fatalf("response body was left open!")
114	}
115}
116
117func TestDeviceCodeReturnsErrorIfEmptyDeviceCode(t *testing.T) {
118	sender := mocks.NewSender()
119	body := mocks.NewBody("")
120	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK"))
121
122	_, err := InitiateDeviceAuth(sender, TestOAuthConfig, TestClientID, TestResource)
123	if err != ErrDeviceCodeEmpty {
124		t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", ErrDeviceCodeEmpty, err.Error())
125	}
126
127	if body.IsOpen() {
128		t.Fatalf("response body was left open!")
129	}
130}
131
132func deviceCode() *DeviceCode {
133	var deviceCode DeviceCode
134	_ = json.Unmarshal([]byte(MockDeviceCodeResponse), &deviceCode)
135	deviceCode.Resource = TestResource
136	deviceCode.ClientID = TestClientID
137	return &deviceCode
138}
139
140func TestDeviceTokenReturns(t *testing.T) {
141	sender := mocks.NewSender()
142	body := mocks.NewBody(MockDeviceTokenResponse)
143	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK"))
144
145	_, err := WaitForUserCompletion(sender, deviceCode())
146	if err != nil {
147		t.Fatalf("adal: got error unexpectedly")
148	}
149
150	if body.IsOpen() {
151		t.Fatalf("response body was left open!")
152	}
153}
154
155func TestDeviceTokenReturnsErrorIfSendingFails(t *testing.T) {
156	sender := mocks.NewSender()
157	sender.SetError(fmt.Errorf("this is an error"))
158
159	_, err := WaitForUserCompletion(sender, deviceCode())
160	if err == nil || !strings.Contains(err.Error(), errTokenSendingFails) {
161		t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errTokenSendingFails, err.Error())
162	}
163}
164
165func TestDeviceTokenReturnsErrorIfServerError(t *testing.T) {
166	sender := mocks.NewSender()
167	body := mocks.NewBody("")
168	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusInternalServerError, "Internal Server Error"))
169
170	_, err := WaitForUserCompletion(sender, deviceCode())
171	if err == nil || !strings.Contains(err.Error(), errTokenHandlingFails) {
172		t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errTokenHandlingFails, err.Error())
173	}
174
175	if body.IsOpen() {
176		t.Fatalf("response body was left open!")
177	}
178}
179
180func TestDeviceTokenReturnsErrorIfCannotDeserializeDeviceToken(t *testing.T) {
181	gibberishJSON := strings.Replace(MockDeviceTokenResponse, "expires_in", ";:\"gibberish", -1)
182	sender := mocks.NewSender()
183	body := mocks.NewBody(gibberishJSON)
184	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK"))
185
186	_, err := WaitForUserCompletion(sender, deviceCode())
187	if err == nil || !strings.Contains(err.Error(), errTokenHandlingFails) {
188		t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errTokenHandlingFails, err.Error())
189	}
190
191	if body.IsOpen() {
192		t.Fatalf("response body was left open!")
193	}
194}
195
196func errorDeviceTokenResponse(message string) string {
197	return `{ "error": "` + message + `" }`
198}
199
200func TestDeviceTokenReturnsErrorIfAuthorizationPending(t *testing.T) {
201	sender := mocks.NewSender()
202	body := mocks.NewBody(errorDeviceTokenResponse("authorization_pending"))
203	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
204
205	_, err := CheckForUserCompletion(sender, deviceCode())
206	if err != ErrDeviceAuthorizationPending {
207		t.Fatalf("!!!")
208	}
209
210	if body.IsOpen() {
211		t.Fatalf("response body was left open!")
212	}
213}
214
215func TestDeviceTokenReturnsErrorIfSlowDown(t *testing.T) {
216	sender := mocks.NewSender()
217	body := mocks.NewBody(errorDeviceTokenResponse("slow_down"))
218	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
219
220	_, err := CheckForUserCompletion(sender, deviceCode())
221	if err != ErrDeviceSlowDown {
222		t.Fatalf("!!!")
223	}
224
225	if body.IsOpen() {
226		t.Fatalf("response body was left open!")
227	}
228}
229
230type deviceTokenSender struct {
231	errorString string
232	attempts    int
233}
234
235func newDeviceTokenSender(deviceErrorString string) *deviceTokenSender {
236	return &deviceTokenSender{errorString: deviceErrorString, attempts: 0}
237}
238
239func (s *deviceTokenSender) Do(req *http.Request) (*http.Response, error) {
240	var resp *http.Response
241	if s.attempts < 1 {
242		s.attempts++
243		resp = mocks.NewResponseWithContent(errorDeviceTokenResponse(s.errorString))
244	} else {
245		resp = mocks.NewResponseWithContent(MockDeviceTokenResponse)
246	}
247	return resp, nil
248}
249
250// since the above only exercise CheckForUserCompletion, we repeat the test here,
251// but with the intent of showing that WaitForUserCompletion loops properly.
252func TestDeviceTokenSucceedsWithIntermediateAuthPending(t *testing.T) {
253	sender := newDeviceTokenSender("authorization_pending")
254
255	_, err := WaitForUserCompletion(sender, deviceCode())
256	if err != nil {
257		t.Fatalf("unexpected error occurred")
258	}
259}
260
261// same as above but with SlowDown now
262func TestDeviceTokenSucceedsWithIntermediateSlowDown(t *testing.T) {
263	sender := newDeviceTokenSender("slow_down")
264
265	_, err := WaitForUserCompletion(sender, deviceCode())
266	if err != nil {
267		t.Fatalf("unexpected error occurred")
268	}
269}
270
271func TestDeviceTokenReturnsErrorIfAccessDenied(t *testing.T) {
272	sender := mocks.NewSender()
273	body := mocks.NewBody(errorDeviceTokenResponse("access_denied"))
274	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
275
276	_, err := WaitForUserCompletion(sender, deviceCode())
277	if err != ErrDeviceAccessDenied {
278		t.Fatalf("adal: got wrong error expected(%s) actual(%s)", ErrDeviceAccessDenied.Error(), err.Error())
279	}
280
281	if body.IsOpen() {
282		t.Fatalf("response body was left open!")
283	}
284}
285
286func TestDeviceTokenReturnsErrorIfCodeExpired(t *testing.T) {
287	sender := mocks.NewSender()
288	body := mocks.NewBody(errorDeviceTokenResponse("code_expired"))
289	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
290
291	_, err := WaitForUserCompletion(sender, deviceCode())
292	if err != ErrDeviceCodeExpired {
293		t.Fatalf("adal: got wrong error expected(%s) actual(%s)", ErrDeviceCodeExpired.Error(), err.Error())
294	}
295
296	if body.IsOpen() {
297		t.Fatalf("response body was left open!")
298	}
299}
300
301func TestDeviceTokenReturnsErrorForUnknownError(t *testing.T) {
302	sender := mocks.NewSender()
303	body := mocks.NewBody(errorDeviceTokenResponse("unknown_error"))
304	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
305
306	_, err := WaitForUserCompletion(sender, deviceCode())
307	if err == nil {
308		t.Fatalf("failed to get error")
309	}
310	if err != ErrDeviceGeneric {
311		t.Fatalf("adal: got wrong error expected(%s) actual(%s)", ErrDeviceGeneric.Error(), err.Error())
312	}
313
314	if body.IsOpen() {
315		t.Fatalf("response body was left open!")
316	}
317}
318
319func TestDeviceTokenReturnsErrorIfTokenEmptyAndStatusOK(t *testing.T) {
320	sender := mocks.NewSender()
321	body := mocks.NewBody("")
322	sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK"))
323
324	_, err := WaitForUserCompletion(sender, deviceCode())
325	if err != ErrOAuthTokenEmpty {
326		t.Fatalf("adal: got wrong error expected(%s) actual(%s)", ErrOAuthTokenEmpty.Error(), err.Error())
327	}
328
329	if body.IsOpen() {
330		t.Fatalf("response body was left open!")
331	}
332}
333
334func TestWaitForUserCompletionWithContext(t *testing.T) {
335	sender := SenderFunc(func(*http.Request) (*http.Response, error) {
336		return mocks.NewResponseWithContent(`{"error":"authorization_pending"}`), nil
337	})
338	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
339	defer cancel()
340	_, err := WaitForUserCompletionWithContext(ctx, sender, deviceCode())
341	if err != context.DeadlineExceeded {
342		t.Fatalf("adal: got wrong error expected(%s) actual(%s)", context.DeadlineExceeded.Error(), err.Error())
343	}
344}
345