1package tfe
2
3import (
4	"bytes"
5	"context"
6	"encoding/json"
7	"testing"
8	"time"
9
10	retryablehttp "github.com/hashicorp/go-retryablehttp"
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/require"
13)
14
15func TestPoliciesList(t *testing.T) {
16	skipIfFreeOnly(t)
17
18	client := testClient(t)
19	ctx := context.Background()
20
21	orgTest, orgTestCleanup := createOrganization(t, client)
22	defer orgTestCleanup()
23
24	pTest1, pTestCleanup1 := createPolicy(t, client, orgTest)
25	defer pTestCleanup1()
26	pTest2, pTestCleanup2 := createPolicy(t, client, orgTest)
27	defer pTestCleanup2()
28
29	t.Run("without list options", func(t *testing.T) {
30		pl, err := client.Policies.List(ctx, orgTest.Name, PolicyListOptions{})
31		require.NoError(t, err)
32		assert.Contains(t, pl.Items, pTest1)
33		assert.Contains(t, pl.Items, pTest2)
34
35		assert.Equal(t, 1, pl.CurrentPage)
36		assert.Equal(t, 2, pl.TotalCount)
37	})
38
39	t.Run("with pagination", func(t *testing.T) {
40		// Request a page number which is out of range. The result should
41		// be successful, but return no results if the paging options are
42		// properly passed along.
43		pl, err := client.Policies.List(ctx, orgTest.Name, PolicyListOptions{
44			ListOptions: ListOptions{
45				PageNumber: 999,
46				PageSize:   100,
47			},
48		})
49		require.NoError(t, err)
50
51		assert.Empty(t, pl.Items)
52		assert.Equal(t, 999, pl.CurrentPage)
53		assert.Equal(t, 2, pl.TotalCount)
54	})
55
56	t.Run("with search", func(t *testing.T) {
57		// Search by one of the policy's names; we should get only that policy
58		// and pagination data should reflect the search as well
59		pl, err := client.Policies.List(ctx, orgTest.Name, PolicyListOptions{
60			Search: &pTest1.Name,
61		})
62		require.NoError(t, err)
63
64		assert.Contains(t, pl.Items, pTest1)
65		assert.NotContains(t, pl.Items, pTest2)
66		assert.Equal(t, 1, pl.CurrentPage)
67		assert.Equal(t, 1, pl.TotalCount)
68	})
69
70	t.Run("without a valid organization", func(t *testing.T) {
71		ps, err := client.Policies.List(ctx, badIdentifier, PolicyListOptions{})
72		assert.Nil(t, ps)
73		assert.EqualError(t, err, ErrInvalidOrg.Error())
74	})
75}
76
77func TestPoliciesCreate(t *testing.T) {
78	skipIfFreeOnly(t)
79
80	client := testClient(t)
81	ctx := context.Background()
82
83	orgTest, orgTestCleanup := createOrganization(t, client)
84	defer orgTestCleanup()
85
86	t.Run("with valid options", func(t *testing.T) {
87		name := randomString(t)
88		options := PolicyCreateOptions{
89			Name:        String(name),
90			Description: String("A sample policy"),
91			Enforce: []*EnforcementOptions{
92				{
93					Path: String(name + ".sentinel"),
94					Mode: EnforcementMode(EnforcementSoft),
95				},
96			},
97		}
98
99		p, err := client.Policies.Create(ctx, orgTest.Name, options)
100		require.NoError(t, err)
101
102		// Get a refreshed view from the API.
103		refreshed, err := client.Policies.Read(ctx, p.ID)
104		require.NoError(t, err)
105
106		for _, item := range []*Policy{
107			p,
108			refreshed,
109		} {
110			assert.NotEmpty(t, item.ID)
111			assert.Equal(t, *options.Name, item.Name)
112			assert.Equal(t, *options.Description, item.Description)
113		}
114	})
115
116	t.Run("when options has an invalid name", func(t *testing.T) {
117		p, err := client.Policies.Create(ctx, orgTest.Name, PolicyCreateOptions{
118			Name: String(badIdentifier),
119			Enforce: []*EnforcementOptions{
120				{
121					Path: String(badIdentifier + ".sentinel"),
122					Mode: EnforcementMode(EnforcementSoft),
123				},
124			},
125		})
126		assert.Nil(t, p)
127		assert.EqualError(t, err, ErrInvalidName.Error())
128	})
129
130	t.Run("when options is missing name", func(t *testing.T) {
131		p, err := client.Policies.Create(ctx, orgTest.Name, PolicyCreateOptions{
132			Enforce: []*EnforcementOptions{
133				{
134					Path: String(randomString(t) + ".sentinel"),
135					Mode: EnforcementMode(EnforcementSoft),
136				},
137			},
138		})
139		assert.Nil(t, p)
140		assert.EqualError(t, err, ErrRequiredName.Error())
141	})
142
143	t.Run("when options is missing an enforcement", func(t *testing.T) {
144		options := PolicyCreateOptions{
145			Name: String(randomString(t)),
146		}
147
148		p, err := client.Policies.Create(ctx, orgTest.Name, options)
149		assert.Nil(t, p)
150		assert.EqualError(t, err, "enforce is required")
151	})
152
153	t.Run("when options is missing enforcement path", func(t *testing.T) {
154		options := PolicyCreateOptions{
155			Name: String(randomString(t)),
156			Enforce: []*EnforcementOptions{
157				{
158					Mode: EnforcementMode(EnforcementSoft),
159				},
160			},
161		}
162
163		p, err := client.Policies.Create(ctx, orgTest.Name, options)
164		assert.Nil(t, p)
165		assert.EqualError(t, err, "enforcement path is required")
166	})
167
168	t.Run("when options is missing enforcement path", func(t *testing.T) {
169		name := randomString(t)
170		options := PolicyCreateOptions{
171			Name: String(name),
172			Enforce: []*EnforcementOptions{
173				{
174					Path: String(name + ".sentinel"),
175				},
176			},
177		}
178
179		p, err := client.Policies.Create(ctx, orgTest.Name, options)
180		assert.Nil(t, p)
181		assert.EqualError(t, err, "enforcement mode is required")
182	})
183
184	t.Run("when options has an invalid organization", func(t *testing.T) {
185		p, err := client.Policies.Create(ctx, badIdentifier, PolicyCreateOptions{
186			Name: String("foo"),
187		})
188		assert.Nil(t, p)
189		assert.EqualError(t, err, ErrInvalidOrg.Error())
190	})
191}
192
193func TestPoliciesRead(t *testing.T) {
194	skipIfFreeOnly(t)
195
196	client := testClient(t)
197	ctx := context.Background()
198
199	orgTest, orgTestCleanup := createOrganization(t, client)
200	defer orgTestCleanup()
201
202	pTest, pTestCleanup := createPolicy(t, client, orgTest)
203	defer pTestCleanup()
204
205	t.Run("when the policy exists without content", func(t *testing.T) {
206		p, err := client.Policies.Read(ctx, pTest.ID)
207		require.NoError(t, err)
208
209		assert.Equal(t, pTest.ID, p.ID)
210		assert.Equal(t, pTest.Name, p.Name)
211		assert.Equal(t, pTest.PolicySetCount, p.PolicySetCount)
212		assert.Empty(t, p.Enforce)
213		assert.Equal(t, pTest.Organization.Name, p.Organization.Name)
214	})
215
216	err := client.Policies.Upload(ctx, pTest.ID, []byte(`main = rule { true }`))
217	require.NoError(t, err)
218
219	t.Run("when the policy exists with content", func(t *testing.T) {
220		p, err := client.Policies.Read(ctx, pTest.ID)
221		require.NoError(t, err)
222
223		assert.Equal(t, pTest.ID, p.ID)
224		assert.Equal(t, pTest.Name, p.Name)
225		assert.Equal(t, pTest.Description, p.Description)
226		assert.Equal(t, pTest.PolicySetCount, p.PolicySetCount)
227		assert.NotEmpty(t, p.Enforce)
228		assert.NotEmpty(t, p.Enforce[0].Path)
229		assert.NotEmpty(t, p.Enforce[0].Mode)
230		assert.Equal(t, pTest.Organization.Name, p.Organization.Name)
231	})
232
233	t.Run("when the policy does not exist", func(t *testing.T) {
234		p, err := client.Policies.Read(ctx, "nonexisting")
235		assert.Nil(t, p)
236		assert.Equal(t, ErrResourceNotFound, err)
237	})
238
239	t.Run("without a valid policy ID", func(t *testing.T) {
240		p, err := client.Policies.Read(ctx, badIdentifier)
241		assert.Nil(t, p)
242		assert.EqualError(t, err, "invalid value for policy ID")
243	})
244}
245
246func TestPoliciesUpdate(t *testing.T) {
247	skipIfFreeOnly(t)
248
249	client := testClient(t)
250	ctx := context.Background()
251
252	orgTest, orgTestCleanup := createOrganization(t, client)
253	defer orgTestCleanup()
254
255	t.Run("when updating with an existing path", func(t *testing.T) {
256		pBefore, pBeforeCleanup := createUploadedPolicy(t, client, true, orgTest)
257		defer pBeforeCleanup()
258
259		require.Equal(t, 1, len(pBefore.Enforce))
260
261		pAfter, err := client.Policies.Update(ctx, pBefore.ID, PolicyUpdateOptions{
262			Enforce: []*EnforcementOptions{
263				{
264					Path: String(pBefore.Enforce[0].Path),
265					Mode: EnforcementMode(EnforcementAdvisory),
266				},
267			},
268		})
269		require.NoError(t, err)
270		require.Equal(t, 1, len(pAfter.Enforce))
271
272		assert.Equal(t, pBefore.ID, pAfter.ID)
273		assert.Equal(t, pBefore.Name, pAfter.Name)
274		assert.Equal(t, pBefore.Description, pAfter.Description)
275		assert.Equal(t, pBefore.Enforce[0].Path, pAfter.Enforce[0].Path)
276		assert.Equal(t, EnforcementAdvisory, pAfter.Enforce[0].Mode)
277	})
278
279	t.Run("when updating with a nonexisting path", func(t *testing.T) {
280		// Weirdly enough pAfter is not equal to pBefore as updating
281		// a nonexisting path causes the enforce mode to reset to the default
282		// hard-mandatory
283		t.Skip("see comment...")
284
285		pBefore, pBeforeCleanup := createUploadedPolicy(t, client, true, orgTest)
286		defer pBeforeCleanup()
287
288		require.Equal(t, 1, len(pBefore.Enforce))
289		pathBefore := pBefore.Enforce[0].Path
290		modeBefore := pBefore.Enforce[0].Mode
291
292		pAfter, err := client.Policies.Update(ctx, pBefore.ID, PolicyUpdateOptions{
293			Enforce: []*EnforcementOptions{
294				{
295					Path: String("nonexisting"),
296					Mode: EnforcementMode(EnforcementAdvisory),
297				},
298			},
299		})
300		require.NoError(t, err)
301
302		require.Equal(t, 1, len(pAfter.Enforce))
303		assert.Equal(t, pBefore, pAfter)
304		assert.Equal(t, pathBefore, pAfter.Enforce[0].Path)
305		assert.Equal(t, modeBefore, pAfter.Enforce[0].Mode)
306	})
307
308	t.Run("with a new description", func(t *testing.T) {
309		pBefore, pBeforeCleanup := createUploadedPolicy(t, client, true, orgTest)
310		defer pBeforeCleanup()
311
312		pAfter, err := client.Policies.Update(ctx, pBefore.ID, PolicyUpdateOptions{
313			Description: String("A brand new description"),
314		})
315		require.NoError(t, err)
316
317		assert.Equal(t, pBefore.Name, pAfter.Name)
318		assert.Equal(t, pBefore.Enforce, pAfter.Enforce)
319		assert.NotEqual(t, pBefore.Description, pAfter.Description)
320		assert.Equal(t, "A brand new description", pAfter.Description)
321	})
322
323	t.Run("without a valid policy ID", func(t *testing.T) {
324		p, err := client.Policies.Update(ctx, badIdentifier, PolicyUpdateOptions{})
325		assert.Nil(t, p)
326		assert.EqualError(t, err, "invalid value for policy ID")
327	})
328}
329
330func TestPoliciesDelete(t *testing.T) {
331	skipIfFreeOnly(t)
332
333	client := testClient(t)
334	ctx := context.Background()
335
336	orgTest, orgTestCleanup := createOrganization(t, client)
337	defer orgTestCleanup()
338
339	pTest, _ := createPolicy(t, client, orgTest)
340
341	t.Run("with valid options", func(t *testing.T) {
342		err := client.Policies.Delete(ctx, pTest.ID)
343		require.NoError(t, err)
344
345		// Try loading the policy - it should fail.
346		_, err = client.Policies.Read(ctx, pTest.ID)
347		assert.Equal(t, err, ErrResourceNotFound)
348	})
349
350	t.Run("when the policy does not exist", func(t *testing.T) {
351		err := client.Policies.Delete(ctx, pTest.ID)
352		assert.Equal(t, err, ErrResourceNotFound)
353	})
354
355	t.Run("when the policy ID is invalid", func(t *testing.T) {
356		err := client.Policies.Delete(ctx, badIdentifier)
357		assert.EqualError(t, err, "invalid value for policy ID")
358	})
359}
360
361func TestPoliciesUpload(t *testing.T) {
362	skipIfFreeOnly(t)
363
364	client := testClient(t)
365	ctx := context.Background()
366
367	pTest, pTestCleanup := createPolicy(t, client, nil)
368	defer pTestCleanup()
369
370	t.Run("with valid options", func(t *testing.T) {
371		err := client.Policies.Upload(ctx, pTest.ID, []byte(`main = rule { true }`))
372		assert.NoError(t, err)
373	})
374
375	t.Run("with empty content", func(t *testing.T) {
376		err := client.Policies.Upload(ctx, pTest.ID, []byte{})
377		assert.NoError(t, err)
378	})
379
380	t.Run("without any content", func(t *testing.T) {
381		err := client.Policies.Upload(ctx, pTest.ID, nil)
382		assert.NoError(t, err)
383	})
384
385	t.Run("without a valid policy ID", func(t *testing.T) {
386		err := client.Policies.Upload(ctx, badIdentifier, []byte(`main = rule { true }`))
387		assert.EqualError(t, err, "invalid value for policy ID")
388	})
389}
390
391func TestPoliciesDownload(t *testing.T) {
392	skipIfFreeOnly(t)
393
394	client := testClient(t)
395	ctx := context.Background()
396
397	pTest, pTestCleanup := createPolicy(t, client, nil)
398	defer pTestCleanup()
399
400	testContent := []byte(`main = rule { true }`)
401
402	t.Run("without existing content", func(t *testing.T) {
403		content, err := client.Policies.Download(ctx, pTest.ID)
404		assert.Equal(t, ErrResourceNotFound, err)
405		assert.Nil(t, content)
406	})
407
408	t.Run("with valid options", func(t *testing.T) {
409		err := client.Policies.Upload(ctx, pTest.ID, testContent)
410		require.NoError(t, err)
411
412		content, err := client.Policies.Download(ctx, pTest.ID)
413		assert.NoError(t, err)
414		assert.Equal(t, testContent, content)
415	})
416
417	t.Run("without a valid policy ID", func(t *testing.T) {
418		content, err := client.Policies.Download(ctx, badIdentifier)
419		assert.EqualError(t, err, "invalid value for policy ID")
420		assert.Nil(t, content)
421	})
422}
423
424func TestPolicy_Unmarshal(t *testing.T) {
425	data := map[string]interface{}{
426		"data": map[string]interface{}{
427			"type": "policies",
428			"id":   "policy-ntv3HbhJqvFzamy7",
429			"attributes": map[string]interface{}{
430				"name":        "general",
431				"description": "general policy",
432				"enforce": []interface{}{
433					map[string]interface{}{
434						"path": "some/path",
435						"mode": string(EnforcementAdvisory),
436					},
437				},
438				"updated-at":       "2018-03-02T23:42:06.651Z",
439				"policy-set-count": 1,
440			},
441		},
442	}
443
444	byteData, err := json.Marshal(data)
445	require.NoError(t, err)
446
447	responseBody := bytes.NewReader(byteData)
448	policy := &Policy{}
449	err = unmarshalResponse(responseBody, policy)
450	require.NoError(t, err)
451
452	iso8601TimeFormat := "2006-01-02T15:04:05Z"
453	parsedTime, err := time.Parse(iso8601TimeFormat, "2018-03-02T23:42:06.651Z")
454	require.NoError(t, err)
455	assert.Equal(t, policy.ID, "policy-ntv3HbhJqvFzamy7")
456	assert.Equal(t, policy.Name, "general")
457	assert.Equal(t, policy.Description, "general policy")
458	assert.Equal(t, policy.PolicySetCount, 1)
459	assert.Equal(t, policy.Enforce[0].Path, "some/path")
460	assert.Equal(t, policy.Enforce[0].Mode, EnforcementAdvisory)
461	assert.Equal(t, policy.UpdatedAt, parsedTime)
462}
463
464func TestPolicyCreateOptions_Marshal(t *testing.T) {
465	opts := PolicyCreateOptions{
466		Name:        String("my-policy"),
467		Description: String("details"),
468		Enforce: []*EnforcementOptions{
469			{
470				Path: String("/foo"),
471				Mode: EnforcementMode(EnforcementSoft),
472			},
473			{
474				Path: String("/bar"),
475				Mode: EnforcementMode(EnforcementSoft),
476			},
477		},
478	}
479
480	reqBody, err := serializeRequestBody(&opts)
481	require.NoError(t, err)
482	req, err := retryablehttp.NewRequest("POST", "url", reqBody)
483	require.NoError(t, err)
484	bodyBytes, err := req.BodyBytes()
485	require.NoError(t, err)
486
487	expectedBody := `{"data":{"type":"policies","attributes":{"description":"details","enforce":[{"path":"/foo","mode":"soft-mandatory"},{"path":"/bar","mode":"soft-mandatory"}],"name":"my-policy"}}}
488`
489	assert.Equal(t, expectedBody, string(bodyBytes))
490}
491
492func TestPolicyUpdateOptions_Marshal(t *testing.T) {
493	opts := PolicyUpdateOptions{
494		Description: String("details"),
495		Enforce: []*EnforcementOptions{
496			{
497				Path: String("/foo"),
498				Mode: EnforcementMode(EnforcementSoft),
499			},
500			{
501				Path: String("/bar"),
502				Mode: EnforcementMode(EnforcementSoft),
503			},
504		},
505	}
506
507	reqBody, err := serializeRequestBody(&opts)
508	require.NoError(t, err)
509	req, err := retryablehttp.NewRequest("POST", "url", reqBody)
510	require.NoError(t, err)
511	bodyBytes, err := req.BodyBytes()
512	require.NoError(t, err)
513
514	expectedBody := `{"data":{"type":"policies","attributes":{"description":"details","enforce":[{"path":"/foo","mode":"soft-mandatory"},{"path":"/bar","mode":"soft-mandatory"}]}}}
515`
516	assert.Equal(t, expectedBody, string(bodyBytes))
517}
518