From 6898303618652a02f52480a7a2484d7337f554fc Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 2 Dec 2023 14:14:22 +0000 Subject: [PATCH 1/7] Update interaction model Update the interaction model to include Name Free Interactions. --- static/interaction-model.json | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/static/interaction-model.json b/static/interaction-model.json index 11f4bf88..87cbc4b8 100644 --- a/static/interaction-model.json +++ b/static/interaction-model.json @@ -940,6 +940,62 @@ ] } ] + }, + "_nameFreeInteraction": { + "ingressPoints": [ + { + "type": "INTENT", + "sampleUtterances": [ + { + "format": "RAW_TEXT", + "value": "Are there any problems with London Underground" + }, + { + "format": "RAW_TEXT", + "value": "Are there any engineering works on the tube today" + }, + { + "format": "RAW_TEXT", + "value": "Are there delays on the underground right now" + }, + { + "format": "RAW_TEXT", + "value": "Is there any disruption on the tube" + }, + { + "format": "RAW_TEXT", + "value": "Tube delays" + } + ], + "name": "DisruptionIntent" + }, + { + "type": "INTENT", + "sampleUtterances": [ + { + "format": "RAW_TEXT", + "value": "Are there any delays on the Central Line" + }, + { + "format": "RAW_TEXT", + "value": "Are there any engineering works on the Victoria Line this weekend" + }, + { + "format": "RAW_TEXT", + "value": "Is the Northern Line suspended" + }, + { + "format": "RAW_TEXT", + "value": "Is there any disruption on the District Line today" + }, + { + "format": "RAW_TEXT", + "value": "What problems are there on the Piccadilly line" + } + ], + "name": "StatusIntent" + } + ] } } } From 851f96f607f93f7c3fd985657d41f3615b00a803 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 2 Dec 2023 14:16:42 +0000 Subject: [PATCH 2/7] Add manfiest Add a copy of the skill manifest to the repo. --- static/skill.json | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 static/skill.json diff --git a/static/skill.json b/static/skill.json new file mode 100644 index 00000000..61bac6fa --- /dev/null +++ b/static/skill.json @@ -0,0 +1,58 @@ +{ + "manifest": { + "publishingInformation": { + "locales": { + "en-GB": { + "summary": "Find out the current status of public transport in London.", + "examplePhrases": [ + "Alexa, ask London Travel what\u0027s the status of the Northern line", + "Alexa, ask London Travel are there any line closures", + "Alexa, ask London Travel about my commute" + ], + "keywords": [ + "london", + "travel", + "tube", + "tfl", + "dlr", + "underground", + "overground" + ], + "name": "London Travel", + "description": "This skill allows you to ask Alexa for information about disruption on public transport in London.\n\nFor example, you can ask about disruption affecting London Underground, London Overground, the Docklands Light Railway (DLR) and TfL Rail as a whole, or you can ask for information about a specific line.\n\nIf you create an account with London Travel and link it with the Alexa app, you can also ask about your commute for your favourite lines. You can create an account with London Travel using a login for existing accounts with other services, such as Amazon, Facebook and Twitter.\n\nYou can manage your London Travel account and set up your preferences at https://londontravel.martincostello.com/.", + "smallIconUri": "file://icon-108x108.png", + "largeIconUri": "file://icon-512x512.png" + } + }, + "isAvailableWorldwide": false, + "testingInstructions": "To create an account for testing account linking, you must have an existing account with any of the following services:\n\nAmazon\nFacebook\nMicrosoft\nTwitter\n\nGoogle can be used to create an account in the website, but it cannot be used to link an account from the skill due to Google restrictions on sign-in from in-app browsers.\n\nTo test the \"commute\" intent, you must have created an account, selected at least one favourite tube line, and linked it to the Alexa app.", + "category": "PUBLIC_TRANSPORTATION", + "distributionCountries": [ + "GB" + ] + }, + "apis": { + "custom": { + "endpoint": { + "uri": "" + }, + "interfaces": [] + } + }, + "manifestVersion": "1.0", + "permissions": [], + "privacyAndCompliance": { + "allowsPurchases": false, + "locales": { + "en-GB": { + "termsOfUseUrl": "https://londontravel.martincostello.com/terms-of-service/", + "privacyPolicyUrl": "https://londontravel.martincostello.com/privacy-policy/" + } + }, + "containsAds": false, + "isExportCompliant": true, + "isChildDirected": false, + "usesPersonalInfo": true + } + } +} From 7599105373fc78f0db96bc2905ac1e14af7f319e Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 2 Dec 2023 15:13:53 +0000 Subject: [PATCH 3/7] Refactor end-to-end tests Rename SkillTests to LambdaTests to prepare for tests that use the simulation API to verify that the skill works for end-users. --- .github/workflows/build.yml | 10 ++++++++++ .github/workflows/deploy.yml | 5 +++++ .../CloudWatchLogsFixture.cs | 6 +++--- .../{SkillTests.cs => LambdaTests.cs} | 8 ++++---- ...{AwsConfiguration.cs => TestConfiguration.cs} | 16 +++++++++++++++- 5 files changed, 37 insertions(+), 8 deletions(-) rename test/LondonTravel.Skill.EndToEndTests/{SkillTests.cs => LambdaTests.cs} (93%) rename test/LondonTravel.Skill.EndToEndTests/{AwsConfiguration.cs => TestConfiguration.cs} (52%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db675b83..5acedad0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -219,6 +219,11 @@ jobs: run: dotnet test ./test/LondonTravel.Skill.EndToEndTests --configuration Release --logger "GitHubActions;report-warnings=false" env: LAMBDA_FUNCTION_NAME: ${{ env.LAMBDA_FUNCTION }}-dev + LWA_CLIENT_ID: ${{ secrets.LWA_CLIENT_ID }} + LWA_CLIENT_SECRET: ${{ secrets.LWA_CLIENT_SECRET }} + LWA_REFRESH_TOKEN: ${{ secrets.LWA_REFRESH_TOKEN }} + SKILL_ID: ${{ secrets.SKILL_ID }} + SKILL_STAGE: development deploy-prod: name: production @@ -327,3 +332,8 @@ jobs: run: dotnet test ./test/LondonTravel.Skill.EndToEndTests --configuration Release --logger "GitHubActions;report-warnings=false" env: LAMBDA_FUNCTION_NAME: ${{ env.LAMBDA_FUNCTION }} + LWA_CLIENT_ID: ${{ secrets.LWA_CLIENT_ID }} + LWA_CLIENT_SECRET: ${{ secrets.LWA_CLIENT_SECRET }} + LWA_REFRESH_TOKEN: ${{ secrets.LWA_REFRESH_TOKEN }} + SKILL_ID: ${{ secrets.SKILL_ID }} + SKILL_STAGE: live diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f9db4fe9..bf239f54 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -387,7 +387,12 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} LAMBDA_FUNCTION_NAME: ${{ needs.setup.outputs.function-name }} + LWA_CLIENT_ID: ${{ secrets.LWA_CLIENT_ID }} + LWA_CLIENT_SECRET: ${{ secrets.LWA_CLIENT_SECRET }} + LWA_REFRESH_TOKEN: ${{ secrets.LWA_REFRESH_TOKEN }} PULL_NUMBER: ${{ github.event.issue.number }} + SKILL_ID: ${{ secrets.SKILL_ID }} + SKILL_STAGE: development - name: Post comment uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 diff --git a/test/LondonTravel.Skill.EndToEndTests/CloudWatchLogsFixture.cs b/test/LondonTravel.Skill.EndToEndTests/CloudWatchLogsFixture.cs index 046c6b3e..08609f05 100644 --- a/test/LondonTravel.Skill.EndToEndTests/CloudWatchLogsFixture.cs +++ b/test/LondonTravel.Skill.EndToEndTests/CloudWatchLogsFixture.cs @@ -24,9 +24,9 @@ public async Task DisposeAsync() return; } - var credentials = AwsConfiguration.GetCredentials(); - string functionName = AwsConfiguration.FunctionName; - string regionName = AwsConfiguration.RegionName; + var credentials = TestConfiguration.GetCredentials(); + string functionName = TestConfiguration.FunctionName; + string regionName = TestConfiguration.RegionName; if (functionName is not null && regionName is not null && diff --git a/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs b/test/LondonTravel.Skill.EndToEndTests/LambdaTests.cs similarity index 93% rename from test/LondonTravel.Skill.EndToEndTests/SkillTests.cs rename to test/LondonTravel.Skill.EndToEndTests/LambdaTests.cs index 37ab35b9..57242b33 100644 --- a/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs +++ b/test/LondonTravel.Skill.EndToEndTests/LambdaTests.cs @@ -11,7 +11,7 @@ namespace MartinCostello.LondonTravel.Skill; [Collection(CloudWatchLogsFixtureCollection.Name)] -public class SkillTests(CloudWatchLogsFixture fixture, ITestOutputHelper outputHelper) +public class LambdaTests(CloudWatchLogsFixture fixture, ITestOutputHelper outputHelper) { public static IEnumerable Payloads { @@ -29,12 +29,12 @@ public static IEnumerable Payloads [MemberData(nameof(Payloads))] public async Task Can_Invoke_Intent_Can_Get_Json_Response(string payloadName) { - var credentials = AwsConfiguration.GetCredentials(); + var credentials = TestConfiguration.GetCredentials(); Skip.If(credentials is null, "No AWS credentials are configured."); - string functionName = AwsConfiguration.FunctionName; - string regionName = AwsConfiguration.RegionName; + string functionName = TestConfiguration.FunctionName; + string regionName = TestConfiguration.RegionName; Skip.If(string.IsNullOrEmpty(functionName), "No Lambda function name is configured."); Skip.If(string.IsNullOrEmpty(regionName), "No AWS region name is configured."); diff --git a/test/LondonTravel.Skill.EndToEndTests/AwsConfiguration.cs b/test/LondonTravel.Skill.EndToEndTests/TestConfiguration.cs similarity index 52% rename from test/LondonTravel.Skill.EndToEndTests/AwsConfiguration.cs rename to test/LondonTravel.Skill.EndToEndTests/TestConfiguration.cs index 2d31d447..f8ea80fe 100644 --- a/test/LondonTravel.Skill.EndToEndTests/AwsConfiguration.cs +++ b/test/LondonTravel.Skill.EndToEndTests/TestConfiguration.cs @@ -1,16 +1,30 @@ // 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.Net.Http.Headers; +using System.Reflection; using Amazon.Runtime; namespace LondonTravel.Skill.EndToEndTests; -internal static class AwsConfiguration +internal static class TestConfiguration { + public static string AlexaClientId => Environment.GetEnvironmentVariable("LWA_CLIENT_ID"); + + public static string AlexaClientSecret => Environment.GetEnvironmentVariable("LWA_CLIENT_SECRET"); + + public static string AlexaRefreshToken => Environment.GetEnvironmentVariable("LWA_REFRESH_TOKEN"); + public static string FunctionName => Environment.GetEnvironmentVariable("LAMBDA_FUNCTION_NAME"); public static string RegionName => Environment.GetEnvironmentVariable("AWS_REGION"); + public static string SkillId => Environment.GetEnvironmentVariable("SKILL_ID"); + + public static string SkillStage => Environment.GetEnvironmentVariable("SKILL_STAGE"); + + public static ProductInfoHeaderValue UserAgent { get; } = new("LondonTravel.Skill.EndToEndTests", typeof(TestConfiguration).Assembly.GetCustomAttribute().InformationalVersion); + public static AWSCredentials GetCredentials() { try From 5d4742ef98493ca62e754a66d76c01c3b90c8206 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 2 Dec 2023 15:18:37 +0000 Subject: [PATCH 4/7] Add Alexa simulator tests Add two end-to-end tests that use the Alexa Skill Management API to simulate requests to verify the skill actually works correctly for end-users. --- .../SkillTests.cs | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 test/LondonTravel.Skill.EndToEndTests/SkillTests.cs diff --git a/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs b/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs new file mode 100644 index 00000000..91648e42 --- /dev/null +++ b/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs @@ -0,0 +1,251 @@ +// 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.Net; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using LondonTravel.Skill.EndToEndTests; + +namespace MartinCostello.LondonTravel.Skill; + +public class SkillTests(ITestOutputHelper outputHelper) +{ + [SkippableTheory] + [InlineData("Alexa, ask London Travel if there is any disruption today.")] + [InlineData("Alexa, ask London Travel about the Victoria line.")] + public async Task Can_Invoke_Skill_And_Get_Valid_Response(string content) + { + // Arrange + string functionName = TestConfiguration.FunctionName; + string regionName = TestConfiguration.RegionName; + string skillId = TestConfiguration.SkillId; + string stage = TestConfiguration.SkillStage; + + Skip.If(string.IsNullOrEmpty(functionName), "No Lambda function name is configured."); + Skip.If(string.IsNullOrEmpty(regionName), "No AWS region name is configured."); + Skip.If(string.IsNullOrEmpty(skillId), "No skill ID is configured."); + Skip.If(string.IsNullOrEmpty(stage), "No skill stage is configured."); + + using var client = await CreateHttpClientAsync(); + + await EnableSkillAsync(client, skillId, stage); + + // Act + var simulation = await SimulateSkillAsync(client, skillId, stage, content); + simulation = await GetSimulationSkillAsync(client, skillId, stage, simulation); + + // Assert + simulation.ShouldNotBeNull(); + simulation.Result.ShouldNotBeNull(); + simulation.Result.SkillExecutionInfo.ShouldNotBeNull(); + simulation.Result.SkillExecutionInfo.Invocations.ShouldNotBeNull(); + simulation.Result.SkillExecutionInfo.Invocations.ShouldNotBeEmpty(); + simulation.Result.SkillExecutionInfo.Invocations.Count.ShouldBeGreaterThanOrEqualTo(1); + + var invocation = simulation.Result.SkillExecutionInfo.Invocations[0]; + + invocation.InvocationRequest.ShouldNotBeNull(); + invocation.InvocationRequest.RootElement.TryGetProperty("body", out _).ShouldBeTrue(); + invocation.InvocationRequest.RootElement.TryGetProperty("endpoint", out var endpoint).ShouldBeTrue(); + + string endpointValue = endpoint.GetString(); + endpointValue.ShouldStartWith($"arn:aws:lambda:{regionName}:"); + endpointValue.ShouldEndWith($":function:{functionName}"); + + invocation.InvocationResponse.ShouldNotBeNull(); + invocation.InvocationResponse.RootElement.TryGetProperty("body", out var body).ShouldBeTrue(); + body.TryGetProperty("response", out var skillResponse).ShouldBeTrue(); + skillResponse.TryGetProperty("outputSpeech", out var outputSpeech).ShouldBeTrue(); + + outputSpeech.TryGetProperty("type", out var speechType).ShouldBeTrue(); + speechType.GetString().ShouldBe("SSML"); + + outputSpeech.TryGetProperty("ssml", out var ssml).ShouldBeTrue(); + string speech = ssml.GetString(); + + speech.ShouldNotBeNullOrWhiteSpace(); + speech.ShouldStartWith(""); + speech.ShouldNotContain("Sorry, something went wrong."); + } + + private static async Task GenerateAccessTokenAsync() + { + // To generate a new refresh token, run the following command: + // npm install -g ask-cli && ask util generate-lwa-tokens --client-id $CLIENT_ID --client-confirmation $CLIENT_SECRET --scopes "alexa::ask:skills:readwrite alexa::ask:skills:test" + string clientId = TestConfiguration.AlexaClientId; + string clientSecret = TestConfiguration.AlexaClientSecret; + string refreshToken = TestConfiguration.AlexaRefreshToken; + + Skip.If(string.IsNullOrEmpty(clientId), "No client ID is configured."); + Skip.If(string.IsNullOrEmpty(clientSecret), "No client secret is configured."); + Skip.If(string.IsNullOrEmpty(refreshToken), "No refresh token is configured."); + + var parameters = new Dictionary() + { + ["client_id"] = clientId, + ["client_secret"] = clientSecret, + ["grant_type"] = "refresh_token", + ["refresh_token"] = refreshToken, + }; + + using var client = new HttpClient() + { + DefaultRequestHeaders = + { + UserAgent = { TestConfiguration.UserAgent }, + }, + }; + + using var content = new FormUrlEncodedContent(parameters); + using var response = await client.PostAsync(new Uri("https://api.amazon.com/auth/o2/token"), content); + + response.EnsureSuccessStatusCode(); + + using var tokens = await response.Content.ReadFromJsonAsync(); + return tokens.RootElement.GetProperty("access_token").GetString(); + } + + private static async Task CreateHttpClientAsync() + { + string token = await GenerateAccessTokenAsync(); + + var client = new HttpClient() + { + BaseAddress = new Uri("https://api.amazonalexa.com", UriKind.Absolute), + DefaultRequestHeaders = + { + Authorization = new("Bearer", token), + UserAgent = { TestConfiguration.UserAgent }, + }, + }; + + return client; + } + + private static async Task EnableSkillAsync(HttpClient client, string skillId, string stage) + { + // See https://developer.amazon.com/en-US/docs/alexa/smapi/skill-enablement.html#enable-skill + using var response = await client.PutAsJsonAsync($"v1/skills/{skillId}/stages/{stage}/enablement", new { }); + + response.StatusCode.ShouldBe(HttpStatusCode.NoContent, $"Failed to enable skill for stage {stage}."); + response.EnsureSuccessStatusCode(); + } + + private static async Task GetSimulationSkillAsync( + HttpClient client, + string skillId, + string stage, + SimulationResponse simulation) + { + string simulationId = simulation.Id; + + const string InProgress = "IN_PROGRESS"; + + if (simulation.Status is InProgress) + { + // Poll for a response to be available + var delay = TimeSpan.FromSeconds(2); + + for (int i = 0; i < 5; i++) + { + await Task.Delay(delay); + + // See https://developer.amazon.com/en-US/docs/alexa/smapi/skill-simulation-api.html#get-simulation-result + simulation = await client.GetFromJsonAsync($"v2/skills/{skillId}/stages/{stage}/simulations/{simulationId}"); + simulation.ShouldNotBeNull(); + + if (simulation.Status is not InProgress) + { + break; + } + } + } + + simulation.ShouldNotBeNull(); + simulation.Id.ShouldBe(simulationId); + simulation.Status.ShouldBe("SUCCESSFUL", $"Code: {simulation.Result?.Code}; Result: {simulation.Result?.Message}"); + + return simulation; + } + + private async Task SimulateSkillAsync( + HttpClient client, + string skillId, + string stage, + string content) + { + var request = new + { + session = new { mode = "DEFAULT" }, + input = new { content }, + device = new { locale = "en-GB" }, + }; + + // See https://developer.amazon.com/en-US/docs/alexa/smapi/skill-simulation-api.html#simulate-skill + using var response = await client.PostAsJsonAsync($"v2/skills/{skillId}/stages/{stage}/simulations", request); + + string requestId = response.Headers.GetValues("x-amzn-requestid").FirstOrDefault(); + outputHelper.WriteLine($"Request Id: {requestId}"); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + + var simulation = await response.Content.ReadFromJsonAsync(); + + simulation.ShouldNotBeNull(); + + outputHelper.WriteLine($"Simulation ID: {simulation.Id}"); + + return simulation; + } + + private sealed class SimulationRequest + { + public string Content { get; set; } + + public string Locale { get; set; } + + public string SkillId { get; set; } + + public string Stage { get; set; } + } + + private sealed class SimulationResponse + { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("result")] + public SimulationResult Result { get; set; } + } + + private sealed class SimulationResult + { + [JsonPropertyName("message")] + public string Message { get; set; } + + [JsonPropertyName("code")] + public string Code { get; set; } + + [JsonPropertyName("skillExecutionInfo")] + public SkillExecutionInfo SkillExecutionInfo { get; set; } + } + + private sealed class SkillExecutionInfo + { + [JsonPropertyName("invocations")] + public IList Invocations { get; set; } + } + + private sealed class SkillInvocation + { + [JsonPropertyName("invocationRequest")] + public JsonDocument InvocationRequest { get; set; } + + [JsonPropertyName("invocationResponse")] + public JsonDocument InvocationResponse { get; set; } + } +} From b7038a0266ea1c85dbb95c35eeb5d9e2c3a8c5e3 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 2 Dec 2023 15:19:21 +0000 Subject: [PATCH 5/7] Use TestConfiguration.UserAgent Use the `TestConfiguration.UserAgent` property. --- test/LondonTravel.Skill.EndToEndTests/CloudWatchLogsFixture.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/LondonTravel.Skill.EndToEndTests/CloudWatchLogsFixture.cs b/test/LondonTravel.Skill.EndToEndTests/CloudWatchLogsFixture.cs index 08609f05..735dc874 100644 --- a/test/LondonTravel.Skill.EndToEndTests/CloudWatchLogsFixture.cs +++ b/test/LondonTravel.Skill.EndToEndTests/CloudWatchLogsFixture.cs @@ -2,7 +2,6 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. using System.Net.Http.Json; -using System.Reflection; using Amazon; using Amazon.CloudWatchLogs; using Xunit.Sdk; @@ -205,7 +204,7 @@ private static async Task TryPostLogsToPullRequestAsync(IEnumerable ev { Accept = { new("application/vnd.github+json") }, Authorization = new("Bearer", token), - UserAgent = { new("LondonTravel.Skill.EndToEndTests", typeof(CloudWatchLogsFixture).Assembly.GetCustomAttribute().InformationalVersion) }, + UserAgent = { TestConfiguration.UserAgent }, }, }; From d94f677a9682a2fddf6bdd0c91529d049aa9f0b9 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 2 Dec 2023 15:26:56 +0000 Subject: [PATCH 6/7] Add comment for token refresh Add comment with link to the documentation to get an access token from a refresh token. --- test/LondonTravel.Skill.EndToEndTests/SkillTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs b/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs index 91648e42..3b313857 100644 --- a/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs +++ b/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs @@ -81,6 +81,7 @@ private static async Task GenerateAccessTokenAsync() Skip.If(string.IsNullOrEmpty(clientSecret), "No client secret is configured."); Skip.If(string.IsNullOrEmpty(refreshToken), "No refresh token is configured."); + // See https://developer.amazon.com/docs/login-with-amazon/authorization-code-grant.html#using-refresh-tokens var parameters = new Dictionary() { ["client_id"] = clientId, From 4a6669b598768f23e7afb1fd2b1f8c504c114c8e Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 2 Dec 2023 15:27:49 +0000 Subject: [PATCH 7/7] Move comment Move comment to sit with the first part, not the POST. --- test/LondonTravel.Skill.EndToEndTests/SkillTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs b/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs index 3b313857..7e2bb95e 100644 --- a/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs +++ b/test/LondonTravel.Skill.EndToEndTests/SkillTests.cs @@ -176,6 +176,7 @@ private async Task SimulateSkillAsync( string stage, string content) { + // See https://developer.amazon.com/en-US/docs/alexa/smapi/skill-simulation-api.html#simulate-skill var request = new { session = new { mode = "DEFAULT" }, @@ -183,7 +184,6 @@ private async Task SimulateSkillAsync( device = new { locale = "en-GB" }, }; - // See https://developer.amazon.com/en-US/docs/alexa/smapi/skill-simulation-api.html#simulate-skill using var response = await client.PostAsJsonAsync($"v2/skills/{skillId}/stages/{stage}/simulations", request); string requestId = response.Headers.GetValues("x-amzn-requestid").FirstOrDefault();