Frontend gaps fill work. Testing fixes work. Auditing in progress.
This commit is contained in:
@@ -233,6 +233,348 @@ public sealed class OfflineKitEndpointsTests
|
||||
Assert.Equal("accepted", entity.Result);
|
||||
}
|
||||
|
||||
#region Sprint 026: OFFLINE-009 - Manifest and Validate Endpoint Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitManifest_WhenNoBundle_ReturnsNoContent()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync("/api/offline-kit/manifest");
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitManifest_AfterImport_ReturnsManifest()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
var bundleBytes = Encoding.UTF8.GetBytes("deterministic-offline-kit-bundle");
|
||||
var bundleSha = ComputeSha256Hex(bundleBytes);
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
config["Scanner:OfflineKit:RequireDsse"] = "false";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
// Import a bundle first
|
||||
var metadataJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
bundleId = "manifest-test-bundle",
|
||||
bundleSha256 = $"sha256:{bundleSha}",
|
||||
bundleSize = bundleBytes.Length,
|
||||
capturedAt = DateTimeOffset.UtcNow.AddDays(-1)
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var importContent = new MultipartFormDataContent();
|
||||
importContent.Add(new StringContent(metadataJson, Encoding.UTF8, "application/json"), "metadata");
|
||||
var bundleContent = new ByteArrayContent(bundleBytes);
|
||||
bundleContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
importContent.Add(bundleContent, "bundle", "bundle.tgz");
|
||||
|
||||
using var importResponse = await client.PostAsync("/api/offline-kit/import", importContent);
|
||||
Assert.Equal(HttpStatusCode.Accepted, importResponse.StatusCode);
|
||||
|
||||
// Now fetch manifest
|
||||
using var manifestResponse = await client.GetAsync("/api/offline-kit/manifest");
|
||||
Assert.Equal(HttpStatusCode.OK, manifestResponse.StatusCode);
|
||||
|
||||
var manifestJson = await manifestResponse.Content.ReadAsStringAsync();
|
||||
using var manifestDoc = JsonDocument.Parse(manifestJson);
|
||||
Assert.Equal("manifest-test-bundle", manifestDoc.RootElement.GetProperty("version").GetString());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitValidate_WithValidManifest_ReturnsSuccess()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var manifestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
version = "2025.01.15",
|
||||
assets = new
|
||||
{
|
||||
feeds = new Dictionary<string, string>
|
||||
{
|
||||
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
|
||||
}
|
||||
},
|
||||
createdAt = DateTimeOffset.UtcNow.AddDays(-2)
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson,
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var resultJson = await response.Content.ReadAsStringAsync();
|
||||
using var resultDoc = JsonDocument.Parse(resultJson);
|
||||
Assert.True(resultDoc.RootElement.GetProperty("valid").GetBoolean());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitValidate_WithInvalidManifest_ReturnsErrors()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var invalidManifestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
version = "", // Empty version - validation error
|
||||
assets = new Dictionary<string, string>() // Empty assets - validation error
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson = invalidManifestJson,
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var resultJson = await response.Content.ReadAsStringAsync();
|
||||
using var resultDoc = JsonDocument.Parse(resultJson);
|
||||
Assert.False(resultDoc.RootElement.GetProperty("valid").GetBoolean());
|
||||
Assert.True(resultDoc.RootElement.GetProperty("errors").GetArrayLength() > 0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitValidate_WithExpiredManifest_ReturnsWarning()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var expiredManifestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
version = "2024.01.15",
|
||||
assets = new
|
||||
{
|
||||
feeds = new Dictionary<string, string>
|
||||
{
|
||||
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
|
||||
}
|
||||
},
|
||||
createdAt = DateTimeOffset.UtcNow.AddDays(-60), // 60 days old - stale warning
|
||||
expiresAt = DateTimeOffset.UtcNow.AddDays(-30) // Already expired
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson = expiredManifestJson,
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var resultJson = await response.Content.ReadAsStringAsync();
|
||||
using var resultDoc = JsonDocument.Parse(resultJson);
|
||||
Assert.True(resultDoc.RootElement.GetProperty("warnings").GetArrayLength() > 0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitValidate_WithSignature_ValidatesSignature()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var validManifestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
version = "2025.01.15",
|
||||
assets = new
|
||||
{
|
||||
feeds = new Dictionary<string, string>
|
||||
{
|
||||
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
|
||||
}
|
||||
},
|
||||
createdAt = DateTimeOffset.UtcNow
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson = validManifestJson,
|
||||
signature = "sha256:" + Convert.ToBase64String(Encoding.UTF8.GetBytes("test-signature")),
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var resultJson = await response.Content.ReadAsStringAsync();
|
||||
using var resultDoc = JsonDocument.Parse(resultJson);
|
||||
var signatureStatus = resultDoc.RootElement.GetProperty("signatureStatus");
|
||||
Assert.True(signatureStatus.GetProperty("valid").GetBoolean());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitValidate_WhenDisabled_ReturnsNotFound()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "false";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson = "{}",
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sprint 026: OFFLINE-012 - V1 Alias Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitV1Alias_Status_Works()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
// Test v1 alias for status
|
||||
using var response = await client.GetAsync("/api/v1/offline-kit/status");
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitV1Alias_Manifest_Works()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
// Test v1 alias for manifest
|
||||
using var response = await client.GetAsync("/api/v1/offline-kit/manifest");
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitV1Alias_Validate_Works()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var validManifestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
version = "2025.01.15",
|
||||
assets = new
|
||||
{
|
||||
feeds = new Dictionary<string, string>
|
||||
{
|
||||
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
|
||||
}
|
||||
},
|
||||
createdAt = DateTimeOffset.UtcNow
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson = validManifestJson,
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
// Test v1 alias for validate
|
||||
using var response = await client.PostAsync("/api/v1/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var resultJson = await response.Content.ReadAsStringAsync();
|
||||
using var resultDoc = JsonDocument.Parse(resultJson);
|
||||
Assert.True(resultDoc.RootElement.GetProperty("valid").GetBoolean());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static string ComputeSha256Hex(byte[] bytes)
|
||||
=> Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user