From c3aeb02091d2dcc449961a7148b5f601490cc7fd Mon Sep 17 00:00:00 2001 From: Scott Bommarito Date: Thu, 11 Oct 2018 17:37:16 -0700 Subject: [PATCH] Status - unit tests (#580) --- src/StatusAggregator/Export/EventExporter.cs | 1 - src/StatusAggregator/Export/IEventExporter.cs | 1 - .../Factory/AggregationStrategy.cs | 2 +- src/StatusAggregator/Job.cs | 26 +- .../Messages/IMessageContentBuilder.cs | 18 +- .../Messages/IMessageFactory.cs | 4 +- .../Messages/MessageChangeEventProvider.cs | 2 +- .../Messages/MessageContentBuilder.cs | 22 +- .../Messages/MessageFactory.cs | 11 +- .../Parse/EnvironmentPrefixIncidentParser.cs | 33 -- ...onmentPrefixIncidentRegexParsingHandler.cs | 36 ++ ...er.cs => EnvironmentRegexParsingFilter.cs} | 8 +- ...lter.cs => IIncidentRegexParsingFilter.cs} | 6 +- .../Parse/IIncidentRegexParsingHandler.cs | 37 ++ ...cidentParser.cs => IncidentRegexParser.cs} | 64 +-- .../Parse/IncidentRegexParsingHandler.cs | 29 ++ ...iceInstanceIncidentRegexParsingHandler.cs} | 22 +- ... => PingdomIncidentRegexParsingHandler.cs} | 20 +- ...ilter.cs => SeverityRegexParsingFilter.cs} | 8 +- ...pointStatusIncidentRegexParsingHandler.cs} | 28 +- ...ionDurationIncidentRegexParsingHandler.cs} | 14 +- src/StatusAggregator/StatusAggregator.csproj | 22 +- .../Update/ActiveEventEntityUpdater.cs | 7 +- .../Update/AggregationEntityUpdater.cs | 5 +- .../EventMessagingUpdater.cs | 4 +- src/StatusAggregator/Update/EventUpdater.cs | 1 - .../Update/IncidentUpdater.cs | 3 +- .../Collector/CursorTests.cs | 93 ++++ .../Collector/EntityCollectorTests.cs | 90 ++++ .../IncidentEntityCollectorProcessorTests.cs | 245 ++++++++++ ...nualStatusChangeCollectorProcessorTests.cs | 143 ++++++ .../EventUpdaterTests.cs | 154 ------ .../Export/ComponentExporterTests.cs | 178 +++++++ .../Export/EventExporterTests.cs | 82 ++++ .../Export/EventsExporterTests.cs | 113 +++++ .../Export/StatusExporterTests.cs | 63 +++ .../Export/StatusSerializerTests.cs | 54 +++ .../Factory/AggregationProviderTests.cs | 277 +++++++++++ .../Factory/AggregationStrategyTests.cs | 206 +++++++++ .../Factory/EventAffectedPathProviderTests.cs | 83 ++++ .../Factory/EventFactoryTests.cs | 80 ++++ ...identAffectedComponentPathProviderTests.cs | 74 +++ .../Factory/IncidentFactoryTests.cs | 170 +++++++ .../Factory/IncidentGroupFactoryTests.cs | 107 +++++ .../NuGetServiceComponentFactoryTests.cs | 127 +++++ .../IncidentFactoryTests.cs | 83 ---- .../IncidentGroupMessageFilterTests.cs | 183 ++++++++ .../MessageChangeEventIteratorTests.cs | 99 ++++ .../MessageChangeEventProcessorTests.cs | 437 ++++++++++++++++++ .../MessageChangeEventProviderTests.cs | 126 +++++ .../Messages/MessageContentBuilderTests.cs | 401 ++++++++++++++++ .../Messages/MessageFactoryTests.cs | 258 +++++++++++ .../Parse/AggregateIncidentParserTests.cs | 76 +++ ...tPrefixIncidentRegexParsingHandlerTests.cs | 34 ++ .../EnvironmentRegexParsingFilterTests.cs | 69 +++ .../Parse/IncidentRegexParserTests.cs | 235 ++++++++++ ...nstanceIncidentRegexParsingHandlerTests.cs | 93 ++++ .../Parse/ParsingUtility.cs | 58 +++ ...PingdomIncidentRegexParsingHandlerTests.cs | 228 +++++++++ .../Parse/SeverityRegexParsingFilterTests.cs | 44 ++ ...tStatusIncidentRegexParsingHandlerTests.cs | 164 +++++++ ...urationIncidentRegexParsingHandlerTests.cs | 80 ++++ .../StatusAggregator.Tests.csproj | 39 ++ tests/StatusAggregator.Tests/TestComponent.cs | 28 ++ .../TestUtility/MockTableWrapperExtensions.cs | 23 + .../Update/ActiveEventEntityUpdaterTests.cs | 85 ++++ .../Update/AggregationEntityUpdaterTests.cs | 370 +++++++++++++++ .../Update/EventMessagingUpdaterTests.cs | 64 +++ .../Update/IncidentUpdaterTests.cs | 126 +++++ 69 files changed, 5740 insertions(+), 436 deletions(-) delete mode 100644 src/StatusAggregator/Parse/EnvironmentPrefixIncidentParser.cs create mode 100644 src/StatusAggregator/Parse/EnvironmentPrefixIncidentRegexParsingHandler.cs rename src/StatusAggregator/Parse/{EnvironmentFilter.cs => EnvironmentRegexParsingFilter.cs} (87%) rename src/StatusAggregator/Parse/{IIncidentParsingFilter.cs => IIncidentRegexParsingFilter.cs} (73%) create mode 100644 src/StatusAggregator/Parse/IIncidentRegexParsingHandler.cs rename src/StatusAggregator/Parse/{IncidentParser.cs => IncidentRegexParser.cs} (51%) create mode 100644 src/StatusAggregator/Parse/IncidentRegexParsingHandler.cs rename src/StatusAggregator/Parse/{OutdatedSearchServiceInstanceIncidentParser.cs => OutdatedSearchServiceInstanceIncidentRegexParsingHandler.cs} (54%) rename src/StatusAggregator/Parse/{PingdomIncidentParser.cs => PingdomIncidentRegexParsingHandler.cs} (86%) rename src/StatusAggregator/Parse/{SeverityFilter.cs => SeverityRegexParsingFilter.cs} (83%) rename src/StatusAggregator/Parse/{TrafficManagerEndpointStatusIncidentParser.cs => TrafficManagerEndpointStatusIncidentRegexParsingHandler.cs} (85%) rename src/StatusAggregator/Parse/{ValidationDurationIncidentParser.cs => ValidationDurationIncidentRegexParsingHandler.cs} (57%) rename src/StatusAggregator/{Messages => Update}/EventMessagingUpdater.cs (95%) create mode 100644 tests/StatusAggregator.Tests/Collector/CursorTests.cs create mode 100644 tests/StatusAggregator.Tests/Collector/EntityCollectorTests.cs create mode 100644 tests/StatusAggregator.Tests/Collector/IncidentEntityCollectorProcessorTests.cs create mode 100644 tests/StatusAggregator.Tests/Collector/ManualStatusChangeCollectorProcessorTests.cs delete mode 100644 tests/StatusAggregator.Tests/EventUpdaterTests.cs create mode 100644 tests/StatusAggregator.Tests/Export/ComponentExporterTests.cs create mode 100644 tests/StatusAggregator.Tests/Export/EventExporterTests.cs create mode 100644 tests/StatusAggregator.Tests/Export/EventsExporterTests.cs create mode 100644 tests/StatusAggregator.Tests/Export/StatusExporterTests.cs create mode 100644 tests/StatusAggregator.Tests/Export/StatusSerializerTests.cs create mode 100644 tests/StatusAggregator.Tests/Factory/AggregationProviderTests.cs create mode 100644 tests/StatusAggregator.Tests/Factory/AggregationStrategyTests.cs create mode 100644 tests/StatusAggregator.Tests/Factory/EventAffectedPathProviderTests.cs create mode 100644 tests/StatusAggregator.Tests/Factory/EventFactoryTests.cs create mode 100644 tests/StatusAggregator.Tests/Factory/IncidentAffectedComponentPathProviderTests.cs create mode 100644 tests/StatusAggregator.Tests/Factory/IncidentFactoryTests.cs create mode 100644 tests/StatusAggregator.Tests/Factory/IncidentGroupFactoryTests.cs create mode 100644 tests/StatusAggregator.Tests/Factory/NuGetServiceComponentFactoryTests.cs delete mode 100644 tests/StatusAggregator.Tests/IncidentFactoryTests.cs create mode 100644 tests/StatusAggregator.Tests/Messages/IncidentGroupMessageFilterTests.cs create mode 100644 tests/StatusAggregator.Tests/Messages/MessageChangeEventIteratorTests.cs create mode 100644 tests/StatusAggregator.Tests/Messages/MessageChangeEventProcessorTests.cs create mode 100644 tests/StatusAggregator.Tests/Messages/MessageChangeEventProviderTests.cs create mode 100644 tests/StatusAggregator.Tests/Messages/MessageContentBuilderTests.cs create mode 100644 tests/StatusAggregator.Tests/Messages/MessageFactoryTests.cs create mode 100644 tests/StatusAggregator.Tests/Parse/AggregateIncidentParserTests.cs create mode 100644 tests/StatusAggregator.Tests/Parse/EnvironmentPrefixIncidentRegexParsingHandlerTests.cs create mode 100644 tests/StatusAggregator.Tests/Parse/EnvironmentRegexParsingFilterTests.cs create mode 100644 tests/StatusAggregator.Tests/Parse/IncidentRegexParserTests.cs create mode 100644 tests/StatusAggregator.Tests/Parse/OutdatedSearchServiceInstanceIncidentRegexParsingHandlerTests.cs create mode 100644 tests/StatusAggregator.Tests/Parse/ParsingUtility.cs create mode 100644 tests/StatusAggregator.Tests/Parse/PingdomIncidentRegexParsingHandlerTests.cs create mode 100644 tests/StatusAggregator.Tests/Parse/SeverityRegexParsingFilterTests.cs create mode 100644 tests/StatusAggregator.Tests/Parse/TrafficManagerEndpointStatusIncidentRegexParsingHandlerTests.cs create mode 100644 tests/StatusAggregator.Tests/Parse/ValidationDurationIncidentRegexParsingHandlerTests.cs create mode 100644 tests/StatusAggregator.Tests/TestComponent.cs create mode 100644 tests/StatusAggregator.Tests/TestUtility/MockTableWrapperExtensions.cs create mode 100644 tests/StatusAggregator.Tests/Update/ActiveEventEntityUpdaterTests.cs create mode 100644 tests/StatusAggregator.Tests/Update/AggregationEntityUpdaterTests.cs create mode 100644 tests/StatusAggregator.Tests/Update/EventMessagingUpdaterTests.cs create mode 100644 tests/StatusAggregator.Tests/Update/IncidentUpdaterTests.cs diff --git a/src/StatusAggregator/Export/EventExporter.cs b/src/StatusAggregator/Export/EventExporter.cs index eabf54942..86e81458a 100644 --- a/src/StatusAggregator/Export/EventExporter.cs +++ b/src/StatusAggregator/Export/EventExporter.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using NuGet.Jobs.Extensions; diff --git a/src/StatusAggregator/Export/IEventExporter.cs b/src/StatusAggregator/Export/IEventExporter.cs index a92761e19..3e924f384 100644 --- a/src/StatusAggregator/Export/IEventExporter.cs +++ b/src/StatusAggregator/Export/IEventExporter.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Collections.Generic; using NuGet.Services.Status; using NuGet.Services.Status.Table; diff --git a/src/StatusAggregator/Factory/AggregationStrategy.cs b/src/StatusAggregator/Factory/AggregationStrategy.cs index 9615669b1..93d2cddd6 100644 --- a/src/StatusAggregator/Factory/AggregationStrategy.cs +++ b/src/StatusAggregator/Factory/AggregationStrategy.cs @@ -47,7 +47,7 @@ public async Task CanBeAggregatedByAsync(ParsedIncident input, TAggregatio // To guarantee that the aggregation reflects the latest information and is actually active, we must update it. await _aggregationUpdater.UpdateAsync(aggregationEntity, input.StartTime); - if (!aggregationEntity.IsActive || input.IsActive) + if (!aggregationEntity.IsActive && input.IsActive) { _logger.LogInformation("Cannot link entity to aggregation because it has been deactivated and the incident has not been."); return false; diff --git a/src/StatusAggregator/Job.cs b/src/StatusAggregator/Job.cs index b8cedf87c..35df0145a 100644 --- a/src/StatusAggregator/Job.cs +++ b/src/StatusAggregator/Job.cs @@ -11,6 +11,7 @@ using Autofac.Core; using Autofac.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.WindowsAzure.Storage; using Newtonsoft.Json.Linq; using NuGet.Jobs; @@ -46,6 +47,7 @@ public override void Init(IServiceContainer serviceContainer, IDictionary(); - serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); serviceCollection.AddTransient(); } @@ -249,6 +251,18 @@ private static void AddEventUpdater(ContainerBuilder containerBuilder) .As>(); } + private static void AddIncidentRegexParser(ContainerBuilder containerBuilder) + { + containerBuilder + .RegisterAdapter( + (ctx, handler) => + { + return new IncidentRegexParser( + handler, + ctx.Resolve>()); + }); + } + private static void AddEntityCollector(ContainerBuilder containerBuilder) { containerBuilder diff --git a/src/StatusAggregator/Messages/IMessageContentBuilder.cs b/src/StatusAggregator/Messages/IMessageContentBuilder.cs index 3ec1b59bd..86b4a876d 100644 --- a/src/StatusAggregator/Messages/IMessageContentBuilder.cs +++ b/src/StatusAggregator/Messages/IMessageContentBuilder.cs @@ -12,23 +12,13 @@ namespace StatusAggregator.Messages public interface IMessageContentBuilder { /// - /// Tries to get contents for a message of type affecting . + /// Builds contents for a message of type affecting . /// - /// The content of the message. - /// - /// True if contents for the message can be generated. - /// False otherwise. - /// - string GetContentsForMessageHelper(MessageType type, IComponent component); + string Build(MessageType type, IComponent component); /// - /// Tries to get contents for a message of type affecting with status . + /// Builds contents for a message of type affecting with status . /// - /// The content of the message. - /// - /// True if contents for the message can be generated. - /// False otherwise. - /// - string GetContentsForMessageHelper(MessageType type, IComponent component, ComponentStatus status); + string Build(MessageType type, IComponent component, ComponentStatus status); } } \ No newline at end of file diff --git a/src/StatusAggregator/Messages/IMessageFactory.cs b/src/StatusAggregator/Messages/IMessageFactory.cs index 4e8b1ffca..3784893fb 100644 --- a/src/StatusAggregator/Messages/IMessageFactory.cs +++ b/src/StatusAggregator/Messages/IMessageFactory.cs @@ -16,12 +16,12 @@ public interface IMessageFactory /// /// Creates a message for at of type affecting . /// - Task CreateMessageAsync(EventEntity eventEntity, DateTime time, MessageType type, IComponent component); + Task CreateMessageAsync(EventEntity eventEntity, DateTime time, MessageType type, IComponent component); /// /// Creates a message for at of type affecting with status . /// - Task CreateMessageAsync(EventEntity eventEntity, DateTime time, MessageType type, IComponent component, ComponentStatus status); + Task CreateMessageAsync(EventEntity eventEntity, DateTime time, MessageType type, IComponent component, ComponentStatus status); /// /// Updates the message for at of type affecting . diff --git a/src/StatusAggregator/Messages/MessageChangeEventProvider.cs b/src/StatusAggregator/Messages/MessageChangeEventProvider.cs index 4f6c518c1..990a50c14 100644 --- a/src/StatusAggregator/Messages/MessageChangeEventProvider.cs +++ b/src/StatusAggregator/Messages/MessageChangeEventProvider.cs @@ -49,7 +49,7 @@ public IEnumerable Get(EventEntity eventEntity, DateTime cur var startTime = linkedGroup.StartTime; _logger.LogInformation("Incident group started at {StartTime}.", startTime); events.Add(new MessageChangeEvent(startTime, path, status, MessageType.Start)); - if (linkedGroup.EndTime.HasValue) + if (!linkedGroup.IsActive) { var endTime = linkedGroup.EndTime.Value; _logger.LogInformation("Incident group ended at {EndTime}.", endTime); diff --git a/src/StatusAggregator/Messages/MessageContentBuilder.cs b/src/StatusAggregator/Messages/MessageContentBuilder.cs index a7cdcba3e..4cd529f2f 100644 --- a/src/StatusAggregator/Messages/MessageContentBuilder.cs +++ b/src/StatusAggregator/Messages/MessageContentBuilder.cs @@ -21,22 +21,22 @@ public MessageContentBuilder(ILogger logger) _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public string GetContentsForMessageHelper( + public string Build( MessageType type, IComponent component) { - return GetContentsForMessageHelper(type, component, component.Status); + return Build(type, component, component.Status); } - public string GetContentsForMessageHelper( + public string Build( MessageType type, IComponent component, ComponentStatus status) { - return GetContentsForMessageHelper(type, component.Path, status); + return Build(type, component.Path, status); } - private string GetContentsForMessageHelper( + private string Build( MessageType type, string path, ComponentStatus status) @@ -51,8 +51,8 @@ private string GetContentsForMessageHelper( _logger.LogInformation("Using template {MessageTemplate}.", messageTemplate); - var componentName = GetPrettyName(path); - _logger.LogInformation("Using {ComponentName} for name of component.", componentName); + var nameString = GetName(path); + _logger.LogInformation("Using {ComponentName} for name of component.", nameString); var actionDescription = GetActionDescriptionFromPath(path); if (actionDescription == null) @@ -60,14 +60,14 @@ private string GetContentsForMessageHelper( throw new ArgumentException("Could not find an action description for path.", nameof(path)); } - var componentStatus = status.ToString().ToLowerInvariant(); - var contents = string.Format(messageTemplate, componentName, componentStatus, actionDescription); + var statusString = status.ToString().ToLowerInvariant(); + var contents = string.Format(messageTemplate, nameString, statusString, actionDescription); _logger.LogInformation("Returned {Contents} for contents of message.", contents); return contents; } } - private string GetPrettyName(string path) + private string GetName(string path) { var componentNames = ComponentUtility.GetNames(path); return string.Join(" ", componentNames.Skip(1).Reverse()); @@ -83,7 +83,7 @@ private string GetActionDescriptionFromPath(string path) { return _actionDescriptionForComponentPathMap .FirstOrDefault(m => m.Matches(path))? - .ActionDescription; ; + .ActionDescription; } /// diff --git a/src/StatusAggregator/Messages/MessageFactory.cs b/src/StatusAggregator/Messages/MessageFactory.cs index 7c1415244..0f6f678a2 100644 --- a/src/StatusAggregator/Messages/MessageFactory.cs +++ b/src/StatusAggregator/Messages/MessageFactory.cs @@ -28,12 +28,12 @@ public MessageFactory( _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public Task CreateMessageAsync(EventEntity eventEntity, DateTime time, MessageType type, IComponent component) + public Task CreateMessageAsync(EventEntity eventEntity, DateTime time, MessageType type, IComponent component) { return CreateMessageAsync(eventEntity, time, type, component, component.Status); } - public async Task CreateMessageAsync(EventEntity eventEntity, DateTime time, MessageType type, IComponent component, ComponentStatus status) + public async Task CreateMessageAsync(EventEntity eventEntity, DateTime time, MessageType type, IComponent component, ComponentStatus status) { using (_logger.Scope("Creating new message of type {Type} for event {EventRowKey} at {Timestamp} affecting {ComponentPath} with status {ComponentStatus}.", type, eventEntity.RowKey, time, component.Path, status)) @@ -42,15 +42,14 @@ public async Task CreateMessageAsync(EventEntity eventEntity, Dat if (existingMessage != null) { _logger.LogInformation("Message already exists, will not recreate."); - return existingMessage; + return; } - var contents = _builder.GetContentsForMessageHelper(type, component, status); + var contents = _builder.Build(type, component, status); var messageEntity = new MessageEntity(eventEntity, time, contents, type); _logger.LogInformation("Creating message with time {MessageTimestamp} and contents {MessageContents}.", messageEntity.Time, messageEntity.Contents); await _table.InsertAsync(messageEntity); - return messageEntity; } } @@ -81,7 +80,7 @@ public async Task UpdateMessageAsync(EventEntity eventEntity, DateTime time, Mes return; } - var newContents = _builder.GetContentsForMessageHelper(type, component); + var newContents = _builder.Build(type, component); _logger.LogInformation("Replacing contents of message with time {MessageTimestamp} and contents {OldMessageContents} with {NewMessageContents}.", existingMessage.Time, existingMessage.Contents, newContents); existingMessage.Contents = newContents; diff --git a/src/StatusAggregator/Parse/EnvironmentPrefixIncidentParser.cs b/src/StatusAggregator/Parse/EnvironmentPrefixIncidentParser.cs deleted file mode 100644 index 85685c72c..000000000 --- a/src/StatusAggregator/Parse/EnvironmentPrefixIncidentParser.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StatusAggregator.Parse -{ - /// - /// Subclass of that expects s are prefixed with "[ENVIRONMENT]". - /// - public abstract class EnvironmentPrefixIncidentParser : IncidentParser - { - public EnvironmentPrefixIncidentParser( - string subtitleRegEx, - IEnumerable filters, - ILogger logger) - : base(GetRegEx(subtitleRegEx), filters, logger) - { - if (!filters.Any(f => f is EnvironmentFilter)) - { - throw new ArgumentException($"A {nameof(EnvironmentPrefixIncidentParser)} must be run with an {nameof(EnvironmentFilter)}!", nameof(filters)); - } - } - - private static string GetRegEx(string subtitleRegEx) - { - return $@"\[(?<{EnvironmentFilter.EnvironmentGroupName}>.*)\] {subtitleRegEx}"; - } - } -} diff --git a/src/StatusAggregator/Parse/EnvironmentPrefixIncidentRegexParsingHandler.cs b/src/StatusAggregator/Parse/EnvironmentPrefixIncidentRegexParsingHandler.cs new file mode 100644 index 000000000..7e4305314 --- /dev/null +++ b/src/StatusAggregator/Parse/EnvironmentPrefixIncidentRegexParsingHandler.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using NuGet.Services.Incidents; + +namespace StatusAggregator.Parse +{ + /// + /// Subclass of that expects s are prefixed with "[ENVIRONMENT]". + /// + public abstract class EnvironmentPrefixIncidentRegexParsingHandler : IncidentRegexParsingHandler + { + public EnvironmentPrefixIncidentRegexParsingHandler( + string subtitleRegEx, + IEnumerable filters) + : base( + PrependEnvironmentRegexGroup(subtitleRegEx), + filters) + { + if (!filters.Any(f => f is EnvironmentRegexParsingFilter)) + { + throw new ArgumentException( + $"A {nameof(EnvironmentPrefixIncidentRegexParsingHandler)} must be run with an {nameof(EnvironmentRegexParsingFilter)}!", + nameof(filters)); + } + } + + private static string PrependEnvironmentRegexGroup(string subtitleRegEx) + { + return $@"\[(?<{EnvironmentRegexParsingFilter.EnvironmentGroupName}>.*)\] {subtitleRegEx}"; + } + } +} diff --git a/src/StatusAggregator/Parse/EnvironmentFilter.cs b/src/StatusAggregator/Parse/EnvironmentRegexParsingFilter.cs similarity index 87% rename from src/StatusAggregator/Parse/EnvironmentFilter.cs rename to src/StatusAggregator/Parse/EnvironmentRegexParsingFilter.cs index 8e9d147aa..d267b5ef6 100644 --- a/src/StatusAggregator/Parse/EnvironmentFilter.cs +++ b/src/StatusAggregator/Parse/EnvironmentRegexParsingFilter.cs @@ -13,17 +13,17 @@ namespace StatusAggregator.Parse /// /// Expects that the contains a named with a whitelisted value. /// - public class EnvironmentFilter : IIncidentParsingFilter + public class EnvironmentRegexParsingFilter : IIncidentRegexParsingFilter { public const string EnvironmentGroupName = "Environment"; private IEnumerable _environments { get; } - private readonly ILogger _logger; + private readonly ILogger _logger; - public EnvironmentFilter( + public EnvironmentRegexParsingFilter( StatusAggregatorConfiguration configuration, - ILogger logger) + ILogger logger) { _environments = configuration?.Environments ?? throw new ArgumentNullException(nameof(configuration)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); diff --git a/src/StatusAggregator/Parse/IIncidentParsingFilter.cs b/src/StatusAggregator/Parse/IIncidentRegexParsingFilter.cs similarity index 73% rename from src/StatusAggregator/Parse/IIncidentParsingFilter.cs rename to src/StatusAggregator/Parse/IIncidentRegexParsingFilter.cs index efed05e9d..cf68cd7b3 100644 --- a/src/StatusAggregator/Parse/IIncidentParsingFilter.cs +++ b/src/StatusAggregator/Parse/IIncidentRegexParsingFilter.cs @@ -7,12 +7,12 @@ namespace StatusAggregator.Parse { /// - /// An additional filter that can be applied to a + /// An additional filter that can be applied to a /// - public interface IIncidentParsingFilter + public interface IIncidentRegexParsingFilter { /// - /// Returns whether or not an should parse . + /// Returns whether or not an should parse . /// bool ShouldParse(Incident incident, GroupCollection groups); } diff --git a/src/StatusAggregator/Parse/IIncidentRegexParsingHandler.cs b/src/StatusAggregator/Parse/IIncidentRegexParsingHandler.cs new file mode 100644 index 000000000..f25a72e41 --- /dev/null +++ b/src/StatusAggregator/Parse/IIncidentRegexParsingHandler.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Text.RegularExpressions; +using NuGet.Services.Incidents; +using NuGet.Services.Status; + +namespace StatusAggregator.Parse +{ + public interface IIncidentRegexParsingHandler + { + string RegexPattern { get; } + IReadOnlyCollection Filters { get; } + + /// + /// Attempts to parse a from . + /// + /// + /// The parsed from or null if could not be parsed. + /// + /// + /// true if a can be parsed from and false otherwise. + /// + bool TryParseAffectedComponentPath(Incident incident, GroupCollection groups, out string affectedComponentPath); + + /// + /// Attempts to parse a from . + /// + /// + /// The parsed from or if could not be parsed. + /// + /// true if a can be parsed from and false otherwise. + /// + bool TryParseAffectedComponentStatus(Incident incident, GroupCollection groups, out ComponentStatus affectedComponentStatus); + } +} diff --git a/src/StatusAggregator/Parse/IncidentParser.cs b/src/StatusAggregator/Parse/IncidentRegexParser.cs similarity index 51% rename from src/StatusAggregator/Parse/IncidentParser.cs rename to src/StatusAggregator/Parse/IncidentRegexParser.cs index 86745c693..99bd42991 100644 --- a/src/StatusAggregator/Parse/IncidentParser.cs +++ b/src/StatusAggregator/Parse/IncidentRegexParser.cs @@ -4,58 +4,44 @@ using Microsoft.Extensions.Logging; using NuGet.Jobs.Extensions; using NuGet.Services.Incidents; -using NuGet.Services.Status; using System; -using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace StatusAggregator.Parse { /// - /// Abstract implementation of that allows specifying a to analyze s with. + /// Implementation of that uses to parse s with. /// - public abstract class IncidentParser : IIncidentParser + public class IncidentRegexParser : IIncidentParser { private readonly static TimeSpan MaxRegexExecutionTime = TimeSpan.FromSeconds(5); - private readonly string _regExPattern; + private readonly IIncidentRegexParsingHandler _handler; - private readonly IEnumerable _filters; - - private readonly ILogger _logger; - - public IncidentParser( - string regExPattern, - ILogger logger) + private readonly ILogger _logger; + + public IncidentRegexParser( + IIncidentRegexParsingHandler handler, + ILogger logger) { - _regExPattern = regExPattern ?? throw new ArgumentNullException(nameof(regExPattern)); - _filters = Enumerable.Empty(); + _handler = handler ?? throw new ArgumentNullException(nameof(_handler)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public IncidentParser( - string regExPattern, - IEnumerable filters, - ILogger logger) - : this(regExPattern, logger) - { - _filters = filters?.ToList() ?? throw new ArgumentNullException(nameof(filters)); - } - public bool TryParseIncident(Incident incident, out ParsedIncident parsedIncident) { var title = incident.Title; using (_logger.Scope("Using parser {IncidentParserType} with pattern {RegExPattern} to parse incident with title {IncidentTitle}", - GetType(), _regExPattern, title)) + GetType(), _handler.RegexPattern, title)) { parsedIncident = null; Match match = null; try { - match = Regex.Match(title, _regExPattern, RegexOptions.None, MaxRegexExecutionTime); + match = Regex.Match(title, _handler.RegexPattern, RegexOptions.None, MaxRegexExecutionTime); } catch (Exception e) { @@ -65,6 +51,7 @@ public bool TryParseIncident(Incident incident, out ParsedIncident parsedInciden if (match == null) { + // According to its documentation, Regex.Match shouldn't return null, but this if statement is in here as a precaution. _logger.LogError("Parsed incident using regex successfully, but was unable to get match information!"); return false; } @@ -78,7 +65,7 @@ private bool TryParseIncident(Incident incident, GroupCollection groups, out Par { parsedIncident = null; - if (_filters.Any(f => + if (_handler.Filters.Any(f => { using (_logger.Scope("Filtering incident using filter {IncidentFilterType}", f.GetType())) { @@ -92,7 +79,7 @@ private bool TryParseIncident(Incident incident, GroupCollection groups, out Par return false; } - if (!TryParseAffectedComponentPath(incident, groups, out var affectedComponentPath)) + if (!_handler.TryParseAffectedComponentPath(incident, groups, out var affectedComponentPath)) { _logger.LogInformation("Could not parse incident component path!"); return false; @@ -100,7 +87,7 @@ private bool TryParseIncident(Incident incident, GroupCollection groups, out Par _logger.LogInformation("Parsed affected component path {AffectedComponentPath}.", affectedComponentPath); - if (!TryParseAffectedComponentStatus(incident, groups, out var affectedComponentStatus)) + if (!_handler.TryParseAffectedComponentStatus(incident, groups, out var affectedComponentStatus)) { _logger.LogInformation("Could not parse incident component status!"); return false; @@ -111,26 +98,5 @@ private bool TryParseIncident(Incident incident, GroupCollection groups, out Par parsedIncident = new ParsedIncident(incident, affectedComponentPath, affectedComponentStatus); return true; } - - /// - /// Attempts to parse a from . - /// - /// - /// The parsed from or null if could not be parsed. - /// - /// - /// true if a can be parsed from and false otherwise. - /// - protected abstract bool TryParseAffectedComponentPath(Incident incident, GroupCollection groups, out string affectedComponentPath); - - /// - /// Attempts to parse a from . - /// - /// - /// The parsed from or if could not be parsed. - /// - /// true if a can be parsed from and false otherwise. - /// - protected abstract bool TryParseAffectedComponentStatus(Incident incident, GroupCollection groups, out ComponentStatus affectedComponentStatus); } } diff --git a/src/StatusAggregator/Parse/IncidentRegexParsingHandler.cs b/src/StatusAggregator/Parse/IncidentRegexParsingHandler.cs new file mode 100644 index 000000000..04c2939b7 --- /dev/null +++ b/src/StatusAggregator/Parse/IncidentRegexParsingHandler.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using NuGet.Services.Incidents; +using NuGet.Services.Status; + +namespace StatusAggregator.Parse +{ + public abstract class IncidentRegexParsingHandler : IIncidentRegexParsingHandler + { + public IncidentRegexParsingHandler( + string regexPattern, + IEnumerable filters) + { + RegexPattern = regexPattern ?? throw new ArgumentNullException(nameof(regexPattern)); + Filters = filters?.ToList() ?? throw new ArgumentNullException(nameof(filters)); + } + + public string RegexPattern { get; } + public IReadOnlyCollection Filters { get; } + + public abstract bool TryParseAffectedComponentPath(Incident incident, GroupCollection groups, out string affectedComponentPath); + public abstract bool TryParseAffectedComponentStatus(Incident incident, GroupCollection groups, out ComponentStatus affectedComponentStatus); + } +} diff --git a/src/StatusAggregator/Parse/OutdatedSearchServiceInstanceIncidentParser.cs b/src/StatusAggregator/Parse/OutdatedSearchServiceInstanceIncidentRegexParsingHandler.cs similarity index 54% rename from src/StatusAggregator/Parse/OutdatedSearchServiceInstanceIncidentParser.cs rename to src/StatusAggregator/Parse/OutdatedSearchServiceInstanceIncidentRegexParsingHandler.cs index 2e5e824cd..c0d640b25 100644 --- a/src/StatusAggregator/Parse/OutdatedSearchServiceInstanceIncidentParser.cs +++ b/src/StatusAggregator/Parse/OutdatedSearchServiceInstanceIncidentRegexParsingHandler.cs @@ -1,37 +1,37 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using NuGet.Services.Incidents; using NuGet.Services.Status; using StatusAggregator.Factory; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; namespace StatusAggregator.Parse { - public class OutdatedSearchServiceInstanceIncidentParser : EnvironmentPrefixIncidentParser + public class OutdatedSearchServiceInstanceIncidentRegexParsingHandler : EnvironmentPrefixIncidentRegexParsingHandler { private const string SubtitleRegEx = "All search service instances are using an outdated index!"; - public OutdatedSearchServiceInstanceIncidentParser( - IEnumerable filters, - ILogger logger) + public OutdatedSearchServiceInstanceIncidentRegexParsingHandler( + IEnumerable filters, + ILogger logger) : base( SubtitleRegEx, - filters.Where(f => !(f is SeverityFilter)), // The incident is always severity 4. - logger) + // The incident is always severity 4. + filters.Where(f => !(f is SeverityRegexParsingFilter))) { } - protected override bool TryParseAffectedComponentPath(Incident incident, GroupCollection groups, out string affectedComponentPath) + public override bool TryParseAffectedComponentPath(Incident incident, GroupCollection groups, out string affectedComponentPath) { affectedComponentPath = ComponentUtility.GetPath(NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.UploadName); return true; } - protected override bool TryParseAffectedComponentStatus(Incident incident, GroupCollection groups, out ComponentStatus affectedComponentStatus) + public override bool TryParseAffectedComponentStatus(Incident incident, GroupCollection groups, out ComponentStatus affectedComponentStatus) { affectedComponentStatus = ComponentStatus.Degraded; return true; diff --git a/src/StatusAggregator/Parse/PingdomIncidentParser.cs b/src/StatusAggregator/Parse/PingdomIncidentRegexParsingHandler.cs similarity index 86% rename from src/StatusAggregator/Parse/PingdomIncidentParser.cs rename to src/StatusAggregator/Parse/PingdomIncidentRegexParsingHandler.cs index d3ac3f546..21cdb4b32 100644 --- a/src/StatusAggregator/Parse/PingdomIncidentParser.cs +++ b/src/StatusAggregator/Parse/PingdomIncidentRegexParsingHandler.cs @@ -11,23 +11,23 @@ namespace StatusAggregator.Parse { - public class PingdomIncidentParser : IncidentParser + public class PingdomIncidentRegexParsingHandler : IncidentRegexParsingHandler { - private const string CheckNameGroupName = "CheckName"; - private const string CheckUrlGroupName = "CheckUrl"; + public const string CheckNameGroupName = "CheckName"; + public const string CheckUrlGroupName = "CheckUrl"; private static string SubtitleRegEx = $@"Pingdom check '(?<{CheckNameGroupName}>.*)' is failing! '(?<{CheckUrlGroupName}>.*)' is DOWN!"; - private readonly ILogger _logger; + private readonly ILogger _logger; - public PingdomIncidentParser( - IEnumerable filters, - ILogger logger) - : base(SubtitleRegEx, filters, logger) + public PingdomIncidentRegexParsingHandler( + IEnumerable filters, + ILogger logger) + : base(SubtitleRegEx, filters) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - protected override bool TryParseAffectedComponentPath(Incident incident, GroupCollection groups, out string affectedComponentPath) + public override bool TryParseAffectedComponentPath(Incident incident, GroupCollection groups, out string affectedComponentPath) { affectedComponentPath = null; @@ -96,7 +96,7 @@ protected override bool TryParseAffectedComponentPath(Incident incident, GroupCo return true; } - protected override bool TryParseAffectedComponentStatus(Incident incident, GroupCollection groups, out ComponentStatus affectedComponentStatus) + public override bool TryParseAffectedComponentStatus(Incident incident, GroupCollection groups, out ComponentStatus affectedComponentStatus) { affectedComponentStatus = ComponentStatus.Degraded; return true; diff --git a/src/StatusAggregator/Parse/SeverityFilter.cs b/src/StatusAggregator/Parse/SeverityRegexParsingFilter.cs similarity index 83% rename from src/StatusAggregator/Parse/SeverityFilter.cs rename to src/StatusAggregator/Parse/SeverityRegexParsingFilter.cs index 53346e5fc..eadaad92f 100644 --- a/src/StatusAggregator/Parse/SeverityFilter.cs +++ b/src/StatusAggregator/Parse/SeverityRegexParsingFilter.cs @@ -11,15 +11,15 @@ namespace StatusAggregator.Parse /// /// Expects that the severity of an must be lower than a threshold. /// - public class SeverityFilter : IIncidentParsingFilter + public class SeverityRegexParsingFilter : IIncidentRegexParsingFilter { private readonly int _maximumSeverity; - private readonly ILogger _logger; + private readonly ILogger _logger; - public SeverityFilter( + public SeverityRegexParsingFilter( StatusAggregatorConfiguration configuration, - ILogger logger) + ILogger logger) { _maximumSeverity = configuration?.MaximumSeverity ?? throw new ArgumentNullException(nameof(configuration)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); diff --git a/src/StatusAggregator/Parse/TrafficManagerEndpointStatusIncidentParser.cs b/src/StatusAggregator/Parse/TrafficManagerEndpointStatusIncidentRegexParsingHandler.cs similarity index 85% rename from src/StatusAggregator/Parse/TrafficManagerEndpointStatusIncidentParser.cs rename to src/StatusAggregator/Parse/TrafficManagerEndpointStatusIncidentRegexParsingHandler.cs index 7698ae2ce..9afbb0581 100644 --- a/src/StatusAggregator/Parse/TrafficManagerEndpointStatusIncidentParser.cs +++ b/src/StatusAggregator/Parse/TrafficManagerEndpointStatusIncidentRegexParsingHandler.cs @@ -11,29 +11,29 @@ namespace StatusAggregator.Parse { - public class TrafficManagerEndpointStatusIncidentParser : EnvironmentPrefixIncidentParser + public class TrafficManagerEndpointStatusIncidentRegexParsingHandler : EnvironmentPrefixIncidentRegexParsingHandler { - private const string DomainGroupName = "Domain"; - private const string TargetGroupName = "Target"; + public const string DomainGroupName = "Domain"; + public const string TargetGroupName = "Target"; private static string SubtitleRegEx = $"Traffic Manager for (?<{DomainGroupName}>.*) is reporting (?<{TargetGroupName}>.*) as not Online!"; - private readonly ILogger _logger; + private readonly ILogger _logger; - public TrafficManagerEndpointStatusIncidentParser( - IEnumerable filters, - ILogger logger) - : base(SubtitleRegEx, filters, logger) + public TrafficManagerEndpointStatusIncidentRegexParsingHandler( + IEnumerable filters, + ILogger logger) + : base(SubtitleRegEx, filters) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - protected override bool TryParseAffectedComponentPath(Incident incident, GroupCollection groups, out string affectedComponentPath) + public override bool TryParseAffectedComponentPath(Incident incident, GroupCollection groups, out string affectedComponentPath) { affectedComponentPath = null; var domain = groups[DomainGroupName].Value; var target = groups[TargetGroupName].Value; - var environment = groups[EnvironmentFilter.EnvironmentGroupName].Value; + var environment = groups[EnvironmentRegexParsingFilter.EnvironmentGroupName].Value; _logger.LogInformation("Domain is {Domain}, target is {Target}, environment is {Environment}.", domain, target, environment); if (EnvironmentToDomainToTargetToPath.TryGetValue(environment, out var domainToTargetToPath) && @@ -46,7 +46,7 @@ protected override bool TryParseAffectedComponentPath(Incident incident, GroupCo return affectedComponentPath != null; } - protected override bool TryParseAffectedComponentStatus(Incident incident, GroupCollection groups, out ComponentStatus affectedComponentStatus) + public override bool TryParseAffectedComponentStatus(Incident incident, GroupCollection groups, out ComponentStatus affectedComponentStatus) { affectedComponentStatus = ComponentStatus.Down; return true; @@ -92,7 +92,7 @@ protected override bool TryParseAffectedComponentStatus(Incident incident, Group { "nuget-dev-ussc-gallery.cloudapp.net", - GalleryUsncPath + GalleryUsscPath } } }, @@ -127,7 +127,7 @@ protected override bool TryParseAffectedComponentStatus(Incident incident, Group { "nuget-int-ussc-gallery.cloudapp.net", - GalleryUsncPath + GalleryUsscPath } } } @@ -147,7 +147,7 @@ protected override bool TryParseAffectedComponentStatus(Incident incident, Group { "nuget-prod-ussc-gallery.cloudapp.net", - GalleryUsncPath + GalleryUsscPath } } }, diff --git a/src/StatusAggregator/Parse/ValidationDurationIncidentParser.cs b/src/StatusAggregator/Parse/ValidationDurationIncidentRegexParsingHandler.cs similarity index 57% rename from src/StatusAggregator/Parse/ValidationDurationIncidentParser.cs rename to src/StatusAggregator/Parse/ValidationDurationIncidentRegexParsingHandler.cs index 1a459390e..2e347d03e 100644 --- a/src/StatusAggregator/Parse/ValidationDurationIncidentParser.cs +++ b/src/StatusAggregator/Parse/ValidationDurationIncidentRegexParsingHandler.cs @@ -10,24 +10,24 @@ namespace StatusAggregator.Parse { - public class ValidationDurationIncidentParser : EnvironmentPrefixIncidentParser + public class ValidationDurationIncidentRegexParsingHandler : EnvironmentPrefixIncidentRegexParsingHandler { private const string SubtitleRegEx = "Too many packages are stuck in the \"Validating\" state!"; - public ValidationDurationIncidentParser( - IEnumerable filters, - ILogger logger) - : base(SubtitleRegEx, filters, logger) + public ValidationDurationIncidentRegexParsingHandler( + IEnumerable filters, + ILogger logger) + : base(SubtitleRegEx, filters) { } - protected override bool TryParseAffectedComponentPath(Incident incident, GroupCollection groups, out string affectedComponentPath) + public override bool TryParseAffectedComponentPath(Incident incident, GroupCollection groups, out string affectedComponentPath) { affectedComponentPath = ComponentUtility.GetPath(NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.UploadName); return true; } - protected override bool TryParseAffectedComponentStatus(Incident incident, GroupCollection groups, out ComponentStatus affectedComponentStatus) + public override bool TryParseAffectedComponentStatus(Incident incident, GroupCollection groups, out ComponentStatus affectedComponentStatus) { affectedComponentStatus = ComponentStatus.Degraded; return true; diff --git a/src/StatusAggregator/StatusAggregator.csproj b/src/StatusAggregator/StatusAggregator.csproj index bedb433f4..72f02622d 100644 --- a/src/StatusAggregator/StatusAggregator.csproj +++ b/src/StatusAggregator/StatusAggregator.csproj @@ -74,7 +74,9 @@ - + + + @@ -109,26 +111,26 @@ - + - - - - + + + + - + - + - + - + diff --git a/src/StatusAggregator/Update/ActiveEventEntityUpdater.cs b/src/StatusAggregator/Update/ActiveEventEntityUpdater.cs index ea8a0dea6..7bc840c60 100644 --- a/src/StatusAggregator/Update/ActiveEventEntityUpdater.cs +++ b/src/StatusAggregator/Update/ActiveEventEntityUpdater.cs @@ -1,7 +1,8 @@ -using System; -using System.Collections.Generic; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NuGet.Jobs.Extensions; diff --git a/src/StatusAggregator/Update/AggregationEntityUpdater.cs b/src/StatusAggregator/Update/AggregationEntityUpdater.cs index fdc342855..15fdaab56 100644 --- a/src/StatusAggregator/Update/AggregationEntityUpdater.cs +++ b/src/StatusAggregator/Update/AggregationEntityUpdater.cs @@ -79,11 +79,10 @@ public async Task UpdateAsync(TAggregationEntity aggregationEntity, DateTime cur if (!hasActiveOrRecentChildren) { _logger.LogInformation("Deactivating aggregation because its children are inactive and too old."); - var lastEndTime = children - .Max(i => i.EndTime ?? DateTime.MinValue); + var lastEndTime = children.Max(i => i.EndTime.Value); aggregationEntity.EndTime = lastEndTime; - await _table.InsertOrReplaceAsync(aggregationEntity); + await _table.ReplaceAsync(aggregationEntity); } else { diff --git a/src/StatusAggregator/Messages/EventMessagingUpdater.cs b/src/StatusAggregator/Update/EventMessagingUpdater.cs similarity index 95% rename from src/StatusAggregator/Messages/EventMessagingUpdater.cs rename to src/StatusAggregator/Update/EventMessagingUpdater.cs index a99c0f444..db115d947 100644 --- a/src/StatusAggregator/Messages/EventMessagingUpdater.cs +++ b/src/StatusAggregator/Update/EventMessagingUpdater.cs @@ -6,9 +6,9 @@ using Microsoft.Extensions.Logging; using NuGet.Jobs.Extensions; using NuGet.Services.Status.Table; -using StatusAggregator.Update; +using StatusAggregator.Messages; -namespace StatusAggregator.Messages +namespace StatusAggregator.Update { public class EventMessagingUpdater : IComponentAffectingEntityUpdater { diff --git a/src/StatusAggregator/Update/EventUpdater.cs b/src/StatusAggregator/Update/EventUpdater.cs index bedd7edaf..a86b8a17e 100644 --- a/src/StatusAggregator/Update/EventUpdater.cs +++ b/src/StatusAggregator/Update/EventUpdater.cs @@ -4,7 +4,6 @@ using System; using System.Threading.Tasks; using NuGet.Services.Status.Table; -using StatusAggregator.Messages; namespace StatusAggregator.Update { diff --git a/src/StatusAggregator/Update/IncidentUpdater.cs b/src/StatusAggregator/Update/IncidentUpdater.cs index 9ba4358b1..6cc972be7 100644 --- a/src/StatusAggregator/Update/IncidentUpdater.cs +++ b/src/StatusAggregator/Update/IncidentUpdater.cs @@ -20,7 +20,6 @@ public class IncidentUpdater : IComponentAffectingEntityUpdater public IncidentUpdater( ITableWrapper table, IIncidentApiClient incidentApiClient, - StatusAggregatorConfiguration configuration, ILogger logger) { _table = table ?? throw new ArgumentNullException(nameof(table)); @@ -44,7 +43,7 @@ public async Task UpdateAsync(IncidentEntity entity, DateTime cursor) { _logger.LogInformation("Updated mitigation time of active incident to {MitigationTime}.", entity.EndTime); entity.EndTime = endTime; - await _table.InsertOrReplaceAsync(entity); + await _table.ReplaceAsync(entity); } } } diff --git a/tests/StatusAggregator.Tests/Collector/CursorTests.cs b/tests/StatusAggregator.Tests/Collector/CursorTests.cs new file mode 100644 index 000000000..40c7b1187 --- /dev/null +++ b/tests/StatusAggregator.Tests/Collector/CursorTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status.Table; +using StatusAggregator.Collector; +using StatusAggregator.Table; +using Xunit; + +namespace StatusAggregator.Tests.Collector +{ + public class CursorTests + { + public class TheGetMethod : CursorTest + { + [Fact] + public async Task ThrowsIfNameNull() + { + await Assert.ThrowsAsync(() => Cursor.Get(null)); + } + + [Fact] + public async Task ReturnsMinValueIfNotInTable() + { + Table + .Setup(x => x.RetrieveAsync(Name)) + .ReturnsAsync((CursorEntity)null); + + var result = await Cursor.Get(Name); + + Assert.Equal(DateTime.MinValue, result); + } + + [Fact] + public async Task ReturnsValueIfInTable() + { + var entity = new CursorEntity(Name, new DateTime(2018, 9, 11)); + + Table + .Setup(x => x.RetrieveAsync(Name)) + .ReturnsAsync(entity); + + var result = await Cursor.Get(Name); + + Assert.Equal(entity.Value, result); + } + } + + public class TheSetMethod : CursorTest + { + [Fact] + public async Task ThrowsIfNameNull() + { + await Assert.ThrowsAsync(() => Cursor.Set(null, new DateTime(2018, 9, 11))); + } + + [Fact] + public async Task InsertsOrReplacesExistingValueInTable() + { + var value = new DateTime(2018, 9, 11); + + Table + .Setup(x => x.InsertOrReplaceAsync( + It.Is(e => e.Name == Name && e.Value == value))) + .Returns(Task.CompletedTask) + .Verifiable(); + + await Cursor.Set(Name, value); + + Table.Verify(); + } + } + + public class CursorTest + { + public string Name => "name"; + public Mock Table { get; } + public Cursor Cursor { get; } + + public CursorTest() + { + Table = new Mock(); + + Cursor = new Cursor( + Table.Object, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Collector/EntityCollectorTests.cs b/tests/StatusAggregator.Tests/Collector/EntityCollectorTests.cs new file mode 100644 index 000000000..a42745615 --- /dev/null +++ b/tests/StatusAggregator.Tests/Collector/EntityCollectorTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Moq; +using StatusAggregator.Collector; +using Xunit; + +namespace StatusAggregator.Tests.Collector +{ + public class EntityCollectorTests + { + public class TheNameProperty : EntityCollectorTest + { + [Fact] + public void ReturnsProcessorName() + { + Assert.Equal(Name, Collector.Name); + } + } + + public class TheFetchLatestMethod : EntityCollectorTest + { + [Fact] + public async Task DoesNotSetValueIfProcessorReturnsNull() + { + Processor + .Setup(x => x.FetchSince(LastCursor)) + .ReturnsAsync((DateTime?)null); + + var result = await Collector.FetchLatest(); + + Assert.Equal(LastCursor, result); + + Cursor + .Verify( + x => x.Set(It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task SetsValueIfProcessorReturnsValue() + { + var nextCursor = new DateTime(2018, 9, 12); + + Processor + .Setup(x => x.FetchSince(LastCursor)) + .ReturnsAsync(nextCursor); + + Cursor + .Setup(x => x.Set(Name, nextCursor)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var result = await Collector.FetchLatest(); + + Assert.Equal(nextCursor, result); + + Cursor.Verify(); + } + } + + public class EntityCollectorTest + { + public string Name => "name"; + public DateTime LastCursor => new DateTime(2018, 9, 11); + public Mock Cursor { get; } + public Mock Processor { get; } + public EntityCollector Collector { get; } + + public EntityCollectorTest() + { + Cursor = new Mock(); + Cursor + .Setup(x => x.Get(Name)) + .ReturnsAsync(LastCursor); + + Processor = new Mock(); + Processor + .Setup(x => x.Name) + .Returns(Name); + + Collector = new EntityCollector( + Cursor.Object, + Processor.Object); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Collector/IncidentEntityCollectorProcessorTests.cs b/tests/StatusAggregator.Tests/Collector/IncidentEntityCollectorProcessorTests.cs new file mode 100644 index 000000000..f3dec4bc9 --- /dev/null +++ b/tests/StatusAggregator.Tests/Collector/IncidentEntityCollectorProcessorTests.cs @@ -0,0 +1,245 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Collector; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using Xunit; + +namespace StatusAggregator.Tests.Collector +{ + public class IncidentEntityCollectorProcessorTests + { + public class TheNameProperty : IncidentEntityCollectorProcessorTest + { + [Fact] + public void ReturnsIncidentsCollectorName() + { + Assert.Equal(IncidentEntityCollectorProcessor.IncidentsCollectorName, Processor.Name); + } + } + + public abstract class TheFetchSinceMethod : IncidentEntityCollectorProcessorTest + { + public abstract DateTime Cursor { get; } + + [Fact] + public async Task ReturnsNullIfNoIncidents() + { + SetupClientQuery(Cursor, Enumerable.Empty()); + + var result = await Processor.FetchSince(Cursor); + + Assert.Null(result); + + Parser + .Verify( + x => x.ParseIncident(It.IsAny()), + Times.Never()); + + Factory + .Verify( + x => x.CreateAsync(It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task DoesNotCreateIncidentsThatCannotBeParsed() + { + var unparsableIncident = new Incident() + { + CreateDate = Cursor + TimeSpan.FromMinutes(1) + }; + + SetupClientQuery( + Cursor, + new[] { unparsableIncident }); + + Parser + .Setup(x => x.ParseIncident(It.IsAny())) + .Returns(Enumerable.Empty()); + + var result = await Processor.FetchSince(Cursor); + + Assert.Equal(unparsableIncident.CreateDate, result); + + Factory + .Verify( + x => x.CreateAsync(It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task CreatesParsedIncidents() + { + var firstIncident = new Incident() + { + CreateDate = Cursor + TimeSpan.FromMinutes(1), + Source = new IncidentSourceData { CreateDate = Cursor + TimeSpan.FromMinutes(5) } + }; + + var secondIncident = new Incident() + { + CreateDate = Cursor + TimeSpan.FromMinutes(2), + Source = new IncidentSourceData { CreateDate = Cursor + TimeSpan.FromMinutes(6) } + }; + + var thirdIncident = new Incident() + { + CreateDate = Cursor + TimeSpan.FromMinutes(3), + Source = new IncidentSourceData { CreateDate = Cursor + TimeSpan.FromMinutes(4) } + }; + + var incidents = new[] { firstIncident, secondIncident, thirdIncident }; + + SetupClientQuery( + Cursor, + incidents); + + var firstFirstParsedIncident = new ParsedIncident(firstIncident, "", ComponentStatus.Up); + var firstSecondParsedIncident = new ParsedIncident(firstIncident, "", ComponentStatus.Up); + var secondFirstParsedIncident = new ParsedIncident(secondIncident, "", ComponentStatus.Up); + var secondSecondParsedIncident = new ParsedIncident(secondIncident, "", ComponentStatus.Up); + var thirdParsedIncident = new ParsedIncident(thirdIncident, "", ComponentStatus.Up); + + Parser + .Setup(x => x.ParseIncident(firstIncident)) + .Returns(new[] { firstFirstParsedIncident, firstSecondParsedIncident }); + + Parser + .Setup(x => x.ParseIncident(secondIncident)) + .Returns(new[] { secondFirstParsedIncident, secondSecondParsedIncident }); + + Parser + .Setup(x => x.ParseIncident(thirdIncident)) + .Returns(new[] { thirdParsedIncident }); + + var lastCreateDate = DateTime.MinValue; + Factory + .Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(new IncidentEntity()) + .Callback(incident => + { + var nextCreateDate = incident.StartTime; + Assert.True(nextCreateDate >= lastCreateDate); + lastCreateDate = nextCreateDate; + }); + + var result = await Processor.FetchSince(Cursor); + + Assert.Equal(incidents.Max(i => i.CreateDate), result); + + Factory + .Verify( + x => x.CreateAsync(firstFirstParsedIncident), + Times.Once()); + + Factory + .Verify( + x => x.CreateAsync(secondFirstParsedIncident), + Times.Once()); + } + } + + public class TheFetchSinceMethodAtMinValue : TheFetchSinceMethod + { + public override DateTime Cursor => DateTime.MinValue; + } + + public class TheFetchSinceMethodAtPresent : TheFetchSinceMethod + { + public override DateTime Cursor => new DateTime(2018, 9, 11); + + [Fact] + public async Task FiltersOutIncidentsBeforeOrAtCursor() + { + SetupClientQuery( + Cursor, + new[] + { + new Incident() + { + CreateDate = Cursor - TimeSpan.FromMinutes(1) + }, + + new Incident() + { + CreateDate = Cursor + } + }); + + var result = await Processor.FetchSince(Cursor); + + Assert.Null(result); + + Parser + .Verify( + x => x.ParseIncident(It.IsAny()), + Times.Never()); + + Factory + .Verify( + x => x.CreateAsync(It.IsAny()), + Times.Never()); + } + } + + public class IncidentEntityCollectorProcessorTest + { + public string TeamId => "teamId"; + public Mock Parser { get; } + public Mock Client { get; } + public Mock> Factory { get; } + public IncidentEntityCollectorProcessor Processor { get; } + + public IncidentEntityCollectorProcessorTest() + { + Parser = new Mock(); + + Client = new Mock(); + + Factory = new Mock>(); + + var config = new StatusAggregatorConfiguration() + { + TeamId = TeamId + }; + + Processor = new IncidentEntityCollectorProcessor( + Client.Object, + Parser.Object, + Factory.Object, + config, + Mock.Of>()); + } + + public void SetupClientQuery(DateTime cursor, IEnumerable incidents) + { + Client + .Setup(x => x.GetIncidents(GetExpectedQuery(cursor))) + .ReturnsAsync(incidents); + } + + private string GetExpectedQuery(DateTime cursor) + { + var query = $"$filter=OwningTeamId eq '{TeamId}'"; + + if (cursor != DateTime.MinValue) + { + query += $" and CreateDate gt datetime'{cursor.ToString("o")}'"; + } + + return query; + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Collector/ManualStatusChangeCollectorProcessorTests.cs b/tests/StatusAggregator.Tests/Collector/ManualStatusChangeCollectorProcessorTests.cs new file mode 100644 index 000000000..8967ae558 --- /dev/null +++ b/tests/StatusAggregator.Tests/Collector/ManualStatusChangeCollectorProcessorTests.cs @@ -0,0 +1,143 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status.Table.Manual; +using StatusAggregator.Collector; +using StatusAggregator.Manual; +using StatusAggregator.Table; +using StatusAggregator.Tests.TestUtility; +using Xunit; + +namespace StatusAggregator.Tests.Collector +{ + public class ManualStatusChangeCollectorProcessorTests + { + public class TheNameProperty : ManualStatusChangeCollectorTest + { + [Fact] + public void ReturnsManualCollectorNamePrefixConcatenation() + { + Assert.Equal( + ManualStatusChangeCollectorProcessor.ManualCollectorNamePrefix + Name, + Processor.Name); + } + } + + public abstract class TheFetchSinceMethod : ManualStatusChangeCollectorTest + { + public abstract DateTime Cursor { get; } + + [Fact] + public async Task ReturnsNullIfNoManualChanges() + { + Table.SetupQuery(); + + var result = await Processor.FetchSince(Cursor); + + Assert.Null(result); + + Handler + .Verify( + x => x.Handle(It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task HandlesManualChanges() + { + var firstChange = new ManualStatusChangeEntity() + { + Timestamp = Cursor + TimeSpan.FromMinutes(1) + }; + + var secondChange = new ManualStatusChangeEntity() + { + Timestamp = Cursor + TimeSpan.FromMinutes(2) + }; + + Table.SetupQuery(secondChange, firstChange); + + var lastTimestamp = DateTimeOffset.MinValue; + Handler + .Setup(x => x.Handle(Table.Object, It.IsAny())) + .Returns(Task.CompletedTask) + .Callback((table, entity) => + { + var nextTimestamp = entity.Timestamp; + Assert.True(nextTimestamp > lastTimestamp); + lastTimestamp = nextTimestamp; + }); + + var result = await Processor.FetchSince(Cursor); + + Assert.Equal(secondChange.Timestamp.UtcDateTime, result); + + Handler + .Verify( + x => x.Handle(Table.Object, firstChange), + Times.Once()); + + Handler + .Verify( + x => x.Handle(Table.Object, secondChange), + Times.Once()); + } + } + + public class TheFetchSinceMethodAtMinValue : TheFetchSinceMethod + { + public override DateTime Cursor => DateTime.MinValue; + + [Fact] + public async Task DoesNotFilterByCursor() + { + var change = new ManualStatusChangeEntity() + { + Timestamp = DateTime.MinValue + }; + + Table.SetupQuery(change); + + Handler + .Setup(x => x.Handle(Table.Object, change)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var result = await Processor.FetchSince(Cursor); + + Assert.Equal(change.Timestamp.UtcDateTime, result); + + Handler.Verify(); + } + } + + public class TheFetchSinceMethodAtPresent : TheFetchSinceMethod + { + public override DateTime Cursor => new DateTime(2018, 9, 12); + } + + public class ManualStatusChangeCollectorTest + { + public string Name => "name"; + public Mock Table { get; } + public Mock Handler { get; } + public ManualStatusChangeCollectorProcessor Processor { get; } + + public ManualStatusChangeCollectorTest() + { + Table = new Mock(); + Handler = new Mock(); + + Processor = new ManualStatusChangeCollectorProcessor( + Name, + Table.Object, + Handler.Object, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/EventUpdaterTests.cs b/tests/StatusAggregator.Tests/EventUpdaterTests.cs deleted file mode 100644 index 6f7d49eb3..000000000 --- a/tests/StatusAggregator.Tests/EventUpdaterTests.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using NuGet.Services.Status.Table; -using StatusAggregator.Table; -using Xunit; - -namespace StatusAggregator.Tests -{ - public class EventUpdaterTests - { - private const string RowKey = "rowkey"; - private const int EventEndDelayMinutes = 5; - private static readonly TimeSpan EventEndDelay = TimeSpan.FromMinutes(EventEndDelayMinutes); - private static readonly DateTime NextCreationTime = new DateTime(2018, 7, 10); - - private static IEnumerable ClosableIncidents => new[] - { - CreateIncidentEntity(new DateTime(2018, 7, 9)), // Recently closed incident - CreateIncidentEntity(DateTime.MinValue), // Old incident - }; - - private static IEnumerable UnclosableIncidents => new[] - { - CreateIncidentEntity(NextCreationTime + EventEndDelay), // Incident closed too recently - CreateIncidentEntity() // Active incident - }; - - private Mock _tableWrapperMock { get; } - private Mock _messageUpdaterMock { get; } - private EventUpdater _eventUpdater { get; } - private EventEntity _eventEntity { get; } - - public EventUpdaterTests() - { - var configuration = new StatusAggregatorConfiguration() - { - EventEndDelayMinutes = EventEndDelayMinutes - }; - - _tableWrapperMock = new Mock(); - _messageUpdaterMock = new Mock(); - _eventUpdater = new EventUpdater( - _tableWrapperMock.Object, - _messageUpdaterMock.Object, - configuration, - Mock.Of>()); - - _eventEntity = new EventEntity() - { - RowKey = RowKey, - StartTime = DateTime.MinValue, - EndTime = null - }; - } - - [Fact] - public async Task ThrowsIfEventNull() - { - await Assert.ThrowsAsync(() => _eventUpdater.UpdateEvent(null, DateTime.MinValue)); - } - - [Fact] - public async Task ReturnsFalseIfNotActive() - { - _eventEntity.EndTime = DateTime.MinValue; - - var result = await _eventUpdater.UpdateEvent(_eventEntity, NextCreationTime); - - Assert.False(result); - } - - [Fact] - public async Task ReturnsFalseIfNoLinkedIncidents() - { - _tableWrapperMock - .Setup(x => x.CreateQuery()) - .Returns(new IncidentEntity[0].AsQueryable()); - - var result = await _eventUpdater.UpdateEvent(_eventEntity, NextCreationTime); - - Assert.False(result); - } - - public static IEnumerable DoesNotCloseEventIfUnclosableIncident_Data - { - get - { - foreach (var unclosableIncident in UnclosableIncidents) - { - yield return new object[] { unclosableIncident }; - } - } - } - - [Theory] - [MemberData(nameof(DoesNotCloseEventIfUnclosableIncident_Data))] - public async Task DoesNotCloseEventIfUnclosableIncident(IncidentEntity unclosableIncident) - { - _tableWrapperMock - .Setup(x => x.CreateQuery()) - .Returns(ClosableIncidents.Concat(new[] { unclosableIncident }).AsQueryable()); - - var result = await _eventUpdater.UpdateEvent(_eventEntity, NextCreationTime); - - Assert.False(result); - Assert.Null(_eventEntity.EndTime); - _messageUpdaterMock.Verify( - x => x.CreateMessageForEventStart(_eventEntity, NextCreationTime), - Times.Once()); - _messageUpdaterMock.Verify( - x => x.CreateMessageForEventEnd(It.IsAny()), - Times.Never()); - } - - [Fact] - public async Task ClosesEventIfClosableIncidents() - { - _tableWrapperMock - .Setup(x => x.CreateQuery()) - .Returns(ClosableIncidents.AsQueryable()); - - var result = await _eventUpdater.UpdateEvent(_eventEntity, NextCreationTime); - - var expectedEndTime = ClosableIncidents.Max(i => i.MitigationTime ?? DateTime.MinValue); - Assert.True(result); - Assert.Equal(expectedEndTime, _eventEntity.EndTime); - _tableWrapperMock.Verify( - x => x.InsertOrReplaceAsync(_eventEntity), - Times.Once()); - _messageUpdaterMock.Verify( - x => x.CreateMessageForEventStart(_eventEntity, expectedEndTime), - Times.Once()); - _messageUpdaterMock.Verify( - x => x.CreateMessageForEventEnd(_eventEntity), - Times.Once()); - } - - private static IncidentEntity CreateIncidentEntity(DateTime? mitigationTime = null) - { - return new IncidentEntity() - { - PartitionKey = IncidentEntity.DefaultPartitionKey, - EventRowKey = RowKey, - CreationTime = DateTime.MinValue, - MitigationTime = mitigationTime - }; - } - } -} diff --git a/tests/StatusAggregator.Tests/Export/ComponentExporterTests.cs b/tests/StatusAggregator.Tests/Export/ComponentExporterTests.cs new file mode 100644 index 000000000..8324d046c --- /dev/null +++ b/tests/StatusAggregator.Tests/Export/ComponentExporterTests.cs @@ -0,0 +1,178 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Export; +using StatusAggregator.Factory; +using StatusAggregator.Table; +using StatusAggregator.Tests.TestUtility; +using Xunit; + +namespace StatusAggregator.Tests.Export +{ + public class ComponentExporterTests + { + public class TheExportMethod : ComponentExporterTest + { + [Fact] + public void ReturnsUnaffectedComponentTreeWithNoEntities() + { + Table.SetupQuery(); + Table.SetupQuery(); + Table.SetupQuery(); + + var result = Exporter.Export(); + + Assert.All( + result.GetAllVisibleComponents(), + c => Assert.Equal(ComponentStatus.Up, c.Status)); + } + + [Fact] + public void ThrowsWithMissingPath() + { + var eventWithMessage = new EventEntity(Level2A.Path, DefaultStartTime, ComponentStatus.Degraded); + var messageForEventWithMessage = new MessageEntity(eventWithMessage, DefaultStartTime, "", MessageType.Manual); + var missingPathIncidentGroupForEventWithMessage = new IncidentGroupEntity(eventWithMessage, "missingPath", ComponentStatus.Degraded, DefaultStartTime); + + Table.SetupQuery(messageForEventWithMessage); + Table.SetupQuery(missingPathIncidentGroupForEventWithMessage); + Table.SetupQuery(eventWithMessage); + + Assert.Throws(() => Exporter.Export()); + } + + [Fact] + public void AppliesActiveEntitiesToComponentTree() + { + var eventWithMessage = new EventEntity(Level2A.Path, DefaultStartTime, ComponentStatus.Degraded); + var messageForEventWithMessage = new MessageEntity(eventWithMessage, DefaultStartTime, "", MessageType.Manual); + var degradedIncidentGroupForEventWithMessage = new IncidentGroupEntity(eventWithMessage, Level3AFrom2A.Path, ComponentStatus.Degraded, DefaultStartTime); + var downIncidentGroupForEventWithMessage = new IncidentGroupEntity(eventWithMessage, Level3AFrom2A.Path, ComponentStatus.Down, DefaultStartTime); + var upIncidentGroupForEventWithMessage = new IncidentGroupEntity(eventWithMessage, Level3AFrom2A.Path, ComponentStatus.Up, DefaultStartTime); + var inactiveIncidentGroupForEventWithMessage = new IncidentGroupEntity(eventWithMessage, Level3BFrom2A.Path, ComponentStatus.Degraded, DefaultStartTime, DefaultStartTime); + + var eventWithoutMessage = new EventEntity(Level2B.Path, DefaultStartTime, ComponentStatus.Degraded); + var incidentGroupForEventWithoutMessage = new IncidentGroupEntity(eventWithoutMessage, Level3AFrom2B.Path, ComponentStatus.Degraded, DefaultStartTime); + + var inactiveEventWithMessage = new EventEntity(Level2B.Path, DefaultStartTime + TimeSpan.FromDays(1), ComponentStatus.Degraded, DefaultStartTime + TimeSpan.FromDays(2)); + var messageForInactiveEventWithMessage = new MessageEntity(inactiveEventWithMessage, DefaultStartTime + TimeSpan.FromDays(1), "", MessageType.Manual); + var incidentGroupForInactiveEventWithMessage = new IncidentGroupEntity(inactiveEventWithMessage, Level3BFrom2B.Path, ComponentStatus.Degraded, DefaultStartTime + TimeSpan.FromDays(1)); + + Table.SetupQuery(messageForEventWithMessage, messageForInactiveEventWithMessage); + Table.SetupQuery( + degradedIncidentGroupForEventWithMessage, downIncidentGroupForEventWithMessage, upIncidentGroupForEventWithMessage, inactiveIncidentGroupForEventWithMessage, + incidentGroupForEventWithoutMessage, incidentGroupForInactiveEventWithMessage); + Table.SetupQuery(eventWithMessage, eventWithoutMessage, inactiveEventWithMessage); + + var result = Exporter.Export(); + + // Status of events with messages are applied. + AssertComponentStatus(ComponentStatus.Degraded, Level2A, eventWithMessage); + + // Most severe status affecting same component is applied. + AssertComponentStatus( + ComponentStatus.Down, + Level3AFrom2A, + degradedIncidentGroupForEventWithMessage, + downIncidentGroupForEventWithMessage, + upIncidentGroupForEventWithMessage); + + // Status of inactive incident groups are not applied. + AssertComponentStatus(ComponentStatus.Up, Level3BFrom2A, inactiveIncidentGroupForEventWithMessage); + + // Status of events without messages are not applied. + // Status of inactive events with messages are not applied. + AssertComponentStatus(ComponentStatus.Up, Level2B, eventWithoutMessage, inactiveEventWithMessage); + + // Status of incident groups for events without messages are not applied. + AssertComponentStatus(ComponentStatus.Up, Level3AFrom2B, incidentGroupForEventWithoutMessage); + + // Status of incident groups for inactive events with messages are not applied. + AssertComponentStatus(ComponentStatus.Up, Level3BFrom2B, incidentGroupForInactiveEventWithMessage); + } + + private void AssertComponentStatus(ComponentStatus expected, IComponent component, params ComponentAffectingEntity[] entities) + { + Assert.Equal(expected, component.Status); + Assert.Equal(component.Path, entities.First().AffectedComponentPath); + + for (var i = 1; i < entities.Count(); i++) + { + Assert.Equal( + entities[i - 1].AffectedComponentPath, + entities[i].AffectedComponentPath); + } + } + } + + public class ComponentExporterTest + { + public DateTime DefaultStartTime => new DateTime(2018, 9, 12); + + public IComponent Root { get; } + public IComponent Level2A { get; } + public IComponent Level2B { get; } + public IComponent Level3AFrom2A { get; } + public IComponent Level3BFrom2A { get; } + public IComponent Level3AFrom2B { get; } + public IComponent Level3BFrom2B { get; } + + public Mock Factory { get; } + public Mock Table { get; } + public ComponentExporter Exporter { get; } + + public ComponentExporterTest() + { + var level3AFrom2A = new TestComponent("3A"); + var level3BFrom2A = new TestComponent("3B"); + var level2A = new TestComponent("2A", + new[] { level3AFrom2A, level3BFrom2A }); + + var level3AFrom2B = new TestComponent("3A"); + var level3BFrom2B = new TestComponent("3B"); + var level2B = new TestComponent("2B", + new[] { level3AFrom2B, level3BFrom2B }); + + Root = new TestComponent("Root", new[] { level2A, level2B }); + + // We have to get the subcomponents by iterating through the tree. + // Components only have a path in the context of accessing them through a parent. + Level2A = Root + .SubComponents.Single(c => c.Name == "2A"); + Level3AFrom2A = Root + .SubComponents.Single(c => c.Name == "2A") + .SubComponents.Single(c => c.Name == "3A"); + Level3BFrom2A = Root + .SubComponents.Single(c => c.Name == "2A") + .SubComponents.Single(c => c.Name == "3B"); + + Level2B = Root + .SubComponents.Single(c => c.Name == "2B"); + Level3AFrom2B = Root + .SubComponents.Single(c => c.Name == "2B") + .SubComponents.Single(c => c.Name == "3A"); + Level3BFrom2B = Root + .SubComponents.Single(c => c.Name == "2B") + .SubComponents.Single(c => c.Name == "3B"); + + Factory = new Mock(); + Factory + .Setup(x => x.Create()) + .Returns(Root); + + Table = new Mock(); + + Exporter = new ComponentExporter( + Table.Object, + Factory.Object, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Export/EventExporterTests.cs b/tests/StatusAggregator.Tests/Export/EventExporterTests.cs new file mode 100644 index 000000000..c0d61f8cf --- /dev/null +++ b/tests/StatusAggregator.Tests/Export/EventExporterTests.cs @@ -0,0 +1,82 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Export; +using StatusAggregator.Table; +using StatusAggregator.Tests.TestUtility; +using Xunit; + +namespace StatusAggregator.Tests.Export +{ + public class EventExporterTests + { + public class TheExportMethod : EventMessageExporterTest + { + [Fact] + public void IgnoresEventsWithoutMessages() + { + var eventEntity = new EventEntity("", DefaultStartTime); + + Table.SetupQuery(); + + var result = Exporter.Export(EventEntity); + + Assert.Null(result); + } + + [Fact] + public void ExportsEventMessagesWithContent() + { + var differentEvent = new EventEntity("", DefaultStartTime + TimeSpan.FromDays(1)); + var differentEventMessage = new MessageEntity(differentEvent, DefaultStartTime, "", MessageType.Manual); + + var emptyMessage = new MessageEntity(EventEntity, DefaultStartTime, "", MessageType.Manual); + var firstMessage = new MessageEntity(EventEntity, DefaultStartTime, "hi", MessageType.Manual); + var secondMessage = new MessageEntity(EventEntity, DefaultStartTime + TimeSpan.FromDays(1), "hi", MessageType.Manual); + + Table.SetupQuery(differentEventMessage, secondMessage, firstMessage, emptyMessage); + + var result = Exporter.Export(EventEntity); + + Assert.Equal(EventEntity.AffectedComponentPath, result.AffectedComponentPath); + Assert.Equal(EventEntity.StartTime, result.StartTime); + Assert.Equal(EventEntity.EndTime, result.EndTime); + + Assert.Equal(2, result.Messages.Count()); + AssertMessageEqual(firstMessage, result.Messages.First()); + AssertMessageEqual(secondMessage, result.Messages.Last()); + } + + private void AssertMessageEqual(MessageEntity expected, Message actual) + { + Assert.Equal(expected.Time, actual.Time); + Assert.Equal(expected.Contents, actual.Contents); + } + } + + public class EventMessageExporterTest + { + public DateTime DefaultStartTime = new DateTime(2018, 9, 12); + public EventEntity EventEntity { get; } + public Mock Table { get; } + public EventExporter Exporter { get; } + + public EventMessageExporterTest() + { + EventEntity = new EventEntity("", DefaultStartTime); + + Table = new Mock(); + + Exporter = new EventExporter( + Table.Object, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Export/EventsExporterTests.cs b/tests/StatusAggregator.Tests/Export/EventsExporterTests.cs new file mode 100644 index 000000000..ba8add829 --- /dev/null +++ b/tests/StatusAggregator.Tests/Export/EventsExporterTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Export; +using StatusAggregator.Table; +using StatusAggregator.Tests.TestUtility; +using Xunit; + +namespace StatusAggregator.Tests.Export +{ + public class EventsExporterTests + { + public class TheExportMethod : EventsExporterTest + { + [Fact] + public void ExportsRecentEvents() + { + var oldEventEntity = new EventEntity("", + new DateTime(2017, 9, 12), + endTime: new DateTime(2017, 9, 13)); + + var activeEvent1Entity = new EventEntity("", + new DateTime(2017, 9, 12)); + var activeEvent2Entity = new EventEntity("", + Cursor); + + var recentEvent1Entity = new EventEntity("", + Cursor - EventVisibilityPeriod, + endTime: Cursor - EventVisibilityPeriod); + var recentEvent2Entity = new EventEntity("", + Cursor - EventVisibilityPeriod, + endTime: Cursor); + + Table.SetupQuery( + oldEventEntity, + activeEvent1Entity, + activeEvent2Entity, + recentEvent1Entity, + recentEvent2Entity); + + var eventForActiveEvent1 = new Event("", DateTime.MinValue, DateTime.MinValue, new[] { new Message(DateTime.MinValue, "") }); + IndividualExporter + .Setup(x => x.Export(activeEvent1Entity)) + .Returns(eventForActiveEvent1) + .Verifiable(); + + IndividualExporter + .Setup(x => x.Export(activeEvent2Entity)) + .Returns(null) + .Verifiable(); + + var eventForRecentEvent1 = new Event("", DateTime.MinValue, DateTime.MinValue, new[] { new Message(DateTime.MinValue, "") }); + IndividualExporter + .Setup(x => x.Export(recentEvent1Entity)) + .Returns(eventForRecentEvent1) + .Verifiable(); + + IndividualExporter + .Setup(x => x.Export(recentEvent2Entity)) + .Returns(null) + .Verifiable(); + + var result = Exporter.Export(Cursor); + + var expectedEvents = new[] { eventForActiveEvent1, eventForRecentEvent1 }; + Assert.Equal(expectedEvents.Count(), result.Count()); + foreach (var expectedEvent in expectedEvents) + { + Assert.Contains(expectedEvent, result); + } + + IndividualExporter.Verify(); + IndividualExporter + .Verify( + x => x.Export(oldEventEntity), + Times.Never()); + } + } + + public class EventsExporterTest + { + public DateTime Cursor => new DateTime(2018, 9, 12); + public TimeSpan EventVisibilityPeriod => TimeSpan.FromDays(10); + public Mock Table { get; } + public Mock IndividualExporter { get; } + public EventsExporter Exporter { get; } + + public EventsExporterTest() + { + Table = new Mock(); + + IndividualExporter = new Mock(); + + var config = new StatusAggregatorConfiguration() + { + EventVisibilityPeriodDays = EventVisibilityPeriod.Days + }; + + Exporter = new EventsExporter( + Table.Object, + IndividualExporter.Object, + config, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Export/StatusExporterTests.cs b/tests/StatusAggregator.Tests/Export/StatusExporterTests.cs new file mode 100644 index 000000000..d5437c077 --- /dev/null +++ b/tests/StatusAggregator.Tests/Export/StatusExporterTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status; +using StatusAggregator.Export; +using Xunit; + +namespace StatusAggregator.Tests.Export +{ + public class StatusExporterTests + { + public class TheExportMethod : StatusExporterTest + { + [Fact] + public async Task ExportsAtCursor() + { + var cursor = new DateTime(2018, 9, 13); + + var component = new TestComponent("hi"); + ComponentExporter + .Setup(x => x.Export()) + .Returns(component); + + var events = new Event[0]; + EventExporter + .Setup(x => x.Export(cursor)) + .Returns(events); + + await Exporter.Export(cursor); + + Serializer + .Verify( + x => x.Serialize(cursor, component, events), + Times.Once()); + } + } + + public class StatusExporterTest + { + public Mock ComponentExporter { get; } + public Mock EventExporter { get; } + public Mock Serializer { get; } + public StatusExporter Exporter { get; } + + public StatusExporterTest() + { + ComponentExporter = new Mock(); + EventExporter = new Mock(); + Serializer = new Mock(); + + Exporter = new StatusExporter( + ComponentExporter.Object, + EventExporter.Object, + Serializer.Object, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Export/StatusSerializerTests.cs b/tests/StatusAggregator.Tests/Export/StatusSerializerTests.cs new file mode 100644 index 000000000..62d501749 --- /dev/null +++ b/tests/StatusAggregator.Tests/Export/StatusSerializerTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json; +using NuGet.Services.Status; +using StatusAggregator.Container; +using StatusAggregator.Export; +using Xunit; + +namespace StatusAggregator.Tests.Export +{ + public class StatusSerializerTests + { + public class TheSerializeMethod : StatusSerializerTest + { + [Fact] + public async Task SerializesStatus() + { + var cursor = new DateTime(2018, 9, 13); + var component = new TestComponent("hi", new[] { new TestComponent("yo"), new TestComponent("what's up") }); + var events = new[] { new Event("", cursor, cursor, new[] { new Message(cursor, "howdy") }) }; + + var expectedStatus = new ServiceStatus(cursor, component, events); + var expectedJson = JsonConvert.SerializeObject(expectedStatus, StatusSerializer.Settings); + + await Serializer.Serialize(cursor, component, events); + + Container + .Verify( + x => x.SaveBlobAsync(StatusSerializer.StatusBlobName, expectedJson), + Times.Once()); + } + } + + public class StatusSerializerTest + { + public Mock Container { get; } + public StatusSerializer Serializer { get; } + + public StatusSerializerTest() + { + Container = new Mock(); + + Serializer = new StatusSerializer( + Container.Object, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Factory/AggregationProviderTests.cs b/tests/StatusAggregator.Tests/Factory/AggregationProviderTests.cs new file mode 100644 index 000000000..cdb72825f --- /dev/null +++ b/tests/StatusAggregator.Tests/Factory/AggregationProviderTests.cs @@ -0,0 +1,277 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using StatusAggregator.Table; +using StatusAggregator.Tests.TestUtility; +using Xunit; + +namespace StatusAggregator.Tests.Factory +{ + public class AggregationProviderTests + { + public class TheIncidentEntityGetAsyncMethod + : TheGetAsyncMethod + { + } + + public class TheIncidentGroupEntityGetAsyncMethod + : TheGetAsyncMethod + { + } + + public abstract class TheGetAsyncMethod + : AggregationProviderTest + where TAggregatedEntity : AggregatedComponentAffectingEntity, new() + where TEntityAggregation : ComponentAffectingEntity, new() + { + [Fact] + public async Task CreatesNewEntityIfNoPossibleAggregation() + { + var inputPath = "howdy"; + var input = new ParsedIncident(Incident, inputPath, ComponentStatus.Degraded); + + var providedPath = "hello"; + PathProvider + .Setup(x => x.Get(input)) + .Returns(providedPath); + + var aggregationWithDifferentPath = new TEntityAggregation + { + AffectedComponentPath = "other path", + StartTime = input.StartTime + }; + + var aggregationAfter = new TEntityAggregation + { + AffectedComponentPath = providedPath, + StartTime = input.StartTime + TimeSpan.FromDays(1) + }; + + var aggregationBefore = new TEntityAggregation + { + AffectedComponentPath = providedPath, + StartTime = input.StartTime - TimeSpan.FromDays(2), + EndTime = input.StartTime - TimeSpan.FromDays(1) + }; + + var activeAggregationToDeactivate = new TEntityAggregation + { + AffectedComponentPath = providedPath, + StartTime = input.StartTime + }; + + var inactiveAggregationToDeactivate = new TEntityAggregation + { + AffectedComponentPath = providedPath, + StartTime = input.StartTime, + EndTime = input.EndTime + }; + + Table.SetupQuery( + aggregationWithDifferentPath, + aggregationAfter, + aggregationBefore, + activeAggregationToDeactivate, + inactiveAggregationToDeactivate); + + Strategy + .Setup(x => x.CanBeAggregatedByAsync(input, activeAggregationToDeactivate)) + .ReturnsAsync(false); + + Strategy + .Setup(x => x.CanBeAggregatedByAsync(input, inactiveAggregationToDeactivate)) + .ReturnsAsync(false); + + var createdAggregation = new TEntityAggregation(); + AggregationFactory + .Setup(x => x.CreateAsync(input)) + .ReturnsAsync(createdAggregation); + + var result = await Provider.GetAsync(input); + + Assert.Equal(createdAggregation, result); + + Strategy + .Verify( + x => x.CanBeAggregatedByAsync(It.IsAny(), aggregationWithDifferentPath), + Times.Never()); + + Strategy + .Verify( + x => x.CanBeAggregatedByAsync(It.IsAny(), aggregationAfter), + Times.Never()); + + Strategy + .Verify( + x => x.CanBeAggregatedByAsync(It.IsAny(), aggregationBefore), + Times.Never()); + + Strategy + .Verify( + x => x.CanBeAggregatedByAsync(It.IsAny(), activeAggregationToDeactivate), + Times.Once()); + + Strategy + .Verify( + x => x.CanBeAggregatedByAsync(It.IsAny(), inactiveAggregationToDeactivate), + Times.Once()); + } + + [Fact] + public async Task ReturnsPossibleAggregation() + { + var inputPath = "howdy"; + var input = new ParsedIncident(Incident, inputPath, ComponentStatus.Degraded); + + var providedPath = "hello"; + PathProvider + .Setup(x => x.Get(input)) + .Returns(providedPath); + + var aggregationWithDifferentPath = new TEntityAggregation + { + AffectedComponentPath = "other path", + StartTime = input.StartTime + }; + + var aggregationAfter = new TEntityAggregation + { + AffectedComponentPath = providedPath, + StartTime = input.StartTime + TimeSpan.FromDays(1) + }; + + var aggregationBefore = new TEntityAggregation + { + AffectedComponentPath = providedPath, + StartTime = input.StartTime - TimeSpan.FromDays(2), + EndTime = input.StartTime - TimeSpan.FromDays(1) + }; + + var activeAggregationToDeactivate = new TEntityAggregation + { + AffectedComponentPath = providedPath, + StartTime = input.StartTime + }; + + var inactiveAggregationToDeactivate = new TEntityAggregation + { + AffectedComponentPath = providedPath, + StartTime = input.StartTime, + EndTime = input.EndTime + }; + + var activeAggregation = new TEntityAggregation + { + AffectedComponentPath = providedPath, + StartTime = input.StartTime + }; + + Table.SetupQuery( + aggregationWithDifferentPath, + aggregationAfter, + aggregationBefore, + activeAggregationToDeactivate, + inactiveAggregationToDeactivate, + activeAggregation); + + Strategy + .Setup(x => x.CanBeAggregatedByAsync(input, activeAggregationToDeactivate)) + .ReturnsAsync(false); + + Strategy + .Setup(x => x.CanBeAggregatedByAsync(input, inactiveAggregationToDeactivate)) + .ReturnsAsync(false); + + Strategy + .Setup(x => x.CanBeAggregatedByAsync(input, activeAggregation)) + .ReturnsAsync(true); + + var result = await Provider.GetAsync(input); + + Assert.Equal(activeAggregation, result); + + AggregationFactory + .Verify( + x => x.CreateAsync(input), + Times.Never()); + + Strategy + .Verify( + x => x.CanBeAggregatedByAsync(It.IsAny(), aggregationWithDifferentPath), + Times.Never()); + + Strategy + .Verify( + x => x.CanBeAggregatedByAsync(It.IsAny(), aggregationAfter), + Times.Never()); + + Strategy + .Verify( + x => x.CanBeAggregatedByAsync(It.IsAny(), aggregationBefore), + Times.Never()); + + Strategy + .Verify( + x => x.CanBeAggregatedByAsync(It.IsAny(), activeAggregationToDeactivate), + Times.Once()); + + Strategy + .Verify( + x => x.CanBeAggregatedByAsync(It.IsAny(), inactiveAggregationToDeactivate), + Times.Once()); + + Strategy + .Verify( + x => x.CanBeAggregatedByAsync(It.IsAny(), activeAggregation), + Times.Once()); + } + } + + public class AggregationProviderTest + where TAggregatedEntity : AggregatedComponentAffectingEntity, new() + where TEntityAggregation : ComponentAffectingEntity, new() + { + public Incident Incident = new Incident() + { + Source = new IncidentSourceData() + { + CreateDate = new DateTime(2018, 9, 13) + } + }; + + public Mock Table { get; } + public Mock> AggregationFactory { get; } + public Mock> PathProvider { get; } + public Mock> Strategy { get; } + + public AggregationProvider Provider { get; } + + public AggregationProviderTest() + { + Table = new Mock(); + + AggregationFactory = new Mock>(); + + PathProvider = new Mock>(); + + Strategy = new Mock>(); + + Provider = new AggregationProvider( + Table.Object, + PathProvider.Object, + Strategy.Object, + AggregationFactory.Object, + Mock.Of>>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Factory/AggregationStrategyTests.cs b/tests/StatusAggregator.Tests/Factory/AggregationStrategyTests.cs new file mode 100644 index 000000000..66ac4b4df --- /dev/null +++ b/tests/StatusAggregator.Tests/Factory/AggregationStrategyTests.cs @@ -0,0 +1,206 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using StatusAggregator.Table; +using StatusAggregator.Tests.TestUtility; +using StatusAggregator.Update; +using Xunit; + +namespace StatusAggregator.Tests.Factory +{ + public class AggregationStrategyTests + { + public class TheIncidentEntityCanBeAggregatedByAsyncMethod + : TheCanBeAggregatedByAsyncMethod + { + } + + public class TheIncidentGroupEntityCanBeAggregatedByAsyncMethod + : TheCanBeAggregatedByAsyncMethod + { + } + + public abstract class TheCanBeAggregatedByAsyncMethod + : AggregationStrategyTest + where TAggregatedEntity : AggregatedComponentAffectingEntity, new() + where TEntityAggregation : ComponentAffectingEntity, new() + { + [Fact] + public async Task ReturnsFalseIfNoLinkedEntities() + { + var aggregatedEntityLinkedToDifferentAggregation = new TAggregatedEntity + { + ParentRowKey = "wrongRowKey" + }; + + Table.SetupQuery(aggregatedEntityLinkedToDifferentAggregation); + + var incident = new Incident() + { + Source = new IncidentSourceData() + { + CreateDate = new DateTime(2018, 9, 13) + } + }; + + var input = new ParsedIncident(incident, "", ComponentStatus.Up); + + var result = await Strategy.CanBeAggregatedByAsync(input, Aggregation); + + Assert.False(result); + + Updater + .Verify( + x => x.UpdateAsync(It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ReturnsTrueIfAggregationStillActive(bool inputIsActive) + { + var aggregatedEntity = new TAggregatedEntity + { + ParentRowKey = AggregationRowKey + }; + + Table.SetupQuery(aggregatedEntity); + + var incident = new Incident() + { + Source = new IncidentSourceData() + { + CreateDate = new DateTime(2018, 9, 13) + } + }; + + if (!inputIsActive) + { + incident.MitigationData = new IncidentStateChangeEventData() + { + Date = new DateTime(2018, 10, 9) + }; + } + + var input = new ParsedIncident(incident, "", ComponentStatus.Up); + + Updater + .Setup(x => x.UpdateAsync(Aggregation, input.StartTime)) + .Returns(Task.CompletedTask); + + var result = await Strategy.CanBeAggregatedByAsync(input, Aggregation); + + Assert.True(result); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ReturnsTrueIfInputInactive(bool aggregationIsActive) + { + var aggregatedEntity = new TAggregatedEntity + { + ParentRowKey = AggregationRowKey + }; + + if (!aggregationIsActive) + { + Aggregation.EndTime = new DateTime(2018, 10, 9); + } + + Table.SetupQuery(aggregatedEntity); + + var incident = new Incident() + { + Source = new IncidentSourceData() + { + CreateDate = new DateTime(2018, 9, 13) + }, + + MitigationData = new IncidentStateChangeEventData() + { + Date = new DateTime(2018, 10, 9) + } + }; + + var input = new ParsedIncident(incident, "", ComponentStatus.Up); + + Updater + .Setup(x => x.UpdateAsync(Aggregation, input.StartTime)) + .Returns(Task.CompletedTask); + + var result = await Strategy.CanBeAggregatedByAsync(input, Aggregation); + + Assert.True(result); + } + + [Fact] + public async Task ReturnsFalseIfAggregationInactiveAndInputActive() + { + Aggregation.EndTime = new DateTime(2018, 10, 9); + + var aggregatedEntity = new TAggregatedEntity + { + ParentRowKey = AggregationRowKey + }; + + var incident = new Incident() + { + Source = new IncidentSourceData() + { + CreateDate = new DateTime(2018, 9, 13) + } + }; + + var input = new ParsedIncident(incident, "", ComponentStatus.Up); + + Table.SetupQuery(aggregatedEntity); + + Updater + .Setup(x => x.UpdateAsync(Aggregation, input.StartTime)) + .Returns(Task.CompletedTask); + + var result = await Strategy.CanBeAggregatedByAsync(input, Aggregation); + + Assert.False(result); + } + } + + public class AggregationStrategyTest + where TAggregatedEntity : AggregatedComponentAffectingEntity, new() + where TEntityAggregation : ComponentAffectingEntity, new() + { + public const string AggregationRowKey = "aggregationRowKey"; + public TEntityAggregation Aggregation = new TEntityAggregation() + { + RowKey = AggregationRowKey + }; + + public Mock Table { get; } + public Mock> Updater { get; } + public AggregationStrategy Strategy { get; } + + public AggregationStrategyTest() + { + Table = new Mock(); + + Updater = new Mock>(); + + Strategy = new AggregationStrategy( + Table.Object, + Updater.Object, + Mock.Of>>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Factory/EventAffectedPathProviderTests.cs b/tests/StatusAggregator.Tests/Factory/EventAffectedPathProviderTests.cs new file mode 100644 index 000000000..669abfcdf --- /dev/null +++ b/tests/StatusAggregator.Tests/Factory/EventAffectedPathProviderTests.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using Xunit; + +namespace StatusAggregator.Tests.Factory +{ + public class EventAffectedComponentPathProviderTests + { + public class TheGetMethod + : EventAffectedComponentPathProviderTest + { + public static IEnumerable GetsAffectedComponentPath_Data + { + get + { + var root = new NuGetServiceComponentFactory().Create(); + + yield return new object[] { root.Path, root.Path }; + + foreach (var subcomponent in root.SubComponents) + { + foreach (var subsubcomponent in GetAllComponents(subcomponent)) + { + yield return new object[] { subcomponent.Path, subsubcomponent.Path }; + } + } + } + } + + private static IEnumerable GetAllComponents(IComponent component) + { + if (component == null) + { + yield break; + } + + yield return component; + + foreach (var subcomponent in component.SubComponents.SelectMany(s => GetAllComponents(s))) + { + yield return subcomponent; + } + } + + [Theory] + [MemberData(nameof(GetsAffectedComponentPath_Data))] + public void GetsAffectedComponentPath(string expectedPath, string inputPath) + { + var incident = new Incident() + { + Source = new IncidentSourceData() + { + CreateDate = new DateTime(2018, 10, 10) + } + }; + + var input = new ParsedIncident(incident, inputPath, ComponentStatus.Up); + + var result = Provider.Get(input); + + Assert.Equal(expectedPath, result); + } + } + + public class EventAffectedComponentPathProviderTest + { + public EventAffectedComponentPathProvider Provider { get; } + + public EventAffectedComponentPathProviderTest() + { + Provider = new EventAffectedComponentPathProvider(); + } + } + } +} diff --git a/tests/StatusAggregator.Tests/Factory/EventFactoryTests.cs b/tests/StatusAggregator.Tests/Factory/EventFactoryTests.cs new file mode 100644 index 000000000..f6babae5c --- /dev/null +++ b/tests/StatusAggregator.Tests/Factory/EventFactoryTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAzure.Storage.Table; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using StatusAggregator.Table; +using Xunit; + +namespace StatusAggregator.Tests.Factory +{ + public class EventFactoryTests + { + public class TheCreateAsyncMethod : EventFactoryTest + { + [Fact] + public async Task CreatesEvent() + { + var input = new ParsedIncident(Incident, "somePath", ComponentStatus.Up); + + EventEntity entity = null; + Table + .Setup(x => x.InsertOrReplaceAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Callback(e => + { + Assert.IsType(e); + entity = e as EventEntity; + }); + + var aggregationPath = "thePath"; + Provider + .Setup(x => x.Get(input)) + .Returns(aggregationPath); + + var result = await Factory.CreateAsync(input); + + Assert.Equal(entity, result); + Assert.Equal(aggregationPath, entity.AffectedComponentPath); + Assert.Equal((int)ComponentStatus.Up, entity.AffectedComponentStatus); + Assert.Equal(input.StartTime, entity.StartTime); + + Table + .Verify( + x => x.InsertOrReplaceAsync(It.IsAny()), + Times.Once()); + } + } + + public class EventFactoryTest + { + public Incident Incident = new Incident() { + Source = new IncidentSourceData() { + CreateDate = new DateTime(2018, 9, 13) } }; + + public Mock Table { get; } + public Mock> Provider { get; } + public EventFactory Factory { get; } + + public EventFactoryTest() + { + Table = new Mock(); + + Provider = new Mock>(); + + Factory = new EventFactory( + Table.Object, + Provider.Object, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Factory/IncidentAffectedComponentPathProviderTests.cs b/tests/StatusAggregator.Tests/Factory/IncidentAffectedComponentPathProviderTests.cs new file mode 100644 index 000000000..6ed0382dd --- /dev/null +++ b/tests/StatusAggregator.Tests/Factory/IncidentAffectedComponentPathProviderTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using Xunit; + +namespace StatusAggregator.Tests.Factory +{ + public class IncidentAffectedComponentPathProviderTests + { + public class TheGetMethod + : IncidentAffectedComponentPathProviderTest + { + public static IEnumerable GetsAffectedComponentPath_Data + { + get + { + var root = new NuGetServiceComponentFactory().Create(); + return GetAllComponents(root).Select(c => new object[] { c.Path }); + } + } + + private static IEnumerable GetAllComponents(IComponent component) + { + if (component == null) + { + yield break; + } + + yield return component; + + foreach (var subcomponent in component.SubComponents.SelectMany(s => GetAllComponents(s))) + { + yield return subcomponent; + } + } + + [Theory] + [MemberData(nameof(GetsAffectedComponentPath_Data))] + public void GetsAffectedComponentPath(string path) + { + var incident = new Incident() + { + Source = new IncidentSourceData() + { + CreateDate = new DateTime(2018, 10, 10) + } + }; + + var input = new ParsedIncident(incident, path, ComponentStatus.Up); + + var result = Provider.Get(input); + + Assert.Equal(path, result); + } + } + + public class IncidentAffectedComponentPathProviderTest + { + public IncidentAffectedComponentPathProvider Provider { get; } + + public IncidentAffectedComponentPathProviderTest() + { + Provider = new IncidentAffectedComponentPathProvider(); + } + } + } +} diff --git a/tests/StatusAggregator.Tests/Factory/IncidentFactoryTests.cs b/tests/StatusAggregator.Tests/Factory/IncidentFactoryTests.cs new file mode 100644 index 000000000..d474dcb17 --- /dev/null +++ b/tests/StatusAggregator.Tests/Factory/IncidentFactoryTests.cs @@ -0,0 +1,170 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAzure.Storage.Table; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using StatusAggregator.Table; +using Xunit; + +namespace StatusAggregator.Tests.Factory +{ + public class IncidentFactoryTests + { + public class TheCreateAsyncMethod : IncidentFactoryTest + { + [Fact] + public async Task CreatesEntityAndIncreasesSeverity() + { + var input = new ParsedIncident(Incident, "the path", ComponentStatus.Down); + + IncidentEntity entity = null; + Table + .Setup(x => x.InsertOrReplaceAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Callback(e => + { + Assert.IsType(e); + entity = e as IncidentEntity; + }); + + var group = new IncidentGroupEntity + { + RowKey = "parentRowKey", + AffectedComponentStatus = (int)ComponentStatus.Degraded + }; + + Provider + .Setup(x => x.GetAsync(input)) + .ReturnsAsync(group); + + var expectedPath = "the provided path"; + PathProvider + .Setup(x => x.Get(input)) + .Returns(expectedPath); + + var result = await Factory.CreateAsync(input); + + Assert.Equal(entity, result); + + Assert.Equal(input.Id, entity.IncidentApiId); + Assert.Equal(group.RowKey, entity.ParentRowKey); + Assert.Equal(expectedPath, entity.AffectedComponentPath); + Assert.Equal((int)input.AffectedComponentStatus, entity.AffectedComponentStatus); + Assert.Equal(input.StartTime, entity.StartTime); + Assert.Equal(input.EndTime, entity.EndTime); + Assert.Equal((int)input.AffectedComponentStatus, group.AffectedComponentStatus); + + Table + .Verify( + x => x.InsertOrReplaceAsync(It.IsAny()), + Times.Once()); + + Table + .Verify( + x => x.ReplaceAsync(It.IsAny()), + Times.Once()); + } + + [Theory] + [InlineData(ComponentStatus.Degraded)] + [InlineData(ComponentStatus.Down)] + public async Task CreatesEntityAndDoesNotIncreaseSeverity(ComponentStatus existingStatus) + { + var input = new ParsedIncident(Incident, "the path", ComponentStatus.Degraded); + + IncidentEntity entity = null; + Table + .Setup(x => x.InsertOrReplaceAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Callback(e => + { + Assert.IsType(e); + entity = e as IncidentEntity; + }); + + var group = new IncidentGroupEntity + { + RowKey = "parentRowKey", + AffectedComponentStatus = (int)existingStatus + }; + + Provider + .Setup(x => x.GetAsync(input)) + .ReturnsAsync(group); + + var expectedPath = "the provided path"; + PathProvider + .Setup(x => x.Get(input)) + .Returns(expectedPath); + + var result = await Factory.CreateAsync(input); + + Assert.Equal(entity, result); + + Assert.Equal(input.Id, entity.IncidentApiId); + Assert.Equal(group.RowKey, entity.ParentRowKey); + Assert.Equal(expectedPath, entity.AffectedComponentPath); + Assert.Equal((int)input.AffectedComponentStatus, entity.AffectedComponentStatus); + Assert.Equal(input.StartTime, entity.StartTime); + Assert.Equal(input.EndTime, entity.EndTime); + Assert.Equal((int)existingStatus, group.AffectedComponentStatus); + + Table + .Verify( + x => x.InsertOrReplaceAsync(It.IsAny()), + Times.Once()); + + Table + .Verify( + x => x.ReplaceAsync(It.IsAny()), + Times.Never()); + } + } + + public class IncidentFactoryTest + { + public Incident Incident = new Incident() + { + Id = "some ID", + + Source = new IncidentSourceData + { + CreateDate = new DateTime(2018, 9, 13) + }, + + MitigationData = new IncidentStateChangeEventData + { + Date = new DateTime(2018, 9, 14) + } + }; + + public Mock Table { get; } + public Mock> Provider { get; } + public Mock> PathProvider { get; } + public IncidentFactory Factory { get; } + + public IncidentFactoryTest() + { + Table = new Mock(); + + Provider = new Mock>(); + + PathProvider = new Mock>(); + + Factory = new IncidentFactory( + Table.Object, + Provider.Object, + PathProvider.Object, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Factory/IncidentGroupFactoryTests.cs b/tests/StatusAggregator.Tests/Factory/IncidentGroupFactoryTests.cs new file mode 100644 index 000000000..459d0bb25 --- /dev/null +++ b/tests/StatusAggregator.Tests/Factory/IncidentGroupFactoryTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAzure.Storage.Table; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using StatusAggregator.Table; +using Xunit; + +namespace StatusAggregator.Tests.Factory +{ + public class IncidentGroupFactoryTests + { + public class TheCreateAsyncMethod : IncidentGroupFactoryTest + { + [Fact] + public async Task CreatesEntity() + { + var input = new ParsedIncident(Incident, "the path", ComponentStatus.Degraded); + + IncidentGroupEntity entity = null; + Table + .Setup(x => x.InsertOrReplaceAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Callback(e => + { + Assert.IsType(e); + entity = e as IncidentGroupEntity; + }); + + var expectedPath = "the provided path"; + PathProvider + .Setup(x => x.Get(input)) + .Returns(expectedPath); + + var eventEntity = new EventEntity + { + RowKey = "parentRowKey" + }; + + Provider + .Setup(x => x.GetAsync(input)) + .ReturnsAsync(eventEntity); + + var result = await Factory.CreateAsync(input); + + Assert.Equal(entity, result); + + Assert.Equal(eventEntity.RowKey, entity.ParentRowKey); + Assert.Equal(expectedPath, entity.AffectedComponentPath); + Assert.Equal((int)input.AffectedComponentStatus, entity.AffectedComponentStatus); + Assert.Equal(input.StartTime, entity.StartTime); + Assert.Null(entity.EndTime); + + Table + .Verify( + x => x.InsertOrReplaceAsync(It.IsAny()), + Times.Once()); + } + } + + public class IncidentGroupFactoryTest + { + public Incident Incident = new Incident() + { + Id = "some ID", + + Source = new IncidentSourceData + { + CreateDate = new DateTime(2018, 9, 13) + }, + + MitigationData = new IncidentStateChangeEventData + { + Date = new DateTime(2018, 9, 14) + } + }; + + public Mock Table { get; } + public Mock> Provider { get; } + public Mock> PathProvider { get; } + public IncidentGroupFactory Factory { get; } + + public IncidentGroupFactoryTest() + { + Table = new Mock(); + + Provider = new Mock>(); + + PathProvider = new Mock>(); + + Factory = new IncidentGroupFactory( + Table.Object, + Provider.Object, + PathProvider.Object, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Factory/NuGetServiceComponentFactoryTests.cs b/tests/StatusAggregator.Tests/Factory/NuGetServiceComponentFactoryTests.cs new file mode 100644 index 000000000..e8a0df52c --- /dev/null +++ b/tests/StatusAggregator.Tests/Factory/NuGetServiceComponentFactoryTests.cs @@ -0,0 +1,127 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using NuGet.Services.Status; +using StatusAggregator.Factory; +using Xunit; + +namespace StatusAggregator.Tests.Factory +{ + public class NuGetServiceComponentFactoryTests + { + public class TheCreateMethod : NuGetServiceComponentFactoryTest + { + /// + /// This test guarantees the shape of the NuGet service's component does not change. + /// + [Fact] + public void ContainsExpectedSchema() + { + AssertConstants(); + + var root = Factory.Create(); + + Assert.Equal(NuGetServiceComponentFactory.RootName, root.Name); + Assert.Equal(4, root.SubComponents.Count()); + + AssertGallery(root); + AssertRestore(root); + AssertSearch(root); + AssertUpload(root); + } + + private void AssertConstants() + { + Assert.Equal("NuGet", NuGetServiceComponentFactory.RootName); + Assert.Equal("NuGet.org", NuGetServiceComponentFactory.GalleryName); + Assert.Equal("Restore", NuGetServiceComponentFactory.RestoreName); + Assert.Equal("Search", NuGetServiceComponentFactory.SearchName); + Assert.Equal("Package Publishing", NuGetServiceComponentFactory.UploadName); + + Assert.Equal("V2 Protocol", NuGetServiceComponentFactory.V2ProtocolName); + Assert.Equal("V3 Protocol", NuGetServiceComponentFactory.V3ProtocolName); + + Assert.Equal("Global", NuGetServiceComponentFactory.GlobalRegionName); + Assert.Equal("China", NuGetServiceComponentFactory.ChinaRegionName); + + Assert.Equal("North Central US", NuGetServiceComponentFactory.UsncInstanceName); + Assert.Equal("South Central US", NuGetServiceComponentFactory.UsscInstanceName); + Assert.Equal("East Asia", NuGetServiceComponentFactory.EaInstanceName); + Assert.Equal("Southeast Asia", NuGetServiceComponentFactory.SeaInstanceName); + } + + private void AssertGallery(IComponent root) + { + var gallery = GetSubComponent(root, NuGetServiceComponentFactory.GalleryName); + Assert.Equal(2, gallery.SubComponents.Count()); + + var galleryUsnc = GetSubComponent(gallery, NuGetServiceComponentFactory.UsncInstanceName); + Assert.Empty(galleryUsnc.SubComponents); + var galleryUssc = GetSubComponent(gallery, NuGetServiceComponentFactory.UsscInstanceName); + Assert.Empty(galleryUssc.SubComponents); + } + + private void AssertRestore(IComponent root) + { + var restore = GetSubComponent(root, NuGetServiceComponentFactory.RestoreName); + Assert.Equal(2, restore.SubComponents.Count()); + + var restoreV2 = GetSubComponent(restore, NuGetServiceComponentFactory.V2ProtocolName); + Assert.Equal(2, restoreV2.SubComponents.Count()); + var restoreV2Usnc = GetSubComponent(restoreV2, NuGetServiceComponentFactory.UsncInstanceName); + Assert.Empty(restoreV2Usnc.SubComponents); + var restoreV2Ussc = GetSubComponent(restoreV2, NuGetServiceComponentFactory.UsscInstanceName); + Assert.Empty(restoreV2Ussc.SubComponents); + + var restoreV3 = GetSubComponent(restore, NuGetServiceComponentFactory.V3ProtocolName); + Assert.Equal(2, restoreV3.SubComponents.Count()); + var restoreV3Global = GetSubComponent(restoreV3, NuGetServiceComponentFactory.GlobalRegionName); + Assert.Empty(restoreV3Global.SubComponents); + var restoreV3China = GetSubComponent(restoreV3, NuGetServiceComponentFactory.ChinaRegionName); + Assert.Empty(restoreV3China.SubComponents); + } + + private void AssertSearch(IComponent root) + { + var search = GetSubComponent(root, NuGetServiceComponentFactory.SearchName); + Assert.Equal(2, search.SubComponents.Count()); + + var searchGlobal = GetSubComponent(search, NuGetServiceComponentFactory.GlobalRegionName); + Assert.Equal(2, searchGlobal.SubComponents.Count()); + var searchGlobalUsnc = GetSubComponent(searchGlobal, NuGetServiceComponentFactory.UsncInstanceName); + Assert.Empty(searchGlobalUsnc.SubComponents); + var searchGlobalUssc = GetSubComponent(searchGlobal, NuGetServiceComponentFactory.UsscInstanceName); + Assert.Empty(searchGlobalUssc.SubComponents); + + var searchChina = GetSubComponent(search, NuGetServiceComponentFactory.ChinaRegionName); + Assert.Equal(2, searchChina.SubComponents.Count()); + var searchChinaEA = GetSubComponent(searchChina, NuGetServiceComponentFactory.EaInstanceName); + Assert.Empty(searchChinaEA.SubComponents); + var searchChinaSea = GetSubComponent(searchChina, NuGetServiceComponentFactory.SeaInstanceName); + Assert.Empty(searchChinaSea.SubComponents); + } + + private void AssertUpload(IComponent root) + { + var upload = GetSubComponent(root, NuGetServiceComponentFactory.UploadName); + Assert.Empty(upload.SubComponents); + } + + private IComponent GetSubComponent(IComponent component, string name) + { + return component.GetByNames(component.Name, name); + } + } + + public class NuGetServiceComponentFactoryTest + { + public NuGetServiceComponentFactory Factory { get; } + + public NuGetServiceComponentFactoryTest() + { + Factory = new NuGetServiceComponentFactory(); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/IncidentFactoryTests.cs b/tests/StatusAggregator.Tests/IncidentFactoryTests.cs deleted file mode 100644 index b5fd3e3af..000000000 --- a/tests/StatusAggregator.Tests/IncidentFactoryTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage.Table; -using Moq; -using NuGet.Services.Incidents; -using NuGet.Services.Status; -using NuGet.Services.Status.Table; -using StatusAggregator.Parse; -using StatusAggregator.Table; -using Xunit; - -namespace StatusAggregator.Tests -{ - public class IncidentFactoryTests - { - private const string Id = "id"; - private const string AffectedComponentPath = "path"; - private const ComponentStatus AffectedComponentStatus = ComponentStatus.Degraded; - private static DateTime CreationTime = new DateTime(2017, 7, 10); - - private Mock _tableWrapperMock { get; } - private Mock _eventUpdaterMock { get; } - private IncidentFactory _incidentFactory { get; } - private ParsedIncident _parsedIncident { get; } - - public IncidentFactoryTests() - { - _tableWrapperMock = new Mock(); - _eventUpdaterMock = new Mock(); - _incidentFactory = new IncidentFactory( - _tableWrapperMock.Object, - _eventUpdaterMock.Object, - Mock.Of>()); - - var incident = new Incident() { Id = Id, Source = new IncidentSourceData() { CreateDate = CreationTime } }; - _parsedIncident = new ParsedIncident(incident, AffectedComponentPath, AffectedComponentStatus); - } - - [Fact] - public async Task CreatesNewEventIfNoPossibleEvents() - { - _tableWrapperMock - .Setup(x => x.CreateQuery()) - .Returns(new EventEntity[0].AsQueryable()); - - _tableWrapperMock - .Setup(x => x.InsertOrReplaceAsync(It.IsAny())) - .Returns(Task.CompletedTask); - - EventEntity eventEntity = null; - _tableWrapperMock - .Setup(x => x.InsertOrReplaceAsync(It.Is(e => e is EventEntity))) - .Callback(e => { eventEntity = e as EventEntity; }) - .Returns(Task.CompletedTask); - - var incidentEntity = await _incidentFactory.CreateIncident(_parsedIncident); - - Assert.Equal(Id, incidentEntity.IncidentApiId); - Assert.Equal(CreationTime, incidentEntity.CreationTime); - Assert.Equal(AffectedComponentPath, incidentEntity.AffectedComponentPath); - Assert.Equal((int)AffectedComponentStatus, incidentEntity.AffectedComponentStatus); - Assert.NotNull(eventEntity); - Assert.Equal(eventEntity.RowKey, incidentEntity.EventRowKey); - Assert.Equal(IncidentEntity.DefaultPartitionKey, incidentEntity.PartitionKey); - Assert.True(incidentEntity.IsLinkedToEvent); - Assert.True(incidentEntity.IsActive); - - _tableWrapperMock.Verify( - x => x.InsertOrReplaceAsync(incidentEntity), - Times.Once()); - - _tableWrapperMock.Verify( - x => x.InsertOrReplaceAsync(eventEntity), - Times.Once()); - - _tableWrapperMock.Verify( - x => x.InsertOrReplaceAsync(It.IsAny()), - Times.Exactly(2)); - } - } -} diff --git a/tests/StatusAggregator.Tests/Messages/IncidentGroupMessageFilterTests.cs b/tests/StatusAggregator.Tests/Messages/IncidentGroupMessageFilterTests.cs new file mode 100644 index 000000000..d44ad40b0 --- /dev/null +++ b/tests/StatusAggregator.Tests/Messages/IncidentGroupMessageFilterTests.cs @@ -0,0 +1,183 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status.Table; +using StatusAggregator.Messages; +using StatusAggregator.Table; +using StatusAggregator.Tests.TestUtility; +using Xunit; + +namespace StatusAggregator.Tests.Messages +{ + public class IncidentGroupMessageFilterTests + { + public class TheCanPostMessagesMethod : IncidentGroupMessageFilterTest + { + public static IEnumerable ReturnsFalseIfDurationOfGroupTooShort_Data + { + get + { + var activeGroup = new IncidentGroupEntity + { + StartTime = Cursor - StartMessageDelay + TimeSpan.FromTicks(1) + }; + + yield return new object[] { activeGroup }; + + var inactiveGroup = new IncidentGroupEntity + { + StartTime = Cursor - StartMessageDelay + TimeSpan.FromTicks(1), + EndTime = Cursor + }; + + yield return new object[] { inactiveGroup }; + } + } + + [Theory] + [MemberData(nameof(ReturnsFalseIfDurationOfGroupTooShort_Data))] + public void ReturnsFalseIfDurationOfGroupTooShort(IncidentGroupEntity group) + { + var result = Filter.CanPostMessages(group, Cursor); + + Assert.False(result); + + Table + .Verify( + x => x.CreateQuery(), + Times.Never()); + } + + [Fact] + public void ReturnsFalseIfNoMatchingIncidents() + { + var parentRowKey = "parentRowKey"; + var group = new IncidentGroupEntity + { + StartTime = Cursor - StartMessageDelay, + RowKey = parentRowKey + }; + + var unlinkedIncident = new IncidentEntity + { + StartTime = Cursor, + ParentRowKey = "something else" + }; + + var shortIncident = new IncidentEntity + { + StartTime = Cursor - StartMessageDelay, + EndTime = Cursor - TimeSpan.FromTicks(1), + ParentRowKey = parentRowKey + }; + + Table.SetupQuery(unlinkedIncident, shortIncident); + + var result = Filter.CanPostMessages(group, Cursor); + + Assert.False(result); + } + + [Fact] + public void ReturnsTrueIfActiveIncident() + { + var parentRowKey = "parentRowKey"; + var group = new IncidentGroupEntity + { + StartTime = Cursor - StartMessageDelay, + RowKey = parentRowKey + }; + + var unlinkedIncident = new IncidentEntity + { + StartTime = Cursor, + ParentRowKey = "something else" + }; + + var shortIncident = new IncidentEntity + { + StartTime = Cursor - StartMessageDelay, + EndTime = Cursor - TimeSpan.FromTicks(1), + ParentRowKey = parentRowKey + }; + + var activeIncident = new IncidentEntity + { + StartTime = Cursor - StartMessageDelay, + ParentRowKey = parentRowKey + }; + + Table.SetupQuery(unlinkedIncident, shortIncident, activeIncident); + + var result = Filter.CanPostMessages(group, Cursor); + + Assert.True(result); + } + + [Fact] + public void ReturnsTrueIfLongIncident() + { + var parentRowKey = "parentRowKey"; + var group = new IncidentGroupEntity + { + StartTime = Cursor - StartMessageDelay, + RowKey = parentRowKey + }; + + var unlinkedIncident = new IncidentEntity + { + StartTime = Cursor, + ParentRowKey = "something else" + }; + + var shortIncident = new IncidentEntity + { + StartTime = Cursor - StartMessageDelay, + EndTime = Cursor - TimeSpan.FromTicks(1), + ParentRowKey = parentRowKey + }; + + var longIncident = new IncidentEntity + { + StartTime = Cursor - StartMessageDelay, + EndTime = Cursor, + ParentRowKey = parentRowKey + }; + + Table.SetupQuery(unlinkedIncident, shortIncident, longIncident); + + var result = Filter.CanPostMessages(group, Cursor); + + Assert.True(result); + } + } + + public class IncidentGroupMessageFilterTest + { + public static readonly DateTime Cursor = new DateTime(2018, 9, 13); + public static readonly TimeSpan StartMessageDelay = TimeSpan.FromDays(1); + + public Mock Table { get; } + public IncidentGroupMessageFilter Filter { get; } + + public IncidentGroupMessageFilterTest() + { + Table = new Mock(); + + var config = new StatusAggregatorConfiguration + { + EventStartMessageDelayMinutes = (int)StartMessageDelay.TotalMinutes + }; + + Filter = new IncidentGroupMessageFilter( + Table.Object, + config, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Messages/MessageChangeEventIteratorTests.cs b/tests/StatusAggregator.Tests/Messages/MessageChangeEventIteratorTests.cs new file mode 100644 index 000000000..ceca8467c --- /dev/null +++ b/tests/StatusAggregator.Tests/Messages/MessageChangeEventIteratorTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Factory; +using StatusAggregator.Messages; +using Xunit; + +namespace StatusAggregator.Tests.Messages +{ + public class MessageChangeEventIteratorTests + { + public class TheIterateMethod : MessageChangeEventIteratorTest + { + [Fact] + public async Task IteratesChangesInOrder() + { + var eventEntity = new EventEntity(); + + var root = new TestComponent("hi"); + Factory + .Setup(x => x.Create()) + .Returns(root); + + var firstChange = CreateChangeEvent(TimeSpan.Zero); + var secondChange = CreateChangeEvent(TimeSpan.FromDays(1)); + var thirdChange = CreateChangeEvent(TimeSpan.FromDays(2)); + var changes = new[] { thirdChange, firstChange, secondChange }; + + var firstComponent = new TestComponent("first"); + var firstContext = new ExistingStartMessageContext(firstChange.Timestamp, firstComponent, ComponentStatus.Degraded); + Processor + .Setup(x => x.ProcessAsync(firstChange, eventEntity, root, null)) + .ReturnsAsync(firstContext) + .Verifiable(); + + var secondComponent = new TestComponent("second"); + var secondContext = new ExistingStartMessageContext(secondChange.Timestamp, secondComponent, ComponentStatus.Degraded); + Processor + .Setup(x => x.ProcessAsync(secondChange, eventEntity, root, firstContext)) + .ReturnsAsync(secondContext) + .Verifiable(); + + var thirdComponent = new TestComponent("third"); + var thirdContext = new ExistingStartMessageContext(thirdChange.Timestamp, thirdComponent, ComponentStatus.Degraded); + Processor + .Setup(x => x.ProcessAsync(thirdChange, eventEntity, root, secondContext)) + .ReturnsAsync(thirdContext) + .Verifiable(); + + await Iterator.IterateAsync(changes, eventEntity); + + Processor.Verify(); + + Processor + .Verify( + x => x.ProcessAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(3)); + } + + private MessageChangeEvent CreateChangeEvent(TimeSpan offset) + { + return new MessageChangeEvent( + new DateTime(2018, 9, 14) + offset, + "", + ComponentStatus.Up, + MessageType.Manual); + } + } + + public class MessageChangeEventIteratorTest + { + public Mock Factory { get; } + public Mock Processor { get; } + public MessageChangeEventIterator Iterator { get; } + + public MessageChangeEventIteratorTest() + { + Factory = new Mock(); + + Processor = new Mock(); + + Iterator = new MessageChangeEventIterator( + Factory.Object, + Processor.Object, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Messages/MessageChangeEventProcessorTests.cs b/tests/StatusAggregator.Tests/Messages/MessageChangeEventProcessorTests.cs new file mode 100644 index 000000000..68d6d985a --- /dev/null +++ b/tests/StatusAggregator.Tests/Messages/MessageChangeEventProcessorTests.cs @@ -0,0 +1,437 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Messages; +using Xunit; + +namespace StatusAggregator.Tests.Messages +{ + public class MessageChangeEventProcessorTests + { + public class TheProcessMethod : MessageChangeEventProcessorTest + { + public static IEnumerable AllMessageTypes_Data + { + get + { + yield return new object[] { MessageType.Start }; + yield return new object[] { MessageType.End }; + yield return new object[] { MessageType.Manual }; + } + } + + public static IEnumerable AllImpactedStatuses_Data + { + get + { + yield return new object[] { ComponentStatus.Degraded }; + yield return new object[] { ComponentStatus.Down }; + } + } + + public static IEnumerable AllImpactedStatusesPairs_Data + { + get + { + yield return new object[] { ComponentStatus.Degraded, ComponentStatus.Degraded }; + yield return new object[] { ComponentStatus.Down, ComponentStatus.Degraded }; + yield return new object[] { ComponentStatus.Degraded, ComponentStatus.Down }; + yield return new object[] { ComponentStatus.Down, ComponentStatus.Down }; + } + } + + [Theory] + [MemberData(nameof(AllMessageTypes_Data))] + public async Task ThrowsIfUnexpectedPath(MessageType type) + { + var change = new MessageChangeEvent( + DefaultTimestamp, + "missingPath", + ComponentStatus.Degraded, + type); + + var root = new TestComponent("hi"); + + var context = new ExistingStartMessageContext( + DefaultTimestamp, + new TestComponent("name"), + ComponentStatus.Down); + + await Assert.ThrowsAsync(() => Processor.ProcessAsync(change, EventEntity, root, context)); + } + + [Fact] + public async Task ThrowsWithUnexpectedType() + { + var root = new TestComponent("root"); + + var change = new MessageChangeEvent( + DefaultTimestamp, + root.Path, + ComponentStatus.Degraded, + MessageType.Manual); + + var context = new ExistingStartMessageContext( + DefaultTimestamp, + new TestComponent("name"), + ComponentStatus.Down); + + await Assert.ThrowsAsync(() => Processor.ProcessAsync(change, EventEntity, root, context)); + } + + [Fact] + public async Task IgnoresStartMessageWhereComponentDoesntAffectStatus() + { + var hiddenChild = new TestComponent("child"); + var root = new TestComponent("hi", new[] { hiddenChild }, false); + + var change = new MessageChangeEvent( + DefaultTimestamp, + root.GetByNames(root.Name, hiddenChild.Name).Path, + ComponentStatus.Degraded, + MessageType.Start); + + var context = new ExistingStartMessageContext( + DefaultTimestamp, + new TestComponent("name"), + ComponentStatus.Down); + + var result = await Processor.ProcessAsync(change, EventEntity, root, context); + + Assert.Equal(context, result); + + Factory + .Verify( + x => x.CreateMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never()); + } + + [Theory] + [MemberData(nameof(AllImpactedStatuses_Data))] + public async Task CreatesStartMessageFromNullContextForHiddenComponent(ComponentStatus status) + { + var child = new TestComponent("child"); + var root = new PrimarySecondaryComponent("hi", "", new[] { child }); + + var change = new MessageChangeEvent( + DefaultTimestamp, + root.GetByNames(root.Name, child.Name).Path, + status, + MessageType.Start); + + var result = await Processor.ProcessAsync(change, EventEntity, root, null); + + Assert.Equal(change.Timestamp, result.Timestamp); + Assert.Equal(root, result.AffectedComponent); + Assert.Equal(root.Status, result.AffectedComponentStatus); + + Factory + .Verify( + x => x.CreateMessageAsync( + EventEntity, + DefaultTimestamp, + MessageType.Start, + root), + Times.Once()); + } + + [Theory] + [MemberData(nameof(AllImpactedStatuses_Data))] + public async Task CreatesStartMessageFromNullContext(ComponentStatus status) + { + var child = new TestComponent("child"); + var root = new TreeComponent("hi", "", new[] { child }); + + var affectedComponent = root.GetByNames(root.Name, child.Name); + var change = new MessageChangeEvent( + DefaultTimestamp, + affectedComponent.Path, + status, + MessageType.Start); + + var result = await Processor.ProcessAsync(change, EventEntity, root, null); + + Assert.Equal(change.Timestamp, result.Timestamp); + Assert.Equal(affectedComponent, result.AffectedComponent); + Assert.Equal(affectedComponent.Status, result.AffectedComponentStatus); + + Factory + .Verify( + x => x.CreateMessageAsync( + EventEntity, + DefaultTimestamp, + MessageType.Start, + affectedComponent), + Times.Once()); + } + + [Theory] + [MemberData(nameof(AllImpactedStatusesPairs_Data))] + public async Task ThrowsWhenUpdatingStartMessageFromContextWithoutLeastCommonAncestor(ComponentStatus changeStatus, ComponentStatus existingStatus) + { + var child = new TestComponent("child"); + var root = new TreeComponent("hi", "", new[] { child }); + + var affectedComponent = root.GetByNames(root.Name, child.Name); + var change = new MessageChangeEvent( + DefaultTimestamp, + affectedComponent.Path, + changeStatus, + MessageType.Start); + + var context = new ExistingStartMessageContext( + new DateTime(2018, 10, 9), + new TestComponent("no common ancestor"), + existingStatus); + + await Assert.ThrowsAsync(() => Processor.ProcessAsync(change, EventEntity, root, context)); + + Factory + .Verify( + x => x.UpdateMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never()); + } + + [Theory] + [MemberData(nameof(AllImpactedStatusesPairs_Data))] + public async Task ThrowsWhenUpdatingStartMessageFromContextIfLeastCommonAncestorUnaffected(ComponentStatus changeStatus, ComponentStatus existingStatus) + { + var changedChild = new TestComponent("child"); + var existingChild = new TestComponent("existing"); + var root = new AlwaysSameValueTestComponent( + ComponentStatus.Up, + "hi", + "", + new[] { changedChild, existingChild }); + + var affectedComponent = root.GetByNames(root.Name, changedChild.Name); + var change = new MessageChangeEvent( + DefaultTimestamp, + affectedComponent.Path, + changeStatus, + MessageType.Start); + + var existingAffectedComponent = root.GetByNames(root.Name, existingChild.Name); + var context = new ExistingStartMessageContext( + new DateTime(2018, 10, 9), + existingAffectedComponent, + existingStatus); + + await Assert.ThrowsAsync(() => Processor.ProcessAsync(change, EventEntity, root, context)); + + Factory + .Verify( + x => x.UpdateMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never()); + } + + [Theory] + [MemberData(nameof(AllImpactedStatusesPairs_Data))] + public async Task UpdatesExistingStartMessageFromContext(ComponentStatus changeStatus, ComponentStatus existingStatus) + { + var changedChild = new TestComponent("child"); + var existingChild = new TestComponent("existing"); + var root = new TreeComponent("hi", "", new[] { changedChild, existingChild }); + + var affectedComponent = root.GetByNames(root.Name, changedChild.Name); + var change = new MessageChangeEvent( + DefaultTimestamp, + affectedComponent.Path, + changeStatus, + MessageType.Start); + + var existingAffectedComponent = root.GetByNames(root.Name, existingChild.Name); + var context = new ExistingStartMessageContext( + new DateTime(2018, 10, 9), + existingAffectedComponent, + existingStatus); + + var result = await Processor.ProcessAsync(change, EventEntity, root, context); + + Assert.Equal(context.Timestamp, result.Timestamp); + Assert.Equal(root, result.AffectedComponent); + Assert.Equal(root.Status, result.AffectedComponentStatus); + + Factory + .Verify( + x => x.UpdateMessageAsync( + EventEntity, + context.Timestamp, + MessageType.Start, + root), + Times.Once()); + } + + [Theory] + [MemberData(nameof(AllImpactedStatuses_Data))] + public async Task IgnoresEndMessageWithNullContext(ComponentStatus changeStatus) + { + var root = new TestComponent("root"); + + var change = new MessageChangeEvent( + DefaultTimestamp, + root.Path, + changeStatus, + MessageType.End); + + var result = await Processor.ProcessAsync(change, EventEntity, root, null); + + Assert.Null(result); + + Factory + .Verify( + x => x.CreateMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never()); + } + + [Theory] + [MemberData(nameof(AllImpactedStatusesPairs_Data))] + public async Task CreatesEndMessageWithContext(ComponentStatus changeStatus, ComponentStatus existingStatus) + { + var child = new TestComponent("child"); + child.Status = changeStatus; + var root = new PrimarySecondaryComponent("hi", "", new[] { child }); + + var affectedComponent = root.GetByNames(root.Name, child.Name); + var change = new MessageChangeEvent( + DefaultTimestamp + TimeSpan.FromDays(1), + affectedComponent.Path, + changeStatus, + MessageType.End); + + var context = new ExistingStartMessageContext( + DefaultTimestamp, + root, + existingStatus); + + var result = await Processor.ProcessAsync(change, EventEntity, root, context); + + Assert.Null(result); + + Factory + .Verify( + x => x.CreateMessageAsync( + EventEntity, + change.Timestamp, + MessageType.End, + context.AffectedComponent, + context.AffectedComponentStatus), + Times.Once()); + } + + [Theory] + [MemberData(nameof(AllImpactedStatusesPairs_Data))] + public async Task IgnoresEndMessageWithContextIfStillAffected(ComponentStatus changeStatus, ComponentStatus existingStatus) + { + var child = new TestComponent("child"); + child.Status = changeStatus; + var root = new AlwaysSameValueTestComponent( + ComponentStatus.Degraded, + "hi", + "", + new[] { child }, + false); + + var affectedComponent = root.GetByNames(root.Name, child.Name); + var change = new MessageChangeEvent( + DefaultTimestamp + TimeSpan.FromDays(1), + affectedComponent.Path, + changeStatus, + MessageType.End); + + var context = new ExistingStartMessageContext( + DefaultTimestamp, + root, + existingStatus); + + var result = await Processor.ProcessAsync(change, EventEntity, root, context); + + Assert.Equal(context, result); + + Factory + .Verify( + x => x.CreateMessageAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never()); + } + + private class AlwaysSameValueTestComponent : Component + { + public AlwaysSameValueTestComponent( + ComponentStatus value, + string name, + string description) + : base(name, description) + { + _returnedStatus = value; + } + + public AlwaysSameValueTestComponent( + ComponentStatus value, + string name, + string description, + IEnumerable subComponents, + bool displaySubComponents = true) + : base(name, description, subComponents, displaySubComponents) + { + _returnedStatus = value; + } + + private ComponentStatus _returnedStatus; + private ComponentStatus _internalStatus; + public override ComponentStatus Status + { + get => _returnedStatus; + set + { + _internalStatus = value; + } + } + } + } + + public class MessageChangeEventProcessorTest + { + public DateTime DefaultTimestamp = new DateTime(2018, 9, 14); + public EventEntity EventEntity = new EventEntity(); + + public Mock Factory { get; } + public MessageChangeEventProcessor Processor { get; } + + public MessageChangeEventProcessorTest() + { + Factory = new Mock(); + + Processor = new MessageChangeEventProcessor( + Factory.Object, + Mock.Of>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Messages/MessageChangeEventProviderTests.cs b/tests/StatusAggregator.Tests/Messages/MessageChangeEventProviderTests.cs new file mode 100644 index 000000000..95c38ab82 --- /dev/null +++ b/tests/StatusAggregator.Tests/Messages/MessageChangeEventProviderTests.cs @@ -0,0 +1,126 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status.Table; +using StatusAggregator.Messages; +using StatusAggregator.Table; +using StatusAggregator.Tests.TestUtility; +using Xunit; + +namespace StatusAggregator.Tests.Messages +{ + public class MessageChangeEventProviderTests + { + public class TheGetMethod : MessageChangeEventProviderTest + { + [Fact] + public void GetsChanges() + { + var cursor = new DateTime(2018, 10, 9); + var eventEntity = new EventEntity + { + RowKey = "rowKey" + }; + + var groupFromDifferentEvent = new IncidentGroupEntity + { + ParentRowKey = "different" + }; + + var filteredGroup = new IncidentGroupEntity + { + ParentRowKey = eventEntity.RowKey + }; + + Filter + .Setup(x => x.CanPostMessages(filteredGroup, cursor)) + .Returns(false); + + var activeGroup = new IncidentGroupEntity + { + ParentRowKey = eventEntity.RowKey, + AffectedComponentPath = "path", + AffectedComponentStatus = 99, + StartTime = new DateTime(2018, 10, 10) + }; + + Filter + .Setup(x => x.CanPostMessages(activeGroup, cursor)) + .Returns(true); + + var inactiveGroup = new IncidentGroupEntity + { + ParentRowKey = eventEntity.RowKey, + AffectedComponentPath = "path 2", + AffectedComponentStatus = 101, + StartTime = new DateTime(2018, 10, 11), + EndTime = new DateTime(2018, 10, 12), + }; + + Filter + .Setup(x => x.CanPostMessages(inactiveGroup, cursor)) + .Returns(true); + + Table.SetupQuery(groupFromDifferentEvent, filteredGroup, activeGroup, inactiveGroup); + + var result = Provider.Get(eventEntity, cursor); + + Assert.Equal(3, result.Count()); + + var firstChange = result.First(); + AssertChange(activeGroup, MessageType.Start, firstChange); + + var secondChange = result.ElementAt(1); + AssertChange(inactiveGroup, MessageType.Start, secondChange); + + var thirdChange = result.ElementAt(2); + AssertChange(inactiveGroup, MessageType.End, thirdChange); + } + + private void AssertChange(IncidentGroupEntity group, MessageType type, MessageChangeEvent change) + { + DateTime expectedTimestamp; + switch (type) + { + case MessageType.Start: + expectedTimestamp = group.StartTime; + break; + case MessageType.End: + expectedTimestamp = group.EndTime.Value; + break; + default: + throw new ArgumentException(nameof(type)); + } + + Assert.Equal(expectedTimestamp, change.Timestamp); + Assert.Equal(group.AffectedComponentPath, change.AffectedComponentPath); + Assert.Equal(group.AffectedComponentStatus, (int)change.AffectedComponentStatus); + Assert.Equal(type, change.Type); + } + } + + public class MessageChangeEventProviderTest + { + public Mock Table { get; } + public Mock Filter { get; } + + public MessageChangeEventProvider Provider { get; } + + public MessageChangeEventProviderTest() + { + Table = new Mock(); + + Filter = new Mock(); + + Provider = new MessageChangeEventProvider( + Table.Object, + Filter.Object, + Mock.Of>()); + } + } + } +} diff --git a/tests/StatusAggregator.Tests/Messages/MessageContentBuilderTests.cs b/tests/StatusAggregator.Tests/Messages/MessageContentBuilderTests.cs new file mode 100644 index 000000000..a4bb6cf74 --- /dev/null +++ b/tests/StatusAggregator.Tests/Messages/MessageContentBuilderTests.cs @@ -0,0 +1,401 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Factory; +using StatusAggregator.Messages; +using Xunit; + +namespace StatusAggregator.Tests.Messages +{ + public class MessageContentBuilderTests + { + public class TheBuildMethodWithImplicitStatus + : TheBuildMethodTest + { + protected override string Invoke(MessageType type, IComponent component, ComponentStatus status) + { + return Builder.Build(type, component); + } + + protected override ComponentStatus GetStatus(IComponent component, ComponentStatus status) + { + return component.Status; + } + } + + public class TheBuildMethodWithExplicitStatus + : TheBuildMethodTest + { + protected override string Invoke(MessageType type, IComponent component, ComponentStatus status) + { + return Builder.Build(type, component, status); + } + + protected override ComponentStatus GetStatus(IComponent component, ComponentStatus status) + { + return status; + } + } + + public abstract class TheBuildMethodTest + : MessageContentBuilderTest + { + [Fact] + public void ThrowsIfMissingTemplateForType() + { + var type = MessageType.Manual; + var component = CreateTestComponent( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.GalleryName); + var status = ComponentStatus.Degraded; + + Assert.Throws(() => Invoke(type, component, status)); + } + + [Theory] + [InlineData(MessageType.Start)] + [InlineData(MessageType.End)] + public void ThrowsIfMissingActionDescriptionForPath(MessageType type) + { + var component = CreateTestComponent("missing"); + var status = ComponentStatus.Degraded; + + Assert.Throws(() => Invoke(type, component, status)); + } + + [Theory] + [ClassData(typeof(BuildsContentsSuccessfully_Data))] + public void BuildsContentsSuccessfully(MessageType type, string[] names, ComponentStatus status, Func getExpected) + { + var root = new NuGetServiceComponentFactory().Create(); + var component = root.GetByNames(names); + var result = Invoke(type, component, status); + Assert.Equal( + getExpected(GetStatus(component, status).ToString().ToLowerInvariant()), + result); + } + + protected abstract string Invoke( + MessageType type, + IComponent component, + ComponentStatus status); + + protected abstract ComponentStatus GetStatus( + IComponent component, + ComponentStatus status); + } + + public class MessageContentBuilderTest + { + public MessageContentBuilder Builder { get; } + + public MessageContentBuilderTest() + { + Builder = new MessageContentBuilder( + Mock.Of>()); + } + } + + public static IComponent CreateTestComponent(params string[] names) + { + IComponent bottom = null; + IComponent root = null; + foreach (var name in names.Reverse()) + { + if (bottom == null) + { + bottom = new TestComponent(name); + root = bottom; + } + else + { + root = new TestComponent(name, new[] { root }); + } + } + + return bottom ?? throw new ArgumentException(nameof(names)); + } + + public class BuildsContentsSuccessfully_Data : IEnumerable + { + public IEnumerable>> Data = + new Tuple>[] + { + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.GalleryName }, + ComponentStatus.Degraded, + status => $"**NuGet.org is {status}.** You may encounter issues browsing the NuGet Gallery."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.GalleryName }, + ComponentStatus.Degraded, + status => $"**NuGet.org is no longer {status}.** You should no longer encounter any issues browsing the NuGet Gallery. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.GalleryName }, + ComponentStatus.Down, + status => $"**NuGet.org is {status}.** You may encounter issues browsing the NuGet Gallery."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.GalleryName }, + ComponentStatus.Down, + status => $"**NuGet.org is no longer {status}.** You should no longer encounter any issues browsing the NuGet Gallery. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName, NuGetServiceComponentFactory.ChinaRegionName }, + ComponentStatus.Degraded, + status => $"**China V3 Protocol Restore is {status}.** You may encounter issues restoring packages from NuGet.org's V3 feed from China."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName, NuGetServiceComponentFactory.ChinaRegionName }, + ComponentStatus.Degraded, + status => $"**China V3 Protocol Restore is no longer {status}.** You should no longer encounter any issues restoring packages from NuGet.org's V3 feed from China. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName, NuGetServiceComponentFactory.ChinaRegionName }, + ComponentStatus.Down, + status => $"**China V3 Protocol Restore is {status}.** You may encounter issues restoring packages from NuGet.org's V3 feed from China."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName, NuGetServiceComponentFactory.ChinaRegionName }, + ComponentStatus.Down, + status => $"**China V3 Protocol Restore is no longer {status}.** You should no longer encounter any issues restoring packages from NuGet.org's V3 feed from China. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName, NuGetServiceComponentFactory.GlobalRegionName }, + ComponentStatus.Degraded, + status => $"**Global V3 Protocol Restore is {status}.** You may encounter issues restoring packages from NuGet.org's V3 feed."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName, NuGetServiceComponentFactory.GlobalRegionName }, + ComponentStatus.Degraded, + status => $"**Global V3 Protocol Restore is no longer {status}.** You should no longer encounter any issues restoring packages from NuGet.org's V3 feed. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName, NuGetServiceComponentFactory.GlobalRegionName }, + ComponentStatus.Down, + status => $"**Global V3 Protocol Restore is {status}.** You may encounter issues restoring packages from NuGet.org's V3 feed."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName, NuGetServiceComponentFactory.GlobalRegionName }, + ComponentStatus.Down, + status => $"**Global V3 Protocol Restore is no longer {status}.** You should no longer encounter any issues restoring packages from NuGet.org's V3 feed. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName }, + ComponentStatus.Degraded, + status => $"**V3 Protocol Restore is {status}.** You may encounter issues restoring packages from NuGet.org's V3 feed."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName }, + ComponentStatus.Degraded, + status => $"**V3 Protocol Restore is no longer {status}.** You should no longer encounter any issues restoring packages from NuGet.org's V3 feed. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName }, + ComponentStatus.Down, + status => $"**V3 Protocol Restore is {status}.** You may encounter issues restoring packages from NuGet.org's V3 feed."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V3ProtocolName }, + ComponentStatus.Down, + status => $"**V3 Protocol Restore is no longer {status}.** You should no longer encounter any issues restoring packages from NuGet.org's V3 feed. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V2ProtocolName }, + ComponentStatus.Degraded, + status => $"**V2 Protocol Restore is {status}.** You may encounter issues restoring packages from NuGet.org's V2 feed."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V2ProtocolName }, + ComponentStatus.Degraded, + status => $"**V2 Protocol Restore is no longer {status}.** You should no longer encounter any issues restoring packages from NuGet.org's V2 feed. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V2ProtocolName }, + ComponentStatus.Down, + status => $"**V2 Protocol Restore is {status}.** You may encounter issues restoring packages from NuGet.org's V2 feed."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName, NuGetServiceComponentFactory.V2ProtocolName }, + ComponentStatus.Down, + status => $"**V2 Protocol Restore is no longer {status}.** You should no longer encounter any issues restoring packages from NuGet.org's V2 feed. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName }, + ComponentStatus.Degraded, + status => $"**Restore is {status}.** You may encounter issues restoring packages."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName }, + ComponentStatus.Degraded, + status => $"**Restore is no longer {status}.** You should no longer encounter any issues restoring packages. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName }, + ComponentStatus.Down, + status => $"**Restore is {status}.** You may encounter issues restoring packages."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.RestoreName }, + ComponentStatus.Down, + status => $"**Restore is no longer {status}.** You should no longer encounter any issues restoring packages. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName, NuGetServiceComponentFactory.ChinaRegionName }, + ComponentStatus.Degraded, + status => $"**China Search is {status}.** You may encounter issues searching for packages from China."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName, NuGetServiceComponentFactory.ChinaRegionName }, + ComponentStatus.Degraded, + status => $"**China Search is no longer {status}.** You should no longer encounter any issues searching for packages from China. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName, NuGetServiceComponentFactory.ChinaRegionName }, + ComponentStatus.Down, + status => $"**China Search is {status}.** You may encounter issues searching for packages from China."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName, NuGetServiceComponentFactory.ChinaRegionName }, + ComponentStatus.Down, + status => $"**China Search is no longer {status}.** You should no longer encounter any issues searching for packages from China. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName, NuGetServiceComponentFactory.GlobalRegionName }, + ComponentStatus.Degraded, + status => $"**Global Search is {status}.** You may encounter issues searching for packages."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName, NuGetServiceComponentFactory.GlobalRegionName }, + ComponentStatus.Degraded, + status => $"**Global Search is no longer {status}.** You should no longer encounter any issues searching for packages. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName, NuGetServiceComponentFactory.GlobalRegionName }, + ComponentStatus.Down, + status => $"**Global Search is {status}.** You may encounter issues searching for packages."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName, NuGetServiceComponentFactory.GlobalRegionName }, + ComponentStatus.Down, + status => $"**Global Search is no longer {status}.** You should no longer encounter any issues searching for packages. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName }, + ComponentStatus.Degraded, + status => $"**Search is {status}.** You may encounter issues searching for packages."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName }, + ComponentStatus.Degraded, + status => $"**Search is no longer {status}.** You should no longer encounter any issues searching for packages. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName }, + ComponentStatus.Down, + status => $"**Search is {status}.** You may encounter issues searching for packages."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.SearchName }, + ComponentStatus.Down, + status => $"**Search is no longer {status}.** You should no longer encounter any issues searching for packages. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.UploadName }, + ComponentStatus.Degraded, + status => $"**Package Publishing is {status}.** You may encounter issues uploading new packages."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.UploadName }, + ComponentStatus.Degraded, + status => $"**Package Publishing is no longer {status}.** You should no longer encounter any issues uploading new packages. Thank you for your patience."), + + Tuple.Create>( + MessageType.Start, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.UploadName }, + ComponentStatus.Down, + status => $"**Package Publishing is {status}.** You may encounter issues uploading new packages."), + + Tuple.Create>( + MessageType.End, + new[] { NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.UploadName }, + ComponentStatus.Down, + status => $"**Package Publishing is no longer {status}.** You should no longer encounter any issues uploading new packages. Thank you for your patience."), + + }; + + public IEnumerator GetEnumerator() => Data + .Select(t => new object[] { t.Item1, t.Item2, t.Item3, t.Item4 }) + .GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + [Fact] + public void BuildsContentSuccessfullyTestsAllVisibleComponents() + { + var root = new NuGetServiceComponentFactory().Create(); + var components = new BuildsContentsSuccessfully_Data().Data + .Select(t => root.GetByNames(t.Item2)); + foreach (var component in root.GetAllVisibleComponents()) + { + if (root == component) + { + continue; + } + + if (!components.Any(c => c == component)) + { + throw new KeyNotFoundException(component.Path); + } + } + } + } +} diff --git a/tests/StatusAggregator.Tests/Messages/MessageFactoryTests.cs b/tests/StatusAggregator.Tests/Messages/MessageFactoryTests.cs new file mode 100644 index 000000000..f17dc599d --- /dev/null +++ b/tests/StatusAggregator.Tests/Messages/MessageFactoryTests.cs @@ -0,0 +1,258 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAzure.Storage.Table; +using Moq; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Messages; +using StatusAggregator.Table; +using Xunit; + +namespace StatusAggregator.Tests.Messages +{ + public class MessageFactoryTests + { + public class TheCreateMessageAsyncMethodWithImplicitStatus + : TheCreateMessageAsyncMethod + { + protected override Task InvokeMethod(EventEntity eventEntity, DateTime time, MessageType type, IComponent component, ComponentStatus status) + { + return Factory.CreateMessageAsync(eventEntity, time, type, component); + } + + protected override ComponentStatus GetExpectedStatus(IComponent component, ComponentStatus status) + { + return component.Status; + } + } + + public class TheCreateMessageAsyncMethodWithExplicitStatus + : TheCreateMessageAsyncMethod + { + protected override Task InvokeMethod(EventEntity eventEntity, DateTime time, MessageType type, IComponent component, ComponentStatus status) + { + return Factory.CreateMessageAsync(eventEntity, time, type, component, status); + } + + protected override ComponentStatus GetExpectedStatus(IComponent component, ComponentStatus status) + { + return status; + } + } + + public abstract class TheCreateMessageAsyncMethod + : MessageFactoryTest + { + [Fact] + public async Task ReturnsExistingMessage() + { + var type = (MessageType)99; + var status = (ComponentStatus)100; + var componentStatus = (ComponentStatus)101; + var component = new TestComponent("component") + { + Status = componentStatus + }; + + var existingMessage = new MessageEntity(EventEntity, Time, "existing", (MessageType)98); + Table + .Setup(x => x.RetrieveAsync(MessageEntity.GetRowKey(EventEntity, Time))) + .ReturnsAsync(existingMessage) + .Verifiable(); + + await InvokeMethod( + EventEntity, + Time, + type, + component, + status); + + Table.Verify(); + + Table + .Verify( + x => x.InsertAsync(It.IsAny()), + Times.Never()); + + Builder + .Verify( + x => x.Build(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task CreatesNewMessage() + { + var type = (MessageType)99; + var status = (ComponentStatus)100; + var componentStatus = (ComponentStatus)101; + var component = new TestComponent("component") + { + Status = componentStatus + }; + + var expectedStatus = GetExpectedStatus(component, status); + + var contents = "new message"; + Builder + .Setup(x => x.Build(type, component, expectedStatus)) + .Returns(contents); + + Table + .Setup(x => x.RetrieveAsync(MessageEntity.GetRowKey(EventEntity, Time))) + .ReturnsAsync((MessageEntity)null) + .Verifiable(); + + Table + .Setup(x => x.InsertAsync(It.IsAny())) + .Callback(entity => + { + var message = entity as MessageEntity; + Assert.NotNull(message); + Assert.Equal(EventEntity.RowKey, message.ParentRowKey); + Assert.Equal(Time, message.Time); + Assert.Equal(contents, message.Contents); + }) + .Returns(Task.CompletedTask) + .Verifiable(); + + await InvokeMethod( + EventEntity, + Time, + type, + component, + status); + + Table.Verify(); + } + + protected abstract Task InvokeMethod( + EventEntity eventEntity, + DateTime time, + MessageType type, + IComponent component, + ComponentStatus status); + + protected abstract ComponentStatus GetExpectedStatus( + IComponent component, + ComponentStatus status); + } + + public class TheUpdateMessageAsyncMethod + : MessageFactoryTest + { + [Fact] + public async Task IgnoresIfMessageDoesNotExist() + { + var type = (MessageType)99; + var component = new TestComponent("component"); + + Table + .Setup(x => x.RetrieveAsync(MessageEntity.GetRowKey(EventEntity, Time))) + .ReturnsAsync((MessageEntity)null); + + await Factory.UpdateMessageAsync(EventEntity, Time, type, component); + + Table + .Verify( + x => x.ReplaceAsync(It.IsAny()), + Times.Never()); + + Builder + .Verify( + x => x.Build(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Theory] + [InlineData(MessageType.Start)] + [InlineData(MessageType.End)] + [InlineData(MessageType.Manual)] + public async Task IgnoresIfExistingMessageTypeDifferent(MessageType existingType) + { + var type = (MessageType)99; + var component = new TestComponent("component"); + + var existingMessage = new MessageEntity(EventEntity, Time, "existing", existingType); + Table + .Setup(x => x.RetrieveAsync(MessageEntity.GetRowKey(EventEntity, Time))) + .ReturnsAsync(existingMessage) + .Verifiable(); + + await Factory.UpdateMessageAsync(EventEntity, Time, type, component); + + Table.Verify(); + + Table + .Verify( + x => x.ReplaceAsync(It.IsAny()), + Times.Never()); + + Builder + .Verify( + x => x.Build(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Theory] + [InlineData(MessageType.Start)] + [InlineData(MessageType.End)] + [InlineData(MessageType.Manual)] + public async Task ReplacesMessage(MessageType type) + { + var component = new TestComponent("component"); + + var contents = "new message"; + Builder + .Setup(x => x.Build(type, component)) + .Returns(contents); + + var message = new MessageEntity(EventEntity, Time, "existing", type); + Table + .Setup(x => x.RetrieveAsync(MessageEntity.GetRowKey(EventEntity, Time))) + .ReturnsAsync(message) + .Verifiable(); + + Table + .Setup(x => x.ReplaceAsync(message)) + .Returns(Task.CompletedTask) + .Verifiable(); + + await Factory.UpdateMessageAsync(EventEntity, Time, type, component); + + Assert.Equal(EventEntity.RowKey, message.ParentRowKey); + Assert.Equal(Time, message.Time); + Assert.Equal(contents, message.Contents); + + Table.Verify(); + } + } + + public class MessageFactoryTest + { + public static EventEntity EventEntity = new EventEntity() { RowKey = "rowKey" }; + public static DateTime Time = new DateTime(2018, 10, 10); + + public Mock Table { get; } + public Mock Builder { get; } + + public MessageFactory Factory { get; } + + public MessageFactoryTest() + { + Table = new Mock(); + + Builder = new Mock(); + + Factory = new MessageFactory( + Table.Object, + Builder.Object, + Mock.Of>()); + } + } + } +} diff --git a/tests/StatusAggregator.Tests/Parse/AggregateIncidentParserTests.cs b/tests/StatusAggregator.Tests/Parse/AggregateIncidentParserTests.cs new file mode 100644 index 000000000..676f09c9c --- /dev/null +++ b/tests/StatusAggregator.Tests/Parse/AggregateIncidentParserTests.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using StatusAggregator.Parse; +using Xunit; + +namespace StatusAggregator.Tests.Parse +{ + public class AggregateIncidentParserTests + { + public class TheParseIncidentMethod + { + private static readonly Incident Incident = new Incident + { + Id = "1111111", + Source = new IncidentSourceData + { + CreateDate = new DateTime(2018, 10, 11) + } + }; + + [Fact] + public void ReturnsListOfParsedIncidents() + { + var failingParser1 = CreateParser(false); + var failingParser2 = CreateParser(false); + + var parsedIncident1 = new ParsedIncident(Incident, "one", ComponentStatus.Degraded); + var successfulParser1 = CreateParser(true, parsedIncident1); + + var parsedIncident2 = new ParsedIncident(Incident, "two", ComponentStatus.Down); + var successfulParser2 = CreateParser(true, parsedIncident2); + + var parsers = new Mock[] + { + failingParser1, + successfulParser1, + successfulParser2, + failingParser2 + }; + + var aggregateParser = new AggregateIncidentParser( + parsers.Select(p => p.Object), + Mock.Of>()); + + var result = aggregateParser.ParseIncident(Incident); + + foreach (var parser in parsers) + { + parser.Verify(); + } + + Assert.Equal(2, result.Count()); + Assert.Contains(parsedIncident1, result); + Assert.Contains(parsedIncident2, result); + } + + private Mock CreateParser(bool result, ParsedIncident returnedIncident = null) + { + var parser = new Mock(); + parser + .Setup(x => x.TryParseIncident(Incident, out returnedIncident)) + .Returns(result) + .Verifiable(); + + return parser; + } + } + } +} diff --git a/tests/StatusAggregator.Tests/Parse/EnvironmentPrefixIncidentRegexParsingHandlerTests.cs b/tests/StatusAggregator.Tests/Parse/EnvironmentPrefixIncidentRegexParsingHandlerTests.cs new file mode 100644 index 000000000..ded989fca --- /dev/null +++ b/tests/StatusAggregator.Tests/Parse/EnvironmentPrefixIncidentRegexParsingHandlerTests.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using StatusAggregator.Parse; +using Xunit; + +namespace StatusAggregator.Tests.Parse +{ + public abstract class EnvironmentPrefixIncidentRegexParsingHandlerTests + { + public abstract class TheConstructor + where THandler : EnvironmentPrefixIncidentRegexParsingHandler + { + [Fact] + public void ThrowsWithoutEnvironmentRegexFilter() + { + var filters = Enumerable.Empty(); + Assert.Throws(() => Construct(filters)); + } + + [Fact] + public void DoesNotThrowWithEnvironmentFilter() + { + var handler = Construct(new[] { ParsingUtility.CreateEnvironmentFilter() }); + Assert.NotNull(handler); + } + + protected abstract THandler Construct(IEnumerable filters); + } + } +} diff --git a/tests/StatusAggregator.Tests/Parse/EnvironmentRegexParsingFilterTests.cs b/tests/StatusAggregator.Tests/Parse/EnvironmentRegexParsingFilterTests.cs new file mode 100644 index 000000000..dc6f1b295 --- /dev/null +++ b/tests/StatusAggregator.Tests/Parse/EnvironmentRegexParsingFilterTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using NuGet.Services.Incidents; +using StatusAggregator.Parse; +using Xunit; +using Match = System.Text.RegularExpressions.Match; + +namespace StatusAggregator.Tests.Parse +{ + public class EnvironmentRegexParsingFilterTests + { + public class TheShouldParseMethod + : EnvironmentRegexParsingFilterTest + { + [Fact] + public void ReturnsTrueWithoutEnvironmentGroup() + { + var match = Match.Empty; + + var result = Filter.ShouldParse(Incident, match.Groups); + + Assert.True(result); + } + + [Fact] + public void ReturnsFalseIfIncorrectEnvironment() + { + var match = GetMatchWithEnvironmentGroup("imaginary"); + + var result = Filter.ShouldParse(Incident, match.Groups); + + Assert.False(result); + } + + [Theory] + [InlineData(Environment1)] + [InlineData(Environment2)] + public void ReturnsTrueIfCorrectEnvironment(string environment) + { + var match = GetMatchWithEnvironmentGroup(environment); + + var result = Filter.ShouldParse(Incident, match.Groups); + + Assert.True(result); + } + + private static Match GetMatchWithEnvironmentGroup(string environment) + { + return ParsingUtility.GetMatchWithGroup(EnvironmentRegexParsingFilter.EnvironmentGroupName, environment); + } + } + + public class EnvironmentRegexParsingFilterTest + { + public const string Environment1 = "env1"; + public const string Environment2 = "env2"; + + public static Incident Incident = new Incident(); + + public EnvironmentRegexParsingFilter Filter { get; } + + public EnvironmentRegexParsingFilterTest() + { + Filter = ParsingUtility.CreateEnvironmentFilter(Environment1, Environment2); + } + } + } +} diff --git a/tests/StatusAggregator.Tests/Parse/IncidentRegexParserTests.cs b/tests/StatusAggregator.Tests/Parse/IncidentRegexParserTests.cs new file mode 100644 index 000000000..abb7186bc --- /dev/null +++ b/tests/StatusAggregator.Tests/Parse/IncidentRegexParserTests.cs @@ -0,0 +1,235 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using StatusAggregator.Parse; +using Xunit; + +namespace StatusAggregator.Tests.Parse +{ + public class IncidentRegexParserTests + { + public class TheTryParseIncidentMethod + : IncidentRegexParserTest + { + [Fact] + public void ReturnsFalseIfRegexFailure() + { + var incident = new Incident(); + + // The Regex matching will throw an ArgumentNullException because the incident's title and the handler's Regex pattern are null. + var result = Parser.TryParseIncident(incident, out var parsedIncident); + + Assert.False(result); + } + + [Fact] + public void ReturnsFalseIfMatchUnsuccessful() + { + var incident = new Incident + { + Title = "title" + }; + + Handler + .Setup(x => x.RegexPattern) + .Returns("not title"); + + var result = Parser.TryParseIncident(incident, out var parsedIncident); + + Assert.False(result); + } + + [Fact] + public void ReturnsFalseIfFilterFails() + { + var incident = new Incident + { + Title = "title" + }; + + Handler + .Setup(x => x.RegexPattern) + .Returns(incident.Title); + + var successfulFilter = CreateFilter(incident, true); + var failureFilter = CreateFilter(incident, false); + var filters = new[] { successfulFilter, failureFilter }; + Handler + .Setup(x => x.Filters) + .Returns(filters + .Select(f => f.Object) + .ToList()); + + var result = Parser.TryParseIncident(incident, out var parsedIncident); + + Assert.False(result); + + foreach (var filter in filters) + { + filter.Verify(); + } + } + + [Fact] + public void ReturnsFalseIfPathCannotBeParsed() + { + var incident = new Incident + { + Title = "title" + }; + + Handler + .Setup(x => x.RegexPattern) + .Returns(incident.Title); + + var filter = CreateFilter(incident, true); + Handler + .Setup(x => x.Filters) + .Returns(new[] { filter.Object }.ToList()); + + var path = "path"; + Handler + .Setup(x => x.TryParseAffectedComponentPath(incident, It.IsAny(), out path)) + .Returns(false) + .Verifiable(); + + var result = Parser.TryParseIncident(incident, out var parsedIncident); + + Assert.False(result); + + filter.Verify(); + + Handler.Verify(); + } + + [Fact] + public void ReturnsFalseIfStatusCannotBeParsed() + { + var incident = new Incident + { + Title = "title" + }; + + Handler + .Setup(x => x.RegexPattern) + .Returns(incident.Title); + + var filter = CreateFilter(incident, true); + Handler + .Setup(x => x.Filters) + .Returns(new[] { filter.Object }.ToList()); + + var path = "path"; + Handler + .Setup(x => x.TryParseAffectedComponentPath(incident, It.IsAny(), out path)) + .Returns(true) + .Verifiable(); + + var status = (ComponentStatus)99; + Handler + .Setup(x => x.TryParseAffectedComponentStatus(incident, It.IsAny(), out status)) + .Returns(false) + .Verifiable(); + + var result = Parser.TryParseIncident(incident, out var parsedIncident); + + Assert.False(result); + + filter.Verify(); + + Handler.Verify(); + } + + [Fact] + public void ReturnsTrueIfSuccessful() + { + var incident = new Incident + { + Id = "id", + Title = "title", + + Source = new IncidentSourceData + { + CreateDate = new DateTime(2018, 10, 11) + }, + + MitigationData = new IncidentStateChangeEventData + { + Date = new DateTime(2018, 10, 12) + } + }; + + Handler + .Setup(x => x.RegexPattern) + .Returns(incident.Title); + + var filter = CreateFilter(incident, true); + Handler + .Setup(x => x.Filters) + .Returns(new[] { filter.Object }.ToList()); + + var path = "path"; + Handler + .Setup(x => x.TryParseAffectedComponentPath(incident, It.IsAny(), out path)) + .Returns(true) + .Verifiable(); + + var status = (ComponentStatus)99; + Handler + .Setup(x => x.TryParseAffectedComponentStatus(incident, It.IsAny(), out status)) + .Returns(true) + .Verifiable(); + + var result = Parser.TryParseIncident(incident, out var parsedIncident); + + Assert.True(result); + + Assert.Equal(incident.Id, parsedIncident.Id); + Assert.Equal(incident.Source.CreateDate, parsedIncident.StartTime); + Assert.Equal(incident.MitigationData.Date, parsedIncident.EndTime); + Assert.Equal(path, parsedIncident.AffectedComponentPath); + Assert.Equal(status, parsedIncident.AffectedComponentStatus); + + filter.Verify(); + + Handler.Verify(); + } + + private Mock CreateFilter(Incident incident, bool result) + { + var filter = new Mock(); + filter + .Setup(x => x.ShouldParse( + incident, + It.IsAny())) + .Returns(result) + .Verifiable(); + + return filter; + } + } + + public class IncidentRegexParserTest + { + public Mock Handler { get; } + + public IncidentRegexParser Parser { get; } + + public IncidentRegexParserTest() + { + Handler = new Mock(); + + Parser = new IncidentRegexParser( + Handler.Object, + Mock.Of>()); + } + } + } +} diff --git a/tests/StatusAggregator.Tests/Parse/OutdatedSearchServiceInstanceIncidentRegexParsingHandlerTests.cs b/tests/StatusAggregator.Tests/Parse/OutdatedSearchServiceInstanceIncidentRegexParsingHandlerTests.cs new file mode 100644 index 000000000..00f15f378 --- /dev/null +++ b/tests/StatusAggregator.Tests/Parse/OutdatedSearchServiceInstanceIncidentRegexParsingHandlerTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using Xunit; +using Match = System.Text.RegularExpressions.Match; + +namespace StatusAggregator.Tests.Parse +{ + public class OutdatedSearchServiceInstanceIncidentRegexParsingHandlerTests + { + public class TheTryParseAffectedComponentPathMethod + : OutdatedSearchServiceInstanceIncidentRegexParsingHandlerTest + { + [Fact] + public void ReturnsExpected() + { + var result = Handler.TryParseAffectedComponentPath(Incident, Groups, out var path); + + Assert.True(result); + + Assert.Equal( + ComponentUtility.GetPath(NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.UploadName), + path); + } + } + + public class TheTryParseAffectedComponentStatusMethod + : OutdatedSearchServiceInstanceIncidentRegexParsingHandlerTest + { + [Fact] + public void ReturnsExpected() + { + var result = Handler.TryParseAffectedComponentStatus(Incident, Groups, out var status); + + Assert.True(result); + + Assert.Equal(ComponentStatus.Degraded, status); + } + } + + public class OutdatedSearchServiceInstanceIncidentRegexParsingHandlerTest + { + public static Incident Incident = new Incident(); + public static GroupCollection Groups = Match.Empty.Groups; + + public OutdatedSearchServiceInstanceIncidentRegexParsingHandler Handler { get; } + + public OutdatedSearchServiceInstanceIncidentRegexParsingHandlerTest() + { + var environmentFilter = ParsingUtility.CreateEnvironmentFilter(); + Handler = Construct(new[] { environmentFilter }); + } + } + + public class TheConstructor + : EnvironmentPrefixIncidentRegexParsingHandlerTests.TheConstructor + { + [Fact] + public void IgnoresSeverityFilter() + { + var severityFilter = ParsingUtility.CreateSeverityFilter(0); + var environmentFilter = ParsingUtility.CreateEnvironmentFilter(); + + var handler = Construct(new IIncidentRegexParsingFilter[] { severityFilter, environmentFilter }); + + Assert.Single(handler.Filters); + Assert.Contains(environmentFilter, handler.Filters); + Assert.DoesNotContain(severityFilter, handler.Filters); + } + + protected override OutdatedSearchServiceInstanceIncidentRegexParsingHandler Construct(IEnumerable filters) + { + return OutdatedSearchServiceInstanceIncidentRegexParsingHandlerTests.Construct(filters.ToArray()); + } + } + + public static OutdatedSearchServiceInstanceIncidentRegexParsingHandler Construct(params IIncidentRegexParsingFilter[] filters) + { + return new OutdatedSearchServiceInstanceIncidentRegexParsingHandler( + filters, + Mock.Of>()); + } + } +} diff --git a/tests/StatusAggregator.Tests/Parse/ParsingUtility.cs b/tests/StatusAggregator.Tests/Parse/ParsingUtility.cs new file mode 100644 index 000000000..43a06d849 --- /dev/null +++ b/tests/StatusAggregator.Tests/Parse/ParsingUtility.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Moq; +using StatusAggregator.Parse; +using Match = System.Text.RegularExpressions.Match; + +namespace StatusAggregator.Tests.Parse +{ + public static class ParsingUtility + { + public static EnvironmentRegexParsingFilter CreateEnvironmentFilter(params string[] environments) + { + var config = new StatusAggregatorConfiguration + { + Environments = environments + }; + + return new EnvironmentRegexParsingFilter( + config, + Mock.Of>()); + } + + public static SeverityRegexParsingFilter CreateSeverityFilter(int severity) + { + var config = new StatusAggregatorConfiguration + { + MaximumSeverity = severity + }; + + return new SeverityRegexParsingFilter( + config, + Mock.Of>()); + } + + public static Match GetMatchWithGroup(string group, string value) + { + return GetMatchWithGroups(new KeyValuePair(group, value)); + } + + public static Match GetMatchWithGroups(params KeyValuePair[] pairs) + { + var pattern = string.Empty; + var input = string.Empty; + + foreach (var pair in pairs) + { + pattern += $@"\[(?<{pair.Key}>.*)\]"; + input += $"[{pair.Value}]"; + } + + return Regex.Match(input, pattern); + } + } +} diff --git a/tests/StatusAggregator.Tests/Parse/PingdomIncidentRegexParsingHandlerTests.cs b/tests/StatusAggregator.Tests/Parse/PingdomIncidentRegexParsingHandlerTests.cs new file mode 100644 index 000000000..f31550e15 --- /dev/null +++ b/tests/StatusAggregator.Tests/Parse/PingdomIncidentRegexParsingHandlerTests.cs @@ -0,0 +1,228 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using Xunit; +using Match = System.Text.RegularExpressions.Match; + +namespace StatusAggregator.Tests.Parse +{ + public class PingdomIncidentRegexParsingHandlerTests + { + public class TheTryParseAffectedComponentPathMethod + : PingdomIncidentRegexParsingHandlerTest + { + [Fact] + public void ReturnsFalseIfUnexpectedCheck() + { + var match = ParsingUtility.GetMatchWithGroup( + PingdomIncidentRegexParsingHandler.CheckNameGroupName, + "invalid"); + + var result = Handler.TryParseAffectedComponentPath(Incident, match.Groups, out var path); + + Assert.False(result); + } + + public static IEnumerable ReturnsExpectedPath_Data => CheckNameMapping.Select(p => new object[] { p.Key, p.Value }); + private static IDictionary CheckNameMapping = new Dictionary + { + { + "CDN DNS", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.RestoreName, + NuGetServiceComponentFactory.V3ProtocolName) + }, + + { + "CDN Global", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.RestoreName, + NuGetServiceComponentFactory.V3ProtocolName, + NuGetServiceComponentFactory.GlobalRegionName) + }, + + { + "CDN China", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.RestoreName, + NuGetServiceComponentFactory.V3ProtocolName, + NuGetServiceComponentFactory.ChinaRegionName) + }, + + { + "Gallery DNS", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.GalleryName) + }, + + { + "Gallery Home", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.GalleryName) + }, + + { + "Gallery USNC /", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.GalleryName, + NuGetServiceComponentFactory.UsncInstanceName) + }, + + { + "Gallery USNC /Packages", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.GalleryName, + NuGetServiceComponentFactory.UsncInstanceName) + }, + + { + "Gallery USSC /", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.GalleryName, + NuGetServiceComponentFactory.UsscInstanceName) + }, + + { + "Gallery USSC /Packages", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.GalleryName, + NuGetServiceComponentFactory.UsscInstanceName) + }, + + { + "Gallery USNC /api/v2/Packages()", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.RestoreName, + NuGetServiceComponentFactory.V2ProtocolName, + NuGetServiceComponentFactory.UsncInstanceName) + }, + + { + "Gallery USNC /api/v2/package/NuGet.GalleryUptime/1.0.0", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.RestoreName, + NuGetServiceComponentFactory.V2ProtocolName, + NuGetServiceComponentFactory.UsncInstanceName) + }, + + { + "Gallery USSC /api/v2/Packages()", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.RestoreName, + NuGetServiceComponentFactory.V2ProtocolName, + NuGetServiceComponentFactory.UsscInstanceName) + }, + + { + "Gallery USSC /api/v2/package/NuGet.GalleryUptime/1.0.0", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.RestoreName, + NuGetServiceComponentFactory.V2ProtocolName, + NuGetServiceComponentFactory.UsscInstanceName) + }, + + { + "Search USNC /query", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.SearchName, + NuGetServiceComponentFactory.GlobalRegionName, + NuGetServiceComponentFactory.UsncInstanceName) + }, + + { + "Search USSC /query", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.SearchName, + NuGetServiceComponentFactory.GlobalRegionName, + NuGetServiceComponentFactory.UsscInstanceName) + }, + + { + "Search EA /query", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.SearchName, + NuGetServiceComponentFactory.ChinaRegionName, + NuGetServiceComponentFactory.EaInstanceName) + }, + + { + "Search SEA /query", + CombineNames( + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.SearchName, + NuGetServiceComponentFactory.ChinaRegionName, + NuGetServiceComponentFactory.SeaInstanceName) + }, + }; + + [Theory] + [MemberData(nameof(ReturnsExpectedPath_Data))] + public void ReturnsExpectedPath(string checkName, string[] names) + { + var match = ParsingUtility.GetMatchWithGroup( + PingdomIncidentRegexParsingHandler.CheckNameGroupName, + checkName); + + var result = Handler.TryParseAffectedComponentPath(Incident, match.Groups, out var path); + + Assert.True(result); + Assert.Equal(ComponentUtility.GetPath(names), path); + } + + private static string[] CombineNames(params string[] names) + { + return names; + } + } + + public class TheTryParseAffectedComponentStatusMethod + : PingdomIncidentRegexParsingHandlerTest + { + [Fact] + public void ReturnsExpected() + { + var result = Handler.TryParseAffectedComponentStatus(Incident, Match.Empty.Groups, out var status); + + Assert.True(result); + Assert.Equal(ComponentStatus.Degraded, status); + } + } + + public class PingdomIncidentRegexParsingHandlerTest + { + public Incident Incident = new Incident(); + public PingdomIncidentRegexParsingHandler Handler { get; } + + public PingdomIncidentRegexParsingHandlerTest() + { + Handler = new PingdomIncidentRegexParsingHandler( + Enumerable.Empty(), + Mock.Of>()); + } + } + } +} diff --git a/tests/StatusAggregator.Tests/Parse/SeverityRegexParsingFilterTests.cs b/tests/StatusAggregator.Tests/Parse/SeverityRegexParsingFilterTests.cs new file mode 100644 index 000000000..8e6f77b86 --- /dev/null +++ b/tests/StatusAggregator.Tests/Parse/SeverityRegexParsingFilterTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using NuGet.Services.Incidents; +using Xunit; + +namespace StatusAggregator.Tests.Parse +{ + public class SeverityRegexParsingFilterTests + { + public static IEnumerable ReturnsTrueWhenSeverityLessThanOrEqualTo_Data + { + get + { + foreach (var max in PossibleSeverities) + { + foreach (var input in PossibleSeverities) + { + yield return new object[] { max, input }; + } + } + } + } + + private static readonly IEnumerable PossibleSeverities = new[] { 1, 2, 3, 4 }; + + [Theory] + [MemberData(nameof(ReturnsTrueWhenSeverityLessThanOrEqualTo_Data))] + public void ReturnsTrueWhenSeverityLessThanOrEqualTo(int maximumSeverity, int inputSeverity) + { + var incident = new Incident + { + Severity = inputSeverity + }; + + var filter = ParsingUtility.CreateSeverityFilter(maximumSeverity); + + var result = filter.ShouldParse(incident, null); + + Assert.Equal(inputSeverity <= maximumSeverity, result); + } + } +} diff --git a/tests/StatusAggregator.Tests/Parse/TrafficManagerEndpointStatusIncidentRegexParsingHandlerTests.cs b/tests/StatusAggregator.Tests/Parse/TrafficManagerEndpointStatusIncidentRegexParsingHandlerTests.cs new file mode 100644 index 000000000..a1e5f2b61 --- /dev/null +++ b/tests/StatusAggregator.Tests/Parse/TrafficManagerEndpointStatusIncidentRegexParsingHandlerTests.cs @@ -0,0 +1,164 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using Xunit; +using Match = System.Text.RegularExpressions.Match; + +namespace StatusAggregator.Tests.Parse +{ + public class TrafficManagerEndpointStatusIncidentRegexParsingHandlerTests + { + public class TheTryParseAffectedComponentPathMethod + : TrafficManagerEndpointStatusIncidentRegexParsingHandlerTest + { + [Fact] + public void ReturnsFalseIfUnexpectedValues() + { + var match = ParsingUtility.GetMatchWithGroups( + new KeyValuePair( + EnvironmentRegexParsingFilter.EnvironmentGroupName, + "environment"), + new KeyValuePair( + TrafficManagerEndpointStatusIncidentRegexParsingHandler.DomainGroupName, + "domain"), + new KeyValuePair( + TrafficManagerEndpointStatusIncidentRegexParsingHandler.TargetGroupName, + "target")); + + var result = Handler.TryParseAffectedComponentPath(Incident, match.Groups, out var path); + + Assert.False(result); + } + + private static readonly string[] GalleryUsncPath = new[] + { + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.GalleryName, + NuGetServiceComponentFactory.UsncInstanceName + }; + + private static readonly string[] GalleryUsscPath = new[] + { + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.GalleryName, + NuGetServiceComponentFactory.UsscInstanceName + }; + + private static readonly string[] RestoreV3GlobalPath = new[] + { + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.RestoreName, + NuGetServiceComponentFactory.V3ProtocolName, + NuGetServiceComponentFactory.GlobalRegionName + }; + + private static readonly string[] RestoreV3ChinaPath = new[] + { + NuGetServiceComponentFactory.RootName, + NuGetServiceComponentFactory.RestoreName, + NuGetServiceComponentFactory.V3ProtocolName, + NuGetServiceComponentFactory.ChinaRegionName + }; + + public static IEnumerable ReturnsExpected_Data => TrafficManagerMapping + .Select(m => new object[] { m.Item1, m.Item2, m.Item3, m.Item4 }); + private static readonly IEnumerable> TrafficManagerMapping = + new Tuple[] + { + CreateMapping(Dev, "devnugettest.trafficmanager.net", "nuget-dev-use2-gallery.cloudapp.net", GalleryUsncPath), + CreateMapping(Dev, "devnugettest.trafficmanager.net", "nuget-dev-ussc-gallery.cloudapp.net", GalleryUsscPath), + CreateMapping(Dev, "nugetapidev.trafficmanager.net", "az635243.vo.msecnd.net", RestoreV3GlobalPath), + CreateMapping(Dev, "nugetapidev.trafficmanager.net", "nugetdevcnredirect.trafficmanager.net", RestoreV3ChinaPath), + + CreateMapping(Int, "nuget-int-test-failover.trafficmanager.net", "nuget-int-0-v2gallery.cloudapp.net", GalleryUsncPath), + CreateMapping(Int, "nuget-int-test-failover.trafficmanager.net", "nuget-int-ussc-gallery.cloudapp.net", GalleryUsscPath), + + CreateMapping(Prod, "nuget-prod-v2gallery.trafficmanager.net", "nuget-prod-0-v2gallery.cloudapp.net", GalleryUsncPath), + CreateMapping(Prod, "nuget-prod-v2gallery.trafficmanager.net", "nuget-prod-ussc-gallery.cloudapp.net", GalleryUsscPath), + CreateMapping(Prod, "nugetapiprod.trafficmanager.net", "az320820.vo.msecnd.net", RestoreV3GlobalPath), + CreateMapping(Prod, "nugetapiprod.trafficmanager.net", "nugetprodcnredirect.trafficmanager.net", RestoreV3ChinaPath), + }; + + [Theory] + [MemberData(nameof(ReturnsExpected_Data))] + public void ReturnsExpected(string environment, string domain, string target, string[] names) + { + var match = ParsingUtility.GetMatchWithGroups( + new KeyValuePair( + EnvironmentRegexParsingFilter.EnvironmentGroupName, + environment), + new KeyValuePair( + TrafficManagerEndpointStatusIncidentRegexParsingHandler.DomainGroupName, + domain), + new KeyValuePair( + TrafficManagerEndpointStatusIncidentRegexParsingHandler.TargetGroupName, + target)); + + var result = Handler.TryParseAffectedComponentPath(Incident, match.Groups, out var path); + + Assert.True(result); + + Assert.Equal(ComponentUtility.GetPath(names), path); + } + + private static Tuple CreateMapping(string environment, string domain, string target, string[] names) + { + return Tuple.Create(environment, domain, target, names); + } + } + + public class TheTryParsedAffectedComponentStatusMethod + : TrafficManagerEndpointStatusIncidentRegexParsingHandlerTest + { + [Fact] + public void ReturnsExpected() + { + var result = Handler.TryParseAffectedComponentStatus(Incident, Match.Empty.Groups, out var status); + Assert.True(result); + Assert.Equal(ComponentStatus.Down, status); + } + } + + public class TrafficManagerEndpointStatusIncidentRegexParsingHandlerTest + { + public const string Dev = "dev"; + public const string Int = "int"; + public const string Prod = "prod"; + + public Incident Incident = new Incident(); + public TrafficManagerEndpointStatusIncidentRegexParsingHandler Handler { get; } + + public TrafficManagerEndpointStatusIncidentRegexParsingHandlerTest() + { + Handler = new TrafficManagerEndpointStatusIncidentRegexParsingHandler( + new[] { ParsingUtility.CreateEnvironmentFilter(Dev, Int, Prod) }, + Mock.Of>()); + } + } + + public class TheConstructor + : EnvironmentPrefixIncidentRegexParsingHandlerTests.TheConstructor + { + protected override TrafficManagerEndpointStatusIncidentRegexParsingHandler Construct(IEnumerable filters) + { + return TrafficManagerEndpointStatusIncidentRegexParsingHandlerTests.Construct(filters.ToArray()); + } + } + + public static TrafficManagerEndpointStatusIncidentRegexParsingHandler Construct(params IIncidentRegexParsingFilter[] filters) + { + return new TrafficManagerEndpointStatusIncidentRegexParsingHandler( + filters, + Mock.Of>()); + } + } +} diff --git a/tests/StatusAggregator.Tests/Parse/ValidationDurationIncidentRegexParsingHandlerTests.cs b/tests/StatusAggregator.Tests/Parse/ValidationDurationIncidentRegexParsingHandlerTests.cs new file mode 100644 index 000000000..4cdbeb956 --- /dev/null +++ b/tests/StatusAggregator.Tests/Parse/ValidationDurationIncidentRegexParsingHandlerTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status; +using StatusAggregator.Factory; +using StatusAggregator.Parse; +using Xunit; +using Match = System.Text.RegularExpressions.Match; + +namespace StatusAggregator.Tests.Parse +{ + public class ValidationDurationIncidentRegexParsingHandlerTests + { + public class TheTryParseAffectedComponentPathMethod + : ValidationDurationIncidentRegexParsingHandlerTest + { + [Fact] + public void ReturnsExpected() + { + var result = Handler.TryParseAffectedComponentPath(Incident, Groups, out var path); + + Assert.True(result); + + Assert.Equal( + ComponentUtility.GetPath(NuGetServiceComponentFactory.RootName, NuGetServiceComponentFactory.UploadName), + path); + } + } + + public class TheTryParseAffectedComponentStatusMethod + : ValidationDurationIncidentRegexParsingHandlerTest + { + [Fact] + public void ReturnsExpected() + { + var result = Handler.TryParseAffectedComponentStatus(Incident, Groups, out var status); + + Assert.True(result); + + Assert.Equal(ComponentStatus.Degraded, status); + } + } + + public class ValidationDurationIncidentRegexParsingHandlerTest + { + public static Incident Incident = new Incident(); + public static GroupCollection Groups = Match.Empty.Groups; + + public ValidationDurationIncidentRegexParsingHandler Handler { get; } + + public ValidationDurationIncidentRegexParsingHandlerTest() + { + var environmentFilter = ParsingUtility.CreateEnvironmentFilter(); + Handler = Construct(new[] { environmentFilter }); + } + } + + public class TheConstructor + : EnvironmentPrefixIncidentRegexParsingHandlerTests.TheConstructor + { + protected override ValidationDurationIncidentRegexParsingHandler Construct(IEnumerable filters) + { + return ValidationDurationIncidentRegexParsingHandlerTests.Construct(filters.ToArray()); + } + } + + public static ValidationDurationIncidentRegexParsingHandler Construct(params IIncidentRegexParsingFilter[] filters) + { + return new ValidationDurationIncidentRegexParsingHandler( + filters, + Mock.Of>()); + } + } +} diff --git a/tests/StatusAggregator.Tests/StatusAggregator.Tests.csproj b/tests/StatusAggregator.Tests/StatusAggregator.Tests.csproj index 6d84e4ebc..26f3c65c0 100644 --- a/tests/StatusAggregator.Tests/StatusAggregator.Tests.csproj +++ b/tests/StatusAggregator.Tests/StatusAggregator.Tests.csproj @@ -44,6 +44,23 @@ + + + + + + + + + + + + + + + + + @@ -51,7 +68,29 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/StatusAggregator.Tests/TestComponent.cs b/tests/StatusAggregator.Tests/TestComponent.cs new file mode 100644 index 000000000..4973c784c --- /dev/null +++ b/tests/StatusAggregator.Tests/TestComponent.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using NuGet.Services.Status; + +namespace StatusAggregator.Tests +{ + public class TestComponent : Component + { + private const string DefaultDescription = ""; + + public TestComponent(string name) + : base(name, DefaultDescription) + { + } + + public TestComponent( + string name, + IEnumerable subComponents, + bool displaySubComponents = true) + : base(name, DefaultDescription, subComponents, displaySubComponents) + { + } + + public override ComponentStatus Status { get; set; } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/TestUtility/MockTableWrapperExtensions.cs b/tests/StatusAggregator.Tests/TestUtility/MockTableWrapperExtensions.cs new file mode 100644 index 000000000..2f0098f22 --- /dev/null +++ b/tests/StatusAggregator.Tests/TestUtility/MockTableWrapperExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.WindowsAzure.Storage.Table; +using Moq; +using StatusAggregator.Table; + +namespace StatusAggregator.Tests.TestUtility +{ + public static class MockTableWrapperExtensions + { + public static void SetupQuery( + this Mock mock, + params T[] results) + where T : ITableEntity, new() + { + mock + .Setup(x => x.CreateQuery()) + .Returns(results.AsQueryable()); + } + } +} diff --git a/tests/StatusAggregator.Tests/Update/ActiveEventEntityUpdaterTests.cs b/tests/StatusAggregator.Tests/Update/ActiveEventEntityUpdaterTests.cs new file mode 100644 index 000000000..3035b63f6 --- /dev/null +++ b/tests/StatusAggregator.Tests/Update/ActiveEventEntityUpdaterTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status.Table; +using StatusAggregator.Table; +using StatusAggregator.Tests.TestUtility; +using StatusAggregator.Update; +using Xunit; + +namespace StatusAggregator.Tests.Update +{ + public class ActiveEventEntityUpdaterTests + { + public class TheUpdateAllAsyncMethod + : ActiveEventEntityUpdaterTest + { + [Fact] + public async Task UpdatesAllActiveEvents() + { + var cursor = new DateTime(2018, 10, 10); + + var inactiveEvent = new EventEntity + { + RowKey = "inactive", + EndTime = new DateTime(2018, 10, 11) + }; + + var activeEvent1 = new EventEntity + { + RowKey = "active1" + }; + + var activeEvent2 = new EventEntity + { + RowKey = "active2" + }; + + Table.SetupQuery(inactiveEvent, activeEvent1, activeEvent2); + + EventUpdater + .Setup(x => x.UpdateAsync(activeEvent1, cursor)) + .Returns(Task.CompletedTask) + .Verifiable(); + + EventUpdater + .Setup(x => x.UpdateAsync(activeEvent2, cursor)) + .Returns(Task.CompletedTask) + .Verifiable(); + + await Updater.UpdateAllAsync(cursor); + + EventUpdater.Verify(); + + EventUpdater + .Verify( + x => x.UpdateAsync(inactiveEvent, It.IsAny()), + Times.Never()); + } + } + + public class ActiveEventEntityUpdaterTest + { + public Mock Table { get; } + public Mock> EventUpdater { get; } + + public ActiveEventEntityUpdater Updater { get; } + + public ActiveEventEntityUpdaterTest() + { + Table = new Mock(); + + EventUpdater = new Mock>(); + + Updater = new ActiveEventEntityUpdater( + Table.Object, + EventUpdater.Object, + Mock.Of>()); + } + } + } +} diff --git a/tests/StatusAggregator.Tests/Update/AggregationEntityUpdaterTests.cs b/tests/StatusAggregator.Tests/Update/AggregationEntityUpdaterTests.cs new file mode 100644 index 000000000..6e32645c8 --- /dev/null +++ b/tests/StatusAggregator.Tests/Update/AggregationEntityUpdaterTests.cs @@ -0,0 +1,370 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAzure.Storage.Table; +using Moq; +using NuGet.Services.Status.Table; +using StatusAggregator.Table; +using StatusAggregator.Tests.TestUtility; +using StatusAggregator.Update; +using Xunit; + +namespace StatusAggregator.Tests.Update +{ + public class AggregationEntityUpdaterTests + { + public class TheIncidentGroupUpdateAsyncMethod + : TheUpdateAsyncMethod + { + } + + public class TheEventUpdateAsyncMethod + : TheUpdateAsyncMethod + { + } + + public class TheUpdateAsyncMethod + : AggregationEntityUpdaterTest + where TChildEntity : AggregatedComponentAffectingEntity, new() + where TAggregationEntity : ComponentAffectingEntity, new() + { + [Fact] + public async Task ThrowsIfAggregationNull() + { + await Assert.ThrowsAsync(() => Updater.UpdateAsync(null, Cursor)); + } + + [Fact] + public async Task IgnoresDeactivatedAggregation() + { + var aggregation = new TAggregationEntity + { + EndTime = new DateTime(2018, 10, 9) + }; + + await Updater.UpdateAsync(aggregation, Cursor); + + Table + .Verify( + x => x.CreateQuery(), + Times.Never()); + + Table + .Verify( + x => x.ReplaceAsync(It.IsAny()), + Times.Never()); + + ChildUpdater + .Verify( + x => x.UpdateAsync(It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task IgnoresAggregationWithoutChildren() + { + var aggregation = new TAggregationEntity + { + RowKey = "rowKey" + }; + + var activeChildDifferentEntity = new TChildEntity + { + ParentRowKey = "different" + }; + + var recentChildDifferentEntity = new TChildEntity + { + ParentRowKey = "different", + EndTime = Cursor + }; + + Table.SetupQuery(activeChildDifferentEntity, recentChildDifferentEntity); + + await Updater.UpdateAsync(aggregation, Cursor); + + Table + .Verify( + x => x.ReplaceAsync(It.IsAny()), + Times.Never()); + + ChildUpdater + .Verify( + x => x.UpdateAsync(It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task DeactivatesAggregationWithoutRecentChildren() + { + var aggregation = new TAggregationEntity + { + RowKey = "rowKey" + }; + + var activeChildDifferentEntity = new TChildEntity + { + ParentRowKey = "different" + }; + + var recentChildDifferentEntity = new TChildEntity + { + ParentRowKey = "different", + EndTime = Cursor + }; + + var oldChildSameEntity = new TChildEntity + { + ParentRowKey = aggregation.RowKey, + EndTime = Cursor - EndMessageDelay + }; + + var olderChildSameEntity = new TChildEntity + { + ParentRowKey = aggregation.RowKey, + EndTime = Cursor - EndMessageDelay - EndMessageDelay + }; + + Table.SetupQuery(activeChildDifferentEntity, recentChildDifferentEntity, oldChildSameEntity, olderChildSameEntity); + + ChildUpdater + .Setup(x => x.UpdateAsync(oldChildSameEntity, Cursor)) + .Returns(Task.CompletedTask) + .Verifiable(); + + Table + .Setup(x => x.ReplaceAsync(aggregation)) + .Returns(Task.CompletedTask) + .Verifiable(); + + await Updater.UpdateAsync(aggregation, Cursor); + + Assert.Equal(oldChildSameEntity.EndTime, aggregation.EndTime); + + Table.Verify(); + ChildUpdater.Verify(); + } + + [Fact] + public async Task DoesNotDeactivateAggregationWithActiveChildren() + { + var aggregation = new TAggregationEntity + { + RowKey = "rowKey" + }; + + var activeChildDifferentEntity = new TChildEntity + { + ParentRowKey = "different" + }; + + var recentChildDifferentEntity = new TChildEntity + { + ParentRowKey = "different", + EndTime = Cursor + }; + + var oldChildSameEntity = new TChildEntity + { + ParentRowKey = aggregation.RowKey, + EndTime = Cursor - EndMessageDelay + }; + + var activeChildSameEntity = new TChildEntity + { + ParentRowKey = aggregation.RowKey + }; + + Table.SetupQuery( + activeChildDifferentEntity, + recentChildDifferentEntity, + oldChildSameEntity, + activeChildSameEntity); + + ChildUpdater + .Setup(x => x.UpdateAsync(oldChildSameEntity, Cursor)) + .Returns(Task.CompletedTask) + .Verifiable(); + + ChildUpdater + .Setup(x => x.UpdateAsync(activeChildSameEntity, Cursor)) + .Returns(Task.CompletedTask) + .Verifiable(); + + await Updater.UpdateAsync(aggregation, Cursor); + + Assert.True(aggregation.IsActive); + + Table + .Verify( + x => x.ReplaceAsync(aggregation), + Times.Never()); + + ChildUpdater.Verify(); + } + + [Fact] + public async Task DoesNotDeactivateAggregationWithRecentChildren() + { + var aggregation = new TAggregationEntity + { + RowKey = "rowKey" + }; + + var activeChildDifferentEntity = new TChildEntity + { + ParentRowKey = "different" + }; + + var recentChildDifferentEntity = new TChildEntity + { + ParentRowKey = "different", + EndTime = Cursor + }; + + var oldChildSameEntity = new TChildEntity + { + ParentRowKey = aggregation.RowKey, + EndTime = Cursor - EndMessageDelay + }; + + var recentChildSameEntity = new TChildEntity + { + ParentRowKey = aggregation.RowKey, + EndTime = Cursor + }; + + Table.SetupQuery( + activeChildDifferentEntity, + recentChildDifferentEntity, + oldChildSameEntity, + recentChildSameEntity); + + ChildUpdater + .Setup(x => x.UpdateAsync(oldChildSameEntity, Cursor)) + .Returns(Task.CompletedTask) + .Verifiable(); + + ChildUpdater + .Setup(x => x.UpdateAsync(recentChildSameEntity, Cursor)) + .Returns(Task.CompletedTask) + .Verifiable(); + + await Updater.UpdateAsync(aggregation, Cursor); + + Assert.True(aggregation.IsActive); + + Table + .Verify( + x => x.ReplaceAsync(aggregation), + Times.Never()); + + ChildUpdater.Verify(); + } + + [Fact] + public async Task DoesNotDeactivateAggregationWithActiveAndRecentChildren() + { + var aggregation = new TAggregationEntity + { + RowKey = "rowKey" + }; + + var activeChildDifferentEntity = new TChildEntity + { + ParentRowKey = "different" + }; + + var recentChildDifferentEntity = new TChildEntity + { + ParentRowKey = "different", + EndTime = Cursor + }; + + var oldChildSameEntity = new TChildEntity + { + ParentRowKey = aggregation.RowKey, + EndTime = Cursor - EndMessageDelay + }; + + var activeChildSameEntity = new TChildEntity + { + ParentRowKey = aggregation.RowKey + }; + + var recentChildSameEntity = new TChildEntity + { + ParentRowKey = aggregation.RowKey, + EndTime = Cursor + }; + + Table.SetupQuery( + activeChildDifferentEntity, + recentChildDifferentEntity, + oldChildSameEntity, + activeChildSameEntity, + recentChildSameEntity); + + ChildUpdater + .Setup(x => x.UpdateAsync(oldChildSameEntity, Cursor)) + .Returns(Task.CompletedTask) + .Verifiable(); + + ChildUpdater + .Setup(x => x.UpdateAsync(activeChildSameEntity, Cursor)) + .Returns(Task.CompletedTask) + .Verifiable(); + + ChildUpdater + .Setup(x => x.UpdateAsync(recentChildSameEntity, Cursor)) + .Returns(Task.CompletedTask) + .Verifiable(); + + await Updater.UpdateAsync(aggregation, Cursor); + + Assert.True(aggregation.IsActive); + + Table + .Verify( + x => x.ReplaceAsync(aggregation), + Times.Never()); + + ChildUpdater.Verify(); + } + } + + public class AggregationEntityUpdaterTest + where TChildEntity : AggregatedComponentAffectingEntity, new() + where TAggregationEntity : ComponentAffectingEntity, new() + { + public static readonly DateTime Cursor = new DateTime(2018, 9, 13); + public static readonly TimeSpan EndMessageDelay = TimeSpan.FromDays(1); + + public Mock Table { get; } + public Mock> ChildUpdater { get; } + + public AggregationEntityUpdater Updater { get; } + + public AggregationEntityUpdaterTest() + { + Table = new Mock(); + + ChildUpdater = new Mock>(); + + var config = new StatusAggregatorConfiguration + { + EventEndDelayMinutes = (int)EndMessageDelay.TotalMinutes + }; + + Updater = new AggregationEntityUpdater( + Table.Object, + ChildUpdater.Object, + config, + Mock.Of>>()); + } + } + } +} \ No newline at end of file diff --git a/tests/StatusAggregator.Tests/Update/EventMessagingUpdaterTests.cs b/tests/StatusAggregator.Tests/Update/EventMessagingUpdaterTests.cs new file mode 100644 index 000000000..39873c978 --- /dev/null +++ b/tests/StatusAggregator.Tests/Update/EventMessagingUpdaterTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NuGet.Services.Status; +using NuGet.Services.Status.Table; +using StatusAggregator.Messages; +using StatusAggregator.Update; +using Xunit; + +namespace StatusAggregator.Tests.Update +{ + public class EventMessagingUpdaterTests + { + public class TheUpdateAsyncMethod : EventMessagingUpdaterTest + { + [Fact] + public void GetsAndIteratesChanges() + { + var eventEntity = new EventEntity(); + var cursor = new DateTime(2018, 10, 9); + + var changes = new MessageChangeEvent[] { + new MessageChangeEvent(new DateTime(2018, 10, 9), "path", ComponentStatus.Degraded, MessageType.Start) }; + + Provider + .Setup(x => x.Get(eventEntity, cursor)) + .Returns(changes); + + var iteratorTask = Task.FromResult("something to make this task unique"); + Iterator + .Setup(x => x.IterateAsync(changes, eventEntity)) + .Returns(iteratorTask); + + var result = Updater.UpdateAsync(eventEntity, cursor); + + Assert.Equal(iteratorTask, result); + } + } + + public class EventMessagingUpdaterTest + { + public Mock Provider { get; } + public Mock Iterator { get; } + + public EventMessagingUpdater Updater { get; } + + public EventMessagingUpdaterTest() + { + Provider = new Mock(); + + Iterator = new Mock(); + + Updater = new EventMessagingUpdater( + Provider.Object, + Iterator.Object, + Mock.Of>()); + } + } + } +} diff --git a/tests/StatusAggregator.Tests/Update/IncidentUpdaterTests.cs b/tests/StatusAggregator.Tests/Update/IncidentUpdaterTests.cs new file mode 100644 index 000000000..c09e6da31 --- /dev/null +++ b/tests/StatusAggregator.Tests/Update/IncidentUpdaterTests.cs @@ -0,0 +1,126 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAzure.Storage.Table; +using Moq; +using NuGet.Services.Incidents; +using NuGet.Services.Status.Table; +using StatusAggregator.Table; +using StatusAggregator.Update; +using Xunit; + +namespace StatusAggregator.Tests.Update +{ + public class IncidentUpdaterTests + { + public class TheUpdateAsyncMethod + : IncidentUpdaterTest + { + [Fact] + public async Task IgnoresDeactivatedIncidents() + { + var cursor = new DateTime(2018, 10, 10); + var endTime = new DateTime(2018, 10, 11); + var incidentEntity = new IncidentEntity + { + EndTime = endTime + }; + + await Updater.UpdateAsync(incidentEntity, cursor); + + Assert.Equal(endTime, incidentEntity.EndTime); + + Client + .Verify( + x => x.GetIncident(It.IsAny()), + Times.Never()); + + Table + .Verify( + x => x.ReplaceAsync(It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task DoesNotSaveIfNotMitigated() + { + var cursor = new DateTime(2018, 10, 10); + var incidentEntity = new IncidentEntity + { + IncidentApiId = "id" + }; + + var incident = new Incident(); + Client + .Setup(x => x.GetIncident(incidentEntity.IncidentApiId)) + .ReturnsAsync(incident); + + await Updater.UpdateAsync(incidentEntity, cursor); + + Assert.True(incidentEntity.IsActive); + + Table + .Verify( + x => x.ReplaceAsync(It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task DeactivatesIfMitigated() + { + var cursor = new DateTime(2018, 10, 10); + var incidentEntity = new IncidentEntity + { + IncidentApiId = "id" + }; + + var incident = new Incident + { + MitigationData = new IncidentStateChangeEventData + { + Date = new DateTime(2018, 10, 11) + } + }; + + Client + .Setup(x => x.GetIncident(incidentEntity.IncidentApiId)) + .ReturnsAsync(incident); + + Table + .Setup(x => x.ReplaceAsync(incidentEntity)) + .Returns(Task.CompletedTask) + .Verifiable(); + + await Updater.UpdateAsync(incidentEntity, cursor); + + Assert.Equal(incident.MitigationData.Date, incidentEntity.EndTime); + Assert.False(incidentEntity.IsActive); + + Table.Verify(); + } + } + + public class IncidentUpdaterTest + { + public Mock Table { get; } + public Mock Client { get; } + + public IncidentUpdater Updater { get; } + + public IncidentUpdaterTest() + { + Table = new Mock(); + + Client = new Mock(); + + Updater = new IncidentUpdater( + Table.Object, + Client.Object, + Mock.Of>()); + } + } + } +}