diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe28ac82..ac77ee62 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,7 +77,7 @@ jobs: - name: Create Lambda ZIP file if: runner.os == 'Linux' run: | - cd "./artifacts/publish/LondonTravel.Skill/release_linux-arm64" || exit + cd "./artifacts/publish/LondonTravel.Skill/release_linux-x64" || exit if [ -f "./bootstrap" ] then chmod +x ./bootstrap diff --git a/Directory.Build.targets b/Directory.Build.targets index 867074bb..bd900015 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -30,4 +30,16 @@ + + + $(NoWarn);IL2026 + false + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 3c38dcbd..46156436 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + @@ -14,7 +14,6 @@ - @@ -23,7 +22,6 @@ - diff --git a/build.ps1 b/build.ps1 index bbb58d80..98b42978 100755 --- a/build.ps1 +++ b/build.ps1 @@ -91,10 +91,11 @@ function DotNetPublish { if ($IsLinux -And (-Not $UseManagedRuntime)) { $additionalArgs += "--runtime" - $additionalArgs += "linux-arm64" + $additionalArgs += "linux-x64" $additionalArgs += "--self-contained" $additionalArgs += "true" $additionalArgs += "/p:AssemblyName=bootstrap" + $additionalArgs += "/p:PublishAot=true" $additionalArgs += "/p:PublishReadyToRun=true" } diff --git a/src/LondonTravel.Skill/AlexaFunction.cs b/src/LondonTravel.Skill/AlexaFunction.cs index 153edfa6..6d0dd412 100644 --- a/src/LondonTravel.Skill/AlexaFunction.cs +++ b/src/LondonTravel.Skill/AlexaFunction.cs @@ -1,12 +1,9 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Response; using MartinCostello.LondonTravel.Skill.Extensions; using MartinCostello.LondonTravel.Skill.Intents; -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.Extensibility; +using MartinCostello.LondonTravel.Skill.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -81,7 +78,11 @@ public async Task HandlerAsync(SkillRequest request) Log.InvokingSkillRequest(logger, request.Request.Type); - return await handler.HandleAsync(request); + var converted = request.FromModel(); + + var response = await handler.HandleAsync(converted); + + return response.ToModel(); } /// @@ -133,8 +134,6 @@ protected virtual void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton, ValidateSkillConfiguration>(); - services.AddSingleton((_) => TelemetryConfiguration.CreateDefault()); - services.AddSingleton(CreateTelemetryClient); services.AddSingleton((p) => p.GetRequiredService>().Value); services.AddSingleton(configuration); @@ -160,27 +159,6 @@ protected virtual void Dispose(bool disposing) } } - /// - /// Creates an . - /// - /// The to use. - /// - /// The created instance of . - /// - private static TelemetryClient CreateTelemetryClient(IServiceProvider serviceProvider) - { - var config = serviceProvider.GetRequiredService(); - - var configuration = serviceProvider.GetRequiredService(); - - if (!string.IsNullOrEmpty(config.ApplicationInsightsConnectionString)) - { - configuration.ConnectionString = config.ApplicationInsightsConnectionString; - } - - return new TelemetryClient(configuration); - } - /// /// Creates the to use. /// diff --git a/src/LondonTravel.Skill/AlexaSkill.cs b/src/LondonTravel.Skill/AlexaSkill.cs index eb18de91..aa9f48ba 100644 --- a/src/LondonTravel.Skill/AlexaSkill.cs +++ b/src/LondonTravel.Skill/AlexaSkill.cs @@ -4,7 +4,6 @@ using Alexa.NET.Request; using Alexa.NET.Request.Type; using Alexa.NET.Response; -using Microsoft.ApplicationInsights; using Microsoft.Extensions.Logging; namespace MartinCostello.LondonTravel.Skill; @@ -16,11 +15,9 @@ namespace MartinCostello.LondonTravel.Skill; /// Initializes a new instance of the class. /// /// The factory to use for the skill intents. -/// The telemetry client to use. /// The logger to use. internal sealed class AlexaSkill( IntentFactory intentFactory, - TelemetryClient telemetry, ILogger logger) { /// @@ -57,8 +54,6 @@ public SkillResponse OnError(Exception exception, Session session) { Log.HandlerException(logger, exception, session.SessionId); - TrackException(exception, session); - return SkillResponseBuilder .Tell(Strings.InternalError) .Build(); @@ -75,8 +70,6 @@ public SkillResponse OnError(Exception exception, Session session) /// public async Task OnIntentAsync(Intent intent, Session session) { - TrackEvent(intent.Name, session, intent); - IIntent userIntent = intentFactory.Create(intent); return await userIntent.RespondAsync(intent, session); @@ -91,7 +84,7 @@ public async Task OnIntentAsync(Intent intent, Session session) /// public SkillResponse OnLaunch(Session session) { - TrackEvent("LaunchRequest", session); + Log.SessionLaunched(logger, session.User.UserId, session.SessionId); return SkillResponseBuilder .Tell(Strings.LaunchResponse) @@ -108,45 +101,10 @@ public SkillResponse OnLaunch(Session session) /// public SkillResponse OnSessionEnded(Session session) { - TrackEvent("SessionEndedRequest", session); + Log.SessionEnded(logger, session.User.UserId, session.SessionId); return SkillResponseBuilder .Tell(Strings.SessionEndResponse) .Build(); } - - private Dictionary ToTelemetryProperties(Session session) - { - bool hasAccessToken = !string.IsNullOrEmpty(session.User?.AccessToken); - -#pragma warning disable CA1308 - return new Dictionary() - { - ["hasAccessToken"] = hasAccessToken.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(), - ["sessionId"] = session.SessionId, - ["userId"] = session.User?.UserId, - }; -#pragma warning restore CA1308 - } - - private void TrackEvent(string eventName, Session session, Intent intent = null) - { - Dictionary properties = ToTelemetryProperties(session); - - if (intent?.Slots?.Count > 0) - { - foreach (var slot in intent.Slots.Values) - { - properties[$"slot:{slot.Name}"] = slot.Value; - } - } - - telemetry.TrackEvent(eventName, properties); - } - - private void TrackException(Exception exception, Session session) - { - Dictionary properties = ToTelemetryProperties(session); - telemetry.TrackException(exception, properties); - } } diff --git a/src/LondonTravel.Skill/AppJsonSerializerContext.cs b/src/LondonTravel.Skill/AppJsonSerializerContext.cs index a752feaa..f871c202 100644 --- a/src/LondonTravel.Skill/AppJsonSerializerContext.cs +++ b/src/LondonTravel.Skill/AppJsonSerializerContext.cs @@ -1,18 +1,25 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using MartinCostello.LondonTravel.Skill.Clients; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; /// /// A class representing the to use for the application. /// +[ExcludeFromCodeCoverage] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IList))] +[JsonSerializable(typeof(LinkAccountCard))] +[JsonSerializable(typeof(SkillRequest))] +[JsonSerializable(typeof(SkillResponse))] [JsonSerializable(typeof(SkillUserPreferences))] +[JsonSerializable(typeof(StandardCard))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -internal sealed partial class AppJsonSerializerContext : JsonSerializerContext +public sealed partial class AppJsonSerializerContext : JsonSerializerContext { } diff --git a/src/LondonTravel.Skill/Clients/Line.cs b/src/LondonTravel.Skill/Clients/Line.cs index c9e230fc..f8ce3f85 100644 --- a/src/LondonTravel.Skill/Clients/Line.cs +++ b/src/LondonTravel.Skill/Clients/Line.cs @@ -8,7 +8,7 @@ namespace MartinCostello.LondonTravel.Skill.Clients; /// /// A class representing a line. This class cannot be inherited. /// -internal sealed class Line +public sealed class Line { /// /// Gets or sets the line's name. diff --git a/src/LondonTravel.Skill/Clients/LineStatus.cs b/src/LondonTravel.Skill/Clients/LineStatus.cs index abf4b3ce..38336b7a 100644 --- a/src/LondonTravel.Skill/Clients/LineStatus.cs +++ b/src/LondonTravel.Skill/Clients/LineStatus.cs @@ -8,7 +8,7 @@ namespace MartinCostello.LondonTravel.Skill.Clients; /// /// A class representing a line's status. This class cannot be inherited. /// -internal sealed class LineStatus +public sealed class LineStatus { /// /// Gets or sets the status severity. diff --git a/src/LondonTravel.Skill/Clients/LineStatusSeverity.cs b/src/LondonTravel.Skill/Clients/LineStatusSeverity.cs index 148b4089..4db5ced5 100644 --- a/src/LondonTravel.Skill/Clients/LineStatusSeverity.cs +++ b/src/LondonTravel.Skill/Clients/LineStatusSeverity.cs @@ -6,7 +6,7 @@ namespace MartinCostello.LondonTravel.Skill.Clients; /// /// An enumeration of line status severities. /// -internal enum LineStatusSeverity +public enum LineStatusSeverity { SpecialService = 0, diff --git a/src/LondonTravel.Skill/Clients/ServiceDisruption.cs b/src/LondonTravel.Skill/Clients/ServiceDisruption.cs index 0c4470a6..f437b3ae 100644 --- a/src/LondonTravel.Skill/Clients/ServiceDisruption.cs +++ b/src/LondonTravel.Skill/Clients/ServiceDisruption.cs @@ -8,7 +8,7 @@ namespace MartinCostello.LondonTravel.Skill.Clients; /// /// A class representing a service disruption. This class cannot be inherited. /// -internal sealed class ServiceDisruption +public sealed class ServiceDisruption { /// /// Gets or sets the description of the disruption. diff --git a/src/LondonTravel.Skill/Clients/SkillUserPreferences.cs b/src/LondonTravel.Skill/Clients/SkillUserPreferences.cs index 2a2b38a8..ef127d8b 100644 --- a/src/LondonTravel.Skill/Clients/SkillUserPreferences.cs +++ b/src/LondonTravel.Skill/Clients/SkillUserPreferences.cs @@ -8,7 +8,7 @@ namespace MartinCostello.LondonTravel.Skill.Clients; /// /// A class representing a user's preferences for the skill. This class cannot be inherited. /// -internal sealed class SkillUserPreferences +public sealed class SkillUserPreferences { /// /// Gets or sets the user's favorite lines. diff --git a/src/LondonTravel.Skill/Extensions/ApplicationJsonSerializerContext.cs b/src/LondonTravel.Skill/Extensions/ApplicationJsonSerializerContext.cs deleted file mode 100644 index 330ee5ec..00000000 --- a/src/LondonTravel.Skill/Extensions/ApplicationJsonSerializerContext.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Martin Costello, 2017. All rights reserved. -// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. - -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; -using MartinCostello.LondonTravel.Skill.Clients; - -namespace MartinCostello.LondonTravel.Skill.Extensions; - -#pragma warning disable SA1601 - -[ExcludeFromCodeCoverage] -[JsonSerializable(typeof(IList))] -[JsonSerializable(typeof(IList))] -[JsonSerializable(typeof(SkillUserPreferences))] -internal sealed partial class ApplicationJsonSerializerContext : JsonSerializerContext -{ -} diff --git a/src/LondonTravel.Skill/Extensions/IHttpClientBuilderExtensions.cs b/src/LondonTravel.Skill/Extensions/IHttpClientBuilderExtensions.cs index 7f81feb7..bc965780 100644 --- a/src/LondonTravel.Skill/Extensions/IHttpClientBuilderExtensions.cs +++ b/src/LondonTravel.Skill/Extensions/IHttpClientBuilderExtensions.cs @@ -39,7 +39,7 @@ public static IHttpClientBuilder ApplyDefaultConfiguration(this IHttpClientBuild private static void ApplyDefaultConfiguration(HttpClient client) { client.DefaultRequestHeaders.UserAgent.Add(_userAgent.Value); - client.Timeout = TimeSpan.FromSeconds(5); + client.Timeout = TimeSpan.FromSeconds(7.5); } /// diff --git a/src/LondonTravel.Skill/FunctionEntrypoint.cs b/src/LondonTravel.Skill/FunctionEntrypoint.cs index 8311194c..64c55b97 100644 --- a/src/LondonTravel.Skill/FunctionEntrypoint.cs +++ b/src/LondonTravel.Skill/FunctionEntrypoint.cs @@ -1,10 +1,9 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Response; using Amazon.Lambda.RuntimeSupport; -using Amazon.Lambda.Serialization.Json; +using Amazon.Lambda.Serialization.SystemTextJson; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; @@ -30,7 +29,7 @@ public static async Task RunAsync( CancellationToken cancellationToken = default) where T : AlexaFunction, new() { - var serializer = new JsonSerializer(); + var serializer = new SourceGeneratorLambdaJsonSerializer(); await using var function = new T(); using var bootstrap = LambdaBootstrapBuilder diff --git a/src/LondonTravel.Skill/Log.cs b/src/LondonTravel.Skill/Log.cs index ac879f2b..465c85fd 100644 --- a/src/LondonTravel.Skill/Log.cs +++ b/src/LondonTravel.Skill/Log.cs @@ -57,4 +57,16 @@ public static partial void SystemError( Level = LogLevel.Information, Message = "Invoking skill request of type {RequestType}.")] public static partial void InvokingSkillRequest(ILogger logger, string requestType); + + [LoggerMessage( + EventId = 8, + Level = LogLevel.Debug, + Message = "Launched session for user Id {UserId} and session Id {SessionId}.")] + public static partial void SessionLaunched(ILogger logger, string userId, string sessionId); + + [LoggerMessage( + EventId = 9, + Level = LogLevel.Debug, + Message = "Ended session for user Id {UserId} and session Id {SessionId}.")] + public static partial void SessionEnded(ILogger logger, string userId, string sessionId); } diff --git a/src/LondonTravel.Skill/LondonTravel.Skill.csproj b/src/LondonTravel.Skill/LondonTravel.Skill.csproj index 899c371a..419f7307 100644 --- a/src/LondonTravel.Skill/LondonTravel.Skill.csproj +++ b/src/LondonTravel.Skill/LondonTravel.Skill.csproj @@ -3,8 +3,10 @@ Lambda true true + true $(NoWarn);CA1822 Exe + true MartinCostello.LondonTravel.Skill net8.0 false @@ -13,8 +15,7 @@ - - + @@ -23,7 +24,6 @@ - @@ -35,12 +35,4 @@ - - - - - - - - diff --git a/src/LondonTravel.Skill/Models/AlexaError.cs b/src/LondonTravel.Skill/Models/AlexaError.cs new file mode 100644 index 00000000..38c304d6 --- /dev/null +++ b/src/LondonTravel.Skill/Models/AlexaError.cs @@ -0,0 +1,16 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class AlexaError +{ + [JsonConverter(typeof(CustomStringEnumConverter))] + [JsonPropertyName("type")] + public AlexaErrorType Type { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/AlexaErrorCause.cs b/src/LondonTravel.Skill/Models/AlexaErrorCause.cs new file mode 100644 index 00000000..efaaecd4 --- /dev/null +++ b/src/LondonTravel.Skill/Models/AlexaErrorCause.cs @@ -0,0 +1,12 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class AlexaErrorCause +{ + [JsonPropertyName("requestId")] + public string RequestId { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/AlexaErrorType.cs b/src/LondonTravel.Skill/Models/AlexaErrorType.cs new file mode 100644 index 00000000..e7b6f253 --- /dev/null +++ b/src/LondonTravel.Skill/Models/AlexaErrorType.cs @@ -0,0 +1,108 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Runtime.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public enum AlexaErrorType +{ + [EnumMember(Value = "INVALID_RESPONSE")] + InvalidResponse = 0, + + [EnumMember(Value = "DEVICE_COMMUNICATION_ERROR")] + DeviceCommunicationError, + + [EnumMember(Value = "INTERNAL_ERROR")] + InternalError, + + [EnumMember(Value = "MEDIA_ERROR_UNKNOWN")] + MediaErrorUnknown, + + [EnumMember(Value = "MEDIA_ERROR_INVALID_REQUEST")] + InvalidMediaRequest, + + [EnumMember(Value = "MEDIA_ERROR_SERVICE_UNAVAILABLE")] + MediaServiceUnavailable, + + [EnumMember(Value = "MEDIA_ERROR_INTERNAL_SERVER_ERROR")] + InternalServerError, + + [EnumMember(Value = "MEDIA_ERROR_INTERNAL_DEVICE_ERROR")] + InternalDeviceError, + + [EnumMember(Value = "ALREADY_IN_OPERATION")] + AlreadyInOperation, + + [EnumMember(Value = "BRIDGE_UNREACHABLE")] + BridgeUnreachable, + + [EnumMember(Value = "ENDPOINT_BUSY")] + EndpointBusy, + + [EnumMember(Value = "ENDPOINT_LOW_POWER")] + EndpointLowPower, + + [EnumMember(Value = "ENDPOINT_UNREACHABLE")] + EndpointUnreachable, + + [EnumMember(Value = "ENDPOINT_CONTROL_UNAVAILABLE")] + EndpointControlUnavailable, + + [EnumMember(Value = "EXPIRED_AUTHORIZATION_CREDENTIAL")] + ExpiredAuthorizationCredential, + + [EnumMember(Value = "FIRMWARE_OUT_OF_DATE")] + FirmwareOutOfDate, + + [EnumMember(Value = "HARDWARE_MALFUNCTION")] + HardwareMalfunction, + + [EnumMember(Value = "INSUFFICIENT_PERMISSIONS")] + InsufficientPermissions, + + [EnumMember(Value = "INTERNAL_SERVICE_ERROR")] + InternalServiceError, + + [EnumMember(Value = "INVALID_AUTHORIZATION_CREDENTIAL")] + InvalidAuthorizationCredential, + + [EnumMember(Value = "INVALID_DIRECTIVE")] + InvalidDirective, + + [EnumMember(Value = "INVALID_VALUE")] + InvalidValue, + + [EnumMember(Value = "NO_SUCH_ENDPOINT")] + NoSuchEndpoint, + + [EnumMember(Value = "NOT_CALIBRATED")] + NotCalibrated, + + [EnumMember(Value = "NOT_IN_OPERATION")] + NotInOperation, + + [EnumMember(Value = "NOT_SUPPORTED_IN_CURRENT_MODE")] + NotSupportedInCurrentMode, + + [EnumMember(Value = "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE")] + NotSupportedWithCurrentBatteryChargeState, + + [EnumMember(Value = "PARTNER_APPLICATION_REDIRECTION")] + PartnerApplicationRedirection, + + [EnumMember(Value = "POWER_LEVEL_NOT_SUPPORTED")] + PowerLevelNotSupported, + + [EnumMember(Value = "RATE_LIMIT_EXCEEDED")] + RateLimitExceeded, + + [EnumMember(Value = "TEMPERATURE_VALUE_OUT_OF_RANGE")] + TemperatureValueOutOfRange, + + [EnumMember(Value = "TOO_MANY_FAILED_ATTEMPTS")] + TooManyFailedAttempts, + + [EnumMember(Value = "VALUE_OUT_OF_RANGE")] + ValueOutOfRange, +} diff --git a/src/LondonTravel.Skill/Models/AlexaSystem.cs b/src/LondonTravel.Skill/Models/AlexaSystem.cs new file mode 100644 index 00000000..9acd801a --- /dev/null +++ b/src/LondonTravel.Skill/Models/AlexaSystem.cs @@ -0,0 +1,24 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class AlexaSystem +{ + [JsonPropertyName("apiAccessToken")] + public string ApiAccessToken { get; set; } + + [JsonPropertyName("apiEndpoint")] + public string ApiEndpoint { get; set; } + + [JsonPropertyName("application")] + public Application Application { get; set; } + + [JsonPropertyName("user")] + public User User { get; set; } + + [JsonPropertyName("device")] + public Device Device { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/Application.cs b/src/LondonTravel.Skill/Models/Application.cs new file mode 100644 index 00000000..0244c67b --- /dev/null +++ b/src/LondonTravel.Skill/Models/Application.cs @@ -0,0 +1,12 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class Application +{ + [JsonPropertyName("applicationId")] + public string ApplicationId { get; set; } +} diff --git a/src/LondonTravel.Skill/LambdaAttributes.cs b/src/LondonTravel.Skill/Models/Card.cs similarity index 57% rename from src/LondonTravel.Skill/LambdaAttributes.cs rename to src/LondonTravel.Skill/Models/Card.cs index cd6c5a58..9892c5ad 100644 --- a/src/LondonTravel.Skill/LambdaAttributes.cs +++ b/src/LondonTravel.Skill/Models/Card.cs @@ -1,7 +1,9 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Amazon.Lambda.Core; -using Amazon.Lambda.Serialization.Json; +namespace MartinCostello.LondonTravel.Skill.Models; -[assembly: LambdaSerializer(typeof(JsonSerializer))] +public abstract class Card +{ + public abstract string Type { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/Context.cs b/src/LondonTravel.Skill/Models/Context.cs new file mode 100644 index 00000000..824d0f70 --- /dev/null +++ b/src/LondonTravel.Skill/Models/Context.cs @@ -0,0 +1,12 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class Context +{ + [JsonPropertyName("System")] + public AlexaSystem System { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/CustomStringEnumConverter`1.cs b/src/LondonTravel.Skill/Models/CustomStringEnumConverter`1.cs new file mode 100644 index 00000000..2b52b1e1 --- /dev/null +++ b/src/LondonTravel.Skill/Models/CustomStringEnumConverter`1.cs @@ -0,0 +1,31 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +internal sealed class CustomStringEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum>() + : JsonStringEnumConverter(namingPolicy: ResolveNamingPolicy()) + where TEnum : struct, Enum +{ + private static EnumMemberNamingPolicy ResolveNamingPolicy() + { + var map = typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static) + .Select((p) => (p.Name, AttributeName: p.GetCustomAttribute()?.Value)) + .Where((p) => p.AttributeName is not null) + .ToDictionary(); + + return map.Count > 0 ? new EnumMemberNamingPolicy(map) : null; + } + + private sealed class EnumMemberNamingPolicy(Dictionary map) : JsonNamingPolicy + { + public override string ConvertName(string name) + => map.TryGetValue(name, out string overriden) ? overriden : name; + } +} diff --git a/src/LondonTravel.Skill/Models/Device.cs b/src/LondonTravel.Skill/Models/Device.cs new file mode 100644 index 00000000..3f1095fc --- /dev/null +++ b/src/LondonTravel.Skill/Models/Device.cs @@ -0,0 +1,19 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class Device +{ + [JsonPropertyName("deviceId")] + public string DeviceId { get; set; } + + [JsonPropertyName("supportedInterfaces")] + public Dictionary SupportedInterfaces { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("persistentEndpointId")] + public string PersistentEndpointId { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/Intent.cs b/src/LondonTravel.Skill/Models/Intent.cs new file mode 100644 index 00000000..0525bbe7 --- /dev/null +++ b/src/LondonTravel.Skill/Models/Intent.cs @@ -0,0 +1,15 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class Intent +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("slots")] + public Dictionary Slots { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/LinkAccountCard.cs b/src/LondonTravel.Skill/Models/LinkAccountCard.cs new file mode 100644 index 00000000..9e59ca8e --- /dev/null +++ b/src/LondonTravel.Skill/Models/LinkAccountCard.cs @@ -0,0 +1,13 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class LinkAccountCard : Card +{ + [JsonPropertyName("type")] + [JsonRequired] + public override string Type { get; set; } = "LinkAccount"; +} diff --git a/src/LondonTravel.Skill/Models/MixedDateTimeConverter.cs b/src/LondonTravel.Skill/Models/MixedDateTimeConverter.cs new file mode 100644 index 00000000..53c109be --- /dev/null +++ b/src/LondonTravel.Skill/Models/MixedDateTimeConverter.cs @@ -0,0 +1,23 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class MixedDateTimeConverter : JsonConverter +{ + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Number => DateTime.UnixEpoch.AddMilliseconds(reader.GetInt64()), + JsonTokenType.String => DateTime.Parse(reader.GetString(), CultureInfo.InvariantCulture), + _ => throw new JsonException($"Unable to deserialize token of type {reader.TokenType} to a {nameof(DateTime)} value."), + }; + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString("O", CultureInfo.InvariantCulture)); +} diff --git a/src/LondonTravel.Skill/Models/Reason.cs b/src/LondonTravel.Skill/Models/Reason.cs new file mode 100644 index 00000000..c4a74ada --- /dev/null +++ b/src/LondonTravel.Skill/Models/Reason.cs @@ -0,0 +1,18 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Runtime.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public enum Reason +{ + [EnumMember(Value = "USER_INITIATED")] + UserInitiated = 0, + + [EnumMember(Value = "ERROR")] + Error, + + [EnumMember(Value = "EXCEEDED_MAX_REPROMPTS")] + ExceededMaxReprompts, +} diff --git a/src/LondonTravel.Skill/Models/Reprompt.cs b/src/LondonTravel.Skill/Models/Reprompt.cs new file mode 100644 index 00000000..d13eb641 --- /dev/null +++ b/src/LondonTravel.Skill/Models/Reprompt.cs @@ -0,0 +1,13 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class Reprompt +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("outputSpeech")] + public SsmlOutputSpeech OutputSpeech { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/Request.cs b/src/LondonTravel.Skill/Models/Request.cs new file mode 100644 index 00000000..0d7b05e9 --- /dev/null +++ b/src/LondonTravel.Skill/Models/Request.cs @@ -0,0 +1,50 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +#pragma warning disable CA1724 + +public sealed class Request +{ + [JsonPropertyName("type")] + [JsonRequired] + public string Type { get; set; } + + [JsonPropertyName("requestId")] + public string RequestId { get; set; } + + [JsonPropertyName("locale")] + public string Locale { get; set; } + + [JsonConverter(typeof(MixedDateTimeConverter))] + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } + + //// Properties for "IntentRequest" + + [JsonPropertyName("dialogState")] + public string DialogState { get; set; } + + [JsonPropertyName("intent")] + public Intent Intent { get; set; } + + //// Properties for "SessionEndedRequest" + + [JsonConverter(typeof(CustomStringEnumConverter))] + [JsonPropertyName("reason")] + public Reason Reason { get; set; } + + //// Properties for "System.ExceptionEncountered" + + [JsonPropertyName("cause")] + public AlexaErrorCause ErrorCause { get; set; } + + //// Properties for "SessionEndedRequest" and "System.ExceptionEncountered" + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public AlexaError Error { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/ResponseBody.cs b/src/LondonTravel.Skill/Models/ResponseBody.cs new file mode 100644 index 00000000..12c16a4b --- /dev/null +++ b/src/LondonTravel.Skill/Models/ResponseBody.cs @@ -0,0 +1,25 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class ResponseBody +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("outputSpeech")] + public SsmlOutputSpeech OutputSpeech { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("card")] + public Card Card { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reprompt")] + public Reprompt Reprompt { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("shouldEndSession")] + public bool? ShouldEndSession { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/Session.cs b/src/LondonTravel.Skill/Models/Session.cs new file mode 100644 index 00000000..4ba4a7b3 --- /dev/null +++ b/src/LondonTravel.Skill/Models/Session.cs @@ -0,0 +1,24 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class Session +{ + [JsonPropertyName("new")] + public bool New { get; set; } + + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } + + [JsonPropertyName("attributes")] + public Dictionary Attributes { get; set; } + + [JsonPropertyName("application")] + public Application Application { get; set; } + + [JsonPropertyName("user")] + public User User { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/SkillRequest.cs b/src/LondonTravel.Skill/Models/SkillRequest.cs new file mode 100644 index 00000000..0e5e95ba --- /dev/null +++ b/src/LondonTravel.Skill/Models/SkillRequest.cs @@ -0,0 +1,21 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class SkillRequest +{ + [JsonPropertyName("version")] + public string Version { get; set; } + + [JsonPropertyName("session")] + public Session Session { get; set; } + + [JsonPropertyName("context")] + public Context Context { get; set; } + + [JsonPropertyName("request")] + public Request Request { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/SkillResponse.cs b/src/LondonTravel.Skill/Models/SkillResponse.cs new file mode 100644 index 00000000..0b8e0a54 --- /dev/null +++ b/src/LondonTravel.Skill/Models/SkillResponse.cs @@ -0,0 +1,17 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class SkillResponse +{ + [JsonPropertyName("version")] + [JsonRequired] + public string Version { get; set; } + + [JsonPropertyName("response")] + [JsonRequired] + public ResponseBody Response { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/Slot.cs b/src/LondonTravel.Skill/Models/Slot.cs new file mode 100644 index 00000000..9bd24eea --- /dev/null +++ b/src/LondonTravel.Skill/Models/Slot.cs @@ -0,0 +1,17 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class Slot +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("value")] + public string Value { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/SsmlOutputSpeech.cs b/src/LondonTravel.Skill/Models/SsmlOutputSpeech.cs new file mode 100644 index 00000000..fa097c51 --- /dev/null +++ b/src/LondonTravel.Skill/Models/SsmlOutputSpeech.cs @@ -0,0 +1,17 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class SsmlOutputSpeech +{ + [JsonPropertyName("type")] + [JsonRequired] + public string Type { get; set; } = "SSML"; + + [JsonPropertyName("ssml")] + [JsonRequired] + public string Ssml { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/StandardCard.cs b/src/LondonTravel.Skill/Models/StandardCard.cs new file mode 100644 index 00000000..d76b3122 --- /dev/null +++ b/src/LondonTravel.Skill/Models/StandardCard.cs @@ -0,0 +1,21 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class StandardCard : Card +{ + [JsonPropertyName("type")] + [JsonRequired] + public override string Type { get; set; } = "Standard"; + + [JsonPropertyName("title")] + [JsonRequired] + public string Title { get; set; } + + [JsonPropertyName("text")] + [JsonRequired] + public string Content { get; set; } +} diff --git a/src/LondonTravel.Skill/Models/User.cs b/src/LondonTravel.Skill/Models/User.cs new file mode 100644 index 00000000..804976ee --- /dev/null +++ b/src/LondonTravel.Skill/Models/User.cs @@ -0,0 +1,15 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class User +{ + [JsonPropertyName("userId")] + public string UserId { get; set; } + + [JsonPropertyName("accessToken")] + public string AccessToken { get; set; } +} diff --git a/src/LondonTravel.Skill/SkillConfiguration.cs b/src/LondonTravel.Skill/SkillConfiguration.cs index a357f39f..6b21e64e 100644 --- a/src/LondonTravel.Skill/SkillConfiguration.cs +++ b/src/LondonTravel.Skill/SkillConfiguration.cs @@ -10,11 +10,6 @@ namespace MartinCostello.LondonTravel.Skill; /// public sealed class SkillConfiguration { - /// - /// Gets or sets the ApplicationInsights connection string to use. - /// - public string ApplicationInsightsConnectionString { get; set; } = string.Empty; - /// /// Gets or sets the URL for the skill API. /// diff --git a/src/LondonTravel.Skill/SkillRequestExtensions.cs b/src/LondonTravel.Skill/SkillRequestExtensions.cs new file mode 100644 index 00000000..387c18f9 --- /dev/null +++ b/src/LondonTravel.Skill/SkillRequestExtensions.cs @@ -0,0 +1,176 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using Alexa.NET.Request.Type; +using SkillRequest = Alexa.NET.Request.SkillRequest; + +namespace MartinCostello.LondonTravel.Skill; + +public static class SkillRequestExtensions +{ + public static SkillRequest FromModel(this Models.SkillRequest request) + { + var result = MapRequest(request); + + result.Request = request.Request.Type switch + { + "IntentRequest" => MapIntent(request.Request), + "LaunchRequest" => new LaunchRequest(), + "SessionEndedRequest" => MapSessionEnd(request.Request), + "System.ExceptionEncountered" => MapException(request.Request), + _ => new UnknownRequest(), + }; + + result.Request.Locale = request.Request.Locale; + result.Request.RequestId = request.Request.RequestId; + result.Request.Timestamp = request.Request.Timestamp; + result.Request.Type = request.Request.Type; + + return result; + } + + private static SkillRequest MapRequest(Models.SkillRequest request) + { + var result = new SkillRequest() { Version = request.Version }; + + if (request.Context is { } context) + { + result.Context = new(); + + if (context.System is { } system) + { + result.Context.System = new() + { + ApiAccessToken = system.ApiAccessToken, + ApiEndpoint = system.ApiEndpoint, + }; + + if (system.Application is { } application) + { + result.Context.System.Application = new() + { + ApplicationId = application.ApplicationId, + }; + } + + if (system.Device is { } device) + { + result.Context.System.Device = new() + { + DeviceID = device.DeviceId, + PersistentEndpointID = device.PersistentEndpointId, + SupportedInterfaces = device.SupportedInterfaces, + }; + } + + if (system.User is { } user) + { + result.Context.System.User = new() + { + AccessToken = user.AccessToken, + Permissions = new(), + UserId = user.UserId, + }; + } + } + } + + if (request.Session is { } session) + { + result.Session = new() + { + Attributes = session.Attributes, + New = session.New, + SessionId = session.SessionId, + }; + + if (session.Application is { } application) + { + result.Session.Application = new() + { + ApplicationId = application.ApplicationId, + }; + } + + if (session.User is { } user) + { + result.Session.User = new() + { + AccessToken = user.AccessToken, + Permissions = new(), + UserId = user.UserId, + }; + } + } + + return result; + } + + private static SystemExceptionRequest MapException(Models.Request exception) + { + var result = new SystemExceptionRequest(); + + if (exception.ErrorCause is { } cause) + { + result.ErrorCause = new() { requestId = cause.RequestId }; + } + + if (exception.Error is { } error) + { + result.Error = new() + { + Message = error.Message, + Type = Enum.Parse(error.Type.ToString()), + }; + } + + return result; + } + + private static IntentRequest MapIntent(Models.Request request) + { + var result = new IntentRequest() + { + DialogState = request.DialogState, + }; + + if (request.Intent is { } intent) + { + result.Intent = new() { Name = intent.Name }; + + if (intent.Slots is { } slots) + { + result.Intent.Slots = slots.Where((p) => p.Value is not null).ToDictionary((p) => p.Key, (p) => new Alexa.NET.Request.Slot() + { + Name = p.Value.Name, + Value = p.Value.Value, + }); + } + } + + return result; + } + + private static SessionEndedRequest MapSessionEnd(Models.Request session) + { + var result = new SessionEndedRequest() + { + Reason = Enum.Parse(session.Reason.ToString()), + }; + + if (session.Error is { } error) + { + result.Error = new() + { + Message = error.Message, + Type = Enum.Parse(error.Type.ToString()), + }; + } + + return result; + } + + private sealed class UnknownRequest : Request + { + } +} diff --git a/src/LondonTravel.Skill/SkillResponseExtensions.cs b/src/LondonTravel.Skill/SkillResponseExtensions.cs new file mode 100644 index 00000000..a66b604d --- /dev/null +++ b/src/LondonTravel.Skill/SkillResponseExtensions.cs @@ -0,0 +1,59 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Diagnostics; +using Alexa.NET.Response; + +namespace MartinCostello.LondonTravel.Skill; + +internal static class SkillResponseExtensions +{ + public static Models.SkillResponse ToModel(this SkillResponse value) + { + var model = new Models.SkillResponse() + { + Response = new(), + Version = value.Version, + }; + + if (value.Response.Card is { } card) + { + model.Response.Card = card switch + { + LinkAccountCard _ => new Models.LinkAccountCard(), + StandardCard standard => new Models.StandardCard() + { + Content = standard.Content, + Title = standard.Title, + }, + _ => throw new UnreachableException($"Unexpected card type {card.GetType()}."), + }; + } + + Debug.Assert( + value.Response.OutputSpeech is null || value.Response.OutputSpeech is SsmlOutputSpeech, + $"Unexpected output speech type {value.Response.OutputSpeech?.GetType()}"); + + if (value.Response.OutputSpeech is SsmlOutputSpeech ssml) + { + model.Response.OutputSpeech = new() { Ssml = ssml.Ssml }; + } + + Debug.Assert( + value.Response.Reprompt is null || value.Response.Reprompt.OutputSpeech is SsmlOutputSpeech, + $"Unexpected output speech type {value.Response.Reprompt?.OutputSpeech.GetType()}"); + + if (value.Response.Reprompt is { } reprompt && + reprompt.OutputSpeech is SsmlOutputSpeech speech) + { + model.Response.Reprompt = new() + { + OutputSpeech = new() { Ssml = speech.Ssml }, + }; + } + + model.Response.ShouldEndSession = value.Response.ShouldEndSession; + + return model; + } +} diff --git a/src/LondonTravel.Skill/appsettings.json b/src/LondonTravel.Skill/appsettings.json index 80a9e532..7574f5cb 100644 --- a/src/LondonTravel.Skill/appsettings.json +++ b/src/LondonTravel.Skill/appsettings.json @@ -8,7 +8,6 @@ } }, "Skill": { - "ApplicationInsightsConnectionString": "", "SkillApiUrl": "", "SkillId": "", "TflApplicationId": "", diff --git a/src/LondonTravel.Skill/aws-lambda-tools-defaults.json b/src/LondonTravel.Skill/aws-lambda-tools-defaults.json index 1dfbe0a8..591770fa 100644 --- a/src/LondonTravel.Skill/aws-lambda-tools-defaults.json +++ b/src/LondonTravel.Skill/aws-lambda-tools-defaults.json @@ -3,7 +3,7 @@ "region": "eu-west-1", "configuration": "Release", "framework": "net8.0", - "function-architecture": "arm64", + "function-architecture": "x86_64", "function-handler": "LondonTravel.Skill", "function-memory-size": 192, "function-runtime": "provided.al2023", diff --git a/test/LondonTravel.Skill.Tests/AlexaFunctionTests.cs b/test/LondonTravel.Skill.Tests/AlexaFunctionTests.cs index 8b27499f..aee9a42c 100644 --- a/test/LondonTravel.Skill.Tests/AlexaFunctionTests.cs +++ b/test/LondonTravel.Skill.Tests/AlexaFunctionTests.cs @@ -1,9 +1,7 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Request.Type; -using Alexa.NET.Response; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; @@ -56,20 +54,20 @@ public async Task Cannot_Invoke_Function_With_System_Failure() // Arrange AlexaFunction function = await CreateFunctionAsync(); - var error = new SystemExceptionRequest() + var error = new Request() { - Error = new Error() + Error = new() { Message = "Internal error.", - Type = ErrorType.InternalError, + Type = AlexaErrorType.InternalError, }, - ErrorCause = new ErrorCause() + ErrorCause = new() { - requestId = "my-request-id", + RequestId = "my-request-id", }, }; - var request = CreateRequest(error); + var request = CreateRequest("System.ExceptionEncountered", error); // Act SkillResponse actual = await function.HandlerAsync(request); diff --git a/test/LondonTravel.Skill.Tests/CancelTests.cs b/test/LondonTravel.Skill.Tests/CancelTests.cs index 7350f9d4..27a3f85b 100644 --- a/test/LondonTravel.Skill.Tests/CancelTests.cs +++ b/test/LondonTravel.Skill.Tests/CancelTests.cs @@ -1,8 +1,7 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Response; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; diff --git a/test/LondonTravel.Skill.Tests/CommuteTests.cs b/test/LondonTravel.Skill.Tests/CommuteTests.cs index ef034b79..89634d38 100644 --- a/test/LondonTravel.Skill.Tests/CommuteTests.cs +++ b/test/LondonTravel.Skill.Tests/CommuteTests.cs @@ -1,9 +1,8 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Response; using JustEat.HttpClientInterception; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; diff --git a/test/LondonTravel.Skill.Tests/DisruptionTests.cs b/test/LondonTravel.Skill.Tests/DisruptionTests.cs index 3966886d..98c9d62d 100644 --- a/test/LondonTravel.Skill.Tests/DisruptionTests.cs +++ b/test/LondonTravel.Skill.Tests/DisruptionTests.cs @@ -1,9 +1,8 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Response; using JustEat.HttpClientInterception; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; diff --git a/test/LondonTravel.Skill.Tests/EndToEndTests.cs b/test/LondonTravel.Skill.Tests/EndToEndTests.cs index f63a02c9..160dddf8 100644 --- a/test/LondonTravel.Skill.Tests/EndToEndTests.cs +++ b/test/LondonTravel.Skill.Tests/EndToEndTests.cs @@ -1,26 +1,126 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Request.Type; -using Alexa.NET.Response; +using System.Text.Json; +using MartinCostello.LondonTravel.Skill.Models; using MartinCostello.Testing.AwsLambdaTestServer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace MartinCostello.LondonTravel.Skill; public class EndToEndTests(ITestOutputHelper outputHelper) : FunctionTests(outputHelper) { [xRetry.RetryFact] - public async Task Alexa_Function_Can_Process_Request() + public async Task Alexa_Function_Can_Process_Intent_Request() { // Arrange - SkillRequest request = CreateRequest(); - request.Request.Type = "LaunchRequest"; + SkillRequest request = CreateIntentRequest("AMAZON.CancelIntent"); - string json = JsonConvert.SerializeObject(request); + // Act + var actual = await ProcessRequestAsync(request); + + // Assert + ResponseBody response = AssertResponse(actual); + + response.Card.ShouldBeNull(); + response.OutputSpeech.ShouldBeNull(); + response.Reprompt.ShouldBeNull(); + } + + [xRetry.RetryFact] + public async Task Alexa_Function_Can_Process_Launch_Request() + { + // Arrange + SkillRequest request = CreateRequest("LaunchRequest"); + + // Act + var actual = await ProcessRequestAsync(request); + + // Assert + ResponseBody response = AssertResponse(actual, shouldEndSession: false); + + response.Card.ShouldBeNull(); + response.Reprompt.ShouldBeNull(); + + response.OutputSpeech.ShouldNotBeNull(); + response.OutputSpeech.Type.ShouldBe("SSML"); + + var ssml = response.OutputSpeech.ShouldBeOfType(); + ssml.Ssml.ShouldBe("Welcome to London Travel. You can ask me about disruption or for the status of any tube line, London Overground, the D.L.R. or the Elizabeth line."); + } + + [xRetry.RetryFact] + public async Task Alexa_Function_Can_Process_Session_Ended_Request() + { + // Arrange + var session = new Request() + { + Reason = Reason.ExceededMaxReprompts, + Error = new() + { + Message = "Too many requests.", + Type = AlexaErrorType.RateLimitExceeded, + }, + }; + + SkillRequest request = CreateRequest("SessionEndedRequest", session); + + // Act + var actual = await ProcessRequestAsync(request); + + ResponseBody response = AssertResponse(actual); + + // Assert + response.Card.ShouldBeNull(); + response.Reprompt.ShouldBeNull(); + + response.OutputSpeech.ShouldNotBeNull(); + response.OutputSpeech.Type.ShouldBe("SSML"); + + var ssml = response.OutputSpeech.ShouldBeOfType(); + ssml.Ssml.ShouldBe("Goodbye."); + } + + [xRetry.RetryFact] + public async Task Alexa_Function_Can_Process_System_Exception_Request() + { + // Arrange + var exception = new Request() + { + Error = new() + { + Message = "An unknown error occurred.", + Type = AlexaErrorType.InternalServerError, + }, + ErrorCause = new() + { + RequestId = "amzn1.echo-api.request.1234", + }, + }; + + SkillRequest request = CreateRequest("System.ExceptionEncountered", exception); + + // Act + var actual = await ProcessRequestAsync(request); + + ResponseBody response = AssertResponse(actual); + + // Assert + response.Card.ShouldBeNull(); + response.Reprompt.ShouldBeNull(); + + response.OutputSpeech.ShouldNotBeNull(); + response.OutputSpeech.Type.ShouldBe("SSML"); + + var ssml = response.OutputSpeech.ShouldBeOfType(); + ssml.Ssml.ShouldBe("Sorry, something went wrong."); + } + + private async Task ProcessRequestAsync(SkillRequest request) + { + // Arrange + string json = JsonSerializer.Serialize(request, AppJsonSerializerContext.Default.SkillRequest); void Configure(IServiceCollection services) { @@ -59,19 +159,10 @@ void Configure(IServiceCollection services) result.Content.ShouldNotBeEmpty(); json = await result.ReadAsStringAsync(); - var actual = JsonConvert.DeserializeObject(json); + var actual = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.SkillResponse); actual.ShouldNotBeNull(); - ResponseBody response = AssertResponse(actual, shouldEndSession: false); - - response.Card.ShouldBeNull(); - response.Reprompt.ShouldBeNull(); - - response.OutputSpeech.ShouldNotBeNull(); - response.OutputSpeech.Type.ShouldBe("SSML"); - - var ssml = response.OutputSpeech.ShouldBeOfType(); - ssml.Ssml.ShouldBe("Welcome to London Travel. You can ask me about disruption or for the status of any tube line, London Overground, the D.L.R. or the Elizabeth line."); + return actual; } } diff --git a/test/LondonTravel.Skill.Tests/FunctionTests.cs b/test/LondonTravel.Skill.Tests/FunctionTests.cs index d695c130..e89caa8b 100644 --- a/test/LondonTravel.Skill.Tests/FunctionTests.cs +++ b/test/LondonTravel.Skill.Tests/FunctionTests.cs @@ -1,11 +1,9 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Request.Type; -using Alexa.NET.Response; using JustEat.HttpClientInterception; using MartinCostello.Logging.XUnit; +using MartinCostello.LondonTravel.Skill.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; @@ -48,7 +46,7 @@ protected virtual async Task CreateFunctionAsync() protected virtual SkillRequest CreateIntentRequest(string name, params Slot[] slots) { - var request = new IntentRequest() + var request = new Request() { Intent = new Intent() { @@ -66,11 +64,10 @@ protected virtual SkillRequest CreateIntentRequest(string name, params Slot[] sl } } - return CreateRequest(request); + return CreateRequest("IntentRequest", request); } - protected virtual SkillRequest CreateRequest(T request = null) - where T : Request, new() + protected virtual SkillRequest CreateRequest(string type, Request request = null) { var application = new Application() { @@ -86,10 +83,6 @@ protected virtual SkillRequest CreateRequest(T request = null) { Context = new() { - AudioPlayer = new() - { - PlayerActivity = "IDLE", - }, System = new() { Application = application, @@ -103,7 +96,7 @@ protected virtual SkillRequest CreateRequest(T request = null) User = user, }, }, - Request = request ?? new T(), + Request = request ?? new(), Session = new() { Application = application, @@ -114,6 +107,7 @@ protected virtual SkillRequest CreateRequest(T request = null) Version = "1.0", }; + result.Request.Type = type; result.Request.Locale = "en-GB"; return result; diff --git a/test/LondonTravel.Skill.Tests/HelpTests.cs b/test/LondonTravel.Skill.Tests/HelpTests.cs index 354cfd20..316b353b 100644 --- a/test/LondonTravel.Skill.Tests/HelpTests.cs +++ b/test/LondonTravel.Skill.Tests/HelpTests.cs @@ -1,8 +1,7 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Response; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; diff --git a/test/LondonTravel.Skill.Tests/LaunchTests.cs b/test/LondonTravel.Skill.Tests/LaunchTests.cs index 3f51b034..3df41c5b 100644 --- a/test/LondonTravel.Skill.Tests/LaunchTests.cs +++ b/test/LondonTravel.Skill.Tests/LaunchTests.cs @@ -1,9 +1,7 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Request.Type; -using Alexa.NET.Response; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; @@ -15,7 +13,7 @@ public async Task Can_Invoke_Function() // Arrange AlexaFunction function = await CreateFunctionAsync(); - SkillRequest request = CreateRequest(); + SkillRequest request = CreateRequest("LaunchRequest"); // Act SkillResponse actual = await function.HandlerAsync(request); diff --git a/test/LondonTravel.Skill.Tests/LondonTravel.Skill.Tests.csproj b/test/LondonTravel.Skill.Tests/LondonTravel.Skill.Tests.csproj index dd42baf8..e70b6f28 100644 --- a/test/LondonTravel.Skill.Tests/LondonTravel.Skill.Tests.csproj +++ b/test/LondonTravel.Skill.Tests/LondonTravel.Skill.Tests.csproj @@ -21,7 +21,7 @@ - + true diff --git a/test/LondonTravel.Skill.Tests/Samples/IntentRequest.json b/test/LondonTravel.Skill.Tests/Samples/IntentRequest.json new file mode 100644 index 00000000..583d9e86 --- /dev/null +++ b/test/LondonTravel.Skill.Tests/Samples/IntentRequest.json @@ -0,0 +1,45 @@ +{ + "version": "1.0", + "session": { + "new": false, + "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", + "application": { + "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" + }, + "attributes": { + "supportedHoroscopePeriods": { + "daily": true, + "weekly": false, + "monthly": false + } + }, + "user": { + "userId": "amzn1.account.AM3B00000000000000000000000", + "accessToken": null, + "permissions": null + } + }, + "context": null, + "request": { + "type": "IntentRequest", + "dialogState": "IN_PROGRESS", + "requestId": " amzn1.echo-api.request.0000000-0000-0000-0000-00000000000", + "timestamp": "2015-05-13T12:34:56Z", + "locale": null, + "intent": { + "name": "GetZodiacHoroscopeIntent", + "confirmationStatus": "DENIED", + "slots": { + "ZodiacSign": { + "name": "ZodiacSign", + "value": "virgo" + }, + "Date": { + "name": "Date", + "value": "2015-11-25", + "confirmationStatus": "CONFIRMED" + } + } + } + } +} diff --git a/test/LondonTravel.Skill.Tests/Samples/LaunchRequest.json b/test/LondonTravel.Skill.Tests/Samples/LaunchRequest.json new file mode 100644 index 00000000..f3790aef --- /dev/null +++ b/test/LondonTravel.Skill.Tests/Samples/LaunchRequest.json @@ -0,0 +1,19 @@ +{ + "version": "1.0", + "session": { + "new": true, + "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", + "application": { + "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" + }, + "attributes": {}, + "user": { + "userId": "amzn1.account.AM3B00000000000000000000000" + } + }, + "request": { + "type": "LaunchRequest", + "requestId": "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000", + "timestamp": "2015-05-13T12:34:56Z" + } +} diff --git a/test/LondonTravel.Skill.Tests/Samples/LaunchRequestWithEpochTimestamp.json b/test/LondonTravel.Skill.Tests/Samples/LaunchRequestWithEpochTimestamp.json new file mode 100644 index 00000000..84088268 --- /dev/null +++ b/test/LondonTravel.Skill.Tests/Samples/LaunchRequestWithEpochTimestamp.json @@ -0,0 +1,19 @@ +{ + "version": "1.0", + "session": { + "new": true, + "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", + "application": { + "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" + }, + "attributes": {}, + "user": { + "userId": "amzn1.account.AM3B00000000000000000000000" + } + }, + "request": { + "type": "LaunchRequest", + "requestId": "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000", + "timestamp": 1431520496000 + } +} diff --git a/test/LondonTravel.Skill.Tests/Samples/SessionEndedRequest.json b/test/LondonTravel.Skill.Tests/Samples/SessionEndedRequest.json new file mode 100644 index 00000000..1c9e44b1 --- /dev/null +++ b/test/LondonTravel.Skill.Tests/Samples/SessionEndedRequest.json @@ -0,0 +1,30 @@ +{ + "version": "1.0", + "session": { + "new": false, + "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000", + "application": { + "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" + }, + "attributes": { + "supportedHoroscopePeriods": { + "daily": true, + "weekly": false, + "monthly": false + } + }, + "user": { + "userId": "amzn1.account.AM3B00000000000000000000000" + } + }, + "request": { + "type": "SessionEndedRequest", + "requestId": "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000", + "timestamp": "2015-05-13T12:34:56Z", + "reason": "USER_INITIATED", + "error": { + "type": "INVALID_RESPONSE", + "message": "test message" + } + } +} diff --git a/test/LondonTravel.Skill.Tests/Samples/SkillResponse.json b/test/LondonTravel.Skill.Tests/Samples/SkillResponse.json new file mode 100644 index 00000000..3e785a01 --- /dev/null +++ b/test/LondonTravel.Skill.Tests/Samples/SkillResponse.json @@ -0,0 +1,22 @@ +{ + "version": "1.0", + "sessionAttributes": { + "supportedHoriscopePeriods": { + "daily": true, + "weekly": false, + "monthly": false + } + }, + "response": { + "outputSpeech": { + "type": "PlainText", + "text": "Today will provide you a new learning opportunity. Stick with it and the possibilities will be endless. Can I help you with anything else?" + }, + "card": { + "type": "Simple", + "title": "Horoscope", + "content": "Today will provide you a new learning opportunity. Stick with it and the possibilities will be endless." + }, + "shouldEndSession": false + } +} diff --git a/test/LondonTravel.Skill.Tests/SerializationTests.cs b/test/LondonTravel.Skill.Tests/SerializationTests.cs new file mode 100644 index 00000000..9731bb36 --- /dev/null +++ b/test/LondonTravel.Skill.Tests/SerializationTests.cs @@ -0,0 +1,41 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json; + +namespace MartinCostello.LondonTravel.Skill; + +public static class SerializationTests +{ + [Theory] + [InlineData("IntentRequest")] + [InlineData("LaunchRequest")] + [InlineData("LaunchRequestWithEpochTimestamp")] + [InlineData("SessionEndedRequest")] + public static async Task Can_Deserialize_Request(string name) + { + // Arrange + string json = await File.ReadAllTextAsync(Path.Combine("Samples", $"{name}.json")); + + // Act + var actual = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.SkillRequest); + + // Assert + actual.ShouldNotBeNull(); + actual.Request.ShouldNotBeNull(); + } + + [Theory] + [InlineData("SkillResponse")] + public static async Task Can_Deserialize_Response(string name) + { + // Arrange + string json = await File.ReadAllTextAsync(Path.Combine("Samples", $"{name}.json")); + + // Act + var actual = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.ResponseBody); + + // Assert + actual.ShouldNotBeNull(); + } +} diff --git a/test/LondonTravel.Skill.Tests/SessionEndedTests.cs b/test/LondonTravel.Skill.Tests/SessionEndedTests.cs index 0686d547..eaf52993 100644 --- a/test/LondonTravel.Skill.Tests/SessionEndedTests.cs +++ b/test/LondonTravel.Skill.Tests/SessionEndedTests.cs @@ -1,9 +1,7 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Request.Type; -using Alexa.NET.Response; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; @@ -15,7 +13,7 @@ public async Task Can_Invoke_Function() // Arrange AlexaFunction function = await CreateFunctionAsync(); - SkillRequest request = CreateRequest(); + SkillRequest request = CreateRequest("SessionEndedRequest"); // Act SkillResponse actual = await function.HandlerAsync(request); diff --git a/test/LondonTravel.Skill.Tests/StatusTests.cs b/test/LondonTravel.Skill.Tests/StatusTests.cs index 8fc86489..d4adbc31 100644 --- a/test/LondonTravel.Skill.Tests/StatusTests.cs +++ b/test/LondonTravel.Skill.Tests/StatusTests.cs @@ -1,9 +1,8 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Response; using JustEat.HttpClientInterception; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; @@ -42,7 +41,7 @@ public class StatusTests(ITestOutputHelper outputHelper) : FunctionTests(outputH [InlineData("Waterloo")] [InlineData("Waterloo & City")] [InlineData("Waterloo and City")] - public async Task Can_Invoke_Function_For_Valid_Line(string id) + public async Task Can_Invoke_Function_For_Valid_Lines(string id) { // Arrange await Interceptor.RegisterBundleAsync(Path.Combine("Bundles", "tfl-line-statuses.json")); diff --git a/test/LondonTravel.Skill.Tests/StopTests.cs b/test/LondonTravel.Skill.Tests/StopTests.cs index e3e9ac86..dadd2b4c 100644 --- a/test/LondonTravel.Skill.Tests/StopTests.cs +++ b/test/LondonTravel.Skill.Tests/StopTests.cs @@ -1,8 +1,7 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Response; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; diff --git a/test/LondonTravel.Skill.Tests/UnknownIntentTests.cs b/test/LondonTravel.Skill.Tests/UnknownIntentTests.cs index 1295a6bc..696ee317 100644 --- a/test/LondonTravel.Skill.Tests/UnknownIntentTests.cs +++ b/test/LondonTravel.Skill.Tests/UnknownIntentTests.cs @@ -1,9 +1,7 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Request.Type; -using Alexa.NET.Response; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; @@ -32,8 +30,4 @@ public async Task Can_Invoke_Function() var ssml = response.OutputSpeech.ShouldBeOfType(); ssml.Ssml.ShouldBe("Sorry, I don't understand how to do that."); } - - private sealed class UnknownRequest : Request - { - } } diff --git a/test/LondonTravel.Skill.Tests/UnknownRequestTests.cs b/test/LondonTravel.Skill.Tests/UnknownRequestTests.cs index 6075b35b..4bab1edd 100644 --- a/test/LondonTravel.Skill.Tests/UnknownRequestTests.cs +++ b/test/LondonTravel.Skill.Tests/UnknownRequestTests.cs @@ -1,9 +1,7 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. -using Alexa.NET.Request; -using Alexa.NET.Request.Type; -using Alexa.NET.Response; +using MartinCostello.LondonTravel.Skill.Models; namespace MartinCostello.LondonTravel.Skill; @@ -15,7 +13,7 @@ public async Task Can_Invoke_Function() // Arrange AlexaFunction function = await CreateFunctionAsync(); - SkillRequest request = CreateRequest(); + SkillRequest request = CreateRequest("Unknown"); // Act SkillResponse actual = await function.HandlerAsync(request); @@ -23,8 +21,4 @@ public async Task Can_Invoke_Function() // Assert AssertResponse(actual); } - - private sealed class UnknownRequest : Request - { - } } diff --git a/test/LondonTravel.Skill.Tests/testsettings.json b/test/LondonTravel.Skill.Tests/testsettings.json index 60d2dfeb..708e9e9e 100644 --- a/test/LondonTravel.Skill.Tests/testsettings.json +++ b/test/LondonTravel.Skill.Tests/testsettings.json @@ -8,7 +8,6 @@ } }, "Skill": { - "ApplicationInsightsConnectionString": "InstrumentationKey=my-application-insights-key;IngestionEndpoint=https://northeurope-0.in.applicationinsights.azure.com/;LiveEndpoint=https://northeurope.livediagnostics.monitor.azure.com/", "SkillApiUrl": "https://londontravel.martincostello.local/", "SkillId": "my-skill-id", "TflApplicationId": "my-tfl-app-id",