diff --git a/packages/repositories.config b/packages/repositories.config index bcd17bb0..3468aa5e 100644 --- a/packages/repositories.config +++ b/packages/repositories.config @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/BuildConfigs.cs b/src/TeamCitySharp/ActionTypes/BuildConfigs.cs index cdcd4a0f..83cb8a85 100644 --- a/src/TeamCitySharp/ActionTypes/BuildConfigs.cs +++ b/src/TeamCitySharp/ActionTypes/BuildConfigs.cs @@ -66,7 +66,7 @@ public BuildConfig ByProjectIdAndConfigurationId(string projectId, string buildC public List ByProjectId(string projectId) { - var buildWrapper = _caller.GetFormat("/app/rest/projects/id:{0}/buildTypes", projectId); + var buildWrapper = _caller.GetFormat("/app/rest/projects/{0}/buildTypes", projectId); if (buildWrapper == null || buildWrapper.BuildType == null) return new List(); return buildWrapper.BuildType; diff --git a/src/TeamCitySharp/ActionTypes/IProjects.cs b/src/TeamCitySharp/ActionTypes/IProjects.cs index febce5c2..b1de0aa4 100644 --- a/src/TeamCitySharp/ActionTypes/IProjects.cs +++ b/src/TeamCitySharp/ActionTypes/IProjects.cs @@ -10,8 +10,10 @@ public interface IProjects Project ById(string projectLocatorId); Project Details(Project project); Project Create(string projectName); + Project Create(string projectName, string projectId); void Delete(string projectName); void DeleteProjectParameter(string projectName, string parameterName); void SetProjectParameter(string projectName, string settingName, string settingValue); + bool SetName(string projectCode, string name); } } \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/IUserGroups.cs b/src/TeamCitySharp/ActionTypes/IUserGroups.cs new file mode 100644 index 00000000..54d35f78 --- /dev/null +++ b/src/TeamCitySharp/ActionTypes/IUserGroups.cs @@ -0,0 +1,9 @@ +namespace TeamCitySharp.ActionTypes +{ + public interface IUserGroups + { + bool AddGroup(string groupKey, string groupName); + bool RemoveGroup(string groupKey); + bool AddRoleToGroup(string groupKey, string roleId, string projectKey); + } +} diff --git a/src/TeamCitySharp/ActionTypes/IUsers.cs b/src/TeamCitySharp/ActionTypes/IUsers.cs index 7d60ec9d..5727be71 100644 --- a/src/TeamCitySharp/ActionTypes/IUsers.cs +++ b/src/TeamCitySharp/ActionTypes/IUsers.cs @@ -14,5 +14,6 @@ public interface IUsers List AllUserRolesByUserGroup(string userGroupName); bool Create(string username, string name, string email, string password); bool AddPassword(string username, string password); + bool AddUserToGroup(string username, string groupKey); } } \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/IVcsRoots.cs b/src/TeamCitySharp/ActionTypes/IVcsRoots.cs index 6e2b381a..cf00a596 100644 --- a/src/TeamCitySharp/ActionTypes/IVcsRoots.cs +++ b/src/TeamCitySharp/ActionTypes/IVcsRoots.cs @@ -11,5 +11,6 @@ public interface IVcsRoots VcsRoot AttachVcsRoot(BuildTypeLocator locator, VcsRoot vcsRoot); void DetachVcsRoot(BuildTypeLocator locator, string vcsRootId); void SetVcsRootField(VcsRoot vcsRoot, VcsRootField field, object value); + VcsRoot Create(VcsRoot root); } } \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/Projects.cs b/src/TeamCitySharp/ActionTypes/Projects.cs index d19ec642..1bdfd640 100644 --- a/src/TeamCitySharp/ActionTypes/Projects.cs +++ b/src/TeamCitySharp/ActionTypes/Projects.cs @@ -5,6 +5,10 @@ namespace TeamCitySharp.ActionTypes { + using System.Net; + + using EasyHttp.Infrastructure; + internal class Projects : IProjects { private readonly TeamCityCaller _caller; @@ -23,16 +27,12 @@ public List All() public Project ByName(string projectLocatorName) { - var project = _caller.GetFormat("/app/rest/projects/name:{0}", projectLocatorName); - - return project; + return GetProject(string.Format("name:{0}", projectLocatorName)); } public Project ById(string projectLocatorId) { - var project = _caller.GetFormat("/app/rest/projects/id:{0}", projectLocatorId); - - return project; + return GetProject(projectLocatorId); } public Project Details(Project project) @@ -45,6 +45,37 @@ public Project Create(string projectName) return _caller.Post(projectName, HttpContentTypes.TextPlain, "/app/rest/projects/", HttpContentTypes.ApplicationJson); } + public bool SetName(string projectCode, string name) + { + try + { + var response = _caller.Put(name, HttpContentTypes.TextPlain, string.Format("/app/rest/projects/{0}/name", projectCode), null); + return response.StatusCode == HttpStatusCode.OK; + } + catch (HttpException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + return false; + } + + throw; + } + } + + public Project Create(string projectName, string projectId) + { + + var content = string.Format + (@" " + , projectName, projectId); + /* extended xml version: + * + * more details could be found in documentation: https://confluence.jetbrains.com/display/TCD9/REST+API#RESTAPI-ProjectSettings + */ + return _caller.Post(content, HttpContentTypes.ApplicationXml, "/app/rest/projects/", HttpContentTypes.ApplicationJson); + } + public void Delete(string projectName) { _caller.DeleteFormat("/app/rest/projects/name:{0}", projectName); @@ -59,5 +90,23 @@ public void SetProjectParameter(string projectName, string settingName, string s { _caller.PutFormat(settingValue, "/app/rest/projects/name:{0}/parameters/{1}", projectName, settingName); } + + private Project GetProject(string locator) + { + try + { + var project = _caller.GetFormat("/app/rest/projects/{0}", locator); + return project; + } + catch (HttpException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + return null; + } + + throw; + } + } } } \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/UserGroups.cs b/src/TeamCitySharp/ActionTypes/UserGroups.cs new file mode 100644 index 00000000..1cdddd18 --- /dev/null +++ b/src/TeamCitySharp/ActionTypes/UserGroups.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Net; + +using EasyHttp.Http; + +using TeamCitySharp.Connection; +using TeamCitySharp.Util; + +namespace TeamCitySharp.ActionTypes +{ + using EasyHttp.Infrastructure; + + internal class UserGroups : IUserGroups + { + private readonly TeamCityCaller _caller; + + internal UserGroups(TeamCityCaller caller) + { + _caller = caller; + } + + public bool AddGroup(string groupKey, string groupName) + { + ArgumentUtil.CheckNotNull(() => groupKey, () => groupName); + + var attributesDictionary = new Dictionary { { "key", groupKey }, { "name", groupName } }; + var payload = XmlUtil.SingleElementDocument("group", attributesDictionary); + var response = _caller.Post(payload, HttpContentTypes.ApplicationXml, "/app/rest/userGroups", null); + + return response.StatusCode == HttpStatusCode.OK; + } + + public bool RemoveGroup(string groupKey) + { + ArgumentUtil.CheckNotNull(() => groupKey); + + try + { + _caller.Delete(string.Format("/app/rest/userGroups/key:{0}", groupKey)); + return true; + } + catch (HttpException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + return false; + } + + throw; + } + } + + public bool AddRoleToGroup(string groupKey, string roleId, string projectKey) + { + ArgumentUtil.CheckNotNull(() => groupKey, () => roleId, () => projectKey); + + var attributesDictionary = new Dictionary { { "roleId", roleId }, { "scope", string.Format("p:{0}", projectKey) } }; + var payload = XmlUtil.SingleElementDocument("role", attributesDictionary); + var response = _caller.Post(payload, HttpContentTypes.ApplicationXml, string.Format("/app/rest/userGroups/key:{0}/roles", groupKey), null); + + return response.StatusCode == HttpStatusCode.OK; + } + } +} diff --git a/src/TeamCitySharp/ActionTypes/Users.cs b/src/TeamCitySharp/ActionTypes/Users.cs index 6b494ac8..3f5b3c91 100644 --- a/src/TeamCitySharp/ActionTypes/Users.cs +++ b/src/TeamCitySharp/ActionTypes/Users.cs @@ -4,8 +4,13 @@ using TeamCitySharp.Connection; using TeamCitySharp.DomainEntities; +using TeamCitySharp.Locators; +using TeamCitySharp.Util; + namespace TeamCitySharp.ActionTypes { + using EasyHttp.Infrastructure; + internal class Users : IUsers { private readonly TeamCityCaller _caller; @@ -100,5 +105,31 @@ public bool AddPassword(string username, string password) return result; } + public bool AddUserToGroup(string username, string groupKey) + { + ArgumentUtil.CheckNotNull(() => username, () => groupKey); + + var attributesDictionary = new Dictionary { { "key", groupKey } }; + var payload = XmlUtil.SingleElementDocument("group", attributesDictionary); + + try + { + var response = _caller.Post( + payload, + HttpContentTypes.ApplicationXml, + string.Format("/app/rest/users/{0}/groups", UserLocator.WithUserName(username)), + null); + return response.StatusCode == HttpStatusCode.OK; + } + catch (HttpException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) + { + return false; + } + + throw; + } + } } } \ No newline at end of file diff --git a/src/TeamCitySharp/ActionTypes/VcsRoots.cs b/src/TeamCitySharp/ActionTypes/VcsRoots.cs index 9e1e95a9..8b3de6a5 100644 --- a/src/TeamCitySharp/ActionTypes/VcsRoots.cs +++ b/src/TeamCitySharp/ActionTypes/VcsRoots.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; using EasyHttp.Http; using TeamCitySharp.Connection; using TeamCitySharp.DomainEntities; @@ -30,6 +32,35 @@ public VcsRoot ById(string vcsRootId) return vcsRoot; } + + public VcsRoot Create(VcsRoot root) + { + var sb = new StringBuilder(); + var rootStr = + string.Format( + @"" + ,root.Id,root.Name,root.vcsName,root.Status); + sb.Append(rootStr); + var projectStr = + string.Format( + @"" + ,root.Project.Id,root.Project.Name); + sb.Append(projectStr); + if (root.Properties != null && root.Properties.Property != null && root.Properties.Property.Any()) + { + sb.Append(string.Format(@"", root.Properties.Property.Count)); + foreach (var property in root.Properties.Property) + { + sb.Append(string.Format(@"", property.Name, property.Value)); + } + sb.Append(@""); + } + sb.Append(""); + var xml = sb.ToString(); + + return _caller.PostFormat(xml, HttpContentTypes.ApplicationXml, HttpContentTypes.ApplicationJson, "/app/rest/vcs-roots/"); + } + public VcsRoot AttachVcsRoot(BuildTypeLocator locator, VcsRoot vcsRoot) { var xml = string.Format(@"", vcsRoot.Id); diff --git a/src/TeamCitySharp/DomainEntities/Role.cs b/src/TeamCitySharp/DomainEntities/Role.cs index b2fc41b0..f423633b 100644 --- a/src/TeamCitySharp/DomainEntities/Role.cs +++ b/src/TeamCitySharp/DomainEntities/Role.cs @@ -2,6 +2,9 @@ { public class Role { + public const string ProjectAdministrator = "PROJECT_ADMIN"; + public const string ProjectDeveloper = "PROJECT_DEVELOPER"; + public string Href { get; set; } public string Scope { get; set; } public string RoleId { get; set; } diff --git a/src/TeamCitySharp/DomainEntities/VcsRoot.cs b/src/TeamCitySharp/DomainEntities/VcsRoot.cs index 93411c59..4c3d92cb 100644 --- a/src/TeamCitySharp/DomainEntities/VcsRoot.cs +++ b/src/TeamCitySharp/DomainEntities/VcsRoot.cs @@ -18,5 +18,7 @@ public override string ToString() } public Properties Properties { get; set; } + public Project Project { get; set; } + } } \ No newline at end of file diff --git a/src/TeamCitySharp/ITeamCityClient.cs b/src/TeamCitySharp/ITeamCityClient.cs index f63f97d3..243ece1d 100644 --- a/src/TeamCitySharp/ITeamCityClient.cs +++ b/src/TeamCitySharp/ITeamCityClient.cs @@ -13,6 +13,7 @@ public interface ITeamCityClient IProjects Projects { get; } IServerInformation ServerInformation { get; } IUsers Users { get; } + IUserGroups UserGroups { get; } IAgents Agents { get; } IVcsRoots VcsRoots { get; } IChanges Changes { get; } diff --git a/src/TeamCitySharp/TeamCityClient.cs b/src/TeamCitySharp/TeamCityClient.cs index eeb9bb82..42c8279a 100644 --- a/src/TeamCitySharp/TeamCityClient.cs +++ b/src/TeamCitySharp/TeamCityClient.cs @@ -11,11 +11,12 @@ public class TeamCityClient : IClientConnection, ITeamCityClient private IBuildConfigs _buildConfigs; private IServerInformation _serverInformation; private IUsers _users; + private IUserGroups _userGroups; private IAgents _agents; private IVcsRoots _vcsRoots; private IChanges _changes; - private IBuildArtifacts _artifacts; - + private IBuildArtifacts _artifacts; + public TeamCityClient(string hostName, bool useSsl = false) { _caller = new TeamCityCaller(hostName, useSsl); @@ -61,6 +62,11 @@ public IUsers Users get { return _users ?? (_users = new Users(_caller)); } } + public IUserGroups UserGroups + { + get { return _userGroups ?? (_userGroups = new UserGroups(_caller)); } + } + public IAgents Agents { get { return _agents ?? (_agents = new Agents(_caller)); } diff --git a/src/TeamCitySharp/TeamCitySharp.csproj b/src/TeamCitySharp/TeamCitySharp.csproj index 2e7df5aa..c5687f98 100644 --- a/src/TeamCitySharp/TeamCitySharp.csproj +++ b/src/TeamCitySharp/TeamCitySharp.csproj @@ -23,6 +23,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -31,6 +32,7 @@ TRACE prompt 4 + false @@ -42,6 +44,7 @@ + @@ -62,10 +65,12 @@ + + @@ -120,6 +125,9 @@ + + + diff --git a/src/TeamCitySharp/Util/ArgumentUtil.cs b/src/TeamCitySharp/Util/ArgumentUtil.cs new file mode 100644 index 00000000..3304cf9e --- /dev/null +++ b/src/TeamCitySharp/Util/ArgumentUtil.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace TeamCitySharp.Util +{ + using System.Linq.Expressions; + + internal static class ArgumentUtil + { + public static void CheckNotNull(params Expression>[] memberExpressions) + { + foreach (var expression in memberExpressions) + { + CheckNotNull(expression); + } + } + + public static void CheckNotNull(Expression> memberExpression) + { + if (memberExpression.Compile().Invoke() == null) + { + throw new ArgumentNullException(ReflectionUtil.GetMemberName(memberExpression)); + } + } + } +} diff --git a/src/TeamCitySharp/Util/ReflectionUtil.cs b/src/TeamCitySharp/Util/ReflectionUtil.cs new file mode 100644 index 00000000..153e9db7 --- /dev/null +++ b/src/TeamCitySharp/Util/ReflectionUtil.cs @@ -0,0 +1,14 @@ +using System; +using System.Linq.Expressions; + +namespace TeamCitySharp.Util +{ + internal static class ReflectionUtil + { + public static string GetMemberName(Expression> memberExpression) + { + var expressionBody = (MemberExpression)memberExpression.Body; + return expressionBody.Member.Name; + } + } +} diff --git a/src/TeamCitySharp/Util/XmlUtil.cs b/src/TeamCitySharp/Util/XmlUtil.cs new file mode 100644 index 00000000..3e10b1d2 --- /dev/null +++ b/src/TeamCitySharp/Util/XmlUtil.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace TeamCitySharp.Util +{ + using System.IO; + using System.Xml; + + internal class XmlUtil + { + public static string SingleElementDocument(string elementName, IDictionary attributes) + { + var stringWriter = new StringWriter(); + + using (var writer = new XmlTextWriter(stringWriter)) + { + writer.WriteStartElement(elementName); + + foreach (var attribute in attributes) + { + if (string.IsNullOrEmpty(attribute.Value)) + { + throw new ArgumentNullException(attribute.Key); + } + + writer.WriteAttributeString(attribute.Key, attribute.Value); + } + + writer.WriteEndElement(); + } + + return stringWriter.ToString(); + } + } +} diff --git a/src/Tests/IntegrationTests/SampleProjectUsage.cs b/src/Tests/IntegrationTests/SampleProjectUsage.cs index 5bcd7bd4..bcd48b06 100644 --- a/src/Tests/IntegrationTests/SampleProjectUsage.cs +++ b/src/Tests/IntegrationTests/SampleProjectUsage.cs @@ -96,6 +96,23 @@ public void it_returns_project_details_when_creating_project() Assert.That(project, Is.Not.Null); Assert.That(project.Name, Is.EqualTo(projectName)); + } + + + + [Test] + public void it_returns_project_details_when_creating_project_with_project_id() + { + var client = new TeamCityClient("localhost:81"); + client.Connect("admin", "qwerty"); + var projectName = Guid.NewGuid().ToString("N"); + var projectId = Guid.NewGuid().ToString("N"); + + var project = client.Projects.Create(projectName, projectId); + + Assert.That(project, Is.Not.Null); + Assert.That(project.Name, Is.EqualTo(projectName)); + Assert.That(project.Id, Is.EqualTo(projectId)); } } } diff --git a/src/Tests/IntegrationTests/SampleVcsUsage.cs b/src/Tests/IntegrationTests/SampleVcsUsage.cs index 9be8c268..4fc7d7e2 100644 --- a/src/Tests/IntegrationTests/SampleVcsUsage.cs +++ b/src/Tests/IntegrationTests/SampleVcsUsage.cs @@ -2,7 +2,9 @@ using System.Net; using NUnit.Framework; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Xml.Serialization; using TeamCitySharp.DomainEntities; namespace TeamCitySharp.IntegrationTests @@ -66,5 +68,24 @@ public void it_returns_vcs_details_when_passing_vcs_root_id(string vcsRootId) Assert.That(rootDetails != null, "Cannot find the specific VCSRoot"); } + + [TestCase("1")] + public void it_serializes_vcs_root_to_xml(string vcsRootId) + { + VcsRoot rootDetails = _client.VcsRoots.ById(vcsRootId); + + + rootDetails.Id += "_new_kii"; + rootDetails.Name+= "_new_kii"; + + var vcsRoot = _client.VcsRoots.Create(rootDetails); + + Assert.NotNull(vcsRootId); + + Assert.Equals(vcsRoot.Id, vcsRootId); + + } + + } } \ No newline at end of file