github.com/DelineaXPM/dsv-cli@v1.40.6/cicd-integration/integration_test.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "flag" 6 "fmt" 7 "log" 8 "os" 9 "path" 10 "path/filepath" 11 "regexp" 12 "runtime" 13 "strconv" 14 "strings" 15 "testing" 16 "text/template" 17 18 "github.com/DelineaXPM/dsv-cli/constants" 19 "github.com/DelineaXPM/dsv-cli/utils/test_helpers" 20 21 "github.com/gobuffalo/uuid" 22 "golang.org/x/sys/execabs" 23 ) 24 25 var update = flag.Bool("update", false, "update golden case files") 26 27 var binaryName = constants.CmdRoot 28 29 const configPath = "cicd-integration/.thy.yml" 30 31 func TestCliArgs(t *testing.T) { 32 workDir, err := os.Getwd() 33 if err != nil { 34 t.Fatalf("[TestCliArgs] Unable to determine working directory: %v.", err) 35 } 36 binary := path.Join(workDir, binaryName+".test") 37 38 t.Logf("[TestCliArgs] Working directory: %s", workDir) 39 t.Logf("[TestCliArgs] Path to binary: %s", binary) 40 41 err = os.Mkdir("coverage", os.ModeDir) 42 targetArtifactDirectory := filepath.Join(".artifacts", "coverage", "integration") 43 if _, err := os.Stat(targetArtifactDirectory); os.IsNotExist(err) { 44 err = os.MkdirAll(targetArtifactDirectory, 0o755) 45 if err != nil { 46 t.Fatalf("unable to create code coverage directory %s: %v", targetArtifactDirectory, err) 47 } 48 } 49 t.Logf("[TestCliArgs] Coverage Results: %s", targetArtifactDirectory) 50 51 // if the error is not nil AND it's not an already exists error 52 if err != nil && !os.IsExist(err) { 53 t.Fatalf("[TestCliArgs] os.Mkdir(coverage, os.ModeDir): %v", err) 54 } 55 t.Log("[TestCliArgs] Successfully created the directory for coverage reports.") 56 57 for _, tt := range synchronousCases { 58 t.Run(tt.name, func(t *testing.T) { 59 outfile := filepath.Join(targetArtifactDirectory, tt.name+"coverage.out") 60 61 args := []string{"-test.coverprofile", outfile} 62 args = append(args, tt.args...) 63 args = append(args, "--config", configPath) 64 65 cmd := execabs.Command(binary, args...) 66 output, _ := cmd.CombinedOutput() 67 68 actual := string(output) 69 if strings.LastIndex(actual, "PASS") > -1 { 70 actual = actual[:strings.LastIndex(actual, "PASS")] 71 } 72 if strings.LastIndex(actual, "FAIL") > -1 { 73 actual = actual[:strings.LastIndex(actual, "FAIL")] 74 } 75 actualTrimmed := strings.TrimSpace(actual) 76 77 if *update { 78 if tt.output.MatchGoldenCase { 79 writeFixture(t, tt.name, []byte(actualTrimmed)) 80 } 81 return 82 } 83 84 expected := tt.output.RegexMatch 85 if tt.output.MatchGoldenCase { 86 expected = loadFixture(t, tt.name) 87 expected = regexp.QuoteMeta(expected) 88 expected = "^" + expected + "$" 89 } 90 91 matcher := regexp.MustCompile(expected) 92 match := matcher.MatchString(actualTrimmed) 93 if !match { 94 t.Fatalf("actual:\n%s,\n expected:\n%s", actualTrimmed, expected) 95 } 96 }) 97 } 98 } 99 100 var ( 101 certPath = strings.Join([]string{"cicd-integration", "data", "cert.pem"}, string(filepath.Separator)) 102 privateKeyPath = strings.Join([]string{"cicd-integration", "data", "key.pem"}, string(filepath.Separator)) 103 csrPath = strings.Join([]string{"cicd-integration", "data", "csr.pem"}, string(filepath.Separator)) 104 ) 105 106 const ( 107 manualKeyPath = "thekey:first" 108 manualPrivateKey = "MnI1dTh4L0E/RChHK0tiUGVTaFZtWXEzczZ2OXkkQiY=" 109 manualKeyNonce = "S1NzeHdFcHB6b1Bz" 110 plaintext = "hello there" 111 ciphertext = "8Tns2mbY/w6YHoICfiDGQM+rDlQzwrZWpqK7" 112 ) 113 114 func TestMain(m *testing.M) { 115 _, err := strconv.ParseBool(os.Getenv("GO_INTEGRATION_TEST")) 116 if err != nil { 117 fmt.Println("[SKIPPED]: GO_INTEGRATION_TEST must be set to 1/true to run integration tests") 118 return 119 } 120 121 var rootDir string 122 if out, err := execabs.Command("git", "rev-parse", "--show-toplevel").CombinedOutput(); err == nil { 123 rootDir = strings.TrimRight(string(out), " \n") 124 } else { 125 rootDir = "../" 126 } 127 128 if err := os.Chdir(rootDir); err != nil { 129 log.Fatal(err) 130 } 131 132 if err := test_helpers.AddEncryptionKey(os.Getenv("TEST_TENANT"), os.Getenv("USER_NAME"), os.Getenv("DSV_USER_PASSWORD")); err != nil { 133 log.Fatalf("could not create encryption key: %v", err) 134 } 135 makeCmd := execabs.Command("make", "build-test") 136 if err := makeCmd.Run(); err != nil { 137 log.Fatalf("could not make binary for %s: %v", binaryName, err) 138 } 139 140 cert, key, err := generateRootWithPrivateKey() 141 csr, err := generateCSR() 142 os.WriteFile(certPath, cert, 0o644) 143 os.WriteFile(privateKeyPath, key, 0o644) 144 os.WriteFile(csrPath, csr, 0o644) 145 146 defer os.Remove(certPath) 147 defer os.Remove(privateKeyPath) 148 defer os.Remove(csrPath) 149 150 // Before and after *all* tests, make sure any modifications to the config are reverted. 151 // Reading and writing the config before and after *each* test is not feasible, as there may be tests that 152 // intentionally modify the config to test for presence or absence of a property or modification of a value. 153 config, err := os.ReadFile(configPath) 154 if err != nil { 155 fmt.Printf("could not read config: %v", err) 156 os.Exit(1) 157 } 158 _ = os.Setenv("IS_SYSTEM_TEST", "true") 159 m.Run() 160 _ = os.Unsetenv("IS_SYSTEM_TEST") 161 162 err = os.WriteFile(configPath, config, 0o644) 163 if err != nil { 164 fmt.Printf("could not write config: %v", err) 165 os.Exit(1) 166 } 167 } 168 169 type outputValidation struct { 170 RegexMatch string 171 MatchGoldenCase bool 172 } 173 174 func outputPattern(regex string) outputValidation { 175 return outputValidation{ 176 RegexMatch: regex, 177 } 178 } 179 180 func outputEmpty() outputValidation { 181 return outputValidation{ 182 RegexMatch: "^$", 183 } 184 } 185 186 func outputGolden() outputValidation { 187 return outputValidation{ 188 MatchGoldenCase: true, 189 } 190 } 191 192 //nolint:gochecknoglobals // Yup, we know. 193 var synchronousCases []struct { 194 name string 195 args []string 196 output outputValidation 197 } 198 199 func init() { 200 if err := generateThyYml(".thy.yml.template", ".thy.yml"); err != nil { 201 panic(err) 202 } 203 204 if err := generateThyYml("data/policy.json", "data/test_policy.json"); err != nil { 205 panic(err) 206 } 207 208 u, _ := uuid.NewV4() 209 t, _ := uuid.NewV4() 210 211 adminPass := os.Getenv("DSV_ADMIN_PASS") 212 213 secret1Name := u.String() + "z" // Avoid UUID detection on the API side. 214 secret1Tag := t.String() 215 //nolint:gosec // Not a hardcoded credentials. 216 secret1Desc := `desc of s1` 217 secret1Data := `{"field":"secret password"}` 218 secret1Attributes := fmt.Sprintf(`{"tag":"%s", "tll": 100}`, secret1Tag) 219 secret1DataFmt := `"field": "secret password"` 220 221 user1 := os.Getenv("USER1_NAME") 222 user1Pass := os.Getenv("DSV_USER1_PASSWORD") 223 groupName := u.String() 224 225 policyName := "secrets:" + secret1Name 226 p2, _ := uuid.NewV4() 227 policy2Name := "secrets:servers:" + p2.String() 228 policy2File := strings.Join([]string{"cicd-integration", "data", "test_policy.json"}, string(filepath.Separator)) 229 230 existingRootSecret := "existingRoot" 231 certStoreSecret := "myroot" 232 leafSecretPath := "myleaf" 233 234 synchronousCases = []struct { 235 name string 236 args []string 237 output outputValidation 238 }{ 239 // secret operations 240 // TODO investigate test setup, as the order of calls matters for some reason. 241 {"secret-create-1-pass", []string{"secret", "create", "--path", secret1Name, "--data", secret1Data, "--attributes", secret1Attributes, "--desc", secret1Desc, "-f", ".data", "-v"}, outputPattern(secret1DataFmt)}, 242 {"secret-update-pass", []string{"secret", "update", "--path", secret1Name, "--desc", "updated secret", "-f", ".data", "-v"}, outputPattern(secret1DataFmt)}, 243 {"secret-rollback-pass", []string{"secret", "rollback", "--path", secret1Name, "-f", ".data"}, outputPattern(secret1DataFmt)}, 244 // {"secret-search-find-pass", []string{"secret", "search", secret1Name[:3], "'data.[0].name'"}, outputPattern(secret1Name)}, 245 {"secret-search-tags", []string{"secret", "search", secret1Tag, "--search-field", "attributes.tag"}, outputPattern(secret1Name)}, 246 {"secret-create-fail-dup", []string{"secret", "create", "--path", secret1Name, "--data", secret1Data, "", ".message"}, outputPattern(`"message": "error creating secret, secret at path already exists"`)}, 247 {"secret-describe-1-pass", []string{"secret", "describe", "--path", secret1Name, "-f", ".description"}, outputPattern("^" + secret1Desc + "$")}, 248 {"secret-read-1-pass", []string{"secret", "read", "--path", secret1Name, "-f", ".data"}, outputPattern(secret1DataFmt)}, 249 {"secret-read-implicit-pass", []string{"secret", secret1Name, "-f", ".data"}, outputPattern(secret1DataFmt)}, 250 {"secret-search-none-pass", []string{"secret", "search", "hjkl"}, outputPattern(`"data": \[\]`)}, 251 {"secret-soft-delete", []string{"secret", "delete", secret1Name}, outputPattern("will be removed")}, 252 {"secret-read-fail", []string{"secret", "read", secret1Name}, outputPattern("will be removed")}, 253 {"secret-restore", []string{"secret", "restore", secret1Name}, outputEmpty()}, 254 255 // policy operations 256 {"policy-help", []string{"policy", ""}, outputPattern(`Execute an action on a policy.*`)}, 257 {"policy-create-pass", []string{"policy", "create", "--path", policyName, "--resources", policyName, "--actions", "read", "--subjects", "users:" + user1}, outputPattern(fmt.Sprintf(`"path":\s*"%s"`, policyName))}, 258 {"policy-create-file-pass", []string{"policy", "create", "--path", policy2Name, "--data", "@" + policy2File}, outputPattern(fmt.Sprintf(`"path":\s*"%s"`, policy2Name))}, 259 {"policy-read-pass", []string{"policy", "read", "--path", policyName}, outputPattern(fmt.Sprintf(`"path":\s*"%s"`, policyName))}, 260 {"policy-search-pass", []string{"policy", "search", "--query", policyName}, outputPattern(fmt.Sprintf(`"path":\s*"%s"`, policyName))}, 261 {"policy-update-pass", []string{"policy", "update", "--path", policyName, "--resources", policyName, "--actions", "read,delete", "--subjects", "users:" + user1}, outputPattern(`"delete"`)}, 262 {"policy-rollback-pass", []string{"policy", "rollback", "--path", policyName}, outputPattern(fmt.Sprintf(`"path":\s*"%s"`, policyName))}, 263 {"policy-soft-delete", []string{"policy", "delete", policyName}, outputPattern("will be removed")}, 264 {"policy-read-fail", []string{"policy", "read", policyName}, outputPattern("will be removed")}, 265 {"policy-restore", []string{"policy", "restore", policyName}, outputEmpty()}, 266 267 // user operations 268 {"user-create-pass", []string{"user", "create", "--username", user1, "--password", user1Pass}, outputPattern(`"userName": "mrmittens"`)}, 269 270 // group operations 271 {"group-help", []string{"group", ""}, outputPattern(`Execute an action on a group.*`)}, 272 {"group-create-pass", []string{"group", "create", "--group-name", groupName, "--members", user1}, outputPattern(`.*` + "\"errors\": {}" + `.*`)}, 273 {"group-read-pass", []string{"group", "read", groupName}, outputPattern(groupName)}, 274 {"group-delete-member-pass", []string{"group", "delete-members", "--group-name", groupName, "--members", user1}, outputEmpty()}, 275 {"group-read-pass", []string{"group", "read", groupName}, outputPattern(groupName)}, 276 {"group-soft-delete", []string{"group", "delete", groupName}, outputPattern("will be removed")}, 277 {"group-read-fail", []string{"group", "read", groupName}, outputPattern("will be removed")}, 278 {"group-restore", []string{"group", "restore", groupName}, outputEmpty()}, 279 280 // delegated access operations 281 {"user-auth-pass", []string{"auth", "-u", user1, "-p", user1Pass}, outputPattern(`"accessToken":\s*"[^"]+",\s*"expiresIn"`)}, 282 {"user-auth-pass-failed", []string{"auth", "-u", user1, "-p", "user1fail"}, outputPattern(`{"code":401,"message":"unable to authenticate"}`)}, 283 {"user-access-pass", []string{"secret", "read", secret1Name, "-u", user1, "-p", user1Pass}, outputPattern(secret1DataFmt)}, 284 {"user-access-fail-action", []string{"secret", "update", secret1Name, "-u", user1, "-p", user1Pass, "-d", `{"field":"updated secret 1"}`}, outputPattern("Invalid permissions")}, 285 {"user-access-fail-resource", []string{"secret", "read", "secret-idonthavepermissionon", "-u", user1, "-p", user1Pass, "-f", ".data"}, outputPattern("Invalid permissions")}, 286 287 // cli-config operations 288 {"cli-config-help", []string{"cli-config", ""}, outputPattern(`Execute an action on the cli config.*`)}, 289 {"cli-config-read-pass", []string{"cli-config", "read"}, outputGolden()}, 290 // Force update to the config with the same correct password. 291 {"cli-config-update-pass", []string{"cli-config", "update", "auth.password", adminPass}, outputEmpty()}, 292 293 // Make sure config now has a `securePassword` key. 294 {"cli-config-read-2-pass", []string{"cli-config", "read"}, outputPattern(`securePassword`)}, 295 296 // Config will not be written, if auth fails upon password update. 297 {"cli-config-update-fail", []string{"cli-config", "update", "auth.password", "wrong-password"}, outputPattern(`Please check your credentials and try again.`)}, 298 299 {"token-clear-pass", []string{"auth", "clear"}, outputEmpty()}, 300 {"user-auth-success", []string{"auth"}, outputPattern(`accessToken`)}, 301 302 {"cli-config-add-key", []string{"cli-config", "update", "key", "value"}, outputEmpty()}, 303 {"cli-config-remove-key", []string{"cli-config", "update", "key", "0"}, outputEmpty()}, 304 305 {"cli-config-update-2-pass", []string{"cli-config", "update", "auth.password", adminPass}, outputEmpty()}, 306 307 // config operations 308 {"config-help", []string{"config", "--help"}, outputPattern(`Execute an action on the.*`)}, 309 {"config-get-implicit-pass", []string{"config"}, outputPattern(`"permissionDocument":`)}, 310 {"config-get-pass", []string{"config", "read"}, outputPattern(`"permissionDocument":`)}, 311 312 // EaaS-Manual 313 {"crypto-manual-key-upload", []string{"crypto", "manual", "key-upload", "--path", manualKeyPath, "--private-key", manualPrivateKey, "--nonce", manualKeyNonce, "--scheme", "symmetric"}, outputPattern(`"version": "0"`)}, 314 {"crypto-manual-key-read", []string{"crypto", "manual", "key-read", "--path", manualKeyPath}, outputPattern(`"version": "0"`)}, 315 {"crypto-manual-encrypt", []string{"crypto", "manual", "encrypt", "--path", manualKeyPath, "--data", plaintext}, outputPattern(`"version": "0"`)}, 316 {"crypto-manual-decrypt", []string{"crypto", "manual", "decrypt", "--path", manualKeyPath, "--data", ciphertext}, outputPattern(`"data": "hello there"`)}, 317 {"crypto-manual-key-update", []string{"crypto", "manual", "key-update", "--path", manualKeyPath, "--private-key", manualPrivateKey}, outputPattern(`"version": "1"`)}, 318 319 // PKI 320 { 321 "register-root-cert", 322 []string{ 323 "pki", "register", "--rootcapath", existingRootSecret, 324 "--certpath", "@" + certPath, "--privkeypath", "@" + privateKeyPath, "--domains", leafCommonName, "--maxttl", "250h", 325 }, 326 outputPattern("certificate"), 327 }, 328 329 { 330 "sign-with-root-cert", 331 []string{ 332 "pki", "sign", "--rootcapath", existingRootSecret, 333 "--csrpath", "@" + csrPath, "--ttl", "100H", 334 }, 335 outputPattern("certificate"), 336 }, 337 338 { 339 "generate-root-cert", 340 []string{ 341 "pki", "generate-root", "--rootcapath", certStoreSecret, 342 "--domains", leafCommonName, "--common-name", "thycotic.com", "--maxttl", "60d", 343 }, 344 outputPattern("certificate"), 345 }, 346 347 { 348 "generate-leaf-cert", 349 []string{ 350 "pki", "leaf", "--rootcapath", certStoreSecret, 351 "--common-name", leafCommonName, "--ttl", "5D", "--store-path", leafSecretPath, 352 }, 353 outputPattern("certificate"), 354 }, 355 356 { 357 "generate-ssh-cert", 358 []string{ 359 "pki", "ssh-cert", "--rootcapath", certStoreSecret, "--leafcapath", 360 leafSecretPath, "--principals", "root,ubuntu", "--ttl", "52w", 361 }, 362 outputPattern("sshCertificate"), 363 }, 364 365 {"user-update-pass", []string{"user", "update", "--username", user1, "--password", "New_password@2"}, outputPattern(`"userName": "mrmittens"`)}, 366 367 // cleanup 368 {"secret-delete-1-pass", []string{"secret", "delete", secret1Name, "--force"}, outputEmpty()}, 369 {"user-delete", []string{"user", "delete", user1, "--force"}, outputEmpty()}, 370 {"policy-delete", []string{"policy", "delete", "--path", policyName, "--force"}, outputEmpty()}, 371 {"policy2-delete", []string{"policy", "delete", "--path", policy2Name, "--force"}, outputEmpty()}, 372 {"cert-secret-delete", []string{"secret", "delete", "--path", certStoreSecret, "--force"}, outputEmpty()}, 373 {"rootCA-secret-delete", []string{"secret", "delete", "--path", existingRootSecret, "--force"}, outputEmpty()}, 374 {"leafCA-secret-delete", []string{"secret", "delete", "--path", leafSecretPath, "--force"}, outputEmpty()}, 375 {"crypto-manual-key-delete", []string{"crypto", "manual", "key-delete", "--path", manualKeyPath, "--force"}, outputEmpty()}, 376 } 377 } 378 379 func fixturePath(t *testing.T, fixture string) string { 380 _, filename, _, ok := runtime.Caller(0) 381 if !ok { 382 t.Fatalf("problems recovering caller information") 383 } 384 return filepath.Join(filepath.Dir(filename), "cases", fixture) 385 } 386 387 func writeFixture(t *testing.T, fixture string, content []byte) { 388 err := os.WriteFile(fixturePath(t, fixture), content, 0o644) 389 if err != nil { 390 t.Fatal(err) 391 } 392 } 393 394 func loadFixture(t *testing.T, fixture string) string { 395 tmpl, err := template.ParseFiles(fixturePath(t, fixture)) 396 if err != nil { 397 t.Fatal(err) 398 } 399 var tmplBytes bytes.Buffer 400 err = tmpl.Execute(&tmplBytes, envToMap()) 401 if err != nil { 402 t.Fatal(err) 403 } 404 return tmplBytes.String() 405 } 406 407 func generateThyYml(inPath, outPath string) error { 408 t, err := template.ParseFiles(inPath) 409 if err != nil { 410 return err 411 } 412 413 outFile, err := os.Create(outPath) 414 if err != nil { 415 return err 416 } 417 defer outFile.Close() 418 419 return t.Execute(outFile, envToMap()) 420 } 421 422 func envToMap() map[string]string { 423 evpMap := map[string]string{} 424 425 for _, v := range os.Environ() { 426 split := strings.Split(v, "=") 427 if len(split) == 2 { 428 evpMap[split[0]] = split[1] 429 } 430 } 431 return evpMap 432 }