summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs46
-rw-r--r--src/SMAPI.Tests.ModApiConsumer/Interfaces/ISimpleApi.cs79
-rw-r--r--src/SMAPI.Tests.ModApiConsumer/README.md3
-rw-r--r--src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj11
-rw-r--r--src/SMAPI.Tests.ModApiProvider/Framework/BaseApi.cs12
-rw-r--r--src/SMAPI.Tests.ModApiProvider/Framework/SimpleApi.cs108
-rw-r--r--src/SMAPI.Tests.ModApiProvider/ProviderMod.cs38
-rw-r--r--src/SMAPI.Tests.ModApiProvider/README.md3
-rw-r--r--src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj7
-rw-r--r--src/SMAPI.Tests/Core/InterfaceProxyTests.cs345
-rw-r--r--src/SMAPI.Tests/SMAPI.Tests.csproj7
-rw-r--r--src/SMAPI.sln14
12 files changed, 669 insertions, 4 deletions
diff --git a/src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs b/src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs
new file mode 100644
index 00000000..2c7f9952
--- /dev/null
+++ b/src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs
@@ -0,0 +1,46 @@
+using System;
+using SMAPI.Tests.ModApiConsumer.Interfaces;
+
+namespace SMAPI.Tests.ModApiConsumer
+{
+ /// <summary>A simulated API consumer.</summary>
+ public class ApiConsumer
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Call the event field on the given API.</summary>
+ /// <param name="api">The API to call.</param>
+ /// <param name="getValues">Get the number of times the event was called and the last value received.</param>
+ public void UseEventField(ISimpleApi api, out Func<(int timesCalled, int actualValue)> getValues)
+ {
+ // act
+ int calls = 0;
+ int lastValue = -1;
+ api.OnEventRaised += (sender, value) =>
+ {
+ calls++;
+ lastValue = value;
+ };
+
+ getValues = () => (timesCalled: calls, actualValue: lastValue);
+ }
+
+ /// <summary>Call the event property on the given API.</summary>
+ /// <param name="api">The API to call.</param>
+ /// <param name="getValues">Get the number of times the event was called and the last value received.</param>
+ public void UseEventProperty(ISimpleApi api, out Func<(int timesCalled, int actualValue)> getValues)
+ {
+ // act
+ int calls = 0;
+ int lastValue = -1;
+ api.OnEventRaisedProperty += (sender, value) =>
+ {
+ calls++;
+ lastValue = value;
+ };
+
+ getValues = () => (timesCalled: calls, actualValue: lastValue);
+ }
+ }
+}
diff --git a/src/SMAPI.Tests.ModApiConsumer/Interfaces/ISimpleApi.cs b/src/SMAPI.Tests.ModApiConsumer/Interfaces/ISimpleApi.cs
new file mode 100644
index 00000000..7f94e137
--- /dev/null
+++ b/src/SMAPI.Tests.ModApiConsumer/Interfaces/ISimpleApi.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace SMAPI.Tests.ModApiConsumer.Interfaces
+{
+ /// <summary>A mod-provided API which provides basic events, properties, and methods.</summary>
+ public interface ISimpleApi
+ {
+ /*********
+ ** Test interface
+ *********/
+ /****
+ ** Events
+ ****/
+ /// <summary>A simple event field.</summary>
+ event EventHandler<int> OnEventRaised;
+
+ /// <summary>A simple event property with custom add/remove logic.</summary>
+ event EventHandler<int> OnEventRaisedProperty;
+
+
+ /****
+ ** Properties
+ ****/
+ /// <summary>A simple numeric property.</summary>
+ int NumberProperty { get; set; }
+
+ /// <summary>A simple object property.</summary>
+ object ObjectProperty { get; set; }
+
+ /// <summary>A simple list property.</summary>
+ List<string> ListProperty { get; set; }
+
+ /// <summary>A simple list property with an interface.</summary>
+ IList<string> ListPropertyWithInterface { get; set; }
+
+ /// <summary>A property with nested generics.</summary>
+ IDictionary<string, IList<string>> GenericsProperty { get; set; }
+
+ /// <summary>A property using an enum available to both mods.</summary>
+ BindingFlags EnumProperty { get; set; }
+
+ /// <summary>A read-only property.</summary>
+ int GetterProperty { get; }
+
+
+ /****
+ ** Methods
+ ****/
+ /// <summary>A simple method with no return value.</summary>
+ void GetNothing();
+
+ /// <summary>A simple method which returns a number.</summary>
+ int GetInt(int value);
+
+ /// <summary>A simple method which returns an object.</summary>
+ object GetObject(object value);
+
+ /// <summary>A simple method which returns a list.</summary>
+ List<string> GetList(string value);
+
+ /// <summary>A simple method which returns a list with an interface.</summary>
+ IList<string> GetListWithInterface(string value);
+
+ /// <summary>A simple method which returns nested generics.</summary>
+ IDictionary<string, IList<string>> GetGenerics(string key, string value);
+
+ /// <summary>A simple method which returns a lambda.</summary>
+ Func<string, string> GetLambda(Func<string, string> value);
+
+
+ /****
+ ** Inherited members
+ ****/
+ /// <summary>A property inherited from a base class.</summary>
+ public string InheritedProperty { get; set; }
+ }
+}
diff --git a/src/SMAPI.Tests.ModApiConsumer/README.md b/src/SMAPI.Tests.ModApiConsumer/README.md
new file mode 100644
index 00000000..ed0c6e3f
--- /dev/null
+++ b/src/SMAPI.Tests.ModApiConsumer/README.md
@@ -0,0 +1,3 @@
+This project contains a simulated [mod-provided API] consumer used in the API proxying unit tests.
+
+[mod-provided API]: https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations
diff --git a/src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj b/src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj
new file mode 100644
index 00000000..7fef4ebd
--- /dev/null
+++ b/src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <Import Project="..\..\build\common.targets" />
+
+ <ItemGroup>
+ <ProjectReference Include="..\SMAPI\SMAPI.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/src/SMAPI.Tests.ModApiProvider/Framework/BaseApi.cs b/src/SMAPI.Tests.ModApiProvider/Framework/BaseApi.cs
new file mode 100644
index 00000000..8092e3e7
--- /dev/null
+++ b/src/SMAPI.Tests.ModApiProvider/Framework/BaseApi.cs
@@ -0,0 +1,12 @@
+namespace SMAPI.Tests.ModApiProvider.Framework
+{
+ /// <summary>The base class for <see cref="SimpleApi"/>.</summary>
+ public class BaseApi
+ {
+ /*********
+ ** Test interface
+ *********/
+ /// <summary>A property inherited from a base class.</summary>
+ public string InheritedProperty { get; set; }
+ }
+}
diff --git a/src/SMAPI.Tests.ModApiProvider/Framework/SimpleApi.cs b/src/SMAPI.Tests.ModApiProvider/Framework/SimpleApi.cs
new file mode 100644
index 00000000..1100af36
--- /dev/null
+++ b/src/SMAPI.Tests.ModApiProvider/Framework/SimpleApi.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace SMAPI.Tests.ModApiProvider.Framework
+{
+ /// <summary>A mod-provided API which provides basic events, properties, and methods.</summary>
+ public class SimpleApi : BaseApi
+ {
+ /*********
+ ** Test interface
+ *********/
+ /****
+ ** Events
+ ****/
+ /// <summary>A simple event field.</summary>
+ public event EventHandler<int> OnEventRaised;
+
+ /// <summary>A simple event property with custom add/remove logic.</summary>
+ public event EventHandler<int> OnEventRaisedProperty
+ {
+ add => this.OnEventRaised += value;
+ remove => this.OnEventRaised -= value;
+ }
+
+
+ /****
+ ** Properties
+ ****/
+ /// <summary>A simple numeric property.</summary>
+ public int NumberProperty { get; set; }
+
+ /// <summary>A simple object property.</summary>
+ public object ObjectProperty { get; set; }
+
+ /// <summary>A simple list property.</summary>
+ public List<string> ListProperty { get; set; }
+
+ /// <summary>A simple list property with an interface.</summary>
+ public IList<string> ListPropertyWithInterface { get; set; }
+
+ /// <summary>A property with nested generics.</summary>
+ public IDictionary<string, IList<string>> GenericsProperty { get; set; }
+
+ /// <summary>A property using an enum available to both mods.</summary>
+ public BindingFlags EnumProperty { get; set; }
+
+ /// <summary>A read-only property.</summary>
+ public int GetterProperty => 42;
+
+
+ /****
+ ** Methods
+ ****/
+ /// <summary>A simple method with no return value.</summary>
+ public void GetNothing() { }
+
+ /// <summary>A simple method which returns a number.</summary>
+ public int GetInt(int value)
+ {
+ return value;
+ }
+
+ /// <summary>A simple method which returns an object.</summary>
+ public object GetObject(object value)
+ {
+ return value;
+ }
+
+ /// <summary>A simple method which returns a list.</summary>
+ public List<string> GetList(string value)
+ {
+ return new() { value };
+ }
+
+ /// <summary>A simple method which returns a list with an interface.</summary>
+ public IList<string> GetListWithInterface(string value)
+ {
+ return new List<string> { value };
+ }
+
+ /// <summary>A simple method which returns nested generics.</summary>
+ public IDictionary<string, IList<string>> GetGenerics(string key, string value)
+ {
+ return new Dictionary<string, IList<string>>
+ {
+ [key] = new List<string> { value }
+ };
+ }
+
+ /// <summary>A simple method which returns a lambda.</summary>
+ public Func<string, string> GetLambda(Func<string, string> value)
+ {
+ return value;
+ }
+
+
+ /*********
+ ** Helper methods
+ *********/
+ /// <summary>Raise the <see cref="OnEventRaised"/> event.</summary>
+ /// <param name="value">The value to pass to the event.</param>
+ public void RaiseEventField(int value)
+ {
+ this.OnEventRaised?.Invoke(null, value);
+ }
+ }
+}
diff --git a/src/SMAPI.Tests.ModApiProvider/ProviderMod.cs b/src/SMAPI.Tests.ModApiProvider/ProviderMod.cs
new file mode 100644
index 00000000..c36e1c6d
--- /dev/null
+++ b/src/SMAPI.Tests.ModApiProvider/ProviderMod.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Reflection;
+using SMAPI.Tests.ModApiProvider.Framework;
+
+namespace SMAPI.Tests.ModApiProvider
+{
+ /// <summary>A simulated mod instance.</summary>
+ public class ProviderMod
+ {
+ /// <summary>The underlying API instance.</summary>
+ private readonly SimpleApi Api = new();
+
+ /// <summary>Get the mod API instance.</summary>
+ public object GetModApi()
+ {
+ return this.Api;
+ }
+
+ /// <summary>Raise the <see cref="SimpleApi.OnEventRaised"/> event.</summary>
+ /// <param name="value">The value to send as an event argument.</param>
+ public void RaiseEvent(int value)
+ {
+ this.Api.RaiseEventField(value);
+ }
+
+ /// <summary>Set the values for the API property.</summary>
+ public void SetPropertyValues(int number, object obj, string listValue, string listWithInterfaceValue, string dictionaryKey, string dictionaryListValue, BindingFlags enumValue, string inheritedValue)
+ {
+ this.Api.NumberProperty = number;
+ this.Api.ObjectProperty = obj;
+ this.Api.ListProperty = new List<string> { listValue };
+ this.Api.ListPropertyWithInterface = new List<string> { listWithInterfaceValue };
+ this.Api.GenericsProperty = new Dictionary<string, IList<string>> { [dictionaryKey] = new List<string> { dictionaryListValue } };
+ this.Api.EnumProperty = enumValue;
+ this.Api.InheritedProperty = inheritedValue;
+ }
+ }
+}
diff --git a/src/SMAPI.Tests.ModApiProvider/README.md b/src/SMAPI.Tests.ModApiProvider/README.md
new file mode 100644
index 00000000..c79838e0
--- /dev/null
+++ b/src/SMAPI.Tests.ModApiProvider/README.md
@@ -0,0 +1,3 @@
+This project contains simulated [mod-provided APIs] used in the API proxying unit tests.
+
+[mod-provided APIs]: https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations
diff --git a/src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj b/src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj
new file mode 100644
index 00000000..70d5a0ce
--- /dev/null
+++ b/src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj
@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ <Import Project="..\..\build\common.targets" />
+</Project>
diff --git a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs
new file mode 100644
index 00000000..99c1298f
--- /dev/null
+++ b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs
@@ -0,0 +1,345 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using FluentAssertions;
+using NUnit.Framework;
+using SMAPI.Tests.ModApiConsumer;
+using SMAPI.Tests.ModApiConsumer.Interfaces;
+using SMAPI.Tests.ModApiProvider;
+using StardewModdingAPI.Framework.Reflection;
+
+namespace SMAPI.Tests.Core
+{
+ /// <summary>Unit tests for <see cref="InterfaceProxyFactory"/>.</summary>
+ [TestFixture]
+ internal class InterfaceProxyTests
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The mod ID providing an API.</summary>
+ private readonly string FromModId = "From.ModId";
+
+ /// <summary>The mod ID consuming an API.</summary>
+ private readonly string ToModId = "From.ModId";
+
+ /// <summary>The random number generator with which to create sample values.</summary>
+ private readonly Random Random = new();
+
+
+ /*********
+ ** Unit tests
+ *********/
+ /****
+ ** Events
+ ****/
+ /// <summary>Assert that an event field can be proxied correctly.</summary>
+ [Test]
+ public void CanProxy_EventField()
+ {
+ // arrange
+ var providerMod = new ProviderMod();
+ object implementation = providerMod.GetModApi();
+ int expectedValue = this.Random.Next();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(implementation);
+ new ApiConsumer().UseEventField(proxy, out Func<(int timesCalled, int lastValue)> getValues);
+ providerMod.RaiseEvent(expectedValue);
+ (int timesCalled, int lastValue) = getValues();
+
+ // assert
+ timesCalled.Should().Be(1, "Expected the proxied event to be raised once.");
+ lastValue.Should().Be(expectedValue, "The proxy received a different event argument than the implementation raised.");
+ }
+
+ /// <summary>Assert that an event property can be proxied correctly.</summary>
+ [Test]
+ public void CanProxy_EventProperty()
+ {
+ // arrange
+ var providerMod = new ProviderMod();
+ object implementation = providerMod.GetModApi();
+ int expectedValue = this.Random.Next();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(implementation);
+ new ApiConsumer().UseEventProperty(proxy, out Func<(int timesCalled, int lastValue)> getValues);
+ providerMod.RaiseEvent(expectedValue);
+ (int timesCalled, int lastValue) = getValues();
+
+ // assert
+ timesCalled.Should().Be(1, "Expected the proxied event to be raised once.");
+ lastValue.Should().Be(expectedValue, "The proxy received a different event argument than the implementation raised.");
+ }
+
+ /****
+ ** Properties
+ ****/
+ /// <summary>Assert that properties can be proxied correctly.</summary>
+ /// <param name="setVia">Whether to set the properties through the <c>provider mod</c> or <c>proxy interface</c>.</param>
+ [TestCase("set via provider mod")]
+ [TestCase("set via proxy interface")]
+ public void CanProxy_Properties(string setVia)
+ {
+ // arrange
+ var providerMod = new ProviderMod();
+ object implementation = providerMod.GetModApi();
+ int expectedNumber = this.Random.Next();
+ int expectedObject = this.Random.Next();
+ string expectedListValue = this.GetRandomString();
+ string expectedListWithInterfaceValue = this.GetRandomString();
+ string expectedDictionaryKey = this.GetRandomString();
+ string expectedDictionaryListValue = this.GetRandomString();
+ string expectedInheritedString = this.GetRandomString();
+ BindingFlags expectedEnum = BindingFlags.Instance | BindingFlags.Public;
+
+ // act
+ ISimpleApi proxy = this.GetProxy(implementation);
+ switch (setVia)
+ {
+ case "set via provider mod":
+ providerMod.SetPropertyValues(
+ number: expectedNumber,
+ obj: expectedObject,
+ listValue: expectedListValue,
+ listWithInterfaceValue: expectedListWithInterfaceValue,
+ dictionaryKey: expectedDictionaryKey,
+ dictionaryListValue: expectedDictionaryListValue,
+ enumValue: expectedEnum,
+ inheritedValue: expectedInheritedString
+ );
+ break;
+
+ case "set via proxy interface":
+ proxy.NumberProperty = expectedNumber;
+ proxy.ObjectProperty = expectedObject;
+ proxy.ListProperty = new() { expectedListValue };
+ proxy.ListPropertyWithInterface = new List<string> { expectedListWithInterfaceValue };
+ proxy.GenericsProperty = new Dictionary<string, IList<string>>
+ {
+ [expectedDictionaryKey] = new List<string> { expectedDictionaryListValue }
+ };
+ proxy.EnumProperty = expectedEnum;
+ proxy.InheritedProperty = expectedInheritedString;
+ break;
+
+ default:
+ throw new InvalidOperationException($"Invalid 'set via' option '{setVia}.");
+ }
+
+ // assert number
+ this
+ .GetPropertyValue(implementation, nameof(proxy.NumberProperty))
+ .Should().Be(expectedNumber);
+ proxy.NumberProperty
+ .Should().Be(expectedNumber);
+
+ // assert object
+ this
+ .GetPropertyValue(implementation, nameof(proxy.ObjectProperty))
+ .Should().Be(expectedObject);
+ proxy.ObjectProperty
+ .Should().Be(expectedObject);
+
+ // assert list
+ (this.GetPropertyValue(implementation, nameof(proxy.ListProperty)) as IList<string>)
+ .Should().NotBeNull()
+ .And.HaveCount(1)
+ .And.BeEquivalentTo(expectedListValue);
+ proxy.ListProperty
+ .Should().NotBeNull()
+ .And.HaveCount(1)
+ .And.BeEquivalentTo(expectedListValue);
+
+ // assert list with interface
+ (this.GetPropertyValue(implementation, nameof(proxy.ListPropertyWithInterface)) as IList<string>)
+ .Should().NotBeNull()
+ .And.HaveCount(1)
+ .And.BeEquivalentTo(expectedListWithInterfaceValue);
+ proxy.ListPropertyWithInterface
+ .Should().NotBeNull()
+ .And.HaveCount(1)
+ .And.BeEquivalentTo(expectedListWithInterfaceValue);
+
+ // assert generics
+ (this.GetPropertyValue(implementation, nameof(proxy.GenericsProperty)) as IDictionary<string, IList<string>>)
+ .Should().NotBeNull()
+ .And.HaveCount(1)
+ .And.ContainKey(expectedDictionaryKey).WhoseValue.Should().BeEquivalentTo(expectedDictionaryListValue);
+ proxy.GenericsProperty
+ .Should().NotBeNull()
+ .And.HaveCount(1)
+ .And.ContainKey(expectedDictionaryKey).WhoseValue.Should().BeEquivalentTo(expectedDictionaryListValue);
+
+ // assert enum
+ this
+ .GetPropertyValue(implementation, nameof(proxy.EnumProperty))
+ .Should().Be(expectedEnum);
+ proxy.EnumProperty
+ .Should().Be(expectedEnum);
+
+ // assert getter
+ this
+ .GetPropertyValue(implementation, nameof(proxy.GetterProperty))
+ .Should().Be(42);
+ proxy.GetterProperty
+ .Should().Be(42);
+
+ // assert inherited methods
+ this
+ .GetPropertyValue(implementation, nameof(proxy.InheritedProperty))
+ .Should().Be(expectedInheritedString);
+ proxy.InheritedProperty
+ .Should().Be(expectedInheritedString);
+ }
+
+ /// <summary>Assert that a simple method with no return value can be proxied correctly.</summary>
+ [Test]
+ public void CanProxy_SimpleMethod_Void()
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(implementation);
+ proxy.GetNothing();
+ }
+
+ /// <summary>Assert that a simple int method can be proxied correctly.</summary>
+ [Test]
+ public void CanProxy_SimpleMethod_Int()
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ int expectedValue = this.Random.Next();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(implementation);
+ int actualValue = proxy.GetInt(expectedValue);
+
+ // assert
+ actualValue.Should().Be(expectedValue);
+ }
+
+ /// <summary>Assert that a simple object method can be proxied correctly.</summary>
+ [Test]
+ public void CanProxy_SimpleMethod_Object()
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ object expectedValue = new();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(implementation);
+ object actualValue = proxy.GetObject(expectedValue);
+
+ // assert
+ actualValue.Should().BeSameAs(expectedValue);
+ }
+
+ /// <summary>Assert that a simple list method can be proxied correctly.</summary>
+ [Test]
+ public void CanProxy_SimpleMethod_List()
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ string expectedValue = this.GetRandomString();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(implementation);
+ IList<string> actualValue = proxy.GetList(expectedValue);
+
+ // assert
+ actualValue.Should().BeEquivalentTo(expectedValue);
+ }
+
+ /// <summary>Assert that a simple list with interface method can be proxied correctly.</summary>
+ [Test]
+ public void CanProxy_SimpleMethod_ListWithInterface()
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ string expectedValue = this.GetRandomString();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(implementation);
+ IList<string> actualValue = proxy.GetListWithInterface(expectedValue);
+
+ // assert
+ actualValue.Should().BeEquivalentTo(expectedValue);
+ }
+
+ /// <summary>Assert that a simple method which returns generic types can be proxied correctly.</summary>
+ [Test]
+ public void CanProxy_SimpleMethod_GenericTypes()
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ string expectedKey = this.GetRandomString();
+ string expectedValue = this.GetRandomString();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(implementation);
+ IDictionary<string, IList<string>> actualValue = proxy.GetGenerics(expectedKey, expectedValue);
+
+ // assert
+ actualValue
+ .Should().NotBeNull()
+ .And.HaveCount(1)
+ .And.ContainKey(expectedKey).WhoseValue.Should().BeEquivalentTo(expectedValue);
+ }
+
+ /// <summary>Assert that a simple lambda method can be proxied correctly.</summary>
+ [Test]
+ [SuppressMessage("ReSharper", "ConvertToLocalFunction")]
+ public void CanProxy_SimpleMethod_Lambda()
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ Func<string, string> expectedValue = _ => "test";
+
+ // act
+ ISimpleApi proxy = this.GetProxy(implementation);
+ object actualValue = proxy.GetObject(expectedValue);
+
+ // assert
+ actualValue.Should().BeSameAs(expectedValue);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get a property value from an instance.</summary>
+ /// <param name="parent">The instance whose property to read.</param>
+ /// <param name="name">The property name.</param>
+ private object GetPropertyValue(object parent, string name)
+ {
+ if (parent is null)
+ throw new ArgumentNullException(nameof(parent));
+
+ Type type = parent.GetType();
+ PropertyInfo property = type.GetProperty(name);
+ if (property is null)
+ throw new InvalidOperationException($"The '{type.FullName}' type has no public property named '{name}'.");
+
+ return property.GetValue(parent);
+ }
+
+ /// <summary>Get a random test string.</summary>
+ private string GetRandomString()
+ {
+ return this.Random.Next().ToString();
+ }
+
+ /// <summary>Get a proxy API instance.</summary>
+ /// <param name="implementation">The underlying API instance.</param>
+ private ISimpleApi GetProxy(object implementation)
+ {
+ var proxyFactory = new InterfaceProxyFactory();
+ return proxyFactory.CreateProxy<ISimpleApi>(implementation, this.FromModId, this.ToModId);
+ }
+ }
+}
diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj
index 8329b2e1..67997b30 100644
--- a/src/SMAPI.Tests/SMAPI.Tests.csproj
+++ b/src/SMAPI.Tests/SMAPI.Tests.csproj
@@ -1,21 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <AssemblyName>SMAPI.Tests</AssemblyName>
- <RootNamespace>SMAPI.Tests</RootNamespace>
<TargetFramework>net5.0</TargetFramework>
- <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
- <LangVersion>latest</LangVersion>
</PropertyGroup>
<Import Project="..\..\build\common.targets" />
<ItemGroup>
+ <ProjectReference Include="..\SMAPI.Tests.ModApiConsumer\SMAPI.Tests.ModApiConsumer.csproj" />
+ <ProjectReference Include="..\SMAPI.Tests.ModApiProvider\SMAPI.Tests.ModApiProvider.csproj" />
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" />
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
<ProjectReference Include="..\SMAPI\SMAPI.csproj" />
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="FluentAssertions" Version="6.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
diff --git a/src/SMAPI.sln b/src/SMAPI.sln
index be5326f7..d9f60a5c 100644
--- a/src/SMAPI.sln
+++ b/src/SMAPI.sln
@@ -101,6 +101,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{3B5BF14D-F61
..\build\windows\lib\in-place-regex.ps1 = ..\build\windows\lib\in-place-regex.ps1
EndProjectSection
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Tests.ModApiProvider", "SMAPI.Tests.ModApiProvider\SMAPI.Tests.ModApiProvider.csproj", "{239AEEAC-07D1-4A3F-AA99-8C74F5038F50}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMAPI.Tests.ModApiConsumer", "SMAPI.Tests.ModApiConsumer\SMAPI.Tests.ModApiConsumer.csproj", "{2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}"
+EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5
@@ -167,6 +171,14 @@ Global
{80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.Build.0 = Release|Any CPU
+ {239AEEAC-07D1-4A3F-AA99-8C74F5038F50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {239AEEAC-07D1-4A3F-AA99-8C74F5038F50}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {239AEEAC-07D1-4A3F-AA99-8C74F5038F50}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {239AEEAC-07D1-4A3F-AA99-8C74F5038F50}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -187,6 +199,8 @@ Global
{4D661178-38FB-43E4-AA5F-9B0406919344} = {09CF91E5-5BAB-4650-A200-E5EA9A633046}
{CAA1488E-842B-433D-994D-1D3D0B5DD125} = {09CF91E5-5BAB-4650-A200-E5EA9A633046}
{3B5BF14D-F612-4C83-9EF6-E3EBFCD08766} = {4D661178-38FB-43E4-AA5F-9B0406919344}
+ {239AEEAC-07D1-4A3F-AA99-8C74F5038F50} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}
+ {2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {70143042-A862-47A8-A677-7C819DDC90DC}