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