const std = @import("std"); const testing = std.testing; const build_options = @import("build_options"); const atom = @import("atom.zig"); const Release = @import("main.zig").Release; const GitHub = @import("providers/GitHub.zig"); const GitLab = @import("providers/GitLab.zig"); const Forgejo = @import("providers/Forgejo.zig"); const SourceHut = @import("providers/SourceHut.zig"); const config = @import("config.zig"); fn testPrint(comptime fmt: []const u8, args: anytype) void { if (build_options.test_debug) { std.debug.print(fmt, args); } } test "Atom feed validates against W3C validator" { const allocator = testing.allocator; // Create sample releases for testing const releases = [_]Release{ Release{ .repo_name = "ziglang/zig", .tag_name = "0.14.0", .published_at = "2024-12-19T00:00:00Z", .html_url = "https://github.com/ziglang/zig/releases/tag/0.14.0", .description = "Zig 0.14.0 release with many improvements", .provider = "github", .is_tag = false, }, Release{ .repo_name = "example/test", .tag_name = "v1.2.3", .published_at = "2024-12-18T12:30:00Z", .html_url = "https://github.com/example/test/releases/tag/v1.2.3", .description = "Bug fixes and performance improvements", .provider = "github", .is_tag = false, }, }; // Generate the Atom feed const atom_content = try atom.generateFeed(allocator, &releases); defer allocator.free(atom_content); // Validate against W3C Feed Validator const http = std.http; var client = http.Client{ .allocator = allocator }; defer client.deinit(); // Prepare the POST request to W3C validator const validator_url = "https://validator.w3.org/feed/check.cgi"; const uri = try std.Uri.parse(validator_url); // Create form data for the validator var form_data = std.ArrayList(u8).init(allocator); defer form_data.deinit(); try form_data.appendSlice("------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n"); try form_data.appendSlice("Content-Disposition: form-data; name=\"rawdata\"\r\n\r\n"); try form_data.appendSlice(atom_content); try form_data.appendSlice("\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n"); try form_data.appendSlice("Content-Disposition: form-data; name=\"manual\"\r\n\r\n"); try form_data.appendSlice("1\r\n"); try form_data.appendSlice("------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n"); var server_header_buffer: [16 * 1024]u8 = undefined; var req = try client.open(.POST, uri, .{ .server_header_buffer = &server_header_buffer, .extra_headers = &.{ .{ .name = "Content-Type", .value = "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" }, .{ .name = "User-Agent", .value = "Release-Tracker-Test/1.0" }, }, }); defer req.deinit(); req.transfer_encoding = .{ .content_length = form_data.items.len }; try req.send(); try req.writeAll(form_data.items); try req.finish(); try req.wait(); // Read the response var response_body = std.ArrayList(u8).init(allocator); defer response_body.deinit(); const max_response_size = 1024 * 1024; // 1MB max try response_body.ensureTotalCapacity(max_response_size); var buf: [4096]u8 = undefined; while (true) { const bytes_read = try req.readAll(buf[0..]); if (bytes_read == 0) break; try response_body.appendSlice(buf[0..bytes_read]); if (response_body.items.len > max_response_size) { return error.ResponseTooLarge; } } const response_text = response_body.items; testPrint("W3C Validator Response Length: {d}\n", .{response_text.len}); // Check for validation success indicators const is_valid = std.mem.indexOf(u8, response_text, "This is a valid Atom 1.0 feed") != null or std.mem.indexOf(u8, response_text, "Congratulations!") != null or (std.mem.indexOf(u8, response_text, "valid") != null and std.mem.indexOf(u8, response_text, "error") == null); // Check for specific error indicators const has_errors = std.mem.indexOf(u8, response_text, "This feed does not validate") != null or std.mem.indexOf(u8, response_text, "Errors:") != null or std.mem.indexOf(u8, response_text, "line") != null and std.mem.indexOf(u8, response_text, "column") != null; if (has_errors) { testPrint("W3C Validator found errors in the feed:\n", .{}); // Print relevant parts of the response for debugging if (std.mem.indexOf(u8, response_text, "
")) |start| { if (std.mem.indexOf(u8, response_text[start..], "")) |end| { const error_section = response_text[start .. start + end + 6]; testPrint("{s}\n", .{error_section}); } } return error.FeedValidationFailed; } if (!is_valid) { // Handle 502/520 errors gracefully - W3C validator is sometimes unavailable if (std.mem.indexOf(u8, response_text, "error code: 502") != null or std.mem.indexOf(u8, response_text, "error code: 520") != null) { testPrint("⚠️ W3C Validator temporarily unavailable (server error) - skipping validation\n", .{}); return; // Skip test instead of failing } testPrint("W3C Validator response unclear - dumping first 1000 chars:\n{s}\n", .{response_text[0..@min(1000, response_text.len)]}); return error.ValidationResponseUnclear; } testPrint("✓ Atom feed validated successfully against W3C Feed Validator\n", .{}); } test "Validate actual releases.xml against W3C validator" { const allocator = testing.allocator; // Read the actual releases.xml file const releases_xml = std.fs.cwd().readFileAlloc(allocator, "releases.xml", 10 * 1024 * 1024) catch |err| { if (err == error.FileNotFound) { testPrint("⚠️ releases.xml not found - skipping validation (run the app first)\n", .{}); return; } return err; }; defer allocator.free(releases_xml); testPrint("Validating actual releases.xml ({d} bytes) against W3C validator...\n", .{releases_xml.len}); // Validate against W3C Feed Validator const http = std.http; var client = http.Client{ .allocator = allocator }; defer client.deinit(); const validator_url = "https://validator.w3.org/feed/check.cgi"; const uri = try std.Uri.parse(validator_url); // Create form data for the validator var form_data = std.ArrayList(u8).init(allocator); defer form_data.deinit(); try form_data.appendSlice("------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n"); try form_data.appendSlice("Content-Disposition: form-data; name=\"rawdata\"\r\n\r\n"); try form_data.appendSlice(releases_xml); try form_data.appendSlice("\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n"); try form_data.appendSlice("Content-Disposition: form-data; name=\"manual\"\r\n\r\n"); try form_data.appendSlice("1\r\n"); try form_data.appendSlice("------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n"); var server_header_buffer: [16 * 1024]u8 = undefined; var req = try client.open(.POST, uri, .{ .server_header_buffer = &server_header_buffer, .extra_headers = &.{ .{ .name = "Content-Type", .value = "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" }, .{ .name = "User-Agent", .value = "Release-Tracker-Test/1.0" }, }, }); defer req.deinit(); req.transfer_encoding = .{ .content_length = form_data.items.len }; try req.send(); try req.writeAll(form_data.items); try req.finish(); try req.wait(); // Read the response var response_body = std.ArrayList(u8).init(allocator); defer response_body.deinit(); const max_response_size = 1024 * 1024; // 1MB max try response_body.ensureTotalCapacity(max_response_size); var buf: [4096]u8 = undefined; while (true) { const bytes_read = try req.readAll(buf[0..]); if (bytes_read == 0) break; try response_body.appendSlice(buf[0..bytes_read]); if (response_body.items.len > max_response_size) { return error.ResponseTooLarge; } } const response_text = response_body.items; testPrint("W3C Validator Response Length: {d}\n", .{response_text.len}); // Check for validation success indicators const is_valid = std.mem.indexOf(u8, response_text, "This is a valid Atom 1.0 feed") != null or std.mem.indexOf(u8, response_text, "Congratulations!") != null or (std.mem.indexOf(u8, response_text, "valid") != null and std.mem.indexOf(u8, response_text, "error") == null); // Check for specific error indicators const has_errors = std.mem.indexOf(u8, response_text, "This feed does not validate") != null or std.mem.indexOf(u8, response_text, "Errors:") != null or std.mem.indexOf(u8, response_text, "line") != null and std.mem.indexOf(u8, response_text, "column") != null; if (has_errors) { testPrint("❌ W3C Validator found errors in releases.xml:\n", .{}); // Print relevant parts of the response for debugging if (std.mem.indexOf(u8, response_text, "
")) |start| { if (std.mem.indexOf(u8, response_text[start..], "")) |end| { const error_section = response_text[start .. start + end + 6]; testPrint("{s}\n", .{error_section}); } } // Also dump more of the response for debugging testPrint("Full response (first 2000 chars):\n{s}\n", .{response_text[0..@min(2000, response_text.len)]}); return error.FeedValidationFailed; } if (!is_valid) { // Handle 502/520 errors gracefully - W3C validator is sometimes unavailable if (std.mem.indexOf(u8, response_text, "error code: 502") != null or std.mem.indexOf(u8, response_text, "error code: 520") != null) { testPrint("⚠️ W3C Validator temporarily unavailable (server error) - skipping validation\n", .{}); return; // Skip test instead of failing } testPrint("W3C Validator response unclear - dumping first 2000 chars:\n{s}\n", .{response_text[0..@min(2000, response_text.len)]}); return error.ValidationResponseUnclear; } testPrint("✅ releases.xml validated successfully against W3C Feed Validator!\n", .{}); } test "Local XML well-formedness validation" { const allocator = testing.allocator; // Read the actual releases.xml file const releases_xml = std.fs.cwd().readFileAlloc(allocator, "releases.xml", 10 * 1024 * 1024) catch |err| { if (err == error.FileNotFound) { testPrint("⚠️ releases.xml not found - skipping validation (run the app first)\n", .{}); return; } return err; }; defer allocator.free(releases_xml); testPrint("Validating XML well-formedness of releases.xml ({d} bytes)...\n", .{releases_xml.len}); // Basic XML well-formedness checks var validation_errors = std.ArrayList([]const u8).init(allocator); defer validation_errors.deinit(); // Check for XML declaration if (!std.mem.startsWith(u8, releases_xml, "")) { try validation_errors.append("Missing or incorrect XML declaration"); } // Check for root element if (std.mem.indexOf(u8, releases_xml, "