Skip to content

Commit

Permalink
fixed buggy handling when jira issues disappear. improved unit tests.
Browse files Browse the repository at this point in the history
jira authentication now uses cookies to keep sessionid. this is to avoid login for every jira-rest-call
  • Loading branch information
discostu105 committed Dec 18, 2018
1 parent 9109759 commit 88e701b
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 22 deletions.
6 changes: 5 additions & 1 deletion src/SuperDumpService.Test.Fakes/FakeJiraApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ public class FakeJiraApiService : IJiraApiService {
private readonly object sync = new object();

public void SetFakeJiraIssues(string bundleId, IEnumerable<JiraIssueModel> jiraIssueModels) {
jiraIssueStore[bundleId] = jiraIssueModels;
if (jiraIssueModels == null) {
jiraIssueStore.Remove(bundleId, out var x);
} else {
jiraIssueStore[bundleId] = jiraIssueModels;
}
}

public Task<IEnumerable<JiraIssueModel>> GetBulkIssues(IEnumerable<string> issueKeys) {
Expand Down
14 changes: 12 additions & 2 deletions src/SuperDumpService.Test/JiraIssueRepositoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public async Task TestJiraIssueRepository() {
// population
await jiraIssueStorage.Store("bundle1", new List<JiraIssueModel> { new JiraIssueModel("JRA-1111") });
await jiraIssueStorage.Store("bundle2", new List<JiraIssueModel> { new JiraIssueModel("JRA-2222"), new JiraIssueModel("JRA-3333") });
await jiraIssueStorage.Store("bundle9", new List<JiraIssueModel> { new JiraIssueModel("JRA-9999") });

await bundleRepo.Populate();
await jiraIssueRepository.Populate();
Expand All @@ -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") }); // 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"),
Expand All @@ -102,8 +106,9 @@ public async Task TestJiraIssueRepository() {
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"],
Expand All @@ -112,6 +117,11 @@ public async Task TestJiraIssueRepository() {
Assert.Collection(res["bundle2"],
item => Assert.Equal("JRA-2222", 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<FakeDump> CreateFakeDumps() {
Expand Down
1 change: 1 addition & 0 deletions src/SuperDumpService/JiraIntegrationSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
1 change: 0 additions & 1 deletion src/SuperDumpService/Models/DumpIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
67 changes: 62 additions & 5 deletions src/SuperDumpService/Services/JiraApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,6 +14,26 @@
using SuperDumpService.Models;

namespace SuperDumpService.Services {
/// <summary>
/// Datastructure that Jira returns on authentication
/// </summary>
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";
Expand All @@ -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<SuperDumpSettings> 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<SessionInfo>();
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<IEnumerable<JiraIssueModel>> GetJiraIssues(string bundleId) {
Expand All @@ -49,10 +106,12 @@ private async Task<IEnumerable<JiraIssueModel>> JiraSearch(string queryString) {
query["fields"] = JiraIssueFields;
uriBuilder.Query = query.ToString();

await EnsureAuthentication();
return await HandleResponse(await client.GetAsync(uriBuilder.ToString()));
}

private async Task<IEnumerable<JiraIssueModel>> JiraPostSearch(string queryString) {
private async Task<IEnumerable<JiraIssueModel>> JiraPostSearch(string queryString, int retry = 3) {
await EnsureAuthentication();
return await HandleResponse(await client.PostAsJsonAsync(settings.JiraApiSearchUrl, new {
jql = queryString,
fields = JiraIssueFieldsArray
Expand All @@ -64,11 +123,9 @@ private async Task<IEnumerable<JiraIssueModel>> HandleResponse(HttpResponseMessa
throw new HttpRequestException($"Jira api call {response.RequestMessage.RequestUri} returned status code {response.StatusCode}");
}
IEnumerable<JiraIssueModel> issues = (await response.Content.ReadAsAsync<JiraSearchResultModel>()).Issues;

foreach (JiraIssueModel issue in issues) {
issue.Url = settings.JiraIssueUrl + issue.Key;
}

return issues;
}

Expand Down
32 changes: 19 additions & 13 deletions src/SuperDumpService/Services/JiraIssueRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class JiraIssueRepository {
private readonly IdenticalDumpRepository identicalDumpRepository;
private readonly JiraIntegrationSettings settings;
private readonly ILogger<JiraIssueRepository> logger;
private readonly ConcurrentDictionary<string, IEnumerable<JiraIssueModel>> bundleIssues = new ConcurrentDictionary<string, IEnumerable<JiraIssueModel>>();
private readonly ConcurrentDictionary<string, IList<JiraIssueModel>> bundleIssues = new ConcurrentDictionary<string, IList<JiraIssueModel>>();
public bool IsPopulated { get; private set; } = false;

public JiraIssueRepository(IOptions<SuperDumpSettings> settings,
Expand All @@ -47,7 +47,7 @@ public async Task Populate() {
try {
IEnumerable<JiraIssueModel> 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());
Expand All @@ -61,7 +61,7 @@ public async Task Populate() {
}

public IEnumerable<JiraIssueModel> GetIssues(string bundleId) {
return bundleIssues.GetValueOrDefault(bundleId, Enumerable.Empty<JiraIssueModel>())
return bundleIssues.GetValueOrDefault(bundleId, Enumerable.Empty<JiraIssueModel>().ToList())
.ToList();
}

Expand All @@ -85,7 +85,7 @@ public async Task<IDictionary<string, IEnumerable<JiraIssueModel>>> GetAllIssues
public async Task WipeJiraIssueCache() {
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try {
foreach (KeyValuePair<string, IEnumerable<JiraIssueModel>> item in bundleIssues) {
foreach (KeyValuePair<string, IList<JiraIssueModel>> item in bundleIssues) {
jiraIssueStorage.Wipe(item.Key);
}
bundleIssues.Clear();
Expand All @@ -105,7 +105,7 @@ public async Task RefreshAllIssuesAsync() {
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try {
//Only update bundles with unresolved issues
IEnumerable<KeyValuePair<string, IEnumerable<JiraIssueModel>>> bundlesToRefresh =
IEnumerable<KeyValuePair<string, IList<JiraIssueModel>>> bundlesToRefresh =
bundleIssues.Where(bundle => bundle.Value.Any(issue => issue.GetStatusName() != "Resolved"));

if (!bundlesToRefresh.Any()) {
Expand Down Expand Up @@ -171,7 +171,7 @@ public async Task SearchBundleIssuesAsync(IEnumerable<BundleMetainfo> bundles, b
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try {
IEnumerable<BundleMetainfo> bundlesToSearch = force ? bundles :
bundles.Where(bundle => !bundleIssues.TryGetValue(bundle.BundleId, out IEnumerable<JiraIssueModel> issues) || !issues.Any()); //All bundles without issues
bundles.Where(bundle => !bundleIssues.TryGetValue(bundle.BundleId, out IList<JiraIssueModel> issues) || !issues.Any()); //All bundles without issues

await Task.WhenAll(bundlesToSearch.Select(bundle => SearchBundleAsync(bundle, force)));
} finally {
Expand All @@ -180,26 +180,32 @@ public async Task SearchBundleIssuesAsync(IEnumerable<BundleMetainfo> bundles, b
}

private async Task SearchBundleAsync(BundleMetainfo bundle, bool force) {
IEnumerable<JiraIssueModel> jiraIssues;
IList<JiraIssueModel> jiraIssues;
if (!force && bundle.CustomProperties.TryGetValue(settings.CustomPropertyJiraIssueKey, out string jiraIssue)) {
jiraIssues = new List<JiraIssueModel>() { 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<JiraIssueModel>());
}
}

private async Task SetBundleIssues(IEnumerable<KeyValuePair<string, IEnumerable<JiraIssueModel>>> bundlesToUpdate, IEnumerable<JiraIssueModel> refreshedIssues) {
private async Task SetBundleIssues(IEnumerable<KeyValuePair<string, IList<JiraIssueModel>>> bundlesToUpdate, IEnumerable<JiraIssueModel> 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<Task>();
foreach (KeyValuePair<string, IEnumerable<JiraIssueModel>> bundle in bundlesToUpdate) {
IEnumerable<JiraIssueModel> 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<string, IList<JiraIssueModel>> bundle in bundlesToUpdate) {
if (issueDictionary.ContainsKey(bundle.Key)) {
IList<JiraIssueModel> 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);
Expand Down

0 comments on commit 88e701b

Please sign in to comment.