Skip to content

Commit 646c0e1

Browse files
authored
Merge pull request #425 from jpg0/feat/add-assetcontroller-test
Feat: Add AssetController test with direct settings DI
2 parents e2797f7 + 21b77dc commit 646c0e1

7 files changed

Lines changed: 257 additions & 31 deletions

File tree

ImmichFrame.Core/ImmichFrame.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2121
</PackageReference>
2222
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.5" />
23+
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
2324
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
2425
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.18.2">
2526
<PrivateAssets>all</PrivateAssets>

ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@ public class PooledImmichFrameLogic : IAccountImmichFrameLogic
1414
private readonly ImmichApi _immichApi;
1515
private readonly string _downloadLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ImageCache");
1616

17-
public PooledImmichFrameLogic(IAccountSettings accountSettings, IGeneralSettings generalSettings)
17+
public PooledImmichFrameLogic(IAccountSettings accountSettings, IGeneralSettings generalSettings, IHttpClientFactory httpClientFactory)
1818
{
1919
_generalSettings = generalSettings;
2020

21-
var httpClient = new HttpClient();
22-
21+
var httpClient = httpClientFactory.CreateClient("ImmichApiAccountClient");
2322
AccountSettings = accountSettings;
23+
2424
httpClient.UseApiKey(accountSettings.ApiKey);
2525
_immichApi = new ImmichApi(accountSettings.ImmichServerUrl, httpClient);
26+
2627
_apiCache = new ApiCache(RefreshInterval(generalSettings.RefreshAlbumPeopleInterval));
2728
_pool = BuildPool(accountSettings);
2829
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
using System.Net;
2+
using System.Net.Http;
3+
using ImmichFrame.WebApi.Tests.Mocks;
4+
using Microsoft.AspNetCore.Mvc.Testing;
5+
using Microsoft.AspNetCore.TestHost;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Http; // Added this
8+
using Moq;
9+
using Moq.Protected;
10+
using ImmichFrame.Core.Api;
11+
using ImmichFrame.WebApi.Models;
12+
using ImmichFrame.Core.Interfaces; // Added this back
13+
using NUnit.Framework;
14+
15+
namespace ImmichFrame.WebApi.Tests.Controllers
16+
{
17+
[TestFixture]
18+
public class AssetControllerTests
19+
{
20+
private WebApplicationFactory<Program> _factory;
21+
private Mock<HttpMessageHandler> _mockHttpMessageHandler;
22+
23+
[SetUp]
24+
public void Setup()
25+
{
26+
_mockHttpMessageHandler = new Mock<HttpMessageHandler>();
27+
_factory = new WebApplicationFactory<Program>()
28+
.WithWebHostBuilder(builder =>
29+
{
30+
builder.ConfigureTestServices(services =>
31+
{
32+
// 1. Mock HttpMessageHandler and IHttpClientFactory
33+
services.AddSingleton<HttpMessageHandler>(_mockHttpMessageHandler.Object);
34+
services.AddHttpClient("ImmichApiAccountClient")
35+
.ConfigurePrimaryHttpMessageHandler(sp => sp.GetRequiredService<HttpMessageHandler>());
36+
services.ConfigureAll<HttpClientFactoryOptions>(options =>
37+
{
38+
options.HttpMessageHandlerBuilderActions.Add(b =>
39+
{
40+
b.PrimaryHandler = b.Services.GetRequiredService<HttpMessageHandler>();
41+
});
42+
});
43+
44+
// 2. Directly instantiate and register settings objects
45+
var generalSettings = new GeneralSettings
46+
{
47+
ShowWeatherDescription = false, // Assuming this corresponds to ShowWeather
48+
ShowClock = true,
49+
ClockFormat = "HH:mm",
50+
Language = "en",
51+
PhotoDateFormat = "MM/dd/yyyy", // Crucial for the NRE
52+
ImageLocationFormat = "City,State,Country",
53+
DownloadImages = false,
54+
RenewImagesDuration = 30,
55+
// Ensure all non-nullable string properties that might be used have defaults if not set here
56+
PrimaryColor = "#FFFFFF", // Example default
57+
SecondaryColor = "#000000", // Example default
58+
Style = "none",
59+
BaseFontSize = "16px",
60+
WeatherApiKey = "",
61+
UnitSystem = "imperial",
62+
WeatherLatLong = "0,0"
63+
};
64+
65+
var accountSettings = new ServerAccountSettings
66+
{
67+
ImmichServerUrl = "http://mock-immich-server.com",
68+
ApiKey = "test-api-key",
69+
ShowMemories = false,
70+
ShowFavorites = true,
71+
ShowArchived = false,
72+
Albums = new List<Guid>(),
73+
ExcludedAlbums = new List<Guid>(),
74+
People = new List<Guid>()
75+
};
76+
77+
var serverSettings = new ServerSettings
78+
{
79+
GeneralSettingsImpl = generalSettings,
80+
AccountsImpl = new List<ServerAccountSettings> { accountSettings }
81+
};
82+
83+
services.AddSingleton<IServerSettings>(serverSettings);
84+
services.AddSingleton<IGeneralSettings>(generalSettings);
85+
// Ensure IAccountSettings can be resolved if needed by MultiImmichFrameLogicDelegate directly
86+
// However, PooledImmichFrameLogic receives IAccountSettings via the factory Func
87+
});
88+
});
89+
}
90+
91+
// Removed OneTimeSetup that created Settings.json
92+
93+
[TearDown]
94+
public void TearDown()
95+
{
96+
_factory.Dispose();
97+
}
98+
99+
[Test]
100+
public async Task GetRandomImage_ReturnsImageFromMockServer()
101+
{
102+
// Arrange
103+
var expectedAssetId = Guid.NewGuid();
104+
var assetDtoJson = $@"
105+
{{
106+
""id"": ""{expectedAssetId}"",
107+
""originalPath"": ""/path/to/image.jpg"",
108+
""type"": ""IMAGE"",
109+
""fileCreatedAt"": ""2023-10-26T10:00:00Z"",
110+
""fileModifiedAt"": ""2023-10-26T10:00:00Z"",
111+
""isFavorite"": true,
112+
""duration"": ""0:00:00"",
113+
""checksum"": ""testchecksum"",
114+
""deviceAssetId"": ""testDeviceAssetId"",
115+
""deviceId"": ""testDeviceId"",
116+
""ownerId"": ""testOwnerId"",
117+
""originalFileName"": ""image.jpg"",
118+
""localDateTime"": ""2023-10-26T10:00:00Z"",
119+
""visibility"": ""timeline"",
120+
""hasMetadata"": true,
121+
""isArchived"": false,
122+
""isOffline"": false,
123+
""isTrashed"": false,
124+
""thumbhash"": ""I0cMCQS94XmImZeXmYd3d3g="",
125+
""updatedAt"": ""2023-10-26T10:00:00Z""
126+
}}";
127+
128+
// JSON structure for SearchResponseDto
129+
var jsonResponse = $@"
130+
{{
131+
""albums"": {{
132+
""count"": 0,
133+
""items"": [],
134+
""total"": 0,
135+
""facets"": []
136+
}},
137+
""assets"": {{
138+
""count"": 1,
139+
""items"": [
140+
{assetDtoJson}
141+
],
142+
""total"": 1,
143+
""facets"": [],
144+
""nextPage"": null
145+
}}
146+
}}";
147+
148+
// Setup for SearchAssetsAsync
149+
_mockHttpMessageHandler.Protected()
150+
.Setup<Task<HttpResponseMessage>>(
151+
"SendAsync",
152+
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.ToString().Contains("/search/metadata")),
153+
ItExpr.IsAny<CancellationToken>()
154+
)
155+
.ReturnsAsync(() => new HttpResponseMessage // Use a Func to return new instance each time
156+
{
157+
StatusCode = HttpStatusCode.OK,
158+
Content = new StringContent(jsonResponse)
159+
});
160+
161+
// Setup for ViewAssetAsync (thumbnail)
162+
var mockImageData = new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }; // Minimal JPEG
163+
_mockHttpMessageHandler.Protected()
164+
.Setup<Task<HttpResponseMessage>>(
165+
"SendAsync",
166+
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.ToString().Contains("/thumbnail")),
167+
ItExpr.IsAny<CancellationToken>()
168+
)
169+
.ReturnsAsync(() => new HttpResponseMessage
170+
{
171+
StatusCode = HttpStatusCode.OK,
172+
Content = new ByteArrayContent(mockImageData)
173+
});
174+
175+
var client = _factory.CreateClient();
176+
177+
// Act
178+
var response = await client.GetAsync("/api/Asset/RandomImageAndInfo");
179+
180+
// Assert
181+
response.EnsureSuccessStatusCode();
182+
var content = await response.Content.ReadAsStringAsync();
183+
// For now, we'll just check if the response is not empty.
184+
// A more robust check would be to deserialize the response and check the asset ID.
185+
Assert.That(content, Is.Not.Empty);
186+
}
187+
}
188+
}

ImmichFrame.WebApi.Tests/ImmichFrame.WebApi.Tests.csproj

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<PropertyGroup>
4-
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
6-
<ImplicitUsings>enable</ImplicitUsings>
7-
<Nullable>enable</Nullable>
8-
</PropertyGroup>
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<IsTestProject>true</IsTestProject>
9+
</PropertyGroup>
910

10-
<ItemGroup>
11-
<PackageReference Include="AwesomeAssertions" Version="9.0.0" />
12-
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
13-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
14-
<PackageReference Include="Moq" Version="4.20.70" />
15-
<PackageReference Include="NUnit" Version="4.3.2" />
16-
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
17-
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
18-
<PrivateAssets>all</PrivateAssets>
19-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
20-
</PackageReference>
21-
<PackageReference Include="coverlet.collector" Version="6.0.2">
22-
<PrivateAssets>all</PrivateAssets>
23-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
24-
</PackageReference>
25-
</ItemGroup>
11+
<ItemGroup>
12+
<PackageReference Include="AwesomeAssertions" Version="9.0.0" />
13+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
15+
<PackageReference Include="Moq" Version="4.20.70" />
16+
<PackageReference Include="NUnit" Version="4.3.2" />
17+
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
18+
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
19+
<PrivateAssets>all</PrivateAssets>
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
</PackageReference>
22+
<PackageReference Include="coverlet.collector" Version="6.0.2">
23+
<PrivateAssets>all</PrivateAssets>
24+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
25+
</PackageReference>
26+
</ItemGroup>
2627

2728
<ItemGroup>
2829
<ProjectReference Include="..\ImmichFrame.WebApi\ImmichFrame.WebApi.csproj" />
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Net;
2+
using System.Net.Http;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
6+
namespace ImmichFrame.WebApi.Tests.Mocks
7+
{
8+
public class MockHttpMessageHandler : HttpMessageHandler
9+
{
10+
private readonly string _responseContent;
11+
private readonly HttpStatusCode _statusCode;
12+
13+
public MockHttpMessageHandler(string responseContent, HttpStatusCode statusCode)
14+
{
15+
_responseContent = responseContent;
16+
_statusCode = statusCode;
17+
}
18+
19+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
20+
{
21+
var response = new HttpResponseMessage
22+
{
23+
StatusCode = _statusCode,
24+
Content = new StringContent(_responseContent)
25+
};
26+
return Task.FromResult(response);
27+
}
28+
}
29+
}

ImmichFrame.WebApi/Controllers/AssetController.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ public async Task<ImageResponse> GetRandomImageAndInfo(string clientIdentifier =
105105

106106
var locationFormat = _settings.ImageLocationFormat ?? "City,State,Country";
107107
var imageLocation = locationFormat
108-
.Replace("City", randomImage.ExifInfo.City ?? string.Empty)
109-
.Replace("State", randomImage.ExifInfo.State ?? string.Empty)
110-
.Replace("Country", randomImage.ExifInfo.Country ?? string.Empty);
108+
.Replace("City", randomImage.ExifInfo?.City ?? string.Empty)
109+
.Replace("State", randomImage.ExifInfo?.State ?? string.Empty)
110+
.Replace("Country", randomImage.ExifInfo?.Country ?? string.Empty);
111111
imageLocation = string.Join(",", imageLocation.Split(',').Where(s => !string.IsNullOrWhiteSpace(s)));
112112

113113
return new ImageResponse

ImmichFrame.WebApi/Program.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Reflection;
66
using ImmichFrame.Core.Logic;
77
using ImmichFrame.Core.Logic.AccountSelection;
8-
using ImmichFrame.WebApi.Helpers;
98
using ImmichFrame.WebApi.Helpers.Config;
109

1110
var builder = WebApplication.CreateBuilder(args);
@@ -59,7 +58,11 @@ _ _ __ ___ _ __ ___ _ ___| |__ | |_ _ __ __ _ _ __ ___ ___
5958
builder.Services.AddSingleton<ICalendarService, IcalCalendarService>();
6059
builder.Services.AddSingleton<IAssetAccountTracker, BloomFilterAssetAccountTracker>();
6160
builder.Services.AddSingleton<IAccountSelectionStrategy, TotalAccountImagesSelectionStrategy>();
62-
builder.Services.AddTransient<Func<IAccountSettings, IAccountImmichFrameLogic>>(srv => account => ActivatorUtilities.CreateInstance<PooledImmichFrameLogic>(srv, account));
61+
builder.Services.AddHttpClient(); // Ensures IHttpClientFactory is available
62+
63+
builder.Services.AddTransient<Func<IAccountSettings, IAccountImmichFrameLogic>>(srv =>
64+
account => ActivatorUtilities.CreateInstance<PooledImmichFrameLogic>(srv, account));
65+
6366
builder.Services.AddSingleton<IImmichFrameLogic, MultiImmichFrameLogicDelegate>();
6467

6568
builder.Services.AddControllers();
@@ -106,4 +109,7 @@ _ _ __ ___ _ __ ___ _ ___| |__ | |_ _ __ __ _ _ __ ___ ___
106109

107110
app.MapFallbackToFile("/index.html");
108111

109-
app.Run();
112+
app.Run();
113+
114+
// Make Program public for WebApplicationFactory
115+
public partial class Program { }

0 commit comments

Comments
 (0)