milestone 8: web server, rest api, sse, auth, metrics and static assets
This commit is contained in:
@@ -0,0 +1,551 @@
|
||||
//! `/api/local-records` and `/api/forward-zones` — the names nxdns answers
|
||||
//! itself and the zones it hands to another resolver.
|
||||
//!
|
||||
//! Both take effect live (ruling 12), and not through the blocklist snapshot:
|
||||
//! the two tables are rebuilt from the database and published into
|
||||
//! `state.local_tables`, so the next query reads the new generation. The reload
|
||||
//! seam is called as well, so the composition root learns about every
|
||||
//! configuration change through one path.
|
||||
//!
|
||||
//! A record's type travels as the word the schema stores — `A`, `AAAA`,
|
||||
//! `CNAME` — which is also the word the config file uses.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const http_util = @import("../http_util.zig");
|
||||
const local_repo = @import("../../storage/repositories/local_repo.zig");
|
||||
const model = @import("../../config/model.zig");
|
||||
const mutations = @import("mutations.zig");
|
||||
const server = @import("../server.zig");
|
||||
|
||||
const Failure = mutations.Failure;
|
||||
const Request = http_util.Request;
|
||||
const HandlerError = http_util.HandlerError;
|
||||
|
||||
const record_conflict = "that name, type and value are already stored";
|
||||
const zone_conflict = "that zone already has a resolver";
|
||||
|
||||
const RecordBody = struct {
|
||||
name: []const u8,
|
||||
rtype: []const u8,
|
||||
value: []const u8,
|
||||
ttl: u32 = 300,
|
||||
};
|
||||
|
||||
const ZoneBody = struct {
|
||||
zone: []const u8,
|
||||
resolver: []const u8,
|
||||
};
|
||||
|
||||
const Created = union(enum) { id: i64, fail: Failure };
|
||||
|
||||
fn toRecord(body: RecordBody) union(enum) { record: model.LocalRecord, fail: Failure } {
|
||||
const rtype = model.RecordType.fromDb(body.rtype) orelse
|
||||
return .{ .fail = .{ .invalid = "rtype must be 'A', 'AAAA' or 'CNAME'" } };
|
||||
return .{ .record = .{
|
||||
.name = body.name,
|
||||
.rtype = rtype,
|
||||
.value = body.value,
|
||||
.ttl = body.ttl,
|
||||
} };
|
||||
}
|
||||
|
||||
/// The wire shape of a local record: the row with its type spelled the way the
|
||||
/// schema spells it.
|
||||
const RecordView = struct {
|
||||
id: i64,
|
||||
name: []const u8,
|
||||
rtype: []const u8,
|
||||
value: []const u8,
|
||||
ttl: u32,
|
||||
|
||||
fn from(row: local_repo.LocalRecordRow) RecordView {
|
||||
return .{
|
||||
.id = row.id,
|
||||
.name = row.name,
|
||||
.rtype = row.rtype.toDb(),
|
||||
.value = row.value,
|
||||
.ttl = row.ttl,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// local records: decisions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Publishes the rebuilt tables and then announces the change. The swap comes
|
||||
/// first because it is what makes the answer live; the seam only tells the rest
|
||||
/// of the server that something moved.
|
||||
///
|
||||
/// Callers hold `state.config_lock` across the database write and this call:
|
||||
/// the rebuild reads the generation the write produced, and the swap publishes
|
||||
/// in write order — a concurrent mutation cannot overwrite a newer generation
|
||||
/// with an older one.
|
||||
fn publish(state: *server.WebState, io: std.Io, arena: Allocator, database: *@import("../../storage/db.zig").Db) ?Failure {
|
||||
if (mutations.swapLocalTables(state, io, arena, database)) |failure| return failure;
|
||||
return mutations.reload(state, io);
|
||||
}
|
||||
|
||||
pub fn applyCreateRecord(
|
||||
state: *server.WebState,
|
||||
io: std.Io,
|
||||
arena: Allocator,
|
||||
item: model.LocalRecord,
|
||||
) error{OutOfMemory}!Created {
|
||||
const database = switch (mutations.configDb(state)) {
|
||||
.database => |value| value,
|
||||
.fail => |failure| return .{ .fail = failure },
|
||||
};
|
||||
if (try mutations.checkLocalRecord(arena, item)) |problem| return .{ .fail = .{ .invalid = problem } };
|
||||
|
||||
state.config_lock.lockUncancelable(io);
|
||||
defer state.config_lock.unlock(io);
|
||||
|
||||
const id = local_repo.insertLocalRecordRow(database, item) catch |err|
|
||||
return .{ .fail = mutations.dbFailure(err, record_conflict) };
|
||||
if (publish(state, io, arena, database)) |failure| return .{ .fail = failure };
|
||||
return .{ .id = id };
|
||||
}
|
||||
|
||||
pub fn applyUpdateRecord(
|
||||
state: *server.WebState,
|
||||
io: std.Io,
|
||||
arena: Allocator,
|
||||
id: i64,
|
||||
item: model.LocalRecord,
|
||||
) error{OutOfMemory}!?Failure {
|
||||
const database = switch (mutations.configDb(state)) {
|
||||
.database => |value| value,
|
||||
.fail => |failure| return failure,
|
||||
};
|
||||
if (try mutations.checkLocalRecord(arena, item)) |problem| return .{ .invalid = problem };
|
||||
|
||||
state.config_lock.lockUncancelable(io);
|
||||
defer state.config_lock.unlock(io);
|
||||
|
||||
local_repo.updateLocalRecord(database, id, item) catch |err|
|
||||
return mutations.dbFailure(err, record_conflict);
|
||||
return publish(state, io, arena, database);
|
||||
}
|
||||
|
||||
pub fn applyDeleteRecord(state: *server.WebState, io: std.Io, arena: Allocator, id: i64) ?Failure {
|
||||
const database = switch (mutations.configDb(state)) {
|
||||
.database => |value| value,
|
||||
.fail => |failure| return failure,
|
||||
};
|
||||
|
||||
state.config_lock.lockUncancelable(io);
|
||||
defer state.config_lock.unlock(io);
|
||||
|
||||
local_repo.deleteLocalRecord(database, id) catch |err|
|
||||
return mutations.dbFailure(err, record_conflict);
|
||||
return publish(state, io, arena, database);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// forward zones: decisions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn applyCreateZone(
|
||||
state: *server.WebState,
|
||||
io: std.Io,
|
||||
arena: Allocator,
|
||||
item: model.ForwardZone,
|
||||
) error{OutOfMemory}!Created {
|
||||
const database = switch (mutations.configDb(state)) {
|
||||
.database => |value| value,
|
||||
.fail => |failure| return .{ .fail = failure },
|
||||
};
|
||||
if (try mutations.checkForwardZone(arena, item)) |problem| return .{ .fail = .{ .invalid = problem } };
|
||||
|
||||
state.config_lock.lockUncancelable(io);
|
||||
defer state.config_lock.unlock(io);
|
||||
|
||||
const id = local_repo.insertForwardZoneRow(database, item) catch |err|
|
||||
return .{ .fail = mutations.dbFailure(err, zone_conflict) };
|
||||
if (publish(state, io, arena, database)) |failure| return .{ .fail = failure };
|
||||
return .{ .id = id };
|
||||
}
|
||||
|
||||
pub fn applyUpdateZone(
|
||||
state: *server.WebState,
|
||||
io: std.Io,
|
||||
arena: Allocator,
|
||||
id: i64,
|
||||
item: model.ForwardZone,
|
||||
) error{OutOfMemory}!?Failure {
|
||||
const database = switch (mutations.configDb(state)) {
|
||||
.database => |value| value,
|
||||
.fail => |failure| return failure,
|
||||
};
|
||||
if (try mutations.checkForwardZone(arena, item)) |problem| return .{ .invalid = problem };
|
||||
|
||||
state.config_lock.lockUncancelable(io);
|
||||
defer state.config_lock.unlock(io);
|
||||
|
||||
local_repo.updateForwardZone(database, id, item) catch |err|
|
||||
return mutations.dbFailure(err, zone_conflict);
|
||||
return publish(state, io, arena, database);
|
||||
}
|
||||
|
||||
pub fn applyDeleteZone(state: *server.WebState, io: std.Io, arena: Allocator, id: i64) ?Failure {
|
||||
const database = switch (mutations.configDb(state)) {
|
||||
.database => |value| value,
|
||||
.fail => |failure| return failure,
|
||||
};
|
||||
|
||||
state.config_lock.lockUncancelable(io);
|
||||
defer state.config_lock.unlock(io);
|
||||
|
||||
local_repo.deleteForwardZone(database, id) catch |err|
|
||||
return mutations.dbFailure(err, zone_conflict);
|
||||
return publish(state, io, arena, database);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// local records: routes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn listRecords(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
|
||||
_ = io;
|
||||
const database = switch (mutations.configDb(state)) {
|
||||
.database => |value| value,
|
||||
.fail => |failure| return mutations.respondFailure(request, failure, "listing local records"),
|
||||
};
|
||||
|
||||
const rows = local_repo.listLocalRecordRows(database, request.arena) catch |err|
|
||||
return mutations.respondFailure(request, .{ .internal = err }, "listing local records");
|
||||
|
||||
const views = try request.arena.alloc(RecordView, rows.items.len);
|
||||
for (views, rows.items) |*view, row| view.* = .from(row);
|
||||
|
||||
return http_util.respondJson(request, .ok, .{ .local_records = views }, &.{});
|
||||
}
|
||||
|
||||
pub fn getRecord(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
|
||||
_ = io;
|
||||
const database = switch (mutations.configDb(state)) {
|
||||
.database => |value| value,
|
||||
.fail => |failure| return mutations.respondFailure(request, failure, "reading a local record"),
|
||||
};
|
||||
|
||||
const row = local_repo.getLocalRecord(database, request.arena, request.id.?) catch |err|
|
||||
return mutations.respondFailure(request, .{ .internal = err }, "reading a local record");
|
||||
const found = row orelse return mutations.respondFailure(request, .not_found, "");
|
||||
|
||||
return http_util.respondJson(request, .ok, RecordView.from(found), &.{});
|
||||
}
|
||||
|
||||
pub fn createRecord(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
|
||||
const parsed = http_util.parseBody(RecordBody, request) catch |err|
|
||||
return mutations.respondBadBody(request, err);
|
||||
const item = switch (toRecord(parsed.value)) {
|
||||
.fail => |failure| return mutations.respondFailure(request, failure, "creating a local record"),
|
||||
.record => |value| value,
|
||||
};
|
||||
|
||||
return switch (try applyCreateRecord(state, io, request.arena, item)) {
|
||||
.fail => |failure| mutations.respondFailure(request, failure, "creating a local record"),
|
||||
.id => |id| http_util.respondJson(request, .created, .{
|
||||
.id = id,
|
||||
.name = item.name,
|
||||
.rtype = item.rtype.toDb(),
|
||||
.value = item.value,
|
||||
.ttl = item.ttl,
|
||||
}, &.{}),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn updateRecord(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
|
||||
const parsed = http_util.parseBody(RecordBody, request) catch |err|
|
||||
return mutations.respondBadBody(request, err);
|
||||
const item = switch (toRecord(parsed.value)) {
|
||||
.fail => |failure| return mutations.respondFailure(request, failure, "updating a local record"),
|
||||
.record => |value| value,
|
||||
};
|
||||
const id = request.id.?;
|
||||
|
||||
if (try applyUpdateRecord(state, io, request.arena, id, item)) |failure| {
|
||||
return mutations.respondFailure(request, failure, "updating a local record");
|
||||
}
|
||||
return http_util.respondJson(request, .ok, .{
|
||||
.id = id,
|
||||
.name = item.name,
|
||||
.rtype = item.rtype.toDb(),
|
||||
.value = item.value,
|
||||
.ttl = item.ttl,
|
||||
}, &.{});
|
||||
}
|
||||
|
||||
pub fn removeRecord(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
|
||||
if (applyDeleteRecord(state, io, request.arena, request.id.?)) |failure| {
|
||||
return mutations.respondFailure(request, failure, "deleting a local record");
|
||||
}
|
||||
return http_util.respondEmpty(request, .no_content);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// forward zones: routes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn listZones(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
|
||||
_ = io;
|
||||
const database = switch (mutations.configDb(state)) {
|
||||
.database => |value| value,
|
||||
.fail => |failure| return mutations.respondFailure(request, failure, "listing forward zones"),
|
||||
};
|
||||
|
||||
const rows = local_repo.listForwardZoneRows(database, request.arena) catch |err|
|
||||
return mutations.respondFailure(request, .{ .internal = err }, "listing forward zones");
|
||||
|
||||
return http_util.respondJson(request, .ok, .{ .forward_zones = rows.items }, &.{});
|
||||
}
|
||||
|
||||
pub fn getZone(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
|
||||
_ = io;
|
||||
const database = switch (mutations.configDb(state)) {
|
||||
.database => |value| value,
|
||||
.fail => |failure| return mutations.respondFailure(request, failure, "reading a forward zone"),
|
||||
};
|
||||
|
||||
const row = local_repo.getForwardZone(database, request.arena, request.id.?) catch |err|
|
||||
return mutations.respondFailure(request, .{ .internal = err }, "reading a forward zone");
|
||||
const found = row orelse return mutations.respondFailure(request, .not_found, "");
|
||||
|
||||
return http_util.respondJson(request, .ok, found, &.{});
|
||||
}
|
||||
|
||||
pub fn createZone(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
|
||||
const parsed = http_util.parseBody(ZoneBody, request) catch |err|
|
||||
return mutations.respondBadBody(request, err);
|
||||
const item: model.ForwardZone = .{ .zone = parsed.value.zone, .resolver = parsed.value.resolver };
|
||||
|
||||
return switch (try applyCreateZone(state, io, request.arena, item)) {
|
||||
.fail => |failure| mutations.respondFailure(request, failure, "creating a forward zone"),
|
||||
.id => |id| http_util.respondJson(request, .created, .{
|
||||
.id = id,
|
||||
.zone = item.zone,
|
||||
.resolver = item.resolver,
|
||||
}, &.{}),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn updateZone(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
|
||||
const parsed = http_util.parseBody(ZoneBody, request) catch |err|
|
||||
return mutations.respondBadBody(request, err);
|
||||
const item: model.ForwardZone = .{ .zone = parsed.value.zone, .resolver = parsed.value.resolver };
|
||||
const id = request.id.?;
|
||||
|
||||
if (try applyUpdateZone(state, io, request.arena, id, item)) |failure| {
|
||||
return mutations.respondFailure(request, failure, "updating a forward zone");
|
||||
}
|
||||
return http_util.respondJson(request, .ok, .{
|
||||
.id = id,
|
||||
.zone = item.zone,
|
||||
.resolver = item.resolver,
|
||||
}, &.{});
|
||||
}
|
||||
|
||||
pub fn removeZone(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
|
||||
if (applyDeleteZone(state, io, request.arena, request.id.?)) |failure| {
|
||||
return mutations.respondFailure(request, failure, "deleting a forward zone");
|
||||
}
|
||||
return http_util.respondEmpty(request, .no_content);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
const nas: model.LocalRecord = .{ .name = "nas.lan", .rtype = .a, .value = "192.168.1.10", .ttl = 60 };
|
||||
const lan: model.ForwardZone = .{ .zone = "lan", .resolver = "udp://10.0.0.1:53" };
|
||||
|
||||
test "a created local record is answered by the published table" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
const created = try applyCreateRecord(&bench.state, bench.io(), bench.arena(), nas);
|
||||
try testing.expect(created == .id);
|
||||
try testing.expectEqual(@as(usize, 1), bench.reloads);
|
||||
|
||||
const handle = bench.tables.acquire(bench.io());
|
||||
defer handle.release(bench.io());
|
||||
try testing.expect(handle.records.hasName("nas.lan"));
|
||||
try testing.expectEqual(@as(usize, 1), handle.records.lookup("nas.lan", .a).len);
|
||||
}
|
||||
|
||||
test "an edited local record replaces what the table answers" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
const created = try applyCreateRecord(&bench.state, bench.io(), bench.arena(), nas);
|
||||
const failure = try applyUpdateRecord(&bench.state, bench.io(), bench.arena(), created.id, .{
|
||||
.name = "printer.lan",
|
||||
.rtype = .a,
|
||||
.value = "192.168.1.11",
|
||||
.ttl = 120,
|
||||
});
|
||||
try testing.expectEqual(@as(?Failure, null), failure);
|
||||
|
||||
const handle = bench.tables.acquire(bench.io());
|
||||
defer handle.release(bench.io());
|
||||
try testing.expect(!handle.records.hasName("nas.lan"));
|
||||
try testing.expect(handle.records.hasName("printer.lan"));
|
||||
}
|
||||
|
||||
test "a deleted local record leaves the published table empty" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
const created = try applyCreateRecord(&bench.state, bench.io(), bench.arena(), nas);
|
||||
try testing.expectEqual(
|
||||
@as(?Failure, null),
|
||||
applyDeleteRecord(&bench.state, bench.io(), bench.arena(), created.id),
|
||||
);
|
||||
|
||||
const handle = bench.tables.acquire(bench.io());
|
||||
defer handle.release(bench.io());
|
||||
try testing.expect(!handle.records.hasName("nas.lan"));
|
||||
try testing.expectEqual(@as(usize, 2), bench.reloads);
|
||||
}
|
||||
|
||||
test "a record value the validator refuses never reaches the database" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
const bad_value = try applyCreateRecord(&bench.state, bench.io(), bench.arena(), .{
|
||||
.name = "nas.lan",
|
||||
.rtype = .a,
|
||||
.value = "2001:db8::1",
|
||||
.ttl = 60,
|
||||
});
|
||||
try testing.expect(bad_value.fail == .invalid);
|
||||
|
||||
const bad_ttl = try applyCreateRecord(&bench.state, bench.io(), bench.arena(), .{
|
||||
.name = "nas.lan",
|
||||
.rtype = .a,
|
||||
.value = "192.168.1.10",
|
||||
.ttl = 0,
|
||||
});
|
||||
try testing.expect(bad_ttl.fail == .invalid);
|
||||
|
||||
try testing.expectEqual(@as(i64, 0), try bench.queryInt("SELECT count(*) FROM local_records"));
|
||||
try testing.expectEqual(@as(usize, 0), bench.reloads);
|
||||
}
|
||||
|
||||
test "the same record twice is a conflict" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
_ = try applyCreateRecord(&bench.state, bench.io(), bench.arena(), nas);
|
||||
const again = try applyCreateRecord(&bench.state, bench.io(), bench.arena(), nas);
|
||||
try testing.expectEqualStrings(record_conflict, again.fail.conflict);
|
||||
}
|
||||
|
||||
test "an id no record holds is a 404 on both update and delete" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
try testing.expectEqual(
|
||||
Failure.not_found,
|
||||
(try applyUpdateRecord(&bench.state, bench.io(), bench.arena(), 999, nas)).?,
|
||||
);
|
||||
try testing.expectEqual(
|
||||
Failure.not_found,
|
||||
applyDeleteRecord(&bench.state, bench.io(), bench.arena(), 999).?,
|
||||
);
|
||||
}
|
||||
|
||||
test "a created forward zone is matched by the published table" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
const created = try applyCreateZone(&bench.state, bench.io(), bench.arena(), lan);
|
||||
try testing.expect(created == .id);
|
||||
|
||||
const handle = bench.tables.acquire(bench.io());
|
||||
defer handle.release(bench.io());
|
||||
try testing.expect(handle.zones.match("nas.lan") != null);
|
||||
try testing.expect(handle.zones.match("example.test") == null);
|
||||
}
|
||||
|
||||
test "a resolver the validator refuses never reaches the database" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
const created = try applyCreateZone(&bench.state, bench.io(), bench.arena(), .{
|
||||
.zone = "lan",
|
||||
.resolver = "https://10.0.0.1",
|
||||
});
|
||||
try testing.expect(created.fail == .invalid);
|
||||
try testing.expectEqual(@as(i64, 0), try bench.queryInt("SELECT count(*) FROM forward_zones"));
|
||||
}
|
||||
|
||||
test "one zone cannot have two resolvers" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
_ = try applyCreateZone(&bench.state, bench.io(), bench.arena(), lan);
|
||||
const again = try applyCreateZone(&bench.state, bench.io(), bench.arena(), .{
|
||||
.zone = "lan",
|
||||
.resolver = "tcp://10.0.0.2:53",
|
||||
});
|
||||
try testing.expectEqualStrings(zone_conflict, again.fail.conflict);
|
||||
}
|
||||
|
||||
test "a deleted zone stops matching" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
const created = try applyCreateZone(&bench.state, bench.io(), bench.arena(), lan);
|
||||
try testing.expectEqual(
|
||||
@as(?Failure, null),
|
||||
applyDeleteZone(&bench.state, bench.io(), bench.arena(), created.id),
|
||||
);
|
||||
|
||||
const handle = bench.tables.acquire(bench.io());
|
||||
defer handle.release(bench.io());
|
||||
try testing.expect(handle.zones.match("nas.lan") == null);
|
||||
}
|
||||
|
||||
test "a record change is visible to a reader that acquires afterwards" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
|
||||
const before = bench.tables.acquire(bench.io());
|
||||
try testing.expect(!before.records.hasName("nas.lan"));
|
||||
before.release(bench.io());
|
||||
|
||||
_ = try applyCreateRecord(&bench.state, bench.io(), bench.arena(), nas);
|
||||
|
||||
const after = bench.tables.acquire(bench.io());
|
||||
defer after.release(bench.io());
|
||||
try testing.expect(after.records.hasName("nas.lan"));
|
||||
}
|
||||
|
||||
test "an unknown record type is a 400 before anything is written" {
|
||||
try testing.expect(toRecord(.{
|
||||
.name = "nas.lan",
|
||||
.rtype = "MX",
|
||||
.value = "mail.lan",
|
||||
}).fail == .invalid);
|
||||
|
||||
const good = toRecord(.{ .name = "nas.lan", .rtype = "CNAME", .value = "other.lan" });
|
||||
try testing.expectEqual(model.RecordType.cname, good.record.rtype);
|
||||
try testing.expectEqual(@as(u32, 300), good.record.ttl);
|
||||
}
|
||||
Reference in New Issue
Block a user