PageRenderTime 50ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/src/UnitTests/GitHub.App/Models/ModelServiceTests.cs

https://gitlab.com/github-cloud-corporation/VisualStudio
C# | 573 lines | 473 code | 91 blank | 9 comment | 8 complexity | 49f9e835bd22fa4db11305b3bd2fd3a7 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Net;
  5. using System.Reactive;
  6. using System.Reactive.Linq;
  7. using System.Threading.Tasks;
  8. using Akavache;
  9. using GitHub.Api;
  10. using GitHub.Caches;
  11. using GitHub.Services;
  12. using NSubstitute;
  13. using Octokit;
  14. using Xunit;
  15. using System.Globalization;
  16. using System.Reactive.Subjects;
  17. using System.Threading;
  18. using GitHub.Models;
  19. using GitHub.Primitives;
  20. using GitHub.Collections;
  21. using ReactiveUI;
  22. using static GitHub.Services.ModelService;
  23. public class ModelServiceTests
  24. {
  25. public class TheGetUserFromCacheMethod : TestBaseClass
  26. {
  27. [Fact]
  28. public async Task RetrievesUserFromCache()
  29. {
  30. var apiClient = Substitute.For<IApiClient>();
  31. var cache = new InMemoryBlobCache();
  32. await cache.InsertObject<AccountCacheItem>("user", new AccountCacheItem(CreateOctokitUser("octocat")));
  33. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  34. var user = await modelService.GetUserFromCache();
  35. Assert.Equal("octocat", user.Login);
  36. }
  37. }
  38. public class TheInsertUserMethod : TestBaseClass
  39. {
  40. [Fact]
  41. public async Task AddsUserToCache()
  42. {
  43. var apiClient = Substitute.For<IApiClient>();
  44. var cache = new InMemoryBlobCache();
  45. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  46. var user = await modelService.InsertUser(new AccountCacheItem(CreateOctokitUser("octocat")));
  47. var cached = await cache.GetObject<AccountCacheItem>("user");
  48. Assert.Equal("octocat", cached.Login);
  49. }
  50. }
  51. public class TheGetGitIgnoreTemplatesMethod : TestBaseClass
  52. {
  53. [Fact]
  54. public async Task CanRetrieveAndCacheGitIgnores()
  55. {
  56. var data = new[] { "dotnet", "peanuts", "bloomcounty" };
  57. var apiClient = Substitute.For<IApiClient>();
  58. apiClient.GetGitIgnoreTemplates().Returns(data.ToObservable());
  59. var cache = new InMemoryBlobCache();
  60. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  61. var fetched = await modelService.GetGitIgnoreTemplates().ToList();
  62. Assert.Equal(3, fetched.Count);
  63. for (int i = 0; i < data.Length; i++)
  64. Assert.Equal(data[i], fetched[i].Name);
  65. var indexKey = CacheIndex.GitIgnoresPrefix;
  66. var cached = await cache.GetObject<CacheIndex>(indexKey);
  67. Assert.Equal(3, cached.Keys.Count);
  68. var items = await cache.GetObjects<GitIgnoreCacheItem>(cached.Keys).Take(1);
  69. for (int i = 0; i < data.Length; i++)
  70. Assert.Equal(data[i], items[indexKey + "|" + data[i]].Name);
  71. }
  72. }
  73. public class TheGetLicensesMethod : TestBaseClass
  74. {
  75. [Fact]
  76. public async Task CanRetrieveAndCacheLicenses()
  77. {
  78. var data = new[]
  79. {
  80. new LicenseMetadata("mit", "MIT", new Uri("https://github.com/")),
  81. new LicenseMetadata("apache", "Apache", new Uri("https://github.com/"))
  82. };
  83. var apiClient = Substitute.For<IApiClient>();
  84. apiClient.GetLicenses().Returns(data.ToObservable());
  85. var cache = new InMemoryBlobCache();
  86. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  87. var fetched = await modelService.GetLicenses().ToList();
  88. Assert.Equal(2, fetched.Count);
  89. for (int i = 0; i < data.Length; i++)
  90. Assert.Equal(data[i].Name, fetched[i].Name);
  91. var indexKey = CacheIndex.LicensesPrefix;
  92. var cached = await cache.GetObject<CacheIndex>(indexKey);
  93. Assert.Equal(2, cached.Keys.Count);
  94. var items = await cache.GetObjects<LicenseCacheItem>(cached.Keys).Take(1);
  95. for (int i = 0; i < data.Length; i++)
  96. Assert.Equal(data[i].Name, items[indexKey + "|" + data[i].Key].Name);
  97. }
  98. [Fact]
  99. public async Task ReturnsEmptyIfLicenseApiNotFound()
  100. {
  101. var apiClient = Substitute.For<IApiClient>();
  102. apiClient.GetLicenses()
  103. .Returns(Observable.Throw<LicenseMetadata>(new NotFoundException("Not Found", HttpStatusCode.NotFound)));
  104. var cache = new InMemoryBlobCache();
  105. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  106. var fetched = await modelService.GetLicenses().ToList();
  107. Assert.Equal(0, fetched.Count);
  108. }
  109. [Fact]
  110. public async Task ReturnsEmptyIfCacheReadFails()
  111. {
  112. var apiClient = Substitute.For<IApiClient>();
  113. var cache = Substitute.For<IBlobCache>();
  114. cache.Get(Args.String)
  115. .Returns(Observable.Throw<byte[]>(new InvalidOperationException("Unknown")));
  116. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  117. var fetched = await modelService.GetLicenses().ToList();
  118. Assert.Equal(0, fetched.Count);
  119. }
  120. }
  121. public class TheGetAccountsMethod : TestBaseClass
  122. {
  123. [Fact]
  124. public async Task CanRetrieveAndCacheUserAndAccounts()
  125. {
  126. var orgs = new[]
  127. {
  128. CreateOctokitOrganization("github"),
  129. CreateOctokitOrganization("fake")
  130. };
  131. var apiClient = Substitute.For<IApiClient>();
  132. apiClient.GetUser().Returns(Observable.Return(CreateUserAndScopes("snoopy")));
  133. apiClient.GetOrganizations().Returns(orgs.ToObservable());
  134. var cache = new InMemoryBlobCache();
  135. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  136. await modelService.InsertUser(new AccountCacheItem { Login = "snoopy" });
  137. var fetched = await modelService.GetAccounts();
  138. Assert.Equal(3, fetched.Count);
  139. Assert.Equal("snoopy", fetched[0].Login);
  140. Assert.Equal("github", fetched[1].Login);
  141. Assert.Equal("fake", fetched[2].Login);
  142. var cachedOrgs = await cache.GetObject<IReadOnlyList<AccountCacheItem>>("snoopy|orgs");
  143. Assert.Equal(2, cachedOrgs.Count);
  144. Assert.Equal("github", cachedOrgs[0].Login);
  145. Assert.Equal("fake", cachedOrgs[1].Login);
  146. var cachedUser = await cache.GetObject<AccountCacheItem>("user");
  147. Assert.Equal("snoopy", cachedUser.Login);
  148. }
  149. [Fact]
  150. public async Task CanRetrieveUserFromCacheAndAccountsFromApi()
  151. {
  152. var orgs = new[]
  153. {
  154. CreateOctokitOrganization("github"),
  155. CreateOctokitOrganization("fake")
  156. };
  157. var apiClient = Substitute.For<IApiClient>();
  158. apiClient.GetOrganizations().Returns(orgs.ToObservable());
  159. var cache = new InMemoryBlobCache();
  160. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  161. await modelService.InsertUser(new AccountCacheItem(CreateOctokitUser("octocat")));
  162. var fetched = await modelService.GetAccounts();
  163. Assert.Equal(3, fetched.Count);
  164. Assert.Equal("octocat", fetched[0].Login);
  165. Assert.Equal("github", fetched[1].Login);
  166. Assert.Equal("fake", fetched[2].Login);
  167. var cachedOrgs = await cache.GetObject<IReadOnlyList<AccountCacheItem>>("octocat|orgs");
  168. Assert.Equal(2, cachedOrgs.Count);
  169. Assert.Equal("github", cachedOrgs[0].Login);
  170. Assert.Equal("fake", cachedOrgs[1].Login);
  171. var cachedUser = await cache.GetObject<AccountCacheItem>("user");
  172. Assert.Equal("octocat", cachedUser.Login);
  173. }
  174. [Fact]
  175. public async Task OnlyRetrievesOneUserEvenIfCacheOrApiReturnsMoreThanOne()
  176. {
  177. // This should be impossible, but let's pretend it does happen.
  178. var users = new[]
  179. {
  180. CreateUserAndScopes("peppermintpatty"),
  181. CreateUserAndScopes("peppermintpatty")
  182. };
  183. var apiClient = Substitute.For<IApiClient>();
  184. apiClient.GetUser().Returns(users.ToObservable());
  185. apiClient.GetOrganizations().Returns(Observable.Empty<Organization>());
  186. var cache = new InMemoryBlobCache();
  187. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  188. var fetched = await modelService.GetAccounts();
  189. Assert.Equal(1, fetched.Count);
  190. Assert.Equal("peppermintpatty", fetched[0].Login);
  191. }
  192. }
  193. public class TheGetRepositoriesMethod : TestBaseClass
  194. {
  195. [Fact]
  196. public async Task CanRetrieveAndCacheRepositoriesForUserAndOrganizations()
  197. {
  198. var orgs = new[]
  199. {
  200. CreateOctokitOrganization("github"),
  201. CreateOctokitOrganization("octokit")
  202. };
  203. var ownedRepos = new[]
  204. {
  205. CreateRepository("haacked", "seegit"),
  206. CreateRepository("haacked", "codehaacks")
  207. };
  208. var memberRepos = new[]
  209. {
  210. CreateRepository("mojombo", "semver"),
  211. CreateRepository("ninject", "ninject"),
  212. CreateRepository("jabbr", "jabbr"),
  213. CreateRepository("fody", "nullguard")
  214. };
  215. var githubRepos = new[]
  216. {
  217. CreateRepository("github", "visualstudio")
  218. };
  219. var octokitRepos = new[]
  220. {
  221. CreateRepository("octokit", "octokit.net"),
  222. CreateRepository("octokit", "octokit.rb"),
  223. CreateRepository("octokit", "octokit.objc")
  224. };
  225. var apiClient = Substitute.For<IApiClient>();
  226. apiClient.GetOrganizations().Returns(orgs.ToObservable());
  227. apiClient.GetUserRepositories(RepositoryType.Owner).Returns(ownedRepos.ToObservable());
  228. apiClient.GetUserRepositories(RepositoryType.Member).Returns(memberRepos.ToObservable());
  229. apiClient.GetRepositoriesForOrganization("github").Returns(githubRepos.ToObservable());
  230. apiClient.GetRepositoriesForOrganization("octokit").Returns(octokitRepos.ToObservable());
  231. var cache = new InMemoryBlobCache();
  232. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  233. await modelService.InsertUser(new AccountCacheItem { Login = "opus" });
  234. var fetched = await modelService.GetRepositories().ToList();
  235. Assert.Equal(4, fetched.Count);
  236. Assert.Equal(2, fetched[0].Count);
  237. Assert.Equal(4, fetched[1].Count);
  238. Assert.Equal(1, fetched[2].Count);
  239. Assert.Equal(3, fetched[3].Count);
  240. Assert.Equal("seegit", fetched[0][0].Name);
  241. Assert.Equal("codehaacks", fetched[0][1].Name);
  242. Assert.Equal("semver", fetched[1][0].Name);
  243. Assert.Equal("ninject", fetched[1][1].Name);
  244. Assert.Equal("jabbr", fetched[1][2].Name);
  245. Assert.Equal("nullguard", fetched[1][3].Name);
  246. Assert.Equal("visualstudio", fetched[2][0].Name);
  247. Assert.Equal("octokit.net", fetched[3][0].Name);
  248. Assert.Equal("octokit.rb", fetched[3][1].Name);
  249. Assert.Equal("octokit.objc", fetched[3][2].Name);
  250. var cachedOwnerRepositories = await cache.GetObject<IReadOnlyList<ModelService.RepositoryCacheItem>>("opus|Owner:repos");
  251. Assert.Equal(2, cachedOwnerRepositories.Count);
  252. Assert.Equal("seegit", cachedOwnerRepositories[0].Name);
  253. Assert.Equal("haacked", cachedOwnerRepositories[0].Owner.Login);
  254. Assert.Equal("codehaacks", cachedOwnerRepositories[1].Name);
  255. Assert.Equal("haacked", cachedOwnerRepositories[1].Owner.Login);
  256. var cachedMemberRepositories = await cache.GetObject<IReadOnlyList<ModelService.RepositoryCacheItem>>("opus|Member:repos");
  257. Assert.Equal(4, cachedMemberRepositories.Count);
  258. Assert.Equal("semver", cachedMemberRepositories[0].Name);
  259. Assert.Equal("mojombo", cachedMemberRepositories[0].Owner.Login);
  260. Assert.Equal("ninject", cachedMemberRepositories[1].Name);
  261. Assert.Equal("ninject", cachedMemberRepositories[1].Owner.Login);
  262. Assert.Equal("jabbr", cachedMemberRepositories[2].Name);
  263. Assert.Equal("jabbr", cachedMemberRepositories[2].Owner.Login);
  264. Assert.Equal("nullguard", cachedMemberRepositories[3].Name);
  265. Assert.Equal("fody", cachedMemberRepositories[3].Owner.Login);
  266. var cachedGitHubRepositories = await cache.GetObject<IReadOnlyList<ModelService.RepositoryCacheItem>>("opus|github|repos");
  267. Assert.Equal(1, cachedGitHubRepositories.Count);
  268. Assert.Equal("seegit", cachedOwnerRepositories[0].Name);
  269. Assert.Equal("haacked", cachedOwnerRepositories[0].Owner.Login);
  270. Assert.Equal("codehaacks", cachedOwnerRepositories[1].Name);
  271. Assert.Equal("haacked", cachedOwnerRepositories[1].Owner.Login);
  272. var cachedOctokitRepositories = await cache.GetObject<IReadOnlyList<ModelService.RepositoryCacheItem>>("opus|octokit|repos");
  273. Assert.Equal("octokit.net", cachedOctokitRepositories[0].Name);
  274. Assert.Equal("octokit", cachedOctokitRepositories[0].Owner.Login);
  275. Assert.Equal("octokit.rb", cachedOctokitRepositories[1].Name);
  276. Assert.Equal("octokit", cachedOctokitRepositories[1].Owner.Login);
  277. Assert.Equal("octokit.objc", cachedOctokitRepositories[2].Name);
  278. Assert.Equal("octokit", cachedOctokitRepositories[2].Owner.Login);
  279. }
  280. [Fact]
  281. public async Task WhenNotLoggedInReturnsEmptyCollection()
  282. {
  283. var apiClient = Substitute.For<IApiClient>();
  284. var modelService = new ModelService(apiClient, new InMemoryBlobCache(), Substitute.For<IAvatarProvider>());
  285. var repos = await modelService.GetRepositories();
  286. Assert.Equal(0, repos.Count);
  287. }
  288. [Fact]
  289. public async Task WhenLoggedInDoesNotBlowUpOnUnexpectedNetworkProblems()
  290. {
  291. var apiClient = Substitute.For<IApiClient>();
  292. var modelService = new ModelService(apiClient, new InMemoryBlobCache(), Substitute.For<IAvatarProvider>());
  293. apiClient.GetOrganizations()
  294. .Returns(Observable.Throw<Organization>(new NotFoundException("Not Found", HttpStatusCode.NotFound)));
  295. var repos = await modelService.GetRepositories();
  296. Assert.Equal(0, repos.Count);
  297. }
  298. }
  299. public class TheInvalidateAllMethod : TestBaseClass
  300. {
  301. [Fact]
  302. public async Task InvalidatesTheCache()
  303. {
  304. var apiClient = Substitute.For<IApiClient>();
  305. var cache = new InMemoryBlobCache();
  306. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  307. var user = await modelService.InsertUser(new AccountCacheItem(CreateOctokitUser("octocat")));
  308. Assert.Equal(1, (await cache.GetAllObjects<AccountCacheItem>()).Count());
  309. await modelService.InvalidateAll();
  310. Assert.Equal(0, (await cache.GetAllObjects<AccountCacheItem>()).Count());
  311. }
  312. [Fact]
  313. public async Task VaccumsTheCache()
  314. {
  315. var apiClient = Substitute.For<IApiClient>();
  316. var cache = Substitute.For<IBlobCache>();
  317. cache.InvalidateAll().Returns(Observable.Return(Unit.Default));
  318. var received = false;
  319. cache.Vacuum().Returns(x =>
  320. {
  321. received = true;
  322. return Observable.Return(Unit.Default);
  323. });
  324. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  325. await modelService.InvalidateAll();
  326. Assert.True(received);
  327. }
  328. }
  329. public class TheGetPullRequestsMethod : TestBaseClass
  330. {
  331. [Fact(Skip = "Pull requests always refresh from the server now. Migrate this test to data that doesn't require constant refreshing.")]
  332. public async Task NonExpiredIndexReturnsCache()
  333. {
  334. var expected = 5;
  335. var username = "octocat";
  336. var reponame = "repo";
  337. var cache = new InMemoryBlobCache();
  338. var apiClient = Substitute.For<IApiClient>();
  339. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  340. var user = CreateOctokitUser(username);
  341. apiClient.GetUser().Returns(Observable.Return(new UserAndScopes(user, null)));
  342. apiClient.GetOrganizations().Returns(Observable.Empty<Organization>());
  343. var act = modelService.GetAccounts().ToEnumerable().First().First();
  344. var repo = Substitute.For<ISimpleRepositoryModel>();
  345. repo.Name.Returns(reponame);
  346. repo.CloneUrl.Returns(new UriString("https://github.com/" + username + "/" + reponame));
  347. var indexKey = string.Format(CultureInfo.InvariantCulture, "{0}|{1}:{2}", CacheIndex.PRPrefix, user.Login, repo.Name);
  348. var prcache = Enumerable.Range(1, expected)
  349. .Select(id => CreatePullRequest(user, id, ItemState.Open, "Cache " + id, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow));
  350. // seed the cache
  351. prcache
  352. .Select(item => new PullRequestCacheItem(item))
  353. .Select(item => item.Save<PullRequestCacheItem>(cache, indexKey).ToEnumerable().First())
  354. .SelectMany(item => CacheIndex.AddAndSaveToIndex(cache, indexKey, item).ToEnumerable())
  355. .ToList();
  356. var prlive = Observable.Range(1, expected)
  357. .Select(id => CreatePullRequest(user, id, ItemState.Open, "Live " + id, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow))
  358. .DelaySubscription(TimeSpan.FromMilliseconds(10));
  359. apiClient.GetPullRequestsForRepository(user.Login, repo.Name).Returns(prlive);
  360. await modelService.InsertUser(new AccountCacheItem(user));
  361. ITrackingCollection<IPullRequestModel> col = new TrackingCollection<IPullRequestModel>();
  362. modelService.GetPullRequests(repo, col);
  363. col.ProcessingDelay = TimeSpan.Zero;
  364. col.Subscribe();
  365. await col.OriginalCompleted;
  366. Assert.Equal(expected, col.Count);
  367. Assert.Collection(col, col.Select(x => new Action<IPullRequestModel>(t => Assert.True(x.Title.StartsWith("Cache")))).ToArray());
  368. }
  369. [Fact]
  370. public async Task ExpiredIndexReturnsLive()
  371. {
  372. var expected = 5;
  373. var username = "octocat";
  374. var reponame = "repo";
  375. var cache = new InMemoryBlobCache();
  376. var apiClient = Substitute.For<IApiClient>();
  377. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  378. var user = CreateOctokitUser(username);
  379. apiClient.GetUser().Returns(Observable.Return(new UserAndScopes(user, null)));
  380. apiClient.GetOrganizations().Returns(Observable.Empty<Organization>());
  381. var act = modelService.GetAccounts().ToEnumerable().First().First();
  382. var repo = Substitute.For<ISimpleRepositoryModel>();
  383. repo.Name.Returns(reponame);
  384. repo.CloneUrl.Returns(new UriString("https://github.com/" + username + "/" + reponame));
  385. var indexKey = string.Format(CultureInfo.InvariantCulture, "{0}|{1}:{2}", CacheIndex.PRPrefix, user.Login, repo.Name);
  386. var prcache = Enumerable.Range(1, expected)
  387. .Select(id => CreatePullRequest(user, id, ItemState.Open, "Cache " + id, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow));
  388. // seed the cache
  389. prcache
  390. .Select(item => new ModelService.PullRequestCacheItem(item))
  391. .Select(item => item.Save<ModelService.PullRequestCacheItem>(cache, indexKey).ToEnumerable().First())
  392. .SelectMany(item => CacheIndex.AddAndSaveToIndex(cache, indexKey, item).ToEnumerable())
  393. .ToList();
  394. // expire the index
  395. var indexobj = await cache.GetObject<CacheIndex>(indexKey);
  396. indexobj.UpdatedAt = DateTimeOffset.UtcNow - TimeSpan.FromMinutes(6);
  397. await cache.InsertObject(indexKey, indexobj);
  398. var prlive = Observable.Range(1, expected)
  399. .Select(id => CreatePullRequest(user, id, ItemState.Open, "Live " + id, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow))
  400. .DelaySubscription(TimeSpan.FromMilliseconds(10));
  401. apiClient.GetPullRequestsForRepository(user.Login, repo.Name).Returns(prlive);
  402. await modelService.InsertUser(new AccountCacheItem(user));
  403. ITrackingCollection<IPullRequestModel> col = new TrackingCollection<IPullRequestModel>();
  404. modelService.GetPullRequests(repo, col);
  405. col.ProcessingDelay = TimeSpan.Zero;
  406. var count = 0;
  407. var done = new ReplaySubject<Unit>();
  408. done.OnNext(Unit.Default);
  409. done.Subscribe();
  410. col.Subscribe(t =>
  411. {
  412. if (++count == expected * 2)
  413. {
  414. done.OnCompleted();
  415. }
  416. }, () => { });
  417. await done;
  418. Assert.Collection(col, col.Select(x => new Action<IPullRequestModel>(t => Assert.True(x.Title.StartsWith("Live")))).ToArray());
  419. }
  420. [Fact]
  421. public async Task ExpiredIndexClearsItems()
  422. {
  423. var expected = 5;
  424. var username = "octocat";
  425. var reponame = "repo";
  426. var cache = new InMemoryBlobCache();
  427. var apiClient = Substitute.For<IApiClient>();
  428. var modelService = new ModelService(apiClient, cache, Substitute.For<IAvatarProvider>());
  429. var user = CreateOctokitUser(username);
  430. apiClient.GetUser().Returns(Observable.Return(new UserAndScopes(user, null)));
  431. apiClient.GetOrganizations().Returns(Observable.Empty<Organization>());
  432. var act = modelService.GetAccounts().ToEnumerable().First().First();
  433. var repo = Substitute.For<ISimpleRepositoryModel>();
  434. repo.Name.Returns(reponame);
  435. repo.CloneUrl.Returns(new UriString("https://github.com/" + username + "/" + reponame));
  436. var indexKey = string.Format(CultureInfo.InvariantCulture, "{0}|{1}:{2}", CacheIndex.PRPrefix, user.Login, repo.Name);
  437. var prcache = Enumerable.Range(1, expected)
  438. .Select(id => CreatePullRequest(user, id, ItemState.Open, "Cache " + id, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow));
  439. // seed the cache
  440. prcache
  441. .Select(item => new ModelService.PullRequestCacheItem(item))
  442. .Select(item => item.Save<ModelService.PullRequestCacheItem>(cache, indexKey).ToEnumerable().First())
  443. .SelectMany(item => CacheIndex.AddAndSaveToIndex(cache, indexKey, item).ToEnumerable())
  444. .ToList();
  445. // expire the index
  446. var indexobj = await cache.GetObject<CacheIndex>(indexKey);
  447. indexobj.UpdatedAt = DateTimeOffset.UtcNow - TimeSpan.FromMinutes(6);
  448. await cache.InsertObject(indexKey, indexobj);
  449. var prlive = Observable.Range(5, expected)
  450. .Select(id => CreatePullRequest(user, id, ItemState.Open, "Live " + id, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, 0))
  451. .DelaySubscription(TimeSpan.FromMilliseconds(10));
  452. apiClient.GetPullRequestsForRepository(user.Login, repo.Name).Returns(prlive);
  453. await modelService.InsertUser(new AccountCacheItem(user));
  454. ITrackingCollection<IPullRequestModel> col = new TrackingCollection<IPullRequestModel>();
  455. modelService.GetPullRequests(repo, col);
  456. col.ProcessingDelay = TimeSpan.Zero;
  457. var count = 0;
  458. var done = new ReplaySubject<Unit>();
  459. done.OnNext(Unit.Default);
  460. done.Subscribe();
  461. col.Subscribe(t =>
  462. {
  463. // we get all the items from the cache (items 1-5), all the items from the live (items 5-9),
  464. // and 4 deletions (items 1-4) because the cache expired the items that were not
  465. // a part of the live data
  466. if (++count == 14)
  467. {
  468. done.OnCompleted();
  469. }
  470. }, () => { });
  471. await done;
  472. Assert.Equal(5, col.Count);
  473. Assert.Collection(col,
  474. t => { Assert.True(t.Title.StartsWith("Live")); Assert.Equal(5, t.Number); },
  475. t => { Assert.True(t.Title.StartsWith("Live")); Assert.Equal(6, t.Number); },
  476. t => { Assert.True(t.Title.StartsWith("Live")); Assert.Equal(7, t.Number); },
  477. t => { Assert.True(t.Title.StartsWith("Live")); Assert.Equal(8, t.Number); },
  478. t => { Assert.True(t.Title.StartsWith("Live")); Assert.Equal(9, t.Number); }
  479. );
  480. }
  481. }
  482. }