Skip to content

Commit 3e69b26

Browse files
committed
Add RemoveArtifactoryRepository function with unified helper utilities
- Add RemoveArtifactoryRepository() to remove all Artifactory configuration from settings.xml - Extract shared helper functions: buildRepositoryURL, findElementByID, removeElementByID, removeEmptyContainer - Refactor findOrCreateElementByID to use new findElementByID helper - Add 6 comprehensive tests for removal functionality (URL verification, idempotency, config preservation, etc.) - Test coverage: 94.4%
1 parent 8a8c042 commit 3e69b26

File tree

2 files changed

+334
-4
lines changed

2 files changed

+334
-4
lines changed

artifactory/utils/maven/settingsxml.go

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ func (sxm *SettingsXmlManager) loadSettings() {
9797
sxm.doc.Indent(2)
9898
}
9999

100+
// buildRepositoryURL constructs the full repository URL from base URL and repository name.
101+
func buildRepositoryURL(artifactoryUrl, repoName string) string {
102+
return strings.TrimRight(artifactoryUrl, "/") + "/" + repoName
103+
}
104+
100105
// ConfigureArtifactoryRepository configures Maven to use Artifactory for both downloading and deployment.
101106
// It updates or creates the following in settings.xml:
102107
// - Mirror configuration for downloading artifacts from Artifactory
@@ -120,7 +125,7 @@ func (sxm *SettingsXmlManager) ConfigureArtifactoryRepository(artifactoryUrl, re
120125
}
121126

122127
// Build repository URL
123-
repoUrl := strings.TrimRight(artifactoryUrl, "/") + "/" + repoName
128+
repoUrl := buildRepositoryURL(artifactoryUrl, repoName)
124129

125130
// Ensure we have a root <settings> element
126131
root := sxm.doc.SelectElement(xmlElementSettings)
@@ -189,14 +194,43 @@ func getOrCreateElement(parent *etree.Element, name string) *etree.Element {
189194
return element
190195
}
191196

192-
// findOrCreateElementByID finds an element with a specific ID or creates a new one.
193-
func findOrCreateElementByID(parent *etree.Element, elementName, id string) *etree.Element {
197+
// findElementByID finds an element with a specific ID within a parent container.
198+
// Returns nil if not found.
199+
func findElementByID(parent *etree.Element, elementName, id string) *etree.Element {
194200
for _, elem := range parent.SelectElements(elementName) {
195201
if idElem := elem.SelectElement(xmlElementID); idElem != nil && idElem.Text() == id {
196202
return elem
197203
}
198204
}
199-
return parent.CreateElement(elementName)
205+
return nil
206+
}
207+
208+
// findOrCreateElementByID finds an element with a specific ID or creates a new one.
209+
func findOrCreateElementByID(parent *etree.Element, elementName, id string) *etree.Element {
210+
elem := findElementByID(parent, elementName, id)
211+
if elem == nil {
212+
elem = parent.CreateElement(elementName)
213+
}
214+
return elem
215+
}
216+
217+
// removeElementByID removes an element with a specific ID from its parent container.
218+
// Returns true if the element was found and removed, false otherwise.
219+
func removeElementByID(parent *etree.Element, elementName, id string) bool {
220+
elem := findElementByID(parent, elementName, id)
221+
if elem != nil {
222+
parent.RemoveChild(elem)
223+
return true
224+
}
225+
return false
226+
}
227+
228+
// removeEmptyContainer removes a container element from its parent if it has no children.
229+
func removeEmptyContainer(root *etree.Element, containerName string) {
230+
container := root.SelectElement(containerName)
231+
if container != nil && len(container.ChildElements()) == 0 {
232+
root.RemoveChild(container)
233+
}
200234
}
201235

202236
// setOrCreateChildElement sets or creates a child element with the given name and text.
@@ -205,6 +239,90 @@ func setOrCreateChildElement(parent *etree.Element, name, text string) {
205239
child.SetText(text)
206240
}
207241

242+
// RemoveArtifactoryRepository removes all Artifactory configuration from settings.xml.
243+
// This includes:
244+
// - Mirror configuration with ArtifactoryMirrorID
245+
// - Server credentials with ArtifactoryMirrorID
246+
// - Deployment profile with ArtifactoryDeployProfileID
247+
//
248+
// Parameters:
249+
// - artifactoryUrl: Base URL of the Artifactory instance (used for verification, optional)
250+
// - repoName: Name of the Artifactory repository (used for verification, optional)
251+
//
252+
// Returns an error if the settings.xml cannot be updated.
253+
func (sxm *SettingsXmlManager) RemoveArtifactoryRepository(artifactoryUrl, repoName string) error {
254+
root := sxm.doc.SelectElement(xmlElementSettings)
255+
if root == nil {
256+
return fmt.Errorf("invalid settings.xml: missing <%s> root element", xmlElementSettings)
257+
}
258+
259+
// Build repository URL for verification if provided
260+
var repoUrl string
261+
if artifactoryUrl != "" && repoName != "" {
262+
repoUrl = buildRepositoryURL(artifactoryUrl, repoName)
263+
}
264+
265+
// Remove mirror
266+
if err := sxm.removeMirror(root, repoUrl); err != nil {
267+
return err
268+
}
269+
270+
// Remove server
271+
sxm.removeServer(root)
272+
273+
// Remove deployment profile
274+
sxm.removeDeploymentProfile(root)
275+
276+
// Write settings to file
277+
return sxm.writeSettingsToFile()
278+
}
279+
280+
// removeMirror removes the Artifactory mirror entry.
281+
func (sxm *SettingsXmlManager) removeMirror(root *etree.Element, expectedUrl string) error {
282+
mirrors := root.SelectElement(xmlElementMirrors)
283+
if mirrors == nil {
284+
return nil
285+
}
286+
287+
// Verify URL if provided before removing
288+
if expectedUrl != "" {
289+
mirror := findElementByID(mirrors, xmlElementMirror, ArtifactoryMirrorID)
290+
if mirror != nil {
291+
urlElem := mirror.SelectElement(xmlElementURL)
292+
if urlElem != nil && urlElem.Text() != expectedUrl {
293+
return fmt.Errorf("mirror URL mismatch: expected %s, found %s", expectedUrl, urlElem.Text())
294+
}
295+
}
296+
}
297+
298+
removeElementByID(mirrors, xmlElementMirror, ArtifactoryMirrorID)
299+
removeEmptyContainer(root, xmlElementMirrors)
300+
301+
return nil
302+
}
303+
304+
// removeServer removes the Artifactory server entry.
305+
func (sxm *SettingsXmlManager) removeServer(root *etree.Element) {
306+
servers := root.SelectElement(xmlElementServers)
307+
if servers == nil {
308+
return
309+
}
310+
311+
removeElementByID(servers, xmlElementServer, ArtifactoryMirrorID)
312+
removeEmptyContainer(root, xmlElementServers)
313+
}
314+
315+
// removeDeploymentProfile removes the Artifactory deployment profile.
316+
func (sxm *SettingsXmlManager) removeDeploymentProfile(root *etree.Element) {
317+
profiles := root.SelectElement(xmlElementProfiles)
318+
if profiles == nil {
319+
return
320+
}
321+
322+
removeElementByID(profiles, xmlElementProfile, ArtifactoryDeployProfileID)
323+
removeEmptyContainer(root, xmlElementProfiles)
324+
}
325+
208326
// writeSettingsToFile writes the document to the settings.xml file.
209327
func (sxm *SettingsXmlManager) writeSettingsToFile() error {
210328
// Ensure directory exists

artifactory/utils/maven/settingsxml_test.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,218 @@ func TestComprehensiveXMLPreservation(t *testing.T) {
834834
t.Logf(" - Element counts correct: %d servers, %d mirrors, %d profiles", serverCount, mirrorCount, profileCount)
835835
}
836836

837+
func TestRemoveArtifactoryRepository(t *testing.T) {
838+
tempDir := t.TempDir()
839+
settingsPath := filepath.Join(tempDir, "settings.xml")
840+
841+
// First configure Artifactory
842+
manager, err := NewSettingsXmlManagerWithPath(settingsPath)
843+
require.NoError(t, err)
844+
845+
err = manager.ConfigureArtifactoryRepository("https://artifactory.example.com", "maven-virtual", "admin", "secret123")
846+
require.NoError(t, err)
847+
848+
// Verify configuration was added
849+
content, err := os.ReadFile(settingsPath)
850+
require.NoError(t, err)
851+
xmlContent := string(content)
852+
assert.Contains(t, xmlContent, "artifactory-mirror")
853+
assert.Contains(t, xmlContent, "artifactory-deploy")
854+
assert.Contains(t, xmlContent, "admin")
855+
856+
// Now remove the configuration
857+
manager, err = NewSettingsXmlManagerWithPath(settingsPath)
858+
require.NoError(t, err)
859+
860+
err = manager.RemoveArtifactoryRepository("https://artifactory.example.com", "maven-virtual")
861+
require.NoError(t, err)
862+
863+
// Verify configuration was removed
864+
content, err = os.ReadFile(settingsPath)
865+
require.NoError(t, err)
866+
xmlContent = string(content)
867+
assert.NotContains(t, xmlContent, "artifactory-mirror")
868+
assert.NotContains(t, xmlContent, "artifactory-deploy")
869+
assert.NotContains(t, xmlContent, "admin")
870+
assert.NotContains(t, xmlContent, "secret123")
871+
872+
// Should still have valid XML structure
873+
manager, err = NewSettingsXmlManagerWithPath(settingsPath)
874+
require.NoError(t, err)
875+
root := manager.doc.SelectElement(xmlElementSettings)
876+
assert.NotNil(t, root)
877+
}
878+
879+
func TestRemoveArtifactoryRepository_WithoutURL(t *testing.T) {
880+
tempDir := t.TempDir()
881+
settingsPath := filepath.Join(tempDir, "settings.xml")
882+
883+
// First configure Artifactory
884+
manager, err := NewSettingsXmlManagerWithPath(settingsPath)
885+
require.NoError(t, err)
886+
887+
err = manager.ConfigureArtifactoryRepository("https://artifactory.example.com", "maven-virtual", "admin", "secret123")
888+
require.NoError(t, err)
889+
890+
// Remove without URL verification (should still work)
891+
manager, err = NewSettingsXmlManagerWithPath(settingsPath)
892+
require.NoError(t, err)
893+
894+
err = manager.RemoveArtifactoryRepository("", "")
895+
require.NoError(t, err)
896+
897+
// Verify configuration was removed
898+
content, err := os.ReadFile(settingsPath)
899+
require.NoError(t, err)
900+
xmlContent := string(content)
901+
assert.NotContains(t, xmlContent, "artifactory-mirror")
902+
assert.NotContains(t, xmlContent, "artifactory-deploy")
903+
}
904+
905+
func TestRemoveArtifactoryRepository_URLMismatch(t *testing.T) {
906+
tempDir := t.TempDir()
907+
settingsPath := filepath.Join(tempDir, "settings.xml")
908+
909+
// Configure with one URL
910+
manager, err := NewSettingsXmlManagerWithPath(settingsPath)
911+
require.NoError(t, err)
912+
913+
err = manager.ConfigureArtifactoryRepository("https://artifactory.example.com", "maven-virtual", "admin", "secret123")
914+
require.NoError(t, err)
915+
916+
// Try to remove with different URL - should fail
917+
manager, err = NewSettingsXmlManagerWithPath(settingsPath)
918+
require.NoError(t, err)
919+
920+
err = manager.RemoveArtifactoryRepository("https://different.example.com", "maven-virtual")
921+
assert.Error(t, err)
922+
assert.Contains(t, err.Error(), "mirror URL mismatch")
923+
924+
// Configuration should still be present
925+
content, err := os.ReadFile(settingsPath)
926+
require.NoError(t, err)
927+
xmlContent := string(content)
928+
assert.Contains(t, xmlContent, "artifactory-mirror")
929+
}
930+
931+
func TestRemoveArtifactoryRepository_PreservesOtherConfig(t *testing.T) {
932+
tempDir := t.TempDir()
933+
settingsPath := filepath.Join(tempDir, "settings.xml")
934+
935+
// Create settings with existing configuration
936+
existingXML := `<?xml version="1.0" encoding="UTF-8"?>
937+
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
938+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
939+
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 http://maven.apache.org/xsd/settings-1.2.0.xsd">
940+
<servers>
941+
<server>
942+
<id>my-server</id>
943+
<username>myuser</username>
944+
<password>mypass</password>
945+
</server>
946+
</servers>
947+
<mirrors>
948+
<mirror>
949+
<id>my-mirror</id>
950+
<url>https://my-mirror.com</url>
951+
<mirrorOf>central</mirrorOf>
952+
</mirror>
953+
</mirrors>
954+
<profiles>
955+
<profile>
956+
<id>my-profile</id>
957+
<activation>
958+
<activeByDefault>true</activeByDefault>
959+
</activation>
960+
</profile>
961+
</profiles>
962+
</settings>`
963+
964+
err := os.WriteFile(settingsPath, []byte(existingXML), 0o644)
965+
require.NoError(t, err)
966+
967+
// Add Artifactory config
968+
manager, err := NewSettingsXmlManagerWithPath(settingsPath)
969+
require.NoError(t, err)
970+
971+
err = manager.ConfigureArtifactoryRepository("https://artifactory.example.com", "maven-virtual", "admin", "secret123")
972+
require.NoError(t, err)
973+
974+
// Remove Artifactory config
975+
manager, err = NewSettingsXmlManagerWithPath(settingsPath)
976+
require.NoError(t, err)
977+
978+
err = manager.RemoveArtifactoryRepository("", "")
979+
require.NoError(t, err)
980+
981+
// Verify original configuration is preserved
982+
content, err := os.ReadFile(settingsPath)
983+
require.NoError(t, err)
984+
xmlContent := string(content)
985+
986+
// Original config should be present
987+
assert.Contains(t, xmlContent, "my-server")
988+
assert.Contains(t, xmlContent, "myuser")
989+
assert.Contains(t, xmlContent, "my-mirror")
990+
assert.Contains(t, xmlContent, "my-profile")
991+
992+
// Artifactory config should be removed
993+
assert.NotContains(t, xmlContent, "artifactory-mirror")
994+
assert.NotContains(t, xmlContent, "artifactory-deploy")
995+
assert.NotContains(t, xmlContent, "admin")
996+
}
997+
998+
func TestRemoveArtifactoryRepository_Idempotent(t *testing.T) {
999+
tempDir := t.TempDir()
1000+
settingsPath := filepath.Join(tempDir, "settings.xml")
1001+
1002+
// Create empty settings
1003+
manager, err := NewSettingsXmlManagerWithPath(settingsPath)
1004+
require.NoError(t, err)
1005+
1006+
// Try to remove from empty file - should not error
1007+
err = manager.RemoveArtifactoryRepository("", "")
1008+
assert.NoError(t, err)
1009+
1010+
// Remove again - should still not error (idempotent)
1011+
manager, err = NewSettingsXmlManagerWithPath(settingsPath)
1012+
require.NoError(t, err)
1013+
1014+
err = manager.RemoveArtifactoryRepository("", "")
1015+
assert.NoError(t, err)
1016+
}
1017+
1018+
func TestRemoveArtifactoryRepository_RemovesEmptyContainers(t *testing.T) {
1019+
tempDir := t.TempDir()
1020+
settingsPath := filepath.Join(tempDir, "settings.xml")
1021+
1022+
// Configure Artifactory only (no other servers/mirrors/profiles)
1023+
manager, err := NewSettingsXmlManagerWithPath(settingsPath)
1024+
require.NoError(t, err)
1025+
1026+
err = manager.ConfigureArtifactoryRepository("https://artifactory.example.com", "maven-virtual", "admin", "secret123")
1027+
require.NoError(t, err)
1028+
1029+
// Remove configuration
1030+
manager, err = NewSettingsXmlManagerWithPath(settingsPath)
1031+
require.NoError(t, err)
1032+
1033+
err = manager.RemoveArtifactoryRepository("", "")
1034+
require.NoError(t, err)
1035+
1036+
// Verify empty container elements are removed
1037+
content, err := os.ReadFile(settingsPath)
1038+
require.NoError(t, err)
1039+
xmlContent := string(content)
1040+
1041+
assert.NotContains(t, xmlContent, "<servers>")
1042+
assert.NotContains(t, xmlContent, "<mirrors>")
1043+
assert.NotContains(t, xmlContent, "<profiles>")
1044+
1045+
// Should still have valid root structure
1046+
assert.Contains(t, xmlContent, "<settings")
1047+
}
1048+
8371049
// Helper function to set home directory for cross-platform testing
8381050
func setTestHomeDir(t *testing.T, tempDir string) {
8391051
originalHome, err := os.UserHomeDir()

0 commit comments

Comments
 (0)