diff --git a/client/ts/admin.ts b/client/ts/admin.ts index 50fc044fb00f4291e943948d45496062cf18f3fc..52bba78c13e0b251e97743e9d6f14c5c14883322 100644 --- a/client/ts/admin.ts +++ b/client/ts/admin.ts @@ -6,17 +6,52 @@ import "bootstrap-select" import * as Waves from "Waves" import "bootstrap" -declare global { - interface JQuery { - jsonViewer(any): void; - } -} - $(async () => { $('.selectpicker').selectpicker(); Utility.fixUtcTime(); + setupDiscrepancyViewer(); + document.dispatchEvent(new Event("page-ready")); }); + +/** + * Controls "load more" and "load less" buttons for + * resolved discrepancies list + * + */ +function setupDiscrepancyViewer() : void { + var resolvedLoaded = 10; + + $("#load-more-resolved-btn").click(() => { + + for (var index = resolvedLoaded + 1; index <= resolvedLoaded + 10; index++) { + $(`.discrepancy-card[data-number="${index}"]`).show(); + } + + resolvedLoaded += 10; + + $("#load-less-resolved-btn").show(); + + if (resolvedLoaded >= $(".discrepancy-resolved").length) { + $("#load-more-resolved-btn").hide(); + } + }); + + $("#load-less-resolved-btn").click(() => { + + for (var index = resolvedLoaded; index > resolvedLoaded - 10; index--) { + $(`.discrepancy-card[data-number="${index}"]`).hide(); + } + + resolvedLoaded -= 10; + + $("#load-more-resolved-btn").show(); + + if (resolvedLoaded <= 10) { + $("#load-less-resolved-btn").hide(); + } + }); +} diff --git a/client/ts/modules/utility.ts b/client/ts/modules/utility.ts index 0a320f2935e9c32a734181e7d103782db6f4eb58..44d31ebbbf4a14e07aa3c0a44f1b816110cfa32e 100644 --- a/client/ts/modules/utility.ts +++ b/client/ts/modules/utility.ts @@ -161,16 +161,24 @@ export class Utility { ); }); + $(".utc-date").each(function () { + $(this).text( + Utility.toLocalTimezone( + new Date($(this).text()) + ).toString() + ); + }); + } - public static toUtcDate(date : Date): number { + public static toUtcDate(date: Date): number { return Date.UTC( - date.getUTCFullYear(), - date.getUTCMonth(), - date.getUTCDate(), - date.getUTCHours(), - date.getUTCMinutes(), - date.getUTCSeconds(), + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), date.getUTCMilliseconds() ); } diff --git a/src/shared/Models/Entities/Discrepancies.cs b/src/shared/Models/Entities/Discrepancies.cs index e69692d0f0bdd7c5dfb32e10918a2b837ec9fbfd..c7e079d7fa01972c55cb702c0336f1f9806f0631 100644 --- a/src/shared/Models/Entities/Discrepancies.cs +++ b/src/shared/Models/Entities/Discrepancies.cs @@ -43,6 +43,26 @@ namespace StatusMonitor.Shared.Models.Entities } public override string ToString() => ToStringWithTimeZone(); + + /// <summary> + /// Human readable description of the discrepancy. + /// </summary> + /// <returns>Human readable description of the discrepancy</returns> + public string Description() { + switch (Type) + { + case DiscrepancyType.GapInData: + return "Occurs if difference in timestamps between two consecutive data points exceeds certain value."; + case DiscrepancyType.HighLoad: + return "Occurs if CPU load exceeds certain value for some number of consecutive data points."; + case DiscrepancyType.LowHealth: + return "Occurs if system health value drops below certain value for some number of consecutive health reports."; + case DiscrepancyType.PingFailedNTimes: + return "Occurs if ping fails certain number of consecutive readings."; + default: + return "Unknown type"; + } + } } public enum DiscrepancyType diff --git a/src/shared/Services/NotificationService.cs b/src/shared/Services/NotificationService.cs index 229a82a24c3c150f519a9a67b82cb8902b73591a..88b4b43e0b2300b7ee1479ac41c647f73b93329b 100644 --- a/src/shared/Services/NotificationService.cs +++ b/src/shared/Services/NotificationService.cs @@ -154,7 +154,7 @@ namespace StatusMonitor.Shared.Services return unresolved == 0 ? "There are no outstanding issues. Well done." : - $"There {(unresolved == 1 ? "is" : "are")} still outstanding {unresolved} issue{(unresolved == 1 ? "" : "s")}." + $"There {(unresolved == 1 ? "is" : "are")} still outstanding {unresolved} issue{(unresolved == 1 ? "" : "s")}. See admin panel." ; } diff --git a/src/web/Controllers/View/AdminController.cs b/src/web/Controllers/View/AdminController.cs index a5bf362a3f48b55b50f2ba099c8663d5f86be44d..599c3ef67a4ee614f617289875c773ec636a8980 100644 --- a/src/web/Controllers/View/AdminController.cs +++ b/src/web/Controllers/View/AdminController.cs @@ -33,6 +33,9 @@ namespace StatusMonitor.Web.Controllers.View private readonly IMetricService _metricService; private readonly IServiceProvider _provider; private readonly ICleanService _cleanService; + private readonly IDataContext _context; + private readonly INotificationService _notification; + private readonly IConfiguration _config; public AdminController( @@ -40,7 +43,10 @@ namespace StatusMonitor.Web.Controllers.View ILogger<AdminController> logger, IMetricService metricService, IServiceProvider provider, - ICleanService cleanService + ICleanService cleanService, + IDataContext context, + INotificationService notification, + IConfiguration congig ) { _metricService = metricService; @@ -48,6 +54,9 @@ namespace StatusMonitor.Web.Controllers.View _loggingService = loggingService; _provider = provider; _cleanService = cleanService; + _context = context; + _notification = notification; + _config = congig; } public async Task<IActionResult> Index() @@ -55,6 +64,11 @@ namespace StatusMonitor.Web.Controllers.View ViewBag.Metrics = await _metricService .GetMetricsAsync(); + ViewBag.Discrepancies = await _context + .Discrepancies + .OrderByDescending(d => d.DateFirstOffense) + .ToListAsync(); + return View(); } @@ -76,6 +90,65 @@ namespace StatusMonitor.Web.Controllers.View } } + [HttpPost] + [ServiceFilter(typeof(ModelValidation))] + [AutoValidateAntiforgeryToken] + public async Task<IActionResult> ResolveDiscrepancy(DiscrepancyResolutionViewModel model) + { + if ( + await _context + .Discrepancies + .AnyAsync( + d => + d.DateFirstOffense == model.DateFirstOffense && + d.MetricSource == model.Source && + d.MetricType == model.EnumMetricType && + d.Type == model.EnumDiscrepancyType + ) + ) + { + + var discrepancy = + await _context + .Discrepancies + .SingleAsync( + d => + d.DateFirstOffense == model.DateFirstOffense && + d.MetricSource == model.Source && + d.MetricType == model.EnumMetricType && + d.Type == model.EnumDiscrepancyType + ); + + if (discrepancy.Resolved) + { + TempData["MessageSeverity"] = "warning"; + TempData["MessageContent"] = $"Discrepancy {model.EnumDiscrepancyType} from {model.EnumMetricType} of {model.Source} at {model.DateFirstOffense} (UTC) has been already resolved."; + } + else + { + discrepancy.Resolved = true; + discrepancy.DateResolved = DateTime.UtcNow; + + await _notification.ScheduleNotificationAsync( + $"Discrepancy \"{discrepancy.ToStringWithTimeZone(_config["ServiceManager:NotificationService:TimeZone"])}\" has been resolved!", + NotificationSeverity.Medium + ); + + await _context.SaveChangesAsync(); + + TempData["MessageSeverity"] = "success"; + TempData["MessageContent"] = $"Discrepancy {model.EnumDiscrepancyType} from {model.EnumMetricType} of {model.Source} at {model.DateFirstOffense} (UTC) has been resolved."; + } + + return RedirectToAction("Index", "Admin"); + } + else + { + return NotFound($"Discrepancy for the following request not found. {model}"); + } + + } + public IActionResult Metric([FromQuery] string metric) { if (metric == null) diff --git a/src/web/TagHelpers/UtcTimeTagHelper.cs b/src/web/TagHelpers/UtcTimeTagHelper.cs index f098874e699b9153c0734854535fb732bab287a5..c350d1047aa1118095734590e9183968ac93c352 100644 --- a/src/web/TagHelpers/UtcTimeTagHelper.cs +++ b/src/web/TagHelpers/UtcTimeTagHelper.cs @@ -12,6 +12,12 @@ namespace StatusMonitor.Web.TagHelpers /// </summary> public class UtcTimeTagHelper : TagHelper { + /// <summary> + /// If true, the full date should be rendered + /// Otherwise, only time part should be rendered + /// </summary> + public bool ShowDate { get; set; } = false; + public DateTime Time { get; set; } [ViewContext] @@ -26,7 +32,8 @@ namespace StatusMonitor.Web.TagHelpers output.TagMode = TagMode.StartTagAndEndTag; output.Attributes.Clear(); - output.Attributes.Add("class", "utc-time"); + + output.Attributes.Add("class", ShowDate ? "utc-date" : "utc-time"); output.Content.SetHtmlContent(Time.ToString()); diff --git a/src/web/ViewModels/DiscrepancyResolutionViewModel.cs b/src/web/ViewModels/DiscrepancyResolutionViewModel.cs new file mode 100644 index 0000000000000000000000000000000000000000..189ef559c94427a7abe6b120d337ce49b3f47ffa --- /dev/null +++ b/src/web/ViewModels/DiscrepancyResolutionViewModel.cs @@ -0,0 +1,102 @@ +using System; +using System.ComponentModel.DataAnnotations; +using StatusMonitor.Shared.Extensions; +using StatusMonitor.Shared.Models.Entities; + +namespace StatusMonitor.Web.ViewModels +{ + public class DiscrepancyResolutionViewModel + { + /// <summary> + /// Timestamp when the discrepancy starts. + /// </summary> + public DateTime DateFirstOffense { get; set; } + + /// <summary> + /// Alias for DateFirstOffense. + /// Should represent the number of ticks (eq. DateTime.UtcNow.Ticks) + /// </summary> + [Required] + public string Date + { + get + { + return DateFirstOffense.ToString(); + } + set + { + try + { + DateFirstOffense = new DateTime(Convert.ToInt64(value)); + } + catch (System.Exception) + { + throw new ArgumentException("Invalid Date parameter."); + } + } + } + + public DiscrepancyType EnumDiscrepancyType { get; set; } + + /// <summary> + /// Alias for EnumDiscrepancyType. + /// </summary> + [Required] + public string DiscrepancyType + { + get + { + return EnumDiscrepancyType.ToString(); + } + set + { + try + { + EnumDiscrepancyType = value.ToEnum<DiscrepancyType>(); + } + catch (System.Exception) + { + throw new ArgumentException("Invalid DiscrepancyType parameter."); + } + } + } + + public Metrics EnumMetricType { get; set; } + + /// <summary> + /// Alias for EnumMetricType. + /// </summary> + [Required] + public string MetricType + { + get + { + return EnumMetricType.ToString(); + } + set + { + try + { + EnumMetricType = value.ToEnum<Metrics>(); + } + catch (System.Exception) + { + throw new ArgumentException("Invalid MetricType parameter."); + } + } + } + + /// <summary> + /// Source identifier. May be server id or website URL. + /// </summary> + [Required] + [StringLength(32)] + [RegularExpression("[a-z0-9\\.\\-]+")] + public string Source { get; set; } + + public override string ToString() + { + return $"Discrepancy removal model: type {DiscrepancyType} from {MetricType} of {Source} at {DateFirstOffense}."; + } + } +} diff --git a/src/web/Views/Admin/Index.cshtml b/src/web/Views/Admin/Index.cshtml index a01a95b2b3a899954dbbde05ce2691325decef4d..25dc3ba382f27f72f72fc7a98ef2b28e2ffacd45 100644 --- a/src/web/Views/Admin/Index.cshtml +++ b/src/web/Views/Admin/Index.cshtml @@ -1,4 +1,7 @@ @using StatusMonitor.Shared.Models.Entities +@using Microsoft.Extensions.Configuration + +@inject IConfiguration Config @{ ViewData["Title"] = "Admin panel"; @@ -11,91 +14,214 @@ </div> <div class="row"> - <div class="col-md-3"> - <div class="card"> - <div class="card-header"> - <h2> - Manual cleaunup - <small> - Run clean service manually with a specified max age - </small> - </h2> - </div> - <div class="card-body card-padding"> - <div class="row"> - <form - role="form" - asp-controller="Admin" - asp-action="Clean" - method="post" - novalidate - > - <div class="col-md-8 col-sm-12"> - - <h5>Set max age</h5> - - <select class="selectpicker" name="maxAge"> - <option value="0">Everything</option> - <option value="1">1 minutes</option> - <option value="10">10 minutes</option> - <option value="20">30 minutes</option> - <option value="60">1 hour</option> - <option value="240">4 hours</option> - <option value="720">12 hours</option> - <option value="1440">1 day</option> - <option value="4320">3 days</option> - <option value="10080">1 week</option> - <option value="43200">1 month</option> - </select> + + <div class="col-md-6"> + + <div class="row"> + + <div class="col-md-6"> + <div class="card"> + <div class="card-header"> + <h2> + Manual cleaunup + <small> + Run clean service manually with a specified max age + </small> + </h2> + </div> + <div class="card-body card-padding"> + <div class="row"> + <form + role="form" + asp-controller="Admin" + asp-action="Clean" + method="post" + novalidate + > + <div class="col-md-8 col-sm-12"> + + <h5>Set max age</h5> + + <select class="selectpicker" name="maxAge"> + <option value="0">Everything</option> + <option value="1">1 minutes</option> + <option value="10">10 minutes</option> + <option value="20">30 minutes</option> + <option value="60">1 hour</option> + <option value="240">4 hours</option> + <option value="720">12 hours</option> + <option value="1440">1 day</option> + <option value="4320">3 days</option> + <option value="10080">1 week</option> + <option value="43200">1 month</option> + </select> + </div> + <div class="col-md-4 col-sm-12"> + <button type="submit" class="btn btn-danger btn-md waves-effect">Clean</button> + </div> + </form> </div> - <div class="col-md-4 col-sm-12"> - <button type="submit" class="btn btn-danger btn-md waves-effect">Clean</button> + </div> + </div> + </div> + + <div class="col-md-6"> + <div class="card"> + <div class="card-header"> + <h2> + Go to metric + <small> + Go to the metric's page + </small> + </h2> + </div> + <div class="card-body card-padding"> + <div class="row"> + <form + role="form" + asp-controller="Admin" + asp-action="Metric" + method="get" + novalidate + > + <div class="col-md-8 col-sm-12"> + + <h5>Metric</h5> + + <select class="selectpicker" name="metric" data-live-search="true"> + @foreach (var metric in ViewBag.Metrics) + { + <option value="@((Metrics)metric.Type)@@@(metric.Source)">@(metric.Title) from @(metric.Source)</option> + } + </select> + </div> + <div class="col-md-4 col-sm-12"> + <button type="submit" class="btn btn-primary btn-md waves-effect">Go</button> + </div> + </form> </div> - </form> + </div> </div> </div> + </div> - </div> - <div class="col-md-3"> - <div class="card"> - <div class="card-header"> - <h2> - Go to metric - <small> - Go to the metric's page - </small> - </h2> - </div> - <div class="card-body card-padding"> - <div class="row"> - <form - role="form" - asp-controller="Admin" - asp-action="Metric" - method="get" - novalidate - > - <div class="col-md-8 col-sm-12"> - - <h5>Metric</h5> - - <select class="selectpicker" name="metric" data-live-search="true"> - @foreach (var metric in ViewBag.Metrics) - { - <option value="@((Metrics)metric.Type)@@@(metric.Source)">@(metric.Title) from @(metric.Source)</option> + <div class="row"> + + <div class="col-md-12"> + + <div class="card"> + <div class="card-header"> + <h2>Discrepancies + <small> + @{ + var discrepancyDataStartDate = DateTime.UtcNow - new TimeSpan(0, 0, Convert.ToInt32(Config["ServiceManager:CleanService:MaxAge"])); } - </select> - </div> - <div class="col-md-4 col-sm-12"> - <button type="submit" class="btn btn-primary btn-md waves-effect">Go</button> - </div> - </form> + View the list of resolved and unresolved dicrepancies from <utc-time time="@discrepancyDataStartDate" show-date=true /> until now. + </small> + </h2> + </div> + + <div class="card-body card-padding"> + <div role="tabpanel"> + <ul class="tab-nav" role="tablist"> + <li class="active"> + <a + href="#unresolved" + aria-controls="unresolved" + role="tab" + data-toggle="tab" + aria-expanded="true" + > + Unresolved ( @(((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Count(d => !d.Resolved)) ) + </a> + </li> + <li class=""> + <a + href="#resovled" + aria-controls="resovled" + role="tab" + data-toggle="tab" + aria-expanded="false" + > + Resolved ( @(((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Count(d => d.Resolved)) ) + </a> + </li> + </ul> + + <div class="tab-content"> + <div role="tabpanel" class="tab-pane active" id="unresolved"> + @if ( ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Any(d => !d.Resolved) ) + { + @foreach (var dicrepancy in ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Where(d => !d.Resolved)) + { + <vc:discrepancy-card + model=dicrepancy + number=0 + hidden=false + ></vc:discrepancy-card> + } + } + else + { + @: <h3>No outstanding issues! Well done!</h3> + } + </div> + <div role="tabpanel" class="tab-pane" id="resovled"> + @if ( ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Any(d => d.Resolved) ) + { + var number = 1; + + @foreach (var dicrepancy in ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Where(d => d.Resolved)) + { + var hidden = number > 10; + + <vc:discrepancy-card + model=dicrepancy + number=number + hidden=hidden + ></vc:discrepancy-card> + + number++; + } + <div class="w-100 text-center p-t-10"> + + <button + type="type" + id="load-more-resolved-btn" + class="btn btn-primary btn-md waves-effect" + style="@( ((IEnumerable<Discrepancy>)ViewBag.Discrepancies).Count(d => d.Resolved) <= 10 ? "display: none;" : "")" + > + Show another 10 + </button> + + <button + type="type" + id="load-less-resolved-btn" + class="btn btn-warning btn-md waves-effect" + style="display: none;" + > + Hide another 10 + </button> + </div> + } + else + { + @: <h3>No discrepancies have been noticed.</h3> + } + </div> + </div> + </div> + + </div> + </div> + </div> + </div> - </div> + </div> <div class="col-md-6"> <div class="card"> diff --git a/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml b/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml new file mode 100644 index 0000000000000000000000000000000000000000..d24685471d1bb7cdae78a0ac34bf7b9982d6e1bd --- /dev/null +++ b/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml @@ -0,0 +1,63 @@ +<div + class="list-group-item media discrepancy-card discrepancy-@(Model.Resolved ? "resolved" : "unresolved")" + data-number="@ViewBag.Number" + style="@(ViewBag.Hidden ? "display: none;" : "")" +> + <div class="pull-left"> + <i + class="zmdi zmdi-@(Model.Resolved ? "check-circle" : "alert-circle-o") zmdi-hc-3x" + style="color: @(Model.Resolved ? "green" : "red");" + > + </i> + </div> + + @if (!Model.Resolved) + { + <div class="pull-right"> + <div class="actions"> + <form asp-controller="Admin" asp-action="ResolveDiscrepancy" asp-anti-forgery="true"> + + <input type="text" hidden name="Date" value="@Model.DateFirstOffense.Ticks"> + <input type="text" hidden name="DiscrepancyType" value="@Model.Type"> + <input type="text" hidden name="MetricType" value="@Model.MetricType"> + <input type="text" hidden name="Source" value="@Model.MetricSource"> + + <button type="submit" class="btn btn-success btn-md waves-effect"> + Resolve + </button> + </form> + </div> + </div> + } + else + { + <div class="pull-right"> + <p>#@ViewBag.Number + </div> + } + + <div class="media-body"> + <div class="lgi-heading"> + Discrepancy of type <strong>@Model.Type</strong> from + <a + asp-controller="Home" + asp-action="Metric" + asp-route-type="@Model.MetricType" + asp-route-source="@Model.MetricSource" + > + <em>@Model.MetricType</em> of <em>@Model.MetricSource</em> + </a> + . + </div> + <small class="lgi-text">@Model.Description()</small> + + <ul class="lgi-attrs"> + <li>Date started: <utc-time time="@Model.DateFirstOffense" /></li> + @if (Model.Resolved) + { + <li>Date resolved: <utc-time time="@Model.DateResolved" /></li> + <li>Duration: @( Math.Round((@Model.DateResolved - @Model.DateFirstOffense).TotalSeconds) ) seconds</li> + } + </ul> + </div> +</div> diff --git a/src/web/Views/ViewComponents/DiscrepancyCardViewComponent.cs b/src/web/Views/ViewComponents/DiscrepancyCardViewComponent.cs new file mode 100644 index 0000000000000000000000000000000000000000..e5ca31a7f4c93d8268870f009def581ded259768 --- /dev/null +++ b/src/web/Views/ViewComponents/DiscrepancyCardViewComponent.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using StatusMonitor.Shared.Extensions; +using StatusMonitor.Shared.Models.Entities; + +namespace StatusMonitor.Web.Views.ViewComponents +{ + /// <summary> + /// View component responsible for rendering discrepancy card. + /// </summary> + [ViewComponent(Name = "DiscrepancyCard")] + public class DiscrepancyCardViewComponent : ViewComponent + { + public async Task<IViewComponentResult> InvokeAsync(Discrepancy model, int number = 0, bool hidden = false) + { + await Task.CompletedTask; + + ViewBag.Number = number; + ViewBag.Hidden = hidden; + + return View(model); + } + } +} diff --git a/test/ControllerTests/AdminController/AdminControllerTest.cs b/test/ControllerTests/AdminController/AdminControllerTest.cs index db3922b63c1c4ae076958c621799cf5d568470b6..3721b2bdebaf7cd6b161de68e7a23589d5ccd573 100644 --- a/test/ControllerTests/AdminController/AdminControllerTest.cs +++ b/test/ControllerTests/AdminController/AdminControllerTest.cs @@ -27,12 +27,30 @@ namespace StatusMonitor.Tests.ControllerTests private readonly Mock<IMetricService> _mockMetricService = new Mock<IMetricService>(); private readonly Mock<IApiController> _mockApiController = new Mock<IApiController>(); private readonly Mock<ICleanService> _mockCleanService = new Mock<ICleanService>(); + private readonly Mock<INotificationService> _mockNotificationService = new Mock<INotificationService>(); + private readonly Mock<IConfiguration> _mockConfig = new Mock<IConfiguration>(); + + private readonly IDataContext _context; private readonly AdminController _controller; public AdminControllerTest() { + var services = new ServiceCollection(); + + var mockEnv = new Mock<IHostingEnvironment>(); + mockEnv + .SetupGet(environment => environment.EnvironmentName) + .Returns("Testing"); + var env = mockEnv.Object; + + services.RegisterSharedServices(env, new Mock<IConfiguration>().Object); + + _context = services + .BuildServiceProvider() + .GetRequiredService<IDataContext>(); + var mockServiceProvider = new Mock<IServiceProvider>(); mockServiceProvider .Setup(provider => provider.GetService(typeof(IApiController))) @@ -43,8 +61,12 @@ namespace StatusMonitor.Tests.ControllerTests new Mock<ILogger<AdminController>>().Object, _mockMetricService.Object, mockServiceProvider.Object, - _mockCleanService.Object + _mockCleanService.Object, + _context, + _mockNotificationService.Object, + _mockConfig.Object ); + _controller.ControllerContext.HttpContext = new DefaultHttpContext(); _controller.TempData = new Mock<ITempDataDictionary>().Object; } diff --git a/test/ControllerTests/AdminController/ResolveDiscrepancy.cs b/test/ControllerTests/AdminController/ResolveDiscrepancy.cs new file mode 100644 index 0000000000000000000000000000000000000000..97786d9e1cb5d75df33d99cc38f41df36a7c5544 --- /dev/null +++ b/test/ControllerTests/AdminController/ResolveDiscrepancy.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using StatusMonitor.Web.Controllers.View; +using StatusMonitor.Shared.Extensions; +using StatusMonitor.Shared.Models.Entities; +using StatusMonitor.Shared.Services; +using Xunit; +using Microsoft.AspNetCore.Http; +using Moq; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using StatusMonitor.Shared.Models; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using StatusMonitor.Web.Services; +using System.Linq; +using StatusMonitor.Web.ViewModels; +using Microsoft.EntityFrameworkCore; + +namespace StatusMonitor.Tests.ControllerTests +{ + public partial class AdminControllerTest + { + [Fact] + public async Task ResolveDiscrepancySuccess() + { + // Arrange + var now = DateTime.UtcNow; + + await _context.Discrepancies.AddAsync( + new Discrepancy + { + Type = DiscrepancyType.GapInData, + MetricType = Metrics.CpuLoad, + MetricSource = "the-source", + DateFirstOffense = now, + Resolved = false + } + ); + await _context.SaveChangesAsync(); + + var input = new DiscrepancyResolutionViewModel + { + DateFirstOffense = now, + EnumDiscrepancyType = DiscrepancyType.GapInData, + EnumMetricType = Metrics.CpuLoad, + Source = "the-source" + }; + + // Act + var result = await _controller.ResolveDiscrepancy(input); + + // Assert + var redirectResult = Assert.IsType<RedirectToActionResult>(result); + + Assert.Equal("Index", redirectResult.ActionName); + Assert.Equal("Admin", redirectResult.ControllerName); + + Assert.True( + (await _context + .Discrepancies + .SingleAsync(d => + d.DateFirstOffense == now && + d.MetricSource == "the-source" && + d.MetricType == Metrics.CpuLoad && + d.Type == DiscrepancyType.GapInData + ) + ).Resolved + ); + Assert.InRange( + (await _context + .Discrepancies + .SingleAsync(d => + d.DateFirstOffense == now && + d.MetricSource == "the-source" && + d.MetricType == Metrics.CpuLoad && + d.Type == DiscrepancyType.GapInData + ) + ).DateResolved, + now, + now.AddMinutes(1) + ); + + _mockNotificationService + .Verify( + n => n.ScheduleNotificationAsync( + It.Is<string>(s => s.Contains("the-source")), + NotificationSeverity.Medium + ) + ); + + _mockConfig.Verify(conf => conf["ServiceManager:NotificationService:TimeZone"]); + } + + [Fact] + public async Task ResolveDiscrepancyWarning() + { + // Arrange + var now = DateTime.UtcNow; + + await _context.Discrepancies.AddAsync( + new Discrepancy + { + Type = DiscrepancyType.GapInData, + MetricType = Metrics.CpuLoad, + MetricSource = "the-source", + DateFirstOffense = now, + Resolved = true + } + ); + await _context.SaveChangesAsync(); + + var input = new DiscrepancyResolutionViewModel + { + DateFirstOffense = now, + EnumDiscrepancyType = DiscrepancyType.GapInData, + EnumMetricType = Metrics.CpuLoad, + Source = "the-source" + }; + + // Act + var result = await _controller.ResolveDiscrepancy(input); + + // Assert + var redirectResult = Assert.IsType<RedirectToActionResult>(result); + + Assert.Equal("Index", redirectResult.ActionName); + Assert.Equal("Admin", redirectResult.ControllerName); + + _mockNotificationService + .Verify( + n => n.ScheduleNotificationAsync( + It.Is<string>(s => s.Contains("the-source")), + NotificationSeverity.Medium + ), + Times.Never() + ); + } + + [Fact] + public async Task ResolveDiscrepancyNotFound() + { + // Arrange + var input = new DiscrepancyResolutionViewModel + { + DateFirstOffense = DateTime.UtcNow, + EnumDiscrepancyType = DiscrepancyType.GapInData, + EnumMetricType = Metrics.CpuLoad, + Source = "the-source" + }; + + // Act + var result = await _controller.ResolveDiscrepancy(input); + + // Assert + var notFoundResult = Assert.IsType<NotFoundObjectResult>(result); + + _mockNotificationService + .Verify( + n => n.ScheduleNotificationAsync( + It.Is<string>(s => s.Contains("the-source")), + NotificationSeverity.Medium + ), + Times.Never() + ); + } + } +}