diff --git a/src/Middleware/Middleware.sln b/src/Middleware/Middleware.sln
index 5b4a846213..4eefacd361 100644
--- a/src/Middleware/Middleware.sln
+++ b/src/Middleware/Middleware.sln
@@ -267,6 +267,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SpaSer
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SpaServices.Extensions", "SpaServices.Extensions\src\Microsoft.AspNetCore.SpaServices.Extensions.csproj", "{5D5B7E54-9323-498A-8983-E9BDFA3B2D07}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.NodeServices.Tests", "NodeServices\test\Microsoft.AspNetCore.NodeServices.Tests.csproj", "{B04E9CB6-0D1C-4C21-B626-89B6926A491F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1465,6 +1467,18 @@ Global
{5D5B7E54-9323-498A-8983-E9BDFA3B2D07}.Release|x64.Build.0 = Release|Any CPU
{5D5B7E54-9323-498A-8983-E9BDFA3B2D07}.Release|x86.ActiveCfg = Release|Any CPU
{5D5B7E54-9323-498A-8983-E9BDFA3B2D07}.Release|x86.Build.0 = Release|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Debug|x64.Build.0 = Debug|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Debug|x86.Build.0 = Debug|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Release|x64.ActiveCfg = Release|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Release|x64.Build.0 = Release|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Release|x86.ActiveCfg = Release|Any CPU
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1580,6 +1594,7 @@ Global
{121DFA13-E965-4C91-A175-19EF20DFD5AC} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472}
{D9D02772-1D53-45C3-B2CC-888F9978958C} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472}
{5D5B7E54-9323-498A-8983-E9BDFA3B2D07} = {D6FA4ABE-E685-4EDD-8B06-D8777E76B472}
+ {B04E9CB6-0D1C-4C21-B626-89B6926A491F} = {17B409B3-7EC6-49D8-847E-CFAA319E01B5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA}
diff --git a/src/Middleware/NodeServices/test/Microsoft.AspNetCore.NodeServices.Tests.csproj b/src/Middleware/NodeServices/test/Microsoft.AspNetCore.NodeServices.Tests.csproj
new file mode 100644
index 0000000000..862335b90b
--- /dev/null
+++ b/src/Middleware/NodeServices/test/Microsoft.AspNetCore.NodeServices.Tests.csproj
@@ -0,0 +1,13 @@
+
+
+
+ netcoreapp3.0
+
+
+
+
+
+
+
+
+
diff --git a/src/Middleware/NodeServices/test/NodeServicesTest.cs b/src/Middleware/NodeServices/test/NodeServicesTest.cs
new file mode 100644
index 0000000000..a5012ea8aa
--- /dev/null
+++ b/src/Middleware/NodeServices/test/NodeServicesTest.cs
@@ -0,0 +1,134 @@
+// 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.AspNetCore.NodeServices.HostingModels;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.AspNetCore.NodeServices
+{
+ public class NodeServicesTest : IDisposable
+ {
+ private readonly INodeServices _nodeServices;
+
+ public NodeServicesTest()
+ {
+ // In typical ASP.NET Core applications, INodeServices is made available
+ // through DI using services.AddNodeServices(). But for these tests we
+ // create our own INodeServices instance manually, since the tests are
+ // not about DI (and we might want different config for each test).
+ var serviceProvider = new ServiceCollection().BuildServiceProvider();
+ var options = new NodeServicesOptions(serviceProvider);
+ _nodeServices = NodeServicesFactory.CreateNodeServices(options);
+ }
+
+ [Fact]
+ public async Task CanGetSuccessResult()
+ {
+ // Act
+ var result = await _nodeServices.InvokeExportAsync(
+ ModulePath("testCases"),
+ "getFixedString");
+
+ // Assert
+ Assert.Equal("test result", result);
+ }
+
+ [Fact]
+ public async Task CanGetErrorResult()
+ {
+ // Act/Assert
+ var ex = await Assert.ThrowsAsync(() =>
+ _nodeServices.InvokeExportAsync(
+ ModulePath("testCases"),
+ "raiseError"));
+ Assert.StartsWith("This is an error from Node", ex.Message);
+ }
+
+ [Fact]
+ public async Task CanGetResultAsynchronously()
+ {
+ // Act
+ // All the invocations are async, but this test shows we're not reliant
+ // on the response coming back immediately
+ var result = await _nodeServices.InvokeExportAsync(
+ ModulePath("testCases"),
+ "getFixedStringWithDelay");
+
+ // Assert
+ Assert.Equal("delayed test result", result);
+ }
+
+ [Fact]
+ public async Task CanPassParameters()
+ {
+ // Act
+ var result = await _nodeServices.InvokeExportAsync(
+ ModulePath("testCases"),
+ "echoSimpleParameters",
+ "Hey",
+ 123);
+
+ // Assert
+ Assert.Equal("Param0: Hey; Param1: 123", result);
+ }
+
+ [Fact]
+ public async Task CanPassParametersWithCamelCaseNameConversion()
+ {
+ // Act
+ var result = await _nodeServices.InvokeExportAsync(
+ ModulePath("testCases"),
+ "echoComplexParameters",
+ new ComplexModel { StringProp = "Abc", IntProp = 123, BoolProp = true });
+
+ // Assert
+ Assert.Equal("Received: [{\"stringProp\":\"Abc\",\"intProp\":123,\"boolProp\":true}]", result);
+ }
+
+ [Fact]
+ public async Task CanReceiveComplexResultWithPascalCaseNameConversion()
+ {
+ // Act
+ var result = await _nodeServices.InvokeExportAsync(
+ ModulePath("testCases"),
+ "getComplexObject");
+
+ // Assert
+ Assert.Equal("Hi from Node", result.StringProp);
+ Assert.Equal(456, result.IntProp);
+ Assert.True(result.BoolProp);
+ }
+
+ [Fact]
+ public async Task CanInvokeDefaultModuleExport()
+ {
+ // Act
+ var result = await _nodeServices.InvokeAsync(
+ ModulePath("moduleWithDefaultExport"),
+ "This is from .NET");
+
+ // Assert
+ Assert.Equal("Hello from the default export. You passed: This is from .NET", result);
+ }
+
+ private static string ModulePath(string testModuleName)
+ => $"../../../js/{testModuleName}";
+
+ public void Dispose()
+ {
+ _nodeServices.Dispose();
+ }
+
+ class ComplexModel
+ {
+ public string StringProp { get; set; }
+
+ public int IntProp { get; set; }
+
+ public bool BoolProp { get; set; }
+ }
+ }
+}
diff --git a/src/Middleware/NodeServices/test/js/moduleWithDefaultExport.js b/src/Middleware/NodeServices/test/js/moduleWithDefaultExport.js
new file mode 100644
index 0000000000..eae7f7ccf4
--- /dev/null
+++ b/src/Middleware/NodeServices/test/js/moduleWithDefaultExport.js
@@ -0,0 +1,3 @@
+module.exports = function (callback, message) {
+ callback(null, `Hello from the default export. You passed: ${message}`);
+};
diff --git a/src/Middleware/NodeServices/test/js/testCases.js b/src/Middleware/NodeServices/test/js/testCases.js
new file mode 100644
index 0000000000..74b3a49ccf
--- /dev/null
+++ b/src/Middleware/NodeServices/test/js/testCases.js
@@ -0,0 +1,28 @@
+// Function signatures follow Node conventions.
+// i.e., parameters: (callback, arg0, arg1, ... etc ...)
+// When done, functions must invoke 'callback', passing (errorInfo, result)
+// where errorInfo should be null/undefined if there was no error.
+
+exports.getFixedString = function (callback) {
+ callback(null, 'test result');
+};
+
+exports.getFixedStringWithDelay = function (callback) {
+ setTimeout(callback(null, 'delayed test result'), 100);
+};
+
+exports.raiseError = function (callback) {
+ callback('This is an error from Node');
+};
+
+exports.echoSimpleParameters = function (callback, param0, param1) {
+ callback(null, `Param0: ${param0}; Param1: ${param1}`);
+};
+
+exports.echoComplexParameters = function (callback, ...otherArgs) {
+ callback(null, `Received: ${JSON.stringify(otherArgs)}`);
+};
+
+exports.getComplexObject = function (callback) {
+ callback(null, { stringProp: 'Hi from Node', intProp: 456, boolProp: true });
+};