diff --git a/src/SuperDumpSelector/SuperDumpSelector.csproj b/src/SuperDumpSelector/SuperDumpSelector.csproj index e21b630..2972b7e 100644 --- a/src/SuperDumpSelector/SuperDumpSelector.csproj +++ b/src/SuperDumpSelector/SuperDumpSelector.csproj @@ -95,7 +95,7 @@ 2.3.0 - 1.0.0 + 1.0.2 4.3.1 diff --git a/src/SuperDumpService.Test.Fakes/FakeDumpStorage.cs b/src/SuperDumpService.Test.Fakes/FakeDumpStorage.cs index efc73d2..70cfe43 100644 --- a/src/SuperDumpService.Test.Fakes/FakeDumpStorage.cs +++ b/src/SuperDumpService.Test.Fakes/FakeDumpStorage.cs @@ -33,6 +33,8 @@ public class FakeDumpStorage : IDumpStorage, IBundleStorage { public bool DelaysEnabled { get; set; } + public FakeDumpStorage() : this(Enumerable.Empty()) { } + public FakeDumpStorage(IEnumerable fakeDumps) { this.fakeDumpsDict = new Dictionary(); this.fakeBundlesDict = new Dictionary(); diff --git a/src/SuperDumpService.Test.Fakes/FakeJiraApiService.cs b/src/SuperDumpService.Test.Fakes/FakeJiraApiService.cs index 0591f8a..dc0e6f7 100644 --- a/src/SuperDumpService.Test.Fakes/FakeJiraApiService.cs +++ b/src/SuperDumpService.Test.Fakes/FakeJiraApiService.cs @@ -12,7 +12,11 @@ public class FakeJiraApiService : IJiraApiService { private readonly object sync = new object(); public void SetFakeJiraIssues(string bundleId, IEnumerable jiraIssueModels) { - jiraIssueStore[bundleId] = jiraIssueModels; + if (jiraIssueModels == null) { + jiraIssueStore.Remove(bundleId, out var x); + } else { + jiraIssueStore[bundleId] = jiraIssueModels; + } } public Task> GetBulkIssues(IEnumerable issueKeys) { diff --git a/src/SuperDumpService.Test/JiraIssueRepositoryTest.cs b/src/SuperDumpService.Test/JiraIssueRepositoryTest.cs index f53e94c..8c05487 100644 --- a/src/SuperDumpService.Test/JiraIssueRepositoryTest.cs +++ b/src/SuperDumpService.Test/JiraIssueRepositoryTest.cs @@ -68,6 +68,7 @@ public async Task TestJiraIssueRepository() { // population await jiraIssueStorage.Store("bundle1", new List { new JiraIssueModel("JRA-1111") }); await jiraIssueStorage.Store("bundle2", new List { new JiraIssueModel("JRA-2222"), new JiraIssueModel("JRA-3333") }); + await jiraIssueStorage.Store("bundle9", new List { new JiraIssueModel("JRA-9999") }); await bundleRepo.Populate(); await jiraIssueRepository.Populate(); @@ -81,14 +82,17 @@ public async Task TestJiraIssueRepository() { item => Assert.Equal("JRA-2222", item.Key), item => Assert.Equal("JRA-3333", item.Key)); + Assert.Collection(jiraIssueRepository.GetIssues("bundle9"), + item => Assert.Equal("JRA-9999", item.Key)); + Assert.Empty(jiraIssueRepository.GetIssues("bundle3")); // fake, that in jira some bundles have been referenced in new issues - jiraApiService.SetFakeJiraIssues("bundle1", new JiraIssueModel[] { new JiraIssueModel("JRA-1111") }); - jiraApiService.SetFakeJiraIssues("bundle2", new JiraIssueModel[] { new JiraIssueModel("JRA-2222"), new JiraIssueModel("JRA-3333"), new JiraIssueModel("JRA-4444") }); - jiraApiService.SetFakeJiraIssues("bundle3", new JiraIssueModel[] { new JiraIssueModel("JRA-1111"), new JiraIssueModel("JRA-5555") }); + jiraApiService.SetFakeJiraIssues("bundle1", new JiraIssueModel[] { new JiraIssueModel("JRA-1111") }); // same + jiraApiService.SetFakeJiraIssues("bundle2", new JiraIssueModel[] { new JiraIssueModel("JRA-2222"), new JiraIssueModel("JRA-4444") }); // one added, one removed + jiraApiService.SetFakeJiraIssues("bundle3", new JiraIssueModel[] { new JiraIssueModel("JRA-1111"), new JiraIssueModel("JRA-5555") }); // new + jiraApiService.SetFakeJiraIssues("bundle9", null ); // removed jira issues - // trigger update of repository await jiraIssueRepository.SearchBundleIssuesAsync(bundleRepo.GetAll(), true); Assert.Collection(jiraIssueRepository.GetIssues("bundle1"), @@ -96,15 +100,15 @@ public async Task TestJiraIssueRepository() { Assert.Collection(jiraIssueRepository.GetIssues("bundle2"), item => Assert.Equal("JRA-2222", item.Key), - item => Assert.Equal("JRA-3333", item.Key), item => Assert.Equal("JRA-4444", item.Key)); Assert.Collection(jiraIssueRepository.GetIssues("bundle3"), item => Assert.Equal("JRA-1111", item.Key), item => Assert.Equal("JRA-5555", item.Key)); + Assert.Empty(jiraIssueRepository.GetIssues("bundle9")); - var res = await jiraIssueRepository.GetAllIssuesByBundleIdsWithoutWait(new string[] { "bundle1", "bundle2", "bundle7", "bundle666" }); + var res = await jiraIssueRepository.GetAllIssuesByBundleIdsWithoutWait(new string[] { "bundle1", "bundle2", "bundle7", "bundle666", "bundle9" }); Assert.Equal(2, res.Count()); Assert.Collection(res["bundle1"], @@ -112,8 +116,12 @@ public async Task TestJiraIssueRepository() { Assert.Collection(res["bundle2"], item => Assert.Equal("JRA-2222", item.Key), - item => Assert.Equal("JRA-3333", item.Key), item => Assert.Equal("JRA-4444", item.Key)); + + + Assert.Empty(jiraIssueRepository.GetIssues("bundle7")); + Assert.Empty(jiraIssueRepository.GetIssues("bundle666")); + Assert.Empty(jiraIssueRepository.GetIssues("bundle9")); } private IEnumerable CreateFakeDumps() { diff --git a/src/SuperDumpService/JiraIntegrationSettings.cs b/src/SuperDumpService/JiraIntegrationSettings.cs index b4e83bb..265f8d1 100644 --- a/src/SuperDumpService/JiraIntegrationSettings.cs +++ b/src/SuperDumpService/JiraIntegrationSettings.cs @@ -11,6 +11,7 @@ public class JiraIntegrationSettings { public int JiraBundleSearchLimit { get; set; } public double JiraBundleSearchDelay { get; set; } public TimeSpan JiraBundleSearchTimeSpan { get; set; } + public string JiraApiAuthUrl { get; set; } public string JiraApiSearchUrl { get; set; } public string JiraApiUsername { get; set; } public string JiraApiPassword { get; set; } diff --git a/src/SuperDumpService/Models/DumpIdentifier.cs b/src/SuperDumpService/Models/DumpIdentifier.cs index 6b7b9cc..36057a8 100644 --- a/src/SuperDumpService/Models/DumpIdentifier.cs +++ b/src/SuperDumpService/Models/DumpIdentifier.cs @@ -61,7 +61,6 @@ private sealed class DumpIdentifierPool { public DumpIdentifier Allocate(string bundleId, string dumpId) { int hash = $"{bundleId}:{dumpId}".GetHashCode(); - if (pool.TryGetValue(hash, out DumpIdentifier id)) return id; // fast path lock (sync) { if (pool.TryGetValue(hash, out DumpIdentifier id2)) return id2; var newId = new DumpIdentifier(bundleId, dumpId); diff --git a/src/SuperDumpService/Services/JiraApiService.cs b/src/SuperDumpService/Services/JiraApiService.cs index 950e3d0..f01a415 100644 --- a/src/SuperDumpService/Services/JiraApiService.cs +++ b/src/SuperDumpService/Services/JiraApiService.cs @@ -3,6 +3,7 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Linq; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -13,6 +14,26 @@ using SuperDumpService.Models; namespace SuperDumpService.Services { + /// + /// Datastructure that Jira returns on authentication + /// + public class SessionInfo { + public Session session; + public LoginInfo loginInfo; + + public class Session { + public string name; + public string value; + } + + public class LoginInfo { + public int failedLoginCount; + public int loginCount; + public DateTime lastFailedLoginTime; + public DateTime previousLoginTime; + } + } + public class JiraApiService : IJiraApiService { private const string JsonMediaType = "application/json"; private const string JiraIssueFields = "status,resolution"; @@ -21,13 +42,49 @@ public class JiraApiService : IJiraApiService { private readonly JiraIntegrationSettings settings; private readonly HttpClient client; + public CookieContainer Cookies { + get { return HttpClientHandler.CookieContainer; } + set { HttpClientHandler.CookieContainer = value; } + } + + public HttpClientHandler HttpClientHandler { get; set; } + + public SessionInfo Session { get; set; } + public JiraApiService(IOptions settings) { this.settings = settings.Value.JiraIntegrationSettings; if (this.settings == null) return; - client = new HttpClient(); + + HttpClientHandler = new HttpClientHandler { + AllowAutoRedirect = true, + UseCookies = true, + CookieContainer = new CookieContainer() + }; + + client = new HttpClient(HttpClientHandler); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(JsonMediaType)); - client.DefaultRequestHeaders.Authorization = GetBasicAuthenticationHeader(this.settings.JiraApiUsername, this.settings.JiraApiPassword); + } + + private async Task Authenticate() { + using (var authClient = new HttpClient()) { + var uriBuilder = new UriBuilder(settings.JiraApiAuthUrl); + var response = await authClient.PostAsJsonAsync(settings.JiraApiAuthUrl, new { + username = this.settings.JiraApiUsername, + password = this.settings.JiraApiPassword + }); + var sessionInfo = await response.Content.ReadAsAsync(); + this.Session = sessionInfo; + var cookieDomain = new Uri(new Uri(settings.JiraApiAuthUrl).GetLeftPart(UriPartial.Authority)); + this.Cookies.Add(cookieDomain, new Cookie(sessionInfo.session.name, sessionInfo.session.value)); + } + } + + private async Task EnsureAuthentication() { + // reauthenticate every 10 minutes + if (Session == null || (DateTime.Now - Session.loginInfo.previousLoginTime).Minutes > 10) { + await Authenticate(); + } } public async Task> GetJiraIssues(string bundleId) { @@ -49,10 +106,12 @@ private async Task> JiraSearch(string queryString) { query["fields"] = JiraIssueFields; uriBuilder.Query = query.ToString(); + await EnsureAuthentication(); return await HandleResponse(await client.GetAsync(uriBuilder.ToString())); } - private async Task> JiraPostSearch(string queryString) { + private async Task> JiraPostSearch(string queryString, int retry = 3) { + await EnsureAuthentication(); return await HandleResponse(await client.PostAsJsonAsync(settings.JiraApiSearchUrl, new { jql = queryString, fields = JiraIssueFieldsArray @@ -64,11 +123,9 @@ private async Task> HandleResponse(HttpResponseMessa throw new HttpRequestException($"Jira api call {response.RequestMessage.RequestUri} returned status code {response.StatusCode}"); } IEnumerable issues = (await response.Content.ReadAsAsync()).Issues; - foreach (JiraIssueModel issue in issues) { issue.Url = settings.JiraIssueUrl + issue.Key; } - return issues; } diff --git a/src/SuperDumpService/Services/JiraIssueRepository.cs b/src/SuperDumpService/Services/JiraIssueRepository.cs index e759356..dd76d34 100644 --- a/src/SuperDumpService/Services/JiraIssueRepository.cs +++ b/src/SuperDumpService/Services/JiraIssueRepository.cs @@ -21,7 +21,7 @@ public class JiraIssueRepository { private readonly IdenticalDumpRepository identicalDumpRepository; private readonly JiraIntegrationSettings settings; private readonly ILogger logger; - private readonly ConcurrentDictionary> bundleIssues = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> bundleIssues = new ConcurrentDictionary>(); public bool IsPopulated { get; private set; } = false; public JiraIssueRepository(IOptions settings, @@ -47,7 +47,7 @@ public async Task Populate() { try { IEnumerable jiraIssues = await jiraIssueStorage.Read(bundle.BundleId); if (jiraIssues != null) { - bundleIssues[bundle.BundleId] = jiraIssues; + bundleIssues[bundle.BundleId] = jiraIssues.ToList(); } } catch (Exception e) { logger.LogError("error reading jira-issue file: " + e.ToString()); @@ -61,7 +61,7 @@ public async Task Populate() { } public IEnumerable GetIssues(string bundleId) { - return bundleIssues.GetValueOrDefault(bundleId, Enumerable.Empty()) + return bundleIssues.GetValueOrDefault(bundleId, Enumerable.Empty().ToList()) .ToList(); } @@ -85,7 +85,7 @@ public async Task>> GetAllIssues public async Task WipeJiraIssueCache() { await semaphoreSlim.WaitAsync().ConfigureAwait(false); try { - foreach (KeyValuePair> item in bundleIssues) { + foreach (KeyValuePair> item in bundleIssues) { jiraIssueStorage.Wipe(item.Key); } bundleIssues.Clear(); @@ -105,7 +105,7 @@ public async Task RefreshAllIssuesAsync() { await semaphoreSlim.WaitAsync().ConfigureAwait(false); try { //Only update bundles with unresolved issues - IEnumerable>> bundlesToRefresh = + IEnumerable>> bundlesToRefresh = bundleIssues.Where(bundle => bundle.Value.Any(issue => issue.GetStatusName() != "Resolved")); if (!bundlesToRefresh.Any()) { @@ -171,7 +171,7 @@ public async Task SearchBundleIssuesAsync(IEnumerable bundles, b await semaphoreSlim.WaitAsync().ConfigureAwait(false); try { IEnumerable bundlesToSearch = force ? bundles : - bundles.Where(bundle => !bundleIssues.TryGetValue(bundle.BundleId, out IEnumerable issues) || !issues.Any()); //All bundles without issues + bundles.Where(bundle => !bundleIssues.TryGetValue(bundle.BundleId, out IList issues) || !issues.Any()); //All bundles without issues await Task.WhenAll(bundlesToSearch.Select(bundle => SearchBundleAsync(bundle, force))); } finally { @@ -180,26 +180,32 @@ public async Task SearchBundleIssuesAsync(IEnumerable bundles, b } private async Task SearchBundleAsync(BundleMetainfo bundle, bool force) { - IEnumerable jiraIssues; + IList jiraIssues; if (!force && bundle.CustomProperties.TryGetValue(settings.CustomPropertyJiraIssueKey, out string jiraIssue)) { jiraIssues = new List() { new JiraIssueModel { Key = jiraIssue } }; } else { - jiraIssues = await apiService.GetJiraIssues(bundle.BundleId); + jiraIssues = (await apiService.GetJiraIssues(bundle.BundleId)).ToList(); } if (jiraIssues.Any()) { - await jiraIssueStorage.Store(bundle.BundleId, bundleIssues[bundle.BundleId] = jiraIssues); + bundleIssues[bundle.BundleId] = jiraIssues; + await jiraIssueStorage.Store(bundle.BundleId, jiraIssues); + } else { + bundleIssues.Remove(bundle.BundleId, out var val); + await jiraIssueStorage.Store(bundle.BundleId, Enumerable.Empty()); } } - private async Task SetBundleIssues(IEnumerable>> bundlesToUpdate, IEnumerable refreshedIssues) { + private async Task SetBundleIssues(IEnumerable>> bundlesToUpdate, IEnumerable refreshedIssues) { var issueDictionary = refreshedIssues.ToDictionary(issue => issue.Key, issue => issue); //Select the issues for each bundle and store them in the bundleIssues Dictionary //I am not sure if this is the best way to do this var fileStorageTasks = new List(); - foreach (KeyValuePair> bundle in bundlesToUpdate) { - IEnumerable issues = bundle.Value.Select(issue => issueDictionary[issue.Key]); - fileStorageTasks.Add(jiraIssueStorage.Store(bundle.Key, bundleIssues[bundle.Key] = issues)); //update the issue file for the bundle + foreach (KeyValuePair> bundle in bundlesToUpdate) { + if (issueDictionary.ContainsKey(bundle.Key)) { + IList issues = bundle.Value.Select(issue => issueDictionary[issue.Key]).ToList(); + fileStorageTasks.Add(jiraIssueStorage.Store(bundle.Key, bundleIssues[bundle.Key] = issues)); //update the issue file for the bundle + } } await Task.WhenAll(fileStorageTasks);