Skip to content

Commit 722edb7

Browse files
committed
feat: Integration test project
1 parent 6b1d7fb commit 722edb7

3 files changed

Lines changed: 216 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
output/
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using System.Text.Json;
2+
3+
namespace OpenApiCodeGenerator.IntegrationTests;
4+
5+
/// <summary>
6+
/// Runs the generator against OpenAPI specifications.
7+
/// These tests are slow (they download specs from the internet) and are meant to be
8+
/// run manually for integration testing and inspection of generated code, not as part of the regular unit test suite.
9+
/// </summary>
10+
public class ApiDirectoryTests : IAsyncLifetime
11+
{
12+
private static readonly string OutputDir = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "output");
13+
14+
private static readonly HttpClient Http = new()
15+
{
16+
Timeout = TimeSpan.FromSeconds(30),
17+
};
18+
19+
private static List<ApiEntry>? _apis;
20+
private static readonly SemaphoreSlim _initLock = new(1, 1);
21+
22+
static ApiDirectoryTests()
23+
{
24+
Http.DefaultRequestHeaders.UserAgent.ParseAdd("OpenApiCodeGenerator-IntegrationTests/1.0");
25+
}
26+
27+
public async ValueTask InitializeAsync()
28+
{
29+
Directory.CreateDirectory(OutputDir);
30+
await EnsureApisLoadedAsync().ConfigureAwait(true);
31+
}
32+
33+
public ValueTask DisposeAsync()
34+
{
35+
GC.SuppressFinalize(this);
36+
return ValueTask.CompletedTask;
37+
}
38+
39+
[Trait("Category", "Integration")]
40+
[Theory(Skip = "Long-running test that generates code from a large collection of OpenAPI specs. Run manually for integration testing and inspection of generated code, not as part of the regular unit test suite.")]
41+
[MemberData(nameof(GetAllApis))]
42+
public async Task GenerateFromSpecAsync(string apiId, string specUrl)
43+
{
44+
// Download spec
45+
using HttpResponseMessage response = await Http.GetAsync(specUrl, TestContext.Current.CancellationToken).ConfigureAwait(true);
46+
response.EnsureSuccessStatusCode();
47+
48+
using Stream stream = (await response.Content.ReadAsStreamAsync(TestContext.Current.CancellationToken).ConfigureAwait(true));
49+
50+
var generator = new CSharpSchemaGenerator(new GeneratorOptions
51+
{
52+
Namespace = "Generated." + NameHelper.ToTypeName(apiId),
53+
GenerateDocComments = true,
54+
GenerateFileHeader = true,
55+
});
56+
57+
// Generate C# code — should not throw
58+
string code = generator.GenerateFromStream(stream);
59+
60+
Assert.False(string.IsNullOrWhiteSpace(code), $"Generated code for {apiId} was empty.");
61+
62+
// Write output for inspection
63+
string safeFileName = SanitizeFileName(apiId);
64+
string outputPath = Path.Combine(OutputDir, $"{safeFileName}.cs");
65+
await File.WriteAllTextAsync(outputPath, code, TestContext.Current.CancellationToken).ConfigureAwait(true);
66+
}
67+
68+
public static IEnumerable<object[]> GetAllApis()
69+
{
70+
// MemberData is evaluated synchronously — load the API list synchronously
71+
List<ApiEntry> apis = LoadApisSync();
72+
foreach (ApiEntry api in apis)
73+
{
74+
yield return new object[] { api.Id, api.SpecUrl };
75+
}
76+
}
77+
78+
private static List<ApiEntry> LoadApisSync()
79+
{
80+
if (_apis != null)
81+
{
82+
return _apis;
83+
}
84+
85+
_initLock.Wait();
86+
try
87+
{
88+
if (_apis != null)
89+
{
90+
return _apis;
91+
}
92+
93+
_apis = FetchApiListAsync().GetAwaiter().GetResult();
94+
return _apis;
95+
}
96+
finally
97+
{
98+
_initLock.Release();
99+
}
100+
}
101+
102+
private static async Task EnsureApisLoadedAsync()
103+
{
104+
if (_apis != null)
105+
{
106+
return;
107+
}
108+
109+
await _initLock.WaitAsync().ConfigureAwait(true);
110+
try
111+
{
112+
_apis ??= await FetchApiListAsync().ConfigureAwait(true);
113+
}
114+
finally
115+
{
116+
_initLock.Release();
117+
}
118+
}
119+
120+
private static async Task<List<ApiEntry>> FetchApiListAsync()
121+
{
122+
string json = await Http.GetStringAsync("https://api.apis.guru/v2/list.json").ConfigureAwait(true);
123+
using var doc = JsonDocument.Parse(json);
124+
125+
var entries = new List<ApiEntry>();
126+
127+
foreach (JsonProperty providerProp in doc.RootElement.EnumerateObject())
128+
{
129+
string providerId = providerProp.Name;
130+
131+
if (!providerProp.Value.TryGetProperty("versions", out JsonElement versions))
132+
{
133+
continue;
134+
}
135+
136+
foreach (JsonProperty versionProp in versions.EnumerateObject())
137+
{
138+
JsonElement versionObj = versionProp.Value;
139+
140+
// Prefer the JSON URL as it's more compact
141+
string? specUrl = null;
142+
if (versionObj.TryGetProperty("swaggerUrl", out JsonElement jsonUrl))
143+
{
144+
specUrl = jsonUrl.GetString();
145+
}
146+
else if (versionObj.TryGetProperty("swaggerYamlUrl", out JsonElement yamlUrl))
147+
{
148+
specUrl = yamlUrl.GetString();
149+
}
150+
151+
if (specUrl == null)
152+
{
153+
continue;
154+
}
155+
156+
string apiId = versions.EnumerateObject().Count() > 1
157+
? $"{providerId}@{versionProp.Name}"
158+
: providerId;
159+
160+
entries.Add(new ApiEntry(apiId, specUrl));
161+
}
162+
}
163+
164+
return entries.OrderBy(e => e.Id).ToList();
165+
}
166+
167+
private static string SanitizeFileName(string name)
168+
{
169+
char[] invalid = Path.GetInvalidFileNameChars();
170+
char[] sanitized = new char[name.Length];
171+
for (int i = 0; i < name.Length; i++)
172+
{
173+
sanitized[i] = Array.IndexOf(invalid, name[i]) >= 0 ? '_' : name[i];
174+
}
175+
return new string(sanitized);
176+
}
177+
178+
private record ApiEntry(string Id, string SpecUrl);
179+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<!-- Exclude generated output from compilation; these are for inspection only -->
11+
<ItemGroup>
12+
<Compile Remove="output\**\*.cs" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="coverlet.collector">
17+
<PrivateAssets>all</PrivateAssets>
18+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
19+
</PackageReference>
20+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
21+
<PackageReference Include="xunit.runner.visualstudio">
22+
<PrivateAssets>all</PrivateAssets>
23+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
24+
</PackageReference>
25+
<PackageReference Include="xunit.v3" />
26+
</ItemGroup>
27+
28+
<ItemGroup>
29+
<Using Include="Xunit" />
30+
</ItemGroup>
31+
32+
<ItemGroup>
33+
<ProjectReference Include="..\..\src\OpenApiCodeGenerator\OpenApiCodeGenerator.csproj" />
34+
</ItemGroup>
35+
36+
</Project>

0 commit comments

Comments
 (0)