1package testing
2
3import (
4	"net/url"
5	"reflect"
6	"testing"
7	"time"
8
9	"github.com/gophercloud/gophercloud"
10	th "github.com/gophercloud/gophercloud/testhelper"
11)
12
13func TestMaybeString(t *testing.T) {
14	testString := ""
15	var expected *string
16	actual := gophercloud.MaybeString(testString)
17	th.CheckDeepEquals(t, expected, actual)
18
19	testString = "carol"
20	expected = &testString
21	actual = gophercloud.MaybeString(testString)
22	th.CheckDeepEquals(t, expected, actual)
23}
24
25func TestMaybeInt(t *testing.T) {
26	testInt := 0
27	var expected *int
28	actual := gophercloud.MaybeInt(testInt)
29	th.CheckDeepEquals(t, expected, actual)
30
31	testInt = 4
32	expected = &testInt
33	actual = gophercloud.MaybeInt(testInt)
34	th.CheckDeepEquals(t, expected, actual)
35}
36
37func TestBuildQueryString(t *testing.T) {
38	type testVar string
39	iFalse := false
40	opts := struct {
41		J  int               `q:"j"`
42		R  string            `q:"r" required:"true"`
43		C  bool              `q:"c"`
44		S  []string          `q:"s"`
45		TS []testVar         `q:"ts"`
46		TI []int             `q:"ti"`
47		F  *bool             `q:"f"`
48		M  map[string]string `q:"m"`
49	}{
50		J:  2,
51		R:  "red",
52		C:  true,
53		S:  []string{"one", "two", "three"},
54		TS: []testVar{"a", "b"},
55		TI: []int{1, 2},
56		F:  &iFalse,
57		M:  map[string]string{"k1": "success1"},
58	}
59	expected := &url.URL{RawQuery: "c=true&f=false&j=2&m=%7B%27k1%27%3A%27success1%27%7D&r=red&s=one&s=two&s=three&ti=1&ti=2&ts=a&ts=b"}
60	actual, err := gophercloud.BuildQueryString(&opts)
61	if err != nil {
62		t.Errorf("Error building query string: %v", err)
63	}
64	th.CheckDeepEquals(t, expected, actual)
65
66	opts = struct {
67		J  int               `q:"j"`
68		R  string            `q:"r" required:"true"`
69		C  bool              `q:"c"`
70		S  []string          `q:"s"`
71		TS []testVar         `q:"ts"`
72		TI []int             `q:"ti"`
73		F  *bool             `q:"f"`
74		M  map[string]string `q:"m"`
75	}{
76		J: 2,
77		C: true,
78	}
79	_, err = gophercloud.BuildQueryString(&opts)
80	if err == nil {
81		t.Errorf("Expected error: 'Required field not set'")
82	}
83	th.CheckDeepEquals(t, expected, actual)
84
85	_, err = gophercloud.BuildQueryString(map[string]interface{}{"Number": 4})
86	if err == nil {
87		t.Errorf("Expected error: 'Options type is not a struct'")
88	}
89}
90
91func TestBuildHeaders(t *testing.T) {
92	testStruct := struct {
93		Accept        string `h:"Accept"`
94		ContentLength int64  `h:"Content-Length"`
95		Num           int    `h:"Number" required:"true"`
96		Style         bool   `h:"Style"`
97	}{
98		Accept:        "application/json",
99		ContentLength: 256,
100		Num:           4,
101		Style:         true,
102	}
103	expected := map[string]string{"Accept": "application/json", "Number": "4", "Style": "true", "Content-Length": "256"}
104	actual, err := gophercloud.BuildHeaders(&testStruct)
105	th.CheckNoErr(t, err)
106	th.CheckDeepEquals(t, expected, actual)
107
108	testStruct.Num = 0
109	_, err = gophercloud.BuildHeaders(&testStruct)
110	if err == nil {
111		t.Errorf("Expected error: 'Required header not set'")
112	}
113
114	_, err = gophercloud.BuildHeaders(map[string]interface{}{"Number": 4})
115	if err == nil {
116		t.Errorf("Expected error: 'Options type is not a struct'")
117	}
118}
119
120func TestQueriesAreEscaped(t *testing.T) {
121	type foo struct {
122		Name  string `q:"something"`
123		Shape string `q:"else"`
124	}
125
126	expected := &url.URL{RawQuery: "else=Triangl+e&something=blah%2B%3F%21%21foo"}
127
128	actual, err := gophercloud.BuildQueryString(foo{Name: "blah+?!!foo", Shape: "Triangl e"})
129	th.AssertNoErr(t, err)
130
131	th.AssertDeepEquals(t, expected, actual)
132}
133
134func TestBuildRequestBody(t *testing.T) {
135	type PasswordCredentials struct {
136		Username string `json:"username" required:"true"`
137		Password string `json:"password" required:"true"`
138	}
139
140	type TokenCredentials struct {
141		ID string `json:"id,omitempty" required:"true"`
142	}
143
144	type orFields struct {
145		Filler int `json:"filler,omitempty"`
146		F1     int `json:"f1,omitempty" or:"F2"`
147		F2     int `json:"f2,omitempty" or:"F1"`
148	}
149
150	// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
151	// interface.
152	type AuthOptions struct {
153		PasswordCredentials *PasswordCredentials `json:"passwordCredentials,omitempty" xor:"TokenCredentials"`
154
155		// The TenantID and TenantName fields are optional for the Identity V2 API.
156		// Some providers allow you to specify a TenantName instead of the TenantId.
157		// Some require both. Your provider's authentication policies will determine
158		// how these fields influence authentication.
159		TenantID   string `json:"tenantId,omitempty"`
160		TenantName string `json:"tenantName,omitempty"`
161
162		// TokenCredentials allows users to authenticate (possibly as another user) with an
163		// authentication token ID.
164		TokenCredentials *TokenCredentials `json:"token,omitempty" xor:"PasswordCredentials"`
165
166		OrFields *orFields `json:"or_fields,omitempty"`
167	}
168
169	var successCases = []struct {
170		opts     AuthOptions
171		expected map[string]interface{}
172	}{
173		{
174			AuthOptions{
175				PasswordCredentials: &PasswordCredentials{
176					Username: "me",
177					Password: "swordfish",
178				},
179			},
180			map[string]interface{}{
181				"auth": map[string]interface{}{
182					"passwordCredentials": map[string]interface{}{
183						"password": "swordfish",
184						"username": "me",
185					},
186				},
187			},
188		},
189		{
190			AuthOptions{
191				TokenCredentials: &TokenCredentials{
192					ID: "1234567",
193				},
194			},
195			map[string]interface{}{
196				"auth": map[string]interface{}{
197					"token": map[string]interface{}{
198						"id": "1234567",
199					},
200				},
201			},
202		},
203	}
204
205	for _, successCase := range successCases {
206		actual, err := gophercloud.BuildRequestBody(successCase.opts, "auth")
207		th.AssertNoErr(t, err)
208		th.AssertDeepEquals(t, successCase.expected, actual)
209	}
210
211	var failCases = []struct {
212		opts     AuthOptions
213		expected error
214	}{
215		{
216			AuthOptions{
217				TenantID:   "987654321",
218				TenantName: "me",
219			},
220			gophercloud.ErrMissingInput{},
221		},
222		{
223			AuthOptions{
224				TokenCredentials: &TokenCredentials{
225					ID: "1234567",
226				},
227				PasswordCredentials: &PasswordCredentials{
228					Username: "me",
229					Password: "swordfish",
230				},
231			},
232			gophercloud.ErrMissingInput{},
233		},
234		{
235			AuthOptions{
236				PasswordCredentials: &PasswordCredentials{
237					Password: "swordfish",
238				},
239			},
240			gophercloud.ErrMissingInput{},
241		},
242		{
243			AuthOptions{
244				PasswordCredentials: &PasswordCredentials{
245					Username: "me",
246					Password: "swordfish",
247				},
248				OrFields: &orFields{
249					Filler: 2,
250				},
251			},
252			gophercloud.ErrMissingInput{},
253		},
254	}
255
256	for _, failCase := range failCases {
257		_, err := gophercloud.BuildRequestBody(failCase.opts, "auth")
258		th.AssertDeepEquals(t, reflect.TypeOf(failCase.expected), reflect.TypeOf(err))
259	}
260
261	createdAt := time.Date(2018, 1, 4, 10, 00, 12, 0, time.UTC)
262	var complexFields = struct {
263		Username  string     `json:"username" required:"true"`
264		CreatedAt *time.Time `json:"-"`
265	}{
266		Username:  "jdoe",
267		CreatedAt: &createdAt,
268	}
269
270	expectedComplexFields := map[string]interface{}{
271		"username": "jdoe",
272	}
273
274	actual, err := gophercloud.BuildRequestBody(complexFields, "")
275	th.AssertNoErr(t, err)
276	th.AssertDeepEquals(t, expectedComplexFields, actual)
277
278}
279