diff --git a/LinkAce.sln b/LinkAce.sln
index ec07926..53f80d6 100644
--- a/LinkAce.sln
+++ b/LinkAce.sln
@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkAce.NET", "src\LinkAce.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestClient", "src\TestClient\TestClient.csproj", "{97052387-BAA1-40E8-A861-C412B373FBDC}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkAce.NET.Tests", "tests\LinkAce.NET.Tests\LinkAce.NET.Tests.csproj", "{671997C6-2DA3-45AB-87C2-4A335C976685}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -33,10 +35,15 @@ Global
{97052387-BAA1-40E8-A861-C412B373FBDC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97052387-BAA1-40E8-A861-C412B373FBDC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97052387-BAA1-40E8-A861-C412B373FBDC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {671997C6-2DA3-45AB-87C2-4A335C976685}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {671997C6-2DA3-45AB-87C2-4A335C976685}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {671997C6-2DA3-45AB-87C2-4A335C976685}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {671997C6-2DA3-45AB-87C2-4A335C976685}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7E05B1B4-5625-4A84-A2B2-4D66C8059904} = {E1115509-DC42-44D8-A031-A207EF90073F}
{6C83B6FB-E141-46BA-A446-8346BB11A7E3} = {22C844A6-ACCE-4473-9C7B-D53D613DA802}
{97052387-BAA1-40E8-A861-C412B373FBDC} = {22C844A6-ACCE-4473-9C7B-D53D613DA802}
+ {671997C6-2DA3-45AB-87C2-4A335C976685} = {66EB4E20-E8C9-43B5-9B51-B3BB962AEEF2}
EndGlobalSection
EndGlobal
diff --git a/src/LinkAce.NET/AssemblyInfo.cs b/src/LinkAce.NET/AssemblyInfo.cs
new file mode 100644
index 0000000..f928090
--- /dev/null
+++ b/src/LinkAce.NET/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("LinkAce.NET.Tests")]
diff --git a/tests/LinkAce.NET.Tests/ApiResponses/SearchLinkResponseTests.cs b/tests/LinkAce.NET.Tests/ApiResponses/SearchLinkResponseTests.cs
new file mode 100644
index 0000000..7346d2f
--- /dev/null
+++ b/tests/LinkAce.NET.Tests/ApiResponses/SearchLinkResponseTests.cs
@@ -0,0 +1,31 @@
+using LinkAce.NET.ApiResponses;
+using LinkAce.NET.Tests.Helpers;
+
+namespace LinkAce.NET.Tests.ApiResponses;
+
+[TestClass]
+public class SearchLinkResponseTests
+{
+ [TestMethod]
+ public void SearchLinkResponse_HasProperties()
+ {
+ // Arrange
+ SearchLinkResponse obj = new();
+ const int expectedCount = 12;
+
+ // Assert
+ Assert.AreEqual(expectedCount, obj.PropertyCount());
+ Assert.IsTrue(obj.HasProperty("CurrentPage"));
+ Assert.IsTrue(obj.HasProperty("Data"));
+ Assert.IsTrue(obj.HasProperty("FirstPageUrl"));
+ Assert.IsTrue(obj.HasProperty("From"));
+ Assert.IsTrue(obj.HasProperty("LastPage"));
+ Assert.IsTrue(obj.HasProperty("LastPageUrl"));
+ Assert.IsTrue(obj.HasProperty("NextPageUrl"));
+ Assert.IsTrue(obj.HasProperty("Path"));
+ Assert.IsTrue(obj.HasProperty("PerPage"));
+ Assert.IsTrue(obj.HasProperty("PreviousPageUrl"));
+ Assert.IsTrue(obj.HasProperty("To"));
+ Assert.IsTrue(obj.HasProperty("Total"));
+ }
+}
diff --git a/tests/LinkAce.NET.Tests/Entities/TagTests.cs b/tests/LinkAce.NET.Tests/Entities/TagTests.cs
new file mode 100644
index 0000000..084cf6a
--- /dev/null
+++ b/tests/LinkAce.NET.Tests/Entities/TagTests.cs
@@ -0,0 +1,40 @@
+using LinkAce.NET.Entites;
+using LinkAce.NET.Tests.Helpers;
+
+namespace LinkAce.NET.Tests.Entities;
+
+[TestClass]
+public class TagTests
+{
+ [TestMethod]
+ public void Tag_HasProperties()
+ {
+ // Arrange
+ Tag obj = new();
+ const int expectedCount = 8;
+
+ // Act
+ Assert.AreEqual(expectedCount, obj.PropertyCount());
+ Assert.IsTrue(obj.HasProperty("CreatedAt"));
+ Assert.IsTrue(obj.HasProperty("DeletedAt"));
+ Assert.IsTrue(obj.HasProperty("Id"));
+ Assert.IsTrue(obj.HasProperty("IsPrivate"));
+ Assert.IsTrue(obj.HasProperty("Name"));
+ Assert.IsTrue(obj.HasProperty("Pivot"));
+ Assert.IsTrue(obj.HasProperty("UpdatedAt"));
+ Assert.IsTrue(obj.HasProperty("UserId"));
+ }
+
+ [TestMethod]
+ public void ImplicitConversion_String_To_Tag()
+ {
+ // Arrange
+ const string expectedName = "TestTag";
+
+ // Act
+ Tag tag = expectedName;
+
+ // Assert
+ Assert.AreEqual(expectedName, tag.Name);
+ }
+}
diff --git a/tests/LinkAce.NET.Tests/Extensions/StringArrayExtensionsTests.cs b/tests/LinkAce.NET.Tests/Extensions/StringArrayExtensionsTests.cs
new file mode 100644
index 0000000..ff3b0c7
--- /dev/null
+++ b/tests/LinkAce.NET.Tests/Extensions/StringArrayExtensionsTests.cs
@@ -0,0 +1,26 @@
+using LinkAce.NET.Entites;
+using LinkAce.NET.Extensions;
+
+namespace LinkAce.NET.Tests.Extensions;
+
+[TestClass]
+public class StringArrayExtensionsTests
+{
+
+ [TestMethod]
+ public void ToTagArray_Success()
+ {
+ // Arrange
+ string[] expectedNames = { "TestTag1", "TestTag2", "TestTag3" };
+
+ // Act
+ Tag[] tags = expectedNames.ToTagArray();
+
+ // Assert
+ Assert.AreEqual(expectedNames.Length, tags.Length);
+ for (int i = 0; i < expectedNames.Length; i++)
+ {
+ Assert.AreEqual(expectedNames[i], tags[i].Name);
+ }
+ }
+}
diff --git a/tests/LinkAce.NET.Tests/GlobalUsings.cs b/tests/LinkAce.NET.Tests/GlobalUsings.cs
new file mode 100644
index 0000000..540383d
--- /dev/null
+++ b/tests/LinkAce.NET.Tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
diff --git a/tests/LinkAce.NET.Tests/Helpers/EntityTests.cs b/tests/LinkAce.NET.Tests/Helpers/EntityTests.cs
new file mode 100644
index 0000000..325c58b
--- /dev/null
+++ b/tests/LinkAce.NET.Tests/Helpers/EntityTests.cs
@@ -0,0 +1,23 @@
+using System.Reflection;
+
+namespace LinkAce.NET.Tests.Helpers;
+
+public static class EntityTests
+{
+ public static bool HasMethod(this object obj, string methodName)
+ {
+ var type = obj.GetType();
+ try
+ {
+ return type.GetMethod(methodName) is not null;
+ }
+ catch (AmbiguousMatchException)
+ {
+ return true;
+ }
+ }
+ public static bool HasProperty(this object obj, string propertyName)
+ => obj.GetType().GetProperty(propertyName) is not null;
+ public static int PropertyCount(this object obj)
+ => obj.GetType().GetProperties().Length;
+}
diff --git a/tests/LinkAce.NET.Tests/LinkAce.NET.Tests.csproj b/tests/LinkAce.NET.Tests/LinkAce.NET.Tests.csproj
new file mode 100644
index 0000000..f3b7e02
--- /dev/null
+++ b/tests/LinkAce.NET.Tests/LinkAce.NET.Tests.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/LinkAce.NET.Tests/LinkAceClientTests.cs b/tests/LinkAce.NET.Tests/LinkAceClientTests.cs
new file mode 100644
index 0000000..b054847
--- /dev/null
+++ b/tests/LinkAce.NET.Tests/LinkAceClientTests.cs
@@ -0,0 +1,66 @@
+using System.Net;
+using LinkAce.NET.Entites;
+using LinkAce.NET.Extensions;
+using Moq;
+using Moq.Protected;
+
+namespace LinkAce.NET.Tests;
+
+[TestClass]
+public class LinkAceClientTests
+{
+ private LinkAceClient _client;
+ private Mock _mockHttpMessageHandler;
+ [TestInitialize]
+ public void Init()
+ {
+ _mockHttpMessageHandler = new Mock();
+ var httpClient = new HttpClient(_mockHttpMessageHandler.Object);
+ _client = new LinkAceClient("https://links.example.com", TestData.TestApiKey, httpClient);
+ }
+ [TestMethod]
+ public async Task SearchLinksByUrl_Success()
+ {
+ // Arrange
+ _mockHttpMessageHandler.Protected()
+ .Setup>("SendAsync", ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(TestData.SearchLinkResponseJson)
+ });
+
+ // Act
+ var result = await _client.SearchLinksByUrl("jrgnsn.net");
+
+ // Assert
+ Assert.IsNotNull(result);
+ }
+ [TestMethod]
+ public async Task CreateLink_Success()
+ {
+ // Arrange
+ _mockHttpMessageHandler.Protected()
+ .Setup>("SendAsync", ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(TestData.CreateLinkResponseJson)
+ });
+
+ // Act
+ var result = await _client.CreateLink(new Link()
+ {
+ Title = "Test Link",
+ Url = "https://jrgnsn.net",
+ Description = "A test link",
+ Tags = new[]{ "test" }.ToTagArray()
+ });
+
+ // Assert
+ Assert.IsNotNull(result);
+
+ }
+}
\ No newline at end of file
diff --git a/tests/LinkAce.NET.Tests/MetaTests.cs b/tests/LinkAce.NET.Tests/MetaTests.cs
new file mode 100644
index 0000000..b8fcc4d
--- /dev/null
+++ b/tests/LinkAce.NET.Tests/MetaTests.cs
@@ -0,0 +1,36 @@
+using System.Net.Http.Headers;
+using System.Reflection;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace LinkAce.NET.Tests
+{
+ [TestClass]
+ public class MetaTests
+ {
+ [TestMethod]
+ public void UserAgent_ShouldContainCorrectNameAndVersion()
+ {
+ // Arrange
+ var expectedName = "LinkAce.NET";
+ var expectedVersion = Assembly.GetEntryAssembly()!.GetName().Version!.ToString();
+ var expectedUserAgent = new ProductInfoHeaderValue(expectedName, expectedVersion);
+
+ // Act
+ var actualUserAgent = Meta.UserAgent;
+
+ // Assert
+ Assert.AreEqual(expectedUserAgent.Product.Name, actualUserAgent.Product.Name);
+ Assert.AreEqual(expectedUserAgent.Product.Version, actualUserAgent.Product.Version);
+ }
+
+ [TestMethod]
+ public void AssemblyVersion_ShouldNotBeNullOrEmpty()
+ {
+ // Act
+ var assemblyVersion = Meta.AssemblyVersion;
+
+ // Assert
+ Assert.IsFalse(string.IsNullOrEmpty(assemblyVersion));
+ }
+ }
+}
diff --git a/tests/LinkAce.NET.Tests/TestData.cs b/tests/LinkAce.NET.Tests/TestData.cs
new file mode 100644
index 0000000..a6bafd0
--- /dev/null
+++ b/tests/LinkAce.NET.Tests/TestData.cs
@@ -0,0 +1,124 @@
+namespace LinkAce.NET.Tests;
+
+public class TestData
+{
+ public const string TestApiKey = "testing-token";
+ public const string SearchLinkResponseJson = @"{
+ ""current_page"": 1,
+ ""data"": [
+ {
+ ""check_disabled"": false,
+ ""created_at"": ""2020-09-21T21:39:11.000000Z"",
+ ""deleted_at"": null,
+ ""description"": ""Pictures from my Atreus keyboard build"",
+ ""icon"": ""link"",
+ ""id"": 1098,
+ ""is_private"": false,
+ ""status"": 1,
+ ""tags"": [
+ {
+ ""created_at"": ""2023-06-09T16:16:36.000000Z"",
+ ""deleted_at"": null,
+ ""id"": 603,
+ ""is_private"": false,
+ ""name"": ""atreus"",
+ ""pivot"": {
+ ""link_id"": 1098,
+ ""tag_id"": 603
+ },
+ ""updated_at"": ""2023-06-09T16:16:36.000000Z"",
+ ""user_id"": 1
+ },
+ {
+ ""created_at"": ""2023-06-09T16:07:30.000000Z"",
+ ""deleted_at"": null,
+ ""id"": 234,
+ ""is_private"": false,
+ ""name"": ""diy"",
+ ""pivot"": {
+ ""link_id"": 1098,
+ ""tag_id"": 234
+ },
+ ""updated_at"": ""2023-06-09T16:07:30.000000Z"",
+ ""user_id"": 1
+ },
+ {
+ ""created_at"": ""2023-06-09T16:16:37.000000Z"",
+ ""deleted_at"": null,
+ ""id"": 605,
+ ""is_private"": false,
+ ""name"": ""keyboard"",
+ ""pivot"": {
+ ""link_id"": 1098,
+ ""tag_id"": 605
+ },
+ ""updated_at"": ""2023-06-09T16:16:37.000000Z"",
+ ""user_id"": 1
+ }
+ ],
+ ""thumbnail"": null,
+ ""title"": ""Classic Atreus Build - jphotos"",
+ ""updated_at"": ""2023-06-09T16:16:37.000000Z"",
+ ""url"": ""https://photos.jrgnsn.net/album/classic-atreus-build"",
+ ""user_id"": 1
+ }
+ ],
+ ""first_page_url"": ""https://links.fminus.co/api/v1/search/links?page=1"",
+ ""from"": 1,
+ ""last_page"": 1,
+ ""last_page_url"": ""https://links.fminus.co/api/v1/search/links?page=1"",
+ ""links"": [
+ {
+ ""active"": false,
+ ""label"": ""« Previous"",
+ ""url"": null
+ },
+ {
+ ""active"": true,
+ ""label"": ""1"",
+ ""url"": ""https://links.fminus.co/api/v1/search/links?page=1""
+ },
+ {
+ ""active"": false,
+ ""label"": ""Next »"",
+ ""url"": null
+ }
+ ],
+ ""next_page_url"": null,
+ ""path"": ""https://links.fminus.co/api/v1/search/links"",
+ ""per_page"": 24,
+ ""prev_page_url"": null,
+ ""to"": 1,
+ ""total"": 1
+}";
+ public const string CreateLinkResponseJson = @"{
+ ""check_disabled"": false,
+ ""created_at"": ""2024-10-04T19:32:31.000000Z"",
+ ""description"": ""A test link"",
+ ""icon"": ""link"",
+ ""id"": 1819,
+ ""is_private"": false,
+ ""lists"": [],
+ ""status"": 0,
+ ""tags"": [
+ {
+ ""created_at"": ""2023-06-13T23:27:14.000000Z"",
+ ""deleted_at"": null,
+ ""id"": 880,
+ ""is_private"": false,
+ ""name"": ""test"",
+ ""pivot"": {
+ ""link_id"": 1819,
+ ""tag_id"": 880
+ },
+ ""updated_at"": ""2023-06-13T23:27:14.000000Z"",
+ ""user_id"": 1
+ }
+ ],
+ ""thumbnail"": null,
+ ""title"": ""Test Link"",
+ ""updated_at"": ""2024-10-04T19:32:31.000000Z"",
+ ""url"": ""https://jrgnsn.net"",
+ ""user_id"": 1
+}";
+}