Skip to content

Commit 9fe662a

Browse files
committed
Added unit tests
1 parent 0a77dab commit 9fe662a

3 files changed

Lines changed: 176 additions & 11 deletions

File tree

src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunner.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,7 @@ public boolean registerKeySshAgent(String keyPath) throws Exception {
884884
* @param hostUsers Map of host names to usernames
885885
* @return YAML content string ready to be written to all.yaml
886886
*/
887-
private String buildGroupVarsYaml(Map<String, String> hostPasswords, Map<String, String> hostUsers) {
887+
String buildGroupVarsYaml(Map<String, String> hostPasswords, Map<String, String> hostUsers) {
888888

889889
StringBuilder yamlContent = new StringBuilder();
890890
yamlContent.append("host_passwords:\n");
@@ -950,13 +950,13 @@ private String buildGroupVarsYaml(Map<String, String> hostPasswords, Map<String,
950950
* @param key The key to escape
951951
* @return Escaped key safe for YAML
952952
*/
953-
private String escapeYamlKey(String key) {
954-
// YAML special characters that require quoting
955-
if (key.matches(".*[:\\[\\]{}#&*!|>'\"%@`].*") ||
953+
String escapeYamlKey(String key) {
954+
// YAML special characters that require quoting (including backslash)
955+
if (key.matches(".*[:\\[\\]{}#&*!|>'\"%@`\\\\].*") ||
956956
key.startsWith("-") ||
957957
key.startsWith("?") ||
958958
key.matches("^[0-9].*")) {
959-
// Escape any existing quotes and wrap in quotes
959+
// Escape any existing backslashes and quotes, then wrap in quotes
960960
return "\"" + key.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
961961
}
962962
return key;
@@ -969,13 +969,13 @@ private String escapeYamlKey(String key) {
969969
* @param value The value to escape
970970
* @return Escaped value safe for YAML
971971
*/
972-
private String escapeYamlValue(String value) {
973-
// Check if value needs quoting
974-
if (value.matches(".*[:\\[\\]{}#&*!|>'\"%@`].*") ||
972+
String escapeYamlValue(String value) {
973+
// Check if value needs quoting (including backslash)
974+
if (value.matches(".*[:\\[\\]{}#&*!|>'\"%@`\\\\].*") ||
975975
value.startsWith("-") ||
976976
value.startsWith("?") ||
977977
value.trim().isEmpty()) {
978-
// Escape any existing quotes and wrap in quotes
978+
// Escape any existing backslashes and quotes, then wrap in quotes
979979
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
980980
}
981981
return value;
@@ -988,7 +988,7 @@ private String escapeYamlValue(String value) {
988988
* @param vaultValue The vault value to validate
989989
* @return true if valid vault format, false otherwise
990990
*/
991-
private boolean isValidVaultFormat(String vaultValue) {
991+
boolean isValidVaultFormat(String vaultValue) {
992992
if (vaultValue == null || vaultValue.trim().isEmpty()) {
993993
return false;
994994
}

src/main/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerContextBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1042,7 +1042,7 @@ public Boolean generateInventoryNodesAuth() {
10421042
*
10431043
* @return The path to the execution-specific directory
10441044
*/
1045-
private String getExecutionSpecificTmpDir() {
1045+
String getExecutionSpecificTmpDir() {
10461046
// Return cached directory if already created
10471047
if (executionSpecificDir != null) {
10481048
System.err.println("DEBUG: Using cached execution-specific directory: " + executionSpecificDir.getAbsolutePath());

src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerSpec.groovy

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,4 +597,169 @@ class AnsibleRunnerSpec extends Specification{
597597
}
598598
599599
600+
def "escapeYamlKey: should quote keys with special characters"() {
601+
given:
602+
def builder = AnsibleRunner.playbookInline("test")
603+
builder.customTmpDirPath("/tmp")
604+
def runner = builder.build()
605+
606+
expect:
607+
runner.escapeYamlKey(key) == expectedResult
608+
609+
where:
610+
key || expectedResult
611+
"simple-key" || "simple-key"
612+
"key:with:colons" || "\"key:with:colons\""
613+
"key[brackets]" || "\"key[brackets]\""
614+
"key{braces}" || "\"key{braces}\""
615+
"key#hash" || "\"key#hash\""
616+
"key&ampersand" || "\"key&ampersand\""
617+
"key*asterisk" || "\"key*asterisk\""
618+
"key!exclamation" || "\"key!exclamation\""
619+
"key|pipe" || "\"key|pipe\""
620+
"-starts-with-dash" || "\"-starts-with-dash\""
621+
"?starts-with-q" || "\"?starts-with-q\""
622+
"123numeric" || "\"123numeric\""
623+
"key with spaces" || "key with spaces" // spaces are handled differently
624+
}
625+
626+
def "escapeYamlValue: should quote values with special characters"() {
627+
given:
628+
def builder = AnsibleRunner.playbookInline("test")
629+
builder.customTmpDirPath("/tmp")
630+
def runner = builder.build()
631+
632+
expect:
633+
runner.escapeYamlValue(value) == expectedResult
634+
635+
where:
636+
value || expectedResult
637+
"simple-value" || "simple-value"
638+
"value:colon" || "\"value:colon\""
639+
"value[bracket]" || "\"value[bracket]\""
640+
" " || "\" \"" // empty/whitespace should be quoted
641+
"value@at" || "\"value@at\""
642+
"-starts-dash" || "\"-starts-dash\""
643+
}
644+
645+
def "escapeYamlKey: should escape backslashes and quotes"() {
646+
given:
647+
def builder = AnsibleRunner.playbookInline("test")
648+
builder.customTmpDirPath("/tmp")
649+
def runner = builder.build()
650+
651+
expect:
652+
runner.escapeYamlKey('key"with"quotes') == '"key\\"with\\"quotes"'
653+
runner.escapeYamlKey('key\\backslash') == '"key\\\\backslash"'
654+
}
655+
656+
def "isValidVaultFormat: should validate vault format correctly"() {
657+
given:
658+
def builder = AnsibleRunner.playbookInline("test")
659+
builder.customTmpDirPath("/tmp")
660+
def runner = builder.build()
661+
662+
expect:
663+
runner.isValidVaultFormat(vaultValue) == expectedResult
664+
665+
where:
666+
vaultValue || expectedResult
667+
"!vault |\n encryptedcontent" || true
668+
"!vault |\n line1\n line2" || true
669+
"!vault\n content" || true // without pipe
670+
"not a vault" || false
671+
null || false
672+
"" || false
673+
"!vault" || true // minimal valid
674+
"!vault |\n" || false // no content
675+
}
676+
677+
def "buildGroupVarsYaml: should create valid YAML with host passwords and users"() {
678+
given:
679+
def builder = AnsibleRunner.playbookInline("test")
680+
builder.customTmpDirPath("/tmp")
681+
def runner = builder.build()
682+
683+
def hostPasswords = [
684+
"web-server-1": "!vault |\n encrypted1",
685+
"db-server-1": "!vault |\n encrypted2"
686+
]
687+
def hostUsers = [
688+
"web-server-1": "webadmin",
689+
"db-server-1": "dbadmin"
690+
]
691+
692+
when:
693+
def yaml = runner.buildGroupVarsYaml(hostPasswords, hostUsers)
694+
695+
then:
696+
yaml.contains("host_passwords:")
697+
yaml.contains("web-server-1: !vault |")
698+
yaml.contains(" encrypted1")
699+
yaml.contains("db-server-1: !vault |")
700+
yaml.contains(" encrypted2")
701+
yaml.contains("host_users:")
702+
yaml.contains("web-server-1: webadmin")
703+
yaml.contains("db-server-1: dbadmin")
704+
}
705+
706+
def "buildGroupVarsYaml: should escape special characters in node names"() {
707+
given:
708+
def builder = AnsibleRunner.playbookInline("test")
709+
builder.customTmpDirPath("/tmp")
710+
def runner = builder.build()
711+
712+
def hostPasswords = [
713+
"web:server:1": "!vault |\n encrypted"
714+
]
715+
def hostUsers = [
716+
"web:server:1": "admin"
717+
]
718+
719+
when:
720+
def yaml = runner.buildGroupVarsYaml(hostPasswords, hostUsers)
721+
722+
then:
723+
yaml.contains('"web:server:1": !vault |')
724+
yaml.contains('"web:server:1": admin')
725+
}
726+
727+
def "buildGroupVarsYaml: should throw exception for invalid vault format"() {
728+
given:
729+
def builder = AnsibleRunner.playbookInline("test")
730+
builder.customTmpDirPath("/tmp")
731+
def runner = builder.build()
732+
733+
def hostPasswords = [
734+
"web-server-1": "not-a-vault-value"
735+
]
736+
def hostUsers = [
737+
"web-server-1": "admin"
738+
]
739+
740+
when:
741+
runner.buildGroupVarsYaml(hostPasswords, hostUsers)
742+
743+
then:
744+
def e = thrown(RuntimeException)
745+
e.message.contains("Invalid vault format for host: web-server-1")
746+
}
747+
748+
def "buildGroupVarsYaml: should handle empty host lists"() {
749+
given:
750+
def builder = AnsibleRunner.playbookInline("test")
751+
builder.customTmpDirPath("/tmp")
752+
def runner = builder.build()
753+
754+
def hostPasswords = [:]
755+
def hostUsers = [:]
756+
757+
when:
758+
def yaml = runner.buildGroupVarsYaml(hostPasswords, hostUsers)
759+
760+
then:
761+
yaml.contains("host_passwords:")
762+
yaml.contains("host_users:")
763+
}
764+
600765
}

0 commit comments

Comments
 (0)