diff --git a/client/ts/metric.ts b/client/ts/metric.ts index 11c8fa55b1888f3b0d90432149c5527e2fa8a6cc..593929b0e4c31b003ca79a6bed71ac3c02ac896c 100644 --- a/client/ts/metric.ts +++ b/client/ts/metric.ts @@ -13,6 +13,7 @@ import "bootstrap" declare var source: string; declare var type: number; + declare var min: number; declare var max: number; diff --git a/client/ts/modules/constants.ts b/client/ts/modules/constants.ts index 3d5e16524a3143b781fe571f4b55513fa66a0f69..67224ffac472ae443106b3b07eea2f368e723680 100644 --- a/client/ts/modules/constants.ts +++ b/client/ts/modules/constants.ts @@ -55,4 +55,10 @@ export class Constants { * of 5 minutes and sums of user actions will be displayed per interval. */ static USER_ACTIONS_AGGREGATION_INTERVAL : number = 30; + + /** + * The interval in milliseconds that defines a default time frame of data + * on metric pages. + */ + static METRIC_PAGE_DATA_PREVIEW : number = 2 * 60 * 60 * 1000; } diff --git a/client/ts/modules/metric-page/abstract.ts b/client/ts/modules/metric-page/abstract.ts index 0d6090499c91642256128ead3014a60ec19aa7e6..26cdd6d6c96e2f3d6360e9c4067acaa4bfe1c855 100644 --- a/client/ts/modules/metric-page/abstract.ts +++ b/client/ts/modules/metric-page/abstract.ts @@ -10,8 +10,12 @@ import "../../vendor/jquery.flot.time.js"; import "../../vendor/jquery.flot.selection.js"; import "../../vendor/jquery.flot.threshold.js"; import "../../vendor/jquery.flot.tooltip.js"; +import { Constants } from "../constants"; +declare var start: number; +declare var end: number; + /** * Represents set of procedures for rendering metric page. * @@ -23,6 +27,7 @@ export abstract class MetricPage<T extends Metric<DataPoint>> { protected metric: T; protected dataTablesRendered: boolean = false; + protected dataTable : DataTables.DataTable; /** * Minimal theoretical value for data series. @@ -47,13 +52,52 @@ export abstract class MetricPage<T extends Metric<DataPoint>> { protected detailedPlotOptions: any; protected overviewPlotOptions: any; + /** + * Date as a number of milliseconds of the first data point + * + * @protected + * @type {number} + * @memberof MetricPage + */ protected minData: number; + /** + * Date as a number of milliseconds of the last data point + * + * @protected + * @type {number} + * @memberof MetricPage + */ protected maxData: number; + /** + * If given, the timestamp of first data point in selected range + * + * @protected + * @type {Date} + * @memberof MetricPage + */ + protected start: Date = null; + /** + * If given, the timestamp of last data point in selected range + * + * @protected + * @type {Date} + * @memberof MetricPage + */ + protected end: Date = null; + constructor(min: number, max: number) { this.max = max; this.min = min; + if (start > 0) { + this.start = new Date(start); + } + + if (end > 0) { + this.end = new Date(end); + } + window.setTimeout(async () => { await this.metric.loadData(60 * 60 * 24 * 3); // 3 days @@ -84,7 +128,7 @@ export abstract class MetricPage<T extends Metric<DataPoint>> { * @memberOf MetricPage */ private renderPlot(): void { - + var plot: any = $.plot( $("#metric-detailed-plot"), this.formattedData, @@ -113,17 +157,47 @@ export abstract class MetricPage<T extends Metric<DataPoint>> { // don't fire event on the overview to prevent eternal loop overview.setSelection(ranges, true); + + // Re-render data table to include only selected data points + this.renderTable(true, new Date(ranges.xaxis.from), new Date(ranges.xaxis.to)); })); $("#metric-overview-plot").bind("plotselected", <any>((event, ranges) => { plot.setSelection(ranges); })); - // if latest data point is more than 2 hours ago - // select recent 2 hours in plot - if (new Date().getTime() - this.minData > 2 * 60 * 60 * 1000) { - let from = new Date().getTime() - 2 * 60 * 60 * 1000; - plot.setSelection({ xaxis: { from: from, to: this.maxData }, yaxis: { from: 0, to: 0 } }); + if (this.start != null) { + + let from = Math.min( + Math.max( + this.start.getTime(), + this.minData + ), + this.maxData) + ; + + let to = + this.end == null ? + Math.min( + this.maxData, + this.start.getTime() + Constants.METRIC_PAGE_DATA_PREVIEW + ) : + Math.max( + Math.min( + this.end.getTime(), + this.maxData + ), + this.minData) + ; + + plot.setSelection({ xaxis: { from: from, to: to }, yaxis: { from: 0, to: 0 } }); + } else { + // if latest data point is more than 2 hours ago + // select recent 2 hours in plot + if (new Date().getTime() - this.minData > Constants.METRIC_PAGE_DATA_PREVIEW) { + let from = new Date().getTime() - Constants.METRIC_PAGE_DATA_PREVIEW; + plot.setSelection({ xaxis: { from: from, to: this.maxData }, yaxis: { from: 0, to: 0 } }); + } } } @@ -135,7 +209,19 @@ export abstract class MetricPage<T extends Metric<DataPoint>> { * * @memberOf MetricPage */ - protected abstract renderTable(): void; + + /** + * Renders data tables in the UI. + * Does not load the data. + * + * @protected + * @abstract + * @param {boolean} redraw if data table has to be force-redrawn; implies non-null values of start and end; + * @param {Date} start start date for filter + * @param {Date} end end date for filter + * @memberof MetricPage + */ + protected abstract renderTable(redraw: boolean, start: Date, end: Date): void; /** * Renders numeric values in the UI. @@ -160,10 +246,10 @@ export abstract class MetricPage<T extends Metric<DataPoint>> { */ public render(): void { - this.configurePlot() + this.configurePlot() this.renderPlot(); - + this.renderValues(); - this.renderTable(); + this.renderTable(false, null, null); } } diff --git a/client/ts/modules/metric-page/cpu-load.ts b/client/ts/modules/metric-page/cpu-load.ts index 3b21f22677303788ed2243b0712a8fe09631da44..5dea416e803c151b5a36871ad7c8627daf71f17a 100644 --- a/client/ts/modules/metric-page/cpu-load.ts +++ b/client/ts/modules/metric-page/cpu-load.ts @@ -106,9 +106,13 @@ export class CpuLoadMetricPage extends MetricPage<Metric<CpuLoadDataPoint>> { }; }; - protected renderTable(): void { + protected renderTable(redraw: boolean, start: Date, end: Date): void { - if (!this.dataTablesRendered) { + if (!this.dataTablesRendered || redraw) { + + if (this.dataTablesRendered) { + this.dataTable.destroy(); + } let header = ` <tr> @@ -124,6 +128,17 @@ export class CpuLoadMetricPage extends MetricPage<Metric<CpuLoadDataPoint>> { this.metric .data .map(dp => <CpuLoadDataPoint>dp) + .filter((value, index, array) => { + if (start != null && value.timestamp < start) { + return false; + } + + if (end != null && value.timestamp > end) { + return false; + } + + return true; + }) .map( dp => ` <tr> @@ -135,7 +150,7 @@ export class CpuLoadMetricPage extends MetricPage<Metric<CpuLoadDataPoint>> { .join() ); - $('#metric-data').DataTable({ + this.dataTable = $('#metric-data').DataTable({ "order": [[0, "desc"]], lengthChange: false, searching: false, diff --git a/client/ts/modules/metric-page/health.ts b/client/ts/modules/metric-page/health.ts index 0756f9d685e03d58d01a3bc0446ba8827e77695b..eca8997897a32cc9c9637f3056d0661b13f9562b 100644 --- a/client/ts/modules/metric-page/health.ts +++ b/client/ts/modules/metric-page/health.ts @@ -111,9 +111,13 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> { }; }; - protected renderTable(): void { + protected renderTable(redraw: boolean, start: Date, end: Date): void { - if (!this.dataTablesRendered) { + if (!this.dataTablesRendered || redraw) { + + if (this.dataTablesRendered) { + this.dataTable.destroy(); + } let header = ` <tr> @@ -130,6 +134,17 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> { this.metric .data .map(dp => <HealthDataPoint>dp) + .filter((value, index, array) => { + if (start != null && value.timestamp < start) { + return false; + } + + if (end != null && value.timestamp > end) { + return false; + } + + return true; + }) .map( dp => ` <tr> @@ -160,7 +175,7 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> { .join() ); - $('#metric-data').DataTable({ + this.dataTable = $('#metric-data').DataTable({ "order": [[0, "desc"]], lengthChange: false, searching: false, @@ -173,13 +188,13 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> { 'showDetails', (e: CustomEvent) => { - let data : any[] = e.detail.data; - let timestamp : Date = e.detail.timestamp; + let data: any[] = e.detail.data; + let timestamp: Date = e.detail.timestamp; let code = ` <div - class="modal fade" - id="modal-details" + class="modal fade health-details-modal" + id="modal-details-${timestamp.getTime()}" tabindex="-1" role="dialog" aria-hidden="true" @@ -192,7 +207,10 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> { <h4 class="modal-title"> Health report details | Health ${e.detail.health}% | ${timestamp} <small> - Inspect metric labels at the moment report was generated + Inspect metric labels at the moment report was generated. + <a href="/home/metric/${this.metric.metricType}/${this.metric.source}/${new Date(timestamp.getTime() - 2 * 60 * 1000).getTime()}/${new Date(timestamp.getTime() + 2 * 60 * 1000).getTime()}"> + View data at that moment. + </a> </small> </h4> </div> @@ -211,17 +229,17 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> { </thead> <tbody> ${ - data - .sortByProperty(el => el.Source) - .map(el => ` + data + .sortByProperty(el => el.Source) + .map(el => ` <tr> <th>${el.Type}</th> <th>${el.Source}</th> <th>${el.Label}</th> </tr> `) - .join("") - } + .join("") + } </tbody> </table> </div> @@ -239,16 +257,23 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> { </div> `; - $("#modal").html(code); + $("body").append(code); $('#details-table').DataTable({ - "order": [[0, "desc"]], + destroy: true, + order: [[0, "desc"]], lengthChange: false, searching: false, pageLength: 10 }); - $("#modal-details").modal(); + $(`#modal-details-${timestamp.getTime()}`).modal(); + + $(`#modal-details-${timestamp.getTime()}`).on( + "hidden.bs.modal", + () => $(".health-details-modal").remove() + ); + }, false); diff --git a/client/ts/modules/metric-page/ping.ts b/client/ts/modules/metric-page/ping.ts index f81fae88f97dc17cd64e7319c2ec9a66cf5a6bcc..90e41a10af42c7bdc12aa402a190b2a438b8f842 100644 --- a/client/ts/modules/metric-page/ping.ts +++ b/client/ts/modules/metric-page/ping.ts @@ -149,9 +149,13 @@ export class PingMetricPage extends MetricPage<Metric<PingDataPoint>> { }; }; - protected renderTable(): void { + protected renderTable(redraw: boolean, start: Date, end: Date): void { - if (!this.dataTablesRendered) { + if (!this.dataTablesRendered || redraw) { + + if (this.dataTablesRendered) { + this.dataTable.destroy(); + } let header = ` <tr> @@ -168,6 +172,17 @@ export class PingMetricPage extends MetricPage<Metric<PingDataPoint>> { this.metric .data .map(dp => <PingDataPoint>dp) + .filter((value, index, array) => { + if (start != null && value.timestamp < start) { + return false; + } + + if (end != null && value.timestamp > end) { + return false; + } + + return true; + }) .map( dp => ` <tr> @@ -180,7 +195,7 @@ export class PingMetricPage extends MetricPage<Metric<PingDataPoint>> { .join() ); - $('#metric-data').DataTable({ + this.dataTable = $('#metric-data').DataTable({ "order": [[0, "desc"]], lengthChange: false, searching: false, diff --git a/client/ts/modules/metric-page/user-action.ts b/client/ts/modules/metric-page/user-action.ts index 2ad736888adbe54e38a66e9d2789a1f5f975c15d..6044c8cb1ef7fa5b1ec0be6d3670ce7810b3f021 100644 --- a/client/ts/modules/metric-page/user-action.ts +++ b/client/ts/modules/metric-page/user-action.ts @@ -154,9 +154,13 @@ export class UserActionMetricPage extends MetricPage<Metric<UserActionDataPoint> }; }; - protected renderTable(): void { + protected renderTable(redraw: boolean, start: Date, end: Date): void { - if (!this.dataTablesRendered) { + if (!this.dataTablesRendered || redraw) { + + if (this.dataTablesRendered) { + this.dataTable.destroy(); + } let header = ` <tr> @@ -173,6 +177,17 @@ export class UserActionMetricPage extends MetricPage<Metric<UserActionDataPoint> this.metric .data .map(dp => <UserActionDataPoint>dp) + .filter((value, index, array) => { + if (start != null && value.timestamp < start) { + return false; + } + + if (end != null && value.timestamp > end) { + return false; + } + + return true; + }) .map( dp => ` <tr> @@ -185,7 +200,7 @@ export class UserActionMetricPage extends MetricPage<Metric<UserActionDataPoint> .join() ); - $('#metric-data').DataTable({ + this.dataTable = $('#metric-data').DataTable({ "order": [[0, "desc"]], lengthChange: false, searching: false, diff --git a/debian/debian/changelog b/debian/debian/changelog index bbc9cb0cf1e922f8b0d93763ad31f0aa35200bbd..19f1ba835a5cff3d0701599744d379d42146d18b 100644 --- a/debian/debian/changelog +++ b/debian/debian/changelog @@ -1,3 +1,9 @@ +status-ctl (1.2.0) unstable; urgency=medium + + * Update docker-compose.yml. + + -- Dmytro Bogatov <dmytro@dbogatov.org> Thu, 08 Aug 2017 04:35:38 +0000 + status-ctl (1.1.1) unstable; urgency=medium * Minor fixes. diff --git a/debian/debian/status-ctl.1 b/debian/debian/status-ctl.1 index e0bfde7d161e4916290c0fefaa33a8b181ef5eaa..02f54b75452719d7fa45787a6309438e594f574d 100644 --- a/debian/debian/status-ctl.1 +++ b/debian/debian/status-ctl.1 @@ -6,7 +6,7 @@ . .\" Manpage for status-ctl. .\" Contact dmytro@dbogatov.org to correct errors or typos. -.TH man 1 "4 August 2017" "1.1.1" "status-ctl man page" +.TH man 1 "4 August 2017" "1.2.0" "status-ctl man page" .SH NAME .B status-ctl \- control tool for status site diff --git a/debian/status-ctl.sh b/debian/status-ctl.sh index 670abb5b569f0d22108d2b3a5f4b95df463cb5f5..e2d337dfc1e61fdfe910287a385cd83cac31e7bd 100755 --- a/debian/status-ctl.sh +++ b/debian/status-ctl.sh @@ -10,7 +10,7 @@ BAD_PARAMETERS=4 # CONSTANTS CONFIG_DIR=/etc/status-site PROJECT="status-site" -VERSION="1.1.1" +VERSION="1.2.0" BRANCH="master" function usage { diff --git a/docker-compose.yml b/docker-compose.yml index 4ee371a973373bdb12ca8d206af42d75d5db8504..fc642b8365026284771b04797334c8946fc0c7b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,7 @@ services: - ./appsettings.yml:/srv/appsettings.production.yml restart: on-failure postgres: - image: postgres:latest + image: postgres:9.6.3-alpine environment: # define connection credentials to be used in app - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} diff --git a/src/shared/Extensions/DateTimeExtensions.cs b/src/shared/Extensions/DateTimeExtensions.cs index 41e0be2d7f6d0749987d230585203e824b154b06..27970d4a9f7f78b0c0c80e1347b797bb098ee9f9 100644 --- a/src/shared/Extensions/DateTimeExtensions.cs +++ b/src/shared/Extensions/DateTimeExtensions.cs @@ -22,5 +22,14 @@ namespace StatusMonitor.Shared.Extensions ? value.ToString() : TimeZoneInfo.ConvertTime(value, TimeZoneInfo.FindSystemTimeZoneById(timeZoneId)).ToString(); } + + /// <summary> + /// Return the number of milliseconds between epoch and given date + /// </summary> + /// <param name="value">The end date of selected range</param> + /// <returns>The number of milliseconds between epoch and given date</returns> + public static long TotalMilliseconds(this DateTime value) => + Convert.ToInt64((value - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds); + } } diff --git a/src/web/Controllers/View/AccountController.cs b/src/web/Controllers/View/AccountController.cs index f6cd84cc8fe6d8d87e6e02a04a5f24dc4d462d16..db7fb58bebf270a509d569fcc10b0c58df4988de 100644 --- a/src/web/Controllers/View/AccountController.cs +++ b/src/web/Controllers/View/AccountController.cs @@ -75,7 +75,7 @@ namespace StatusMonitor.Web.Controllers.View TempData["MessageSeverity"] = "info"; TempData["MessageContent"] = $"You have logged in."; - + if (string.IsNullOrEmpty(Request.Query["returnurl"])) { return RedirectToAction("Index", "Home"); diff --git a/src/web/Controllers/View/HomeController.cs b/src/web/Controllers/View/HomeController.cs index 63404c8994176ff4bb32fe0af9d716a44f15893e..14371aaff6a6543d491a574cb84e10fba6de66cf 100755 --- a/src/web/Controllers/View/HomeController.cs +++ b/src/web/Controllers/View/HomeController.cs @@ -65,8 +65,8 @@ namespace StatusMonitor.Web.Controllers.View return View(model); } - [Route("Home/Metric/{type}/{source}")] - public async Task<IActionResult> Metric(string type, string source) + [Route("Home/Metric/{type}/{source}/{start?}/{end?}")] + public async Task<IActionResult> Metric(string type, string source, string start = null, string end = null) { Metrics metricType; @@ -79,6 +79,41 @@ namespace StatusMonitor.Web.Controllers.View return BadRequest("Bad type. Needs to be one of Metrics type."); } + if (start != null) + { + try + { + DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).AddMilliseconds(Convert.ToInt64(start)); + } + catch (System.Exception) + { + return BadRequest("Bad start date. Needs to be the number of milliseconds since Epoch."); + } + } + + if (end != null) + { + try + { + DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).AddMilliseconds(Convert.ToInt64(end)); + } + catch (System.Exception) + { + return BadRequest("Bad end date. Needs to be the number of milliseconds since Epoch."); + } + } + + if (start != null && end != null) + { + if ( + DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).AddMilliseconds(Convert.ToInt64(start)) >= + DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).AddMilliseconds(Convert.ToInt64(end)) + ) + { + return BadRequest("Bad dates. End date needs to be greater than the start date."); + } + } + var model = await _metricService.GetMetricsAsync(metricType, source); if (model.Count() == 0) @@ -107,6 +142,9 @@ namespace StatusMonitor.Web.Controllers.View ViewBag.Uptime = await _uptime.ComputeUptimeAsync(source); } + ViewBag.Start = start ?? 0.ToString(); + ViewBag.End = end ?? 0.ToString(); + return View(model.First()); } diff --git a/src/web/Views/Home/Metric.cshtml b/src/web/Views/Home/Metric.cshtml index 296f68696890d7481291629b9cb29617d11f7db9..358d237e1d738e37cd81eedd44ce38daa842a62b 100644 --- a/src/web/Views/Home/Metric.cshtml +++ b/src/web/Views/Home/Metric.cshtml @@ -22,7 +22,13 @@ <div class="card"> <div class="card-header"> - <h2>Plotted data <small>Data is requested for a day back.</small></h2> + <h2> + Plotted data + <small> + Data is requested for a 3 days back. + Select a range on the plot to zoom. + </small> + </h2> </div> <div class="card-body card-padding"> <div id="metric-detailed-plot" class="flot-chart"></div> @@ -38,7 +44,7 @@ <div class="card"> <div class="card-header"> - <h2>Data points <small>Data is requested for a day back.</small></h2> + <h2>Data points <small>Data is shown for the same interval as selected plot.</small></h2> </div> <div class="card-body card-padding"> @@ -186,8 +192,12 @@ <script> var source = "@Model.Source"; var type = @Model.Type; + var min = @ViewData["Min"]; var max = @ViewData["Max"]; + + var start = @ViewData["Start"]; + var end = @ViewData["End"]; </script> <environment names="Development"> diff --git a/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml b/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml index d24685471d1bb7cdae78a0ac34bf7b9982d6e1bd..0ae544086a1c067754516397f8deb0cbc8063c48 100644 --- a/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml +++ b/src/web/Views/Shared/Components/DiscrepancyCard/Default.cshtml @@ -1,3 +1,5 @@ +@using StatusMonitor.Shared.Extensions + <div class="list-group-item media discrepancy-card discrepancy-@(Model.Resolved ? "resolved" : "unresolved")" data-number="@ViewBag.Number" @@ -39,11 +41,20 @@ <div class="media-body"> <div class="lgi-heading"> Discrepancy of type <strong>@Model.Type</strong> from + @{ + var start = Convert.ToInt64((Model.DateFirstOffense.AddMinutes(-2) - new DateTime(1970, 1, 1)).TotalMilliseconds); + var end = + Model.Resolved ? + Convert.ToInt64((Model.DateResolved.AddMinutes(2) - new DateTime(1970, 1, 1)).TotalMilliseconds) : + Convert.ToInt64((Model.DateFirstOffense.AddMinutes(15) - new DateTime(1970, 1, 1)).TotalMilliseconds); + } <a asp-controller="Home" asp-action="Metric" asp-route-type="@Model.MetricType" asp-route-source="@Model.MetricSource" + asp-route-start="@start" + asp-route-end="@end" > <em>@Model.MetricType</em> of <em>@Model.MetricSource</em> </a> diff --git a/test/ControllerTests/HomeConroller/Metric.cs b/test/ControllerTests/HomeConroller/Metric.cs index 8f2a4a8c64b44cb77ff4ce00acd99ce70c9fec63..b6d2ce9373ab293cdfded0dfb2b57eccaff428b7 100644 --- a/test/ControllerTests/HomeConroller/Metric.cs +++ b/test/ControllerTests/HomeConroller/Metric.cs @@ -28,11 +28,11 @@ namespace StatusMonitor.Tests.ControllerTests _mockMetricService .Setup(mock => mock.GetMetricsAsync(It.IsAny<Metrics>(), It.IsAny<string>())) .ReturnsAsync( - new List<Metric> { + new List<Metric> { new Metric { - CurrentValue = 50, + CurrentValue = 50, Public = true - } + } } ); @@ -70,7 +70,7 @@ namespace StatusMonitor.Tests.ControllerTests // Arrange _mockMetricService .Setup(mock => mock.GetMetricsAsync(It.IsAny<Metrics>(), It.IsAny<string>())) - .ReturnsAsync(new List<Metric> { new Metric { Public = false }}); + .ReturnsAsync(new List<Metric> { new Metric { Public = false } }); _mockAuth .Setup(auth => auth.IsAuthenticated()) @@ -94,5 +94,123 @@ namespace StatusMonitor.Tests.ControllerTests Assert.Contains("type", (string)badRequestObjectResult.Value); } + + [Fact] + public async Task MetricStartDateRequest() + { + // Act + var result = await _controller.Metric(Metrics.CpuLoad.ToString(), "any-source", "invalid-date"); + + // Assert + var badRequestObjectResult = Assert.IsType<BadRequestObjectResult>(result); + + Assert.Contains("start", (string)badRequestObjectResult.Value); + } + + [Fact] + public async Task MetricEndDateRequest() + { + // Act + var result = await _controller.Metric( + Metrics.CpuLoad.ToString(), + "any-source", + DateTime.UtcNow.TotalMilliseconds().ToString(), + "invalid-date" + ); + + // Assert + var badRequestObjectResult = Assert.IsType<BadRequestObjectResult>(result); + + Assert.Contains("end", (string)badRequestObjectResult.Value); + } + + [Fact] + public async Task MetricStartAfterEndDateRequest() + { + // Act + var result = await _controller.Metric( + Metrics.CpuLoad.ToString(), + "any-source", + DateTime.UtcNow.TotalMilliseconds().ToString(), + DateTime.UtcNow.AddHours(-1).TotalMilliseconds().ToString() + ); + + // Assert + var badRequestObjectResult = Assert.IsType<BadRequestObjectResult>(result); + + Assert.Contains("greater than", (string)badRequestObjectResult.Value); + } + + [Fact] + public async Task MetricDatesOK() + { + // Arrange + _mockMetricService + .Setup(mock => mock.GetMetricsAsync(It.IsAny<Metrics>(), It.IsAny<string>())) + .ReturnsAsync( + new List<Metric> { + new Metric { + CurrentValue = 50, + Public = true + } + } + ); + + var start = DateTime.UtcNow.AddHours(-1); + var end = DateTime.UtcNow; + + // Act + var result = await _controller.Metric( + Metrics.CpuLoad.ToString(), + "existing-source", + start.TotalMilliseconds().ToString(), + end.TotalMilliseconds().ToString() + ); + + // Assert + var viewResult = Assert.IsType<ViewResult>(result); + + var model = Assert.IsAssignableFrom<Metric>( + viewResult.ViewData.Model + ); + + Assert.Equal(start.TotalMilliseconds().ToString(), viewResult.ViewData["Start"]); + Assert.Equal(end.TotalMilliseconds().ToString(), viewResult.ViewData["End"]); + } + + [Fact] + public async Task MetricDatesEndNullOK() + { + // Arrange + _mockMetricService + .Setup(mock => mock.GetMetricsAsync(It.IsAny<Metrics>(), It.IsAny<string>())) + .ReturnsAsync( + new List<Metric> { + new Metric { + CurrentValue = 50, + Public = true + } + } + ); + + var start = DateTime.UtcNow; + + // Act + var result = await _controller.Metric( + Metrics.CpuLoad.ToString(), + "existing-source", + start.TotalMilliseconds().ToString() + ); + + // Assert + var viewResult = Assert.IsType<ViewResult>(result); + + var model = Assert.IsAssignableFrom<Metric>( + viewResult.ViewData.Model + ); + + Assert.Equal(start.TotalMilliseconds().ToString(), viewResult.ViewData["Start"]); + Assert.Equal(0.ToString(), viewResult.ViewData["End"]); + } } }