Skip to content

Commit 062e472

Browse files
committed
Add test projects and fix duplicate host name conflict in code generation
- Add MoEmbed.App.Tests: integration tests for root and API endpoints using WebApplicationFactory - Add MoEmbed.CodeGeneration.Tests: tests for host name similarity resolution logic - Fix duplicate host name registration (e.g. v.afree.ca claimed by both afreecatv and sooplive) by resolving conflicts based on longest common substring similarity between provider name and host name - Extract HostNameResolver class from Program.cs for testability - Regenerate OEmbedProxyMetadataProviders.cs with resolved host names
1 parent b54eaff commit 062e472

10 files changed

Lines changed: 3095 additions & 2562 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System.Net;
2+
using System.Text.Json;
3+
4+
namespace MoEmbed;
5+
6+
public class ApiEndpointTest
7+
{
8+
[Test]
9+
public async Task Api_WithoutUrl_Returns404WithError()
10+
{
11+
var response = await SharedAppFactory.Client.GetAsync("/api");
12+
13+
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
14+
15+
var content = await response.Content.ReadAsStringAsync();
16+
var json = JsonDocument.Parse(content);
17+
var error = json.RootElement.GetProperty("error").GetString();
18+
19+
await Assert.That(error).IsNotNull();
20+
}
21+
22+
[Test]
23+
public async Task Api_WithInvalidUrl_Returns404WithInvalidUrlError()
24+
{
25+
var response = await SharedAppFactory.Client.GetAsync("/api?url=not-a-valid-url");
26+
27+
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
28+
29+
var content = await response.Content.ReadAsStringAsync();
30+
var json = JsonDocument.Parse(content);
31+
var error = json.RootElement.GetProperty("error").GetString();
32+
33+
await Assert.That(error).IsEqualTo("Invalid URL.");
34+
}
35+
36+
[Test]
37+
public async Task Api_WithInvalidFormat_ThrowsDueToHandlerBug()
38+
{
39+
// NOTE: The handler has a bug where writer is null for invalid formats,
40+
// causing a NullReferenceException that propagates through TestServer.
41+
await Assert.ThrowsAsync<NullReferenceException>(
42+
() => SharedAppFactory.Client.GetAsync("/api?url=https://example.com&format=invalid"));
43+
}
44+
45+
[Test]
46+
public async Task Api_WithJsonFormat_ReturnsJsonContentType()
47+
{
48+
var response = await SharedAppFactory.Client.GetAsync("/api?url=https://example.com&format=json");
49+
50+
await Assert.That(response.Content.Headers.ContentType?.MediaType).IsEqualTo("application/json");
51+
}
52+
53+
[Test]
54+
public async Task Api_WithXmlFormat_ReturnsXmlContentType()
55+
{
56+
var response = await SharedAppFactory.Client.GetAsync("/api?url=https://example.com&format=xml");
57+
58+
await Assert.That(response.Content.Headers.ContentType?.MediaType).IsEqualTo("text/xml");
59+
}
60+
61+
[Test]
62+
public async Task Api_WithDefaultFormat_ReturnsJsonContentType()
63+
{
64+
var response = await SharedAppFactory.Client.GetAsync("/api?url=https://example.com");
65+
66+
await Assert.That(response.Content.Headers.ContentType?.MediaType).IsEqualTo("application/json");
67+
}
68+
69+
[Test]
70+
public async Task Api_WithUrlContainingHash_ReturnsResponse()
71+
{
72+
var response = await SharedAppFactory.Client.GetAsync("/api?url=https://example.com%23section");
73+
74+
await Assert.That(response.Content.Headers.ContentType?.MediaType).IsEqualTo("application/json");
75+
}
76+
77+
[Test]
78+
[Arguments("max_width", "400")]
79+
[Arguments("max_height", "300")]
80+
public async Task Api_WithDimensionParameters_ReturnsResponse(string param, string value)
81+
{
82+
var response = await SharedAppFactory.Client.GetAsync($"/api?url=https://example.com&{param}={value}");
83+
84+
await Assert.That(response.Content.Headers.ContentType?.MediaType).IsEqualTo("application/json");
85+
}
86+
}

MoEmbed.App.Tests/AppTestBase.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Microsoft.AspNetCore.Hosting;
2+
using Microsoft.AspNetCore.Mvc.Testing;
3+
using Microsoft.Extensions.Hosting;
4+
using Microsoft.Extensions.Configuration;
5+
6+
namespace MoEmbed;
7+
8+
public class TestWebApplicationFactory : WebApplicationFactory<Program>
9+
{
10+
protected override IHostBuilder? CreateHostBuilder()
11+
{
12+
return Host.CreateDefaultBuilder()
13+
.ConfigureHostConfiguration(config =>
14+
{
15+
config.AddInMemoryCollection(new Dictionary<string, string?>
16+
{
17+
{ "hostBuilder:reloadConfigOnChange", "false" }
18+
});
19+
})
20+
.ConfigureWebHostDefaults(webBuilder =>
21+
{
22+
webBuilder.UseStartup<Startup>();
23+
});
24+
}
25+
}
26+
27+
public static class SharedAppFactory
28+
{
29+
private static readonly Lazy<TestWebApplicationFactory> _factory = new(() => new TestWebApplicationFactory());
30+
private static readonly Lazy<HttpClient> _client = new(() => _factory.Value.CreateClient());
31+
32+
public static HttpClient Client => _client.Value;
33+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net10.0</TargetFramework>
4+
<OutputType>Exe</OutputType>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<IsPackable>false</IsPackable>
7+
<RootNamespace>MoEmbed</RootNamespace>
8+
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
9+
</PropertyGroup>
10+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
11+
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
12+
<WarningsAsErrors />
13+
</PropertyGroup>
14+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
15+
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
16+
<WarningsAsErrors />
17+
</PropertyGroup>
18+
<ItemGroup>
19+
<PackageReference Include="TUnit" Version="1.28.7" />
20+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.5" />
21+
</ItemGroup>
22+
<ItemGroup>
23+
<ProjectReference Include="..\MoEmbed.App\MoEmbed.App.csproj" />
24+
</ItemGroup>
25+
</Project>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Net;
2+
3+
namespace MoEmbed;
4+
5+
public class RootEndpointTest
6+
{
7+
[Test]
8+
public async Task Root_ReturnsHtml()
9+
{
10+
var response = await SharedAppFactory.Client.GetAsync("/");
11+
12+
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
13+
await Assert.That(response.Content.Headers.ContentType?.MediaType).IsEqualTo("text/html");
14+
}
15+
16+
[Test]
17+
public async Task Root_ReturnsIndexHtmlContent()
18+
{
19+
var response = await SharedAppFactory.Client.GetAsync("/");
20+
var content = await response.Content.ReadAsStringAsync();
21+
22+
await Assert.That(content).Contains("<html");
23+
}
24+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
namespace MoEmbed.CodeGeneration;
2+
3+
public class LongestCommonSubstringLengthTest
4+
{
5+
[Test]
6+
[Arguments("abc", "abc", 3)]
7+
[Arguments("abc", "def", 0)]
8+
[Arguments("abcdef", "xcdey", 3)] // "cde"
9+
[Arguments("", "abc", 0)]
10+
[Arguments("abc", "", 0)]
11+
[Arguments("afreecatv", "vafreeca", 7)] // "afreeca"
12+
[Arguments("sooplive", "vafreeca", 1)] // "a"
13+
public async Task ReturnsExpectedLength(string a, string b, int expected)
14+
{
15+
var result = HostNameResolver.LongestCommonSubstringLength(a, b);
16+
await Assert.That(result).IsEqualTo(expected);
17+
}
18+
}
19+
20+
public class HostNameSimilarityTest
21+
{
22+
[Test]
23+
[Arguments("afreecatv", "v.afree.ca", 7)] // normalized: "afreecatv" vs "vafreeca" -> "afreeca" (7)
24+
[Arguments("sooplive", "v.afree.ca", 1)] // normalized: "sooplive" vs "vafreeca" -> "a" (1)
25+
[Arguments("sooplive", "vod.sooplive.com", 8)] // normalized: "sooplive" vs "vodsooplivecom" -> "sooplive" (8)
26+
[Arguments("afreecatv", "vod.afreecatv.com", 9)] // normalized: "afreecatv" vs "vodafreecatvcom" -> "afreecatv" (9)
27+
[Arguments("my-service", "my-service.example.com", 9)] // hyphens stripped from both
28+
public async Task ReturnsExpectedSimilarity(string providerName, string hostName, int expected)
29+
{
30+
var result = HostNameResolver.HostNameSimilarity(providerName, hostName);
31+
await Assert.That(result).IsEqualTo(expected);
32+
}
33+
}
34+
35+
public class HostNameResolverTest
36+
{
37+
[Test]
38+
public async Task NoConflict_AllHostNamesPreserved()
39+
{
40+
var entries = new[]
41+
{
42+
new ProviderHostEntry { ProviderName = "youtube", CandidateHostNames = ["youtube.com", "youtu.be"] },
43+
new ProviderHostEntry { ProviderName = "vimeo", CandidateHostNames = ["vimeo.com"] }
44+
};
45+
46+
var result = HostNameResolver.Resolve(entries);
47+
48+
await Assert.That(result["youtube"]).IsEquivalentTo(new[] { "youtube.com", "youtu.be" });
49+
await Assert.That(result["vimeo"]).IsEquivalentTo(new[] { "vimeo.com" });
50+
}
51+
52+
[Test]
53+
public async Task ConflictingHost_AssignedToMoreSimilarProvider()
54+
{
55+
// Real-world case: afreecatv and sooplive both claim v.afree.ca
56+
var entries = new[]
57+
{
58+
new ProviderHostEntry
59+
{
60+
ProviderName = "afreecatv",
61+
CandidateHostNames = ["vod.afreecatv.com", "afreecatv.com", "v.afree.ca", "afree.ca", "play.afreecatv.com"]
62+
},
63+
new ProviderHostEntry
64+
{
65+
ProviderName = "sooplive",
66+
CandidateHostNames = ["vod.sooplive.com", "sooplive.com", "v.afree.ca", "afree.ca", "play.sooplive.com"]
67+
}
68+
};
69+
70+
var result = HostNameResolver.Resolve(entries);
71+
72+
// v.afree.ca and afree.ca should go to afreecatv (higher similarity)
73+
await Assert.That(result["afreecatv"]).Contains("v.afree.ca");
74+
await Assert.That(result["afreecatv"]).Contains("afree.ca");
75+
await Assert.That(result["sooplive"]).DoesNotContain("v.afree.ca");
76+
await Assert.That(result["sooplive"]).DoesNotContain("afree.ca");
77+
78+
// Each provider's own hosts are unaffected
79+
await Assert.That(result["afreecatv"]).Contains("vod.afreecatv.com");
80+
await Assert.That(result["sooplive"]).Contains("vod.sooplive.com");
81+
await Assert.That(result["sooplive"]).Contains("play.sooplive.com");
82+
}
83+
84+
[Test]
85+
public async Task ConflictingHost_TieBreaksAlphabetically()
86+
{
87+
// Both providers have equal similarity to the shared host
88+
var entries = new[]
89+
{
90+
new ProviderHostEntry
91+
{
92+
ProviderName = "bravo",
93+
CandidateHostNames = ["shared.example.com"]
94+
},
95+
new ProviderHostEntry
96+
{
97+
ProviderName = "alpha",
98+
CandidateHostNames = ["shared.example.com"]
99+
}
100+
};
101+
102+
var result = HostNameResolver.Resolve(entries);
103+
104+
// Equal similarity -> alphabetical tie-break -> "alpha" wins
105+
await Assert.That(result["alpha"]).Contains("shared.example.com");
106+
await Assert.That(result["bravo"]).DoesNotContain("shared.example.com");
107+
}
108+
109+
[Test]
110+
public async Task ThreeProviders_ConflictResolvedCorrectly()
111+
{
112+
var entries = new[]
113+
{
114+
new ProviderHostEntry
115+
{
116+
ProviderName = "foobar",
117+
CandidateHostNames = ["foobar.com", "api.foo.com"]
118+
},
119+
new ProviderHostEntry
120+
{
121+
ProviderName = "foobaz",
122+
CandidateHostNames = ["foobaz.net", "api.foo.com"]
123+
},
124+
new ProviderHostEntry
125+
{
126+
ProviderName = "qux",
127+
CandidateHostNames = ["qux.io", "api.foo.com"]
128+
}
129+
};
130+
131+
var result = HostNameResolver.Resolve(entries);
132+
133+
// "api.foo.com" -> normalized "apifoocom"
134+
// "foobar" LCS with "apifoocom" = "foo" (3)
135+
// "foobaz" LCS with "apifoocom" = "foo" (3)
136+
// "qux" LCS with "apifoocom" = 0
137+
// Tie between foobar and foobaz -> alphabetical -> "foobar" wins
138+
await Assert.That(result["foobar"]).Contains("api.foo.com");
139+
await Assert.That(result["foobaz"]).DoesNotContain("api.foo.com");
140+
await Assert.That(result["qux"]).DoesNotContain("api.foo.com");
141+
142+
// Unique hosts unaffected
143+
await Assert.That(result["foobar"]).Contains("foobar.com");
144+
await Assert.That(result["foobaz"]).Contains("foobaz.net");
145+
await Assert.That(result["qux"]).Contains("qux.io");
146+
}
147+
148+
[Test]
149+
public async Task EmptyCandidates_ReturnsEmptyList()
150+
{
151+
var entries = new[]
152+
{
153+
new ProviderHostEntry { ProviderName = "empty", CandidateHostNames = [] }
154+
};
155+
156+
var result = HostNameResolver.Resolve(entries);
157+
158+
await Assert.That(result["empty"]).IsEmpty();
159+
}
160+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net10.0</TargetFramework>
4+
<OutputType>Exe</OutputType>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<IsPackable>false</IsPackable>
7+
<RootNamespace>MoEmbed.CodeGeneration</RootNamespace>
8+
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
9+
</PropertyGroup>
10+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
11+
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
12+
<WarningsAsErrors />
13+
</PropertyGroup>
14+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
15+
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
16+
<WarningsAsErrors />
17+
</PropertyGroup>
18+
<ItemGroup>
19+
<PackageReference Include="TUnit" Version="1.28.7" />
20+
</ItemGroup>
21+
<ItemGroup>
22+
<ProjectReference Include="..\MoEmbed.CodeGeneration\MoEmbed.CodeGeneration.csproj" />
23+
</ItemGroup>
24+
</Project>

0 commit comments

Comments
 (0)