//! Network-dependent test for `doh_client.zig`. //! //! It lives in its own file because it needs `@import("build_options")`, which //! only exists when the compilation is driven by build.zig. The test is //! compiled by every `zig build test` run, so it cannot rot, and it skips at run //! time unless `-Dlive` is passed. (`-Dintegration` stays hermetic: loopback //! only. `-Dlive` is the gate for tests that leave the machine.) const std = @import("std"); const build_options = @import("build_options"); const doh_client = @import("doh_client.zig"); const transport = @import("transport.zig"); const packet = @import("../dns/packet.zig"); /// No HTTP call in 0.16.0 takes a deadline, so the whole exchange runs as one /// task raced against a sleep and the loser is canceled. const budget: std.Io.Clock.Duration = .{ .raw = .fromSeconds(10), .clock = .awake }; const Outcome = union(enum) { exchange: anyerror!usize, expiry: std.Io.Cancelable!void, }; /// A query for example.com A: id 0x1234, RD set, one question. const query_bytes = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++ "\x07example\x03com\x00\x00\x01\x00\x01"; const Params = struct { gpa: std.mem.Allocator, response_buf: []u8, }; /// Returns the reply length; the bytes stay in the caller's `response_buf` so /// they outlive this task. fn runExchange(io: std.Io, params: Params) anyerror!usize { var http: std.http.Client = .{ .allocator = params.gpa, .io = io }; defer http.deinit(); var request_buf: [1024]u8 = undefined; var transfer_buf: [4096]u8 = undefined; const endpoint = try transport.Endpoint.parse("https://cloudflare-dns.com/dns-query"); var doh = try doh_client.DohClient.init(&http, endpoint, &request_buf, &transfer_buf); const reply = try doh.client().exchange(io, query_bytes, params.response_buf); return reply.len; } fn expire(io: std.Io, duration: std.Io.Clock.Duration) std.Io.Cancelable!void { return duration.sleep(io); } test "live DoH exchange against cloudflare-dns.com" { if (!build_options.live) return error.SkipZigTest; const gpa = std.testing.allocator; var threaded: std.Io.Threaded = .init(gpa, .{}); defer threaded.deinit(); const io = threaded.io(); var response_buf: [4096]u8 = undefined; var outcomes: [2]Outcome = undefined; var race: std.Io.Select(Outcome) = .init(io, &outcomes); defer race.cancelDiscard(); try race.concurrent(.exchange, runExchange, .{ io, Params{ .gpa = gpa, .response_buf = &response_buf, } }); try race.concurrent(.expiry, expire, .{ io, budget }); const len = switch (try race.await()) { .exchange => |result| result catch |err| { std.debug.print("DoH exchange failed: {s}\n", .{@errorName(err)}); return err; }, .expiry => |result| { try result; return error.DohExchangeTimedOut; }, }; // `exchange` already ran `validateResponse`; re-run it so the assertion is // in the test and not only in the code under test. try transport.validateResponse(query_bytes, response_buf[0..len]); const reply = try packet.parse(response_buf[0..len]); try std.testing.expect(reply.header.ancount >= 1); }