diff --git a/FluentTc.Tests/AcceptanceTests.cs b/FluentTc.Tests/AcceptanceTests.cs index 0117933..8a92e99 100644 --- a/FluentTc.Tests/AcceptanceTests.cs +++ b/FluentTc.Tests/AcceptanceTests.cs @@ -1194,7 +1194,43 @@ public void GetAllProjects() // Assert A.CallTo(() => teamCityCaller.Get(@"/app/rest/projects/")).MustHaveHappened(); - } + } + + [TestCase("FluentTC")] + [TestCase("Test 1")] + public void GetProject_ByName(string projectName) + { + // Arrange + var teamCityCaller = CreateTeamCityCaller(); + A.CallTo(() => teamCityCaller.GetFormat(A._, A._)) + .Returns(new ProjectWrapper { Count = "1", Project = new List { new Project{Name = projectName} } }); + //Act + var result = new RemoteTc().Connect(_ => _.AsGuest(), teamCityCaller) + .GetProject(project => project.Name(projectName)); + + // Assert + result.Should().NotBeNull(); + result.Name.Should().Be(projectName); + } + + [TestCase("1234")] + [TestCase("ABcd")] + [TestCase("129@$$jd")] + public void GetProject_ById(string projectId) + { + // Arrange + var teamCityCaller = CreateTeamCityCaller(); + A.CallTo(() => teamCityCaller.GetFormat(A._, A._)) + .Returns(new ProjectWrapper { Count = "1", Project = new List { new Project{Id = projectId}} }); + + //Act + var result = new RemoteTc().Connect(_ => _.AsGuest(), teamCityCaller) + .GetProject(project => project.Name(projectId)); + + // Assert + result.Should().NotBeNull(); + result.Id.Should().Be(projectId); + } [Test] public void GetAllBuildConfigurationTemplates() diff --git a/FluentTc.Tests/FluentTc.Tests.csproj b/FluentTc.Tests/FluentTc.Tests.csproj index 6411b37..66e4b83 100644 --- a/FluentTc.Tests/FluentTc.Tests.csproj +++ b/FluentTc.Tests/FluentTc.Tests.csproj @@ -137,6 +137,7 @@ + diff --git a/FluentTc.Tests/ProjectRetrievalTests.cs b/FluentTc.Tests/ProjectRetrievalTests.cs new file mode 100644 index 0000000..9dd67c8 --- /dev/null +++ b/FluentTc.Tests/ProjectRetrievalTests.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using FakeItEasy; +using FluentAssertions; +using FluentTc.Domain; +using FluentTc.Engine; +using FluentTc.Exceptions; +using FluentTc.Locators; +using NUnit.Framework; + +namespace FluentTc.Tests +{ + [TestFixture] + public class ProjectRetrievalTests + { + [TestCase("FluentTC")] + [TestCase("Test 1")] + [TestCase("Test!@#$%^&*()")] + public void GetProject_ById(string projectId) + { + // Arrange + var teamCityCaller = A.Fake(); + A.CallTo( + () => + teamCityCaller.GetFormat("/app/rest/projects/{0}", + string.Format("id:{0}", projectId))) + .Returns(new ProjectWrapper + { + Project = new List { new Project { Id = projectId, Name = "Test 1" } }, + Count = "1" + }); + + var projectsRetriever = new ProjectsRetriever(new BuildProjectHavingBuilderFactory(), teamCityCaller); + + // Act + var result = projectsRetriever.GetProject(project => project.Id(projectId)); + + // Assert + result.Should().NotBeNull(); + result.Name.Should().Be("Test 1"); + result.Id.Should().Be(projectId); + } + + [TestCase("1234")] + [TestCase("ABcd")] + [TestCase("129@$$jd")] + public void GetProject_ByName(string projectName) + { + // Arrange + var teamCityCaller = A.Fake(); + A.CallTo( + () => + teamCityCaller.GetFormat("/app/rest/projects/{0}", + string.Format("name:{0}", projectName))) + .Returns(new ProjectWrapper + { + Project = new List { new Project { Id = "Project1", Name = projectName } }, + Count = "1" + }); + + var projectsRetriever = new ProjectsRetriever(new BuildProjectHavingBuilderFactory(), teamCityCaller); + + // Act + var result = projectsRetriever.GetProject(project => project.Name(projectName)); + + // Assert + result.Should().NotBeNull(); + result.Name.Should().Be(projectName); + result.Id.Should().Be("Project1"); + } + + [Test] + public void GetProject_MultipleProjects_MoreThanOneProjectFoundExceptionThrown() + { + // Arrange + var teamCityCaller = A.Fake(); + A.CallTo( + () => + teamCityCaller.GetFormat("/app/rest/projects/{0}", "name:exception")) + .Returns(new ProjectWrapper { Project = new List{new Project(), new Project()}, Count = "2" }); + + var projectsRetriever = new ProjectsRetriever(new BuildProjectHavingBuilderFactory(), teamCityCaller); + + // Act + Action action = () => projectsRetriever.GetProject(project => project.Name("exception")); + + // Assert + action.ShouldThrow(); + } + + [Test] + public void GetProject_NoProjects_ProjectNotFoundException() + { + // Arrange + var teamCityCaller = A.Fake(); + A.CallTo( + () => + teamCityCaller.GetFormat( + "/app/rest/projects?locator={0}", + A.That.IsSameSequenceAs(new[] { "enabled:False" }))) + .Returns(new ProjectWrapper { Project = new List(), Count = "0" }); + + var projectsRetriever = new ProjectsRetriever(new BuildProjectHavingBuilderFactory(), teamCityCaller); + + // Act + Action action = () => projectsRetriever.GetProject(project => project.Name("exception")); + + // Assert + action.ShouldThrow(); + } + } +} diff --git a/FluentTc/ConnectedTc.cs b/FluentTc/ConnectedTc.cs index c8a001c..1c0e03e 100644 --- a/FluentTc/ConnectedTc.cs +++ b/FluentTc/ConnectedTc.cs @@ -129,7 +129,18 @@ BuildConfiguration CreateBuildConfiguration(Action h void DisableAgent(Action having); void EnableAgent(Action having); void AttachBuildConfigurationToTemplate(Action having, string buildTemplateId); + /// + /// Retrieves a project that has the projectId as an Id. + /// + /// The expected id for the wanted project. + /// Project Project GetProjectById(string projectId); + /// + /// Retrieves a project that matches having parameter. + /// + /// Retrieve project that matches the criteria + /// Project + Project GetProject(Action having); IList GetBuildConfigurationsRecursively(string projectId); IList GetAllProjects(); IList DownloadArtifacts(int buildId, string destinationPath); @@ -406,6 +417,11 @@ public Project GetProjectById(string projectId) return m_ProjectsRetriever.GetProject(projectId); } + public Project GetProject(Action having) + { + return m_ProjectsRetriever.GetProject(having); + } + public IList GetProjects(Action having) { return m_ProjectsRetriever.GetProjects(having); diff --git a/FluentTc/Domain/ProjectWrapper.cs b/FluentTc/Domain/ProjectWrapper.cs index fb33eed..7b2fae8 100644 --- a/FluentTc/Domain/ProjectWrapper.cs +++ b/FluentTc/Domain/ProjectWrapper.cs @@ -4,6 +4,7 @@ namespace FluentTc.Domain { public class ProjectWrapper { + public string Count { get; set; } public List Project { get; set; } } } \ No newline at end of file diff --git a/FluentTc/Engine/ProjectsRetriever.cs b/FluentTc/Engine/ProjectsRetriever.cs index 861912c..5abd3b2 100644 --- a/FluentTc/Engine/ProjectsRetriever.cs +++ b/FluentTc/Engine/ProjectsRetriever.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using FluentTc.Domain; using FluentTc.Locators; using EasyHttp.Http; +using FluentTc.Exceptions; namespace FluentTc.Engine { @@ -10,15 +12,15 @@ internal interface IProjectsRetriever { IList GetProjects(Action having = null); Project GetProject(string projectId); - + Project GetProject(Action having = null); void SetFields(Action having, Action fields); - } internal class ProjectsRetriever : IProjectsRetriever { private readonly IBuildProjectHavingBuilderFactory m_BuildProjectHavingBuilderFactory; private readonly ITeamCityCaller m_TeamCityCaller; + private const string TeamCityProjectPrefix = "/app/rest/projects"; public ProjectsRetriever(IBuildProjectHavingBuilderFactory buildProjectHavingBuilderFactory, ITeamCityCaller teamCityCaller) @@ -30,19 +32,21 @@ public ProjectsRetriever(IBuildProjectHavingBuilderFactory buildProjectHavingBui public IList GetProjects(Action having = null) { var locator = having == null ? string.Empty : GetLocator(having); - return m_TeamCityCaller.GetFormat("/app/rest/projects/{0}", locator).Project; + var projects = m_TeamCityCaller.GetFormat(GetApiCall("/{0}"), locator).Project; + return projects ?? new List(); } - private string GetLocator(Action having) + public Project GetProject(string projectId) { - var buildProjectHavingBuilder = m_BuildProjectHavingBuilderFactory.CreateBuildProjectHavingBuilder(); - having(buildProjectHavingBuilder); - return buildProjectHavingBuilder.GetLocator(); + return m_TeamCityCaller.GetFormat(GetApiCall("/id:{0}"), projectId); } - public Project GetProject(string projectId) + public Project GetProject(Action having) { - return m_TeamCityCaller.GetFormat("/app/rest/projects/id:{0}", projectId); + var projects = GetProjects(having); + if (!projects.Any()) throw new ProjectNotFoundException(); + if (projects.Count > 1) throw new MoreThanOneProjectFoundException(); + return projects.Single(); } public void SetFields(Action having, Action fields) @@ -57,8 +61,20 @@ public void SetFields(Action having, Action m_TeamCityCaller.PutFormat(f.Value, HttpContentTypes.TextPlain, - "/app/rest/projects/{0}/{1}", projectConfigurationHavingBuilder.GetLocator(), f.Name)); + GetApiCall("/{0}/{1}"), projectConfigurationHavingBuilder.GetLocator(), f.Name)); + } + private string GetLocator(Action having) + { + var buildProjectHavingBuilder = m_BuildProjectHavingBuilderFactory.CreateBuildProjectHavingBuilder(); + having(buildProjectHavingBuilder); + return buildProjectHavingBuilder.GetLocator(); + } + + private string GetApiCall(string appendix) + { + var result = TeamCityProjectPrefix + appendix; + return result; } } } \ No newline at end of file diff --git a/FluentTc/Exceptions/MoreThanOneProjectFoundException.cs b/FluentTc/Exceptions/MoreThanOneProjectFoundException.cs new file mode 100644 index 0000000..308a031 --- /dev/null +++ b/FluentTc/Exceptions/MoreThanOneProjectFoundException.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.Serialization; + +namespace FluentTc.Exceptions +{ + [Serializable] + public class MoreThanOneProjectFoundException : Exception + { + public MoreThanOneProjectFoundException() + { + } + + public MoreThanOneProjectFoundException(string message) : base(message) + { + } + + public MoreThanOneProjectFoundException(string message, Exception innerException) : base(message, innerException) + { + } + + protected MoreThanOneProjectFoundException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/FluentTc/Exceptions/ProjectNotFoundException.cs b/FluentTc/Exceptions/ProjectNotFoundException.cs new file mode 100644 index 0000000..35fc933 --- /dev/null +++ b/FluentTc/Exceptions/ProjectNotFoundException.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.Serialization; + +namespace FluentTc.Exceptions +{ + [Serializable] + public class ProjectNotFoundException : Exception + { + public ProjectNotFoundException() + { + } + + public ProjectNotFoundException(string message) : base(message) + { + } + + public ProjectNotFoundException(string message, Exception innerException) : base(message, innerException) + { + } + + protected ProjectNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/FluentTc/FluentTc.csproj b/FluentTc/FluentTc.csproj index 139516f..d57553e 100644 --- a/FluentTc/FluentTc.csproj +++ b/FluentTc/FluentTc.csproj @@ -73,6 +73,8 @@ + + diff --git a/FluentTc/RemoteTc.cs b/FluentTc/RemoteTc.cs index c1bdc3a..69f1b10 100644 --- a/FluentTc/RemoteTc.cs +++ b/FluentTc/RemoteTc.cs @@ -1,7 +1,5 @@ using System; using System.Linq; -using FluentTc.Domain; -using FluentTc.Engine; using FluentTc.Locators; namespace FluentTc