562 lines
22 KiB
Zig
562 lines
22 KiB
Zig
//! `local_records` and `forward_zones`.
|
|
//!
|
|
//! The import path is list / insert / deleteAll / count. The REST surface
|
|
//! follows each table's section: it speaks row ids, because that is what an
|
|
//! `/api/local-records/{id}` or `/api/forward-zones/{id}` request names.
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const db = @import("../db.zig");
|
|
const migrations = @import("../migrations.zig");
|
|
const model = @import("../../config/model.zig");
|
|
const context = @import("context.zig");
|
|
const crud = @import("crud.zig");
|
|
|
|
const InsertContext = context.InsertContext;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// local_records
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const list_local_records_sql =
|
|
\\SELECT name, rtype, value, ttl FROM local_records ORDER BY name, rtype, value
|
|
;
|
|
|
|
/// Every string in the result is a heap copy owned by `gpa`.
|
|
pub fn listLocalRecords(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.LocalRecord) {
|
|
return crud.listRows(model.LocalRecord, database, gpa, list_local_records_sql, readLocalRecord);
|
|
}
|
|
|
|
fn readLocalRecord(stmt: *db.Stmt, gpa: Allocator) db.Error!model.LocalRecord {
|
|
// The DDL's CHECK constraint makes the decode total for any row nxdns
|
|
// wrote; `error.Mismatch` covers a row that something else wrote.
|
|
const rtype = model.RecordType.fromDb(stmt.columnText(1)) orelse return error.Mismatch;
|
|
const ttl = std.math.cast(u32, stmt.columnInt(3)) orelse return error.Mismatch;
|
|
const name = try stmt.columnTextAlloc(gpa, 0);
|
|
errdefer gpa.free(name);
|
|
const value = try stmt.columnTextAlloc(gpa, 2);
|
|
errdefer gpa.free(value);
|
|
return .{ .name = name, .rtype = rtype, .value = value, .ttl = ttl };
|
|
}
|
|
|
|
pub fn freeLocalRecords(gpa: Allocator, items: []const model.LocalRecord) void {
|
|
crud.freeRows(model.LocalRecord, gpa, items);
|
|
}
|
|
|
|
const insert_local_record_sql =
|
|
\\INSERT INTO local_records (name, rtype, value, ttl) VALUES (?1, ?2, ?3, ?4)
|
|
;
|
|
|
|
pub fn insertLocalRecord(database: *db.Db, item: model.LocalRecord, ctx: InsertContext) db.Error!void {
|
|
_ = ctx;
|
|
var stmt = try database.prepare(insert_local_record_sql);
|
|
defer stmt.deinit();
|
|
try stmt.bindText(1, item.name);
|
|
try stmt.bindText(2, item.rtype.toDb());
|
|
try stmt.bindText(3, item.value);
|
|
try stmt.bindInt(4, item.ttl);
|
|
try stmt.exec();
|
|
}
|
|
|
|
pub fn deleteAllLocalRecords(database: *db.Db) db.Error!void {
|
|
return database.exec("DELETE FROM local_records;");
|
|
}
|
|
|
|
pub fn countLocalRecords(database: *db.Db) db.Error!i64 {
|
|
return database.queryInt("SELECT count(*) FROM local_records");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// forward_zones
|
|
// ---------------------------------------------------------------------------
|
|
|
|
pub fn listForwardZones(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(model.ForwardZone) {
|
|
return crud.listRows(
|
|
model.ForwardZone,
|
|
database,
|
|
gpa,
|
|
"SELECT zone, resolver FROM forward_zones ORDER BY zone",
|
|
readForwardZone,
|
|
);
|
|
}
|
|
|
|
fn readForwardZone(stmt: *db.Stmt, gpa: Allocator) db.Error!model.ForwardZone {
|
|
const zone = try stmt.columnTextAlloc(gpa, 0);
|
|
errdefer gpa.free(zone);
|
|
const resolver = try stmt.columnTextAlloc(gpa, 1);
|
|
errdefer gpa.free(resolver);
|
|
return .{ .zone = zone, .resolver = resolver };
|
|
}
|
|
|
|
pub fn freeForwardZones(gpa: Allocator, items: []const model.ForwardZone) void {
|
|
crud.freeRows(model.ForwardZone, gpa, items);
|
|
}
|
|
|
|
pub fn insertForwardZone(database: *db.Db, item: model.ForwardZone, ctx: InsertContext) db.Error!void {
|
|
_ = ctx;
|
|
var stmt = try database.prepare("INSERT INTO forward_zones (zone, resolver) VALUES (?1, ?2)");
|
|
defer stmt.deinit();
|
|
try stmt.bindText(1, item.zone);
|
|
try stmt.bindText(2, item.resolver);
|
|
try stmt.exec();
|
|
}
|
|
|
|
pub fn deleteAllForwardZones(database: *db.Db) db.Error!void {
|
|
return database.exec("DELETE FROM forward_zones;");
|
|
}
|
|
|
|
pub fn countForwardZones(database: *db.Db) db.Error!i64 {
|
|
return database.queryInt("SELECT count(*) FROM forward_zones");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// REST surface (milestone 8)
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Neither table references another, so the write shapes are `model.LocalRecord`
|
|
// and `model.ForwardZone` unchanged: their fields are exactly the columns.
|
|
|
|
pub const LocalRecordRow = struct {
|
|
id: i64,
|
|
name: []const u8,
|
|
rtype: model.RecordType,
|
|
value: []const u8,
|
|
ttl: u32,
|
|
};
|
|
|
|
const list_local_record_rows_sql =
|
|
\\SELECT id, name, rtype, value, ttl FROM local_records ORDER BY name, rtype, value
|
|
;
|
|
|
|
/// Same order as `listLocalRecords`; every string is a heap copy owned by `gpa`.
|
|
pub fn listLocalRecordRows(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(LocalRecordRow) {
|
|
return crud.listRows(LocalRecordRow, database, gpa, list_local_record_rows_sql, readLocalRecordRow);
|
|
}
|
|
|
|
pub fn freeLocalRecordRow(gpa: Allocator, row: LocalRecordRow) void {
|
|
crud.freeRow(LocalRecordRow, gpa, row);
|
|
}
|
|
|
|
pub fn freeLocalRecordRows(gpa: Allocator, items: []const LocalRecordRow) void {
|
|
crud.freeRows(LocalRecordRow, gpa, items);
|
|
}
|
|
|
|
pub fn getLocalRecord(database: *db.Db, gpa: Allocator, id: i64) db.Error!?LocalRecordRow {
|
|
var stmt = try database.prepare("SELECT id, name, rtype, value, ttl FROM local_records WHERE id = ?1");
|
|
defer stmt.deinit();
|
|
try stmt.bindInt(1, id);
|
|
if (!try stmt.step()) return null;
|
|
return try readLocalRecordRow(&stmt, gpa);
|
|
}
|
|
|
|
fn readLocalRecordRow(stmt: *db.Stmt, gpa: Allocator) db.Error!LocalRecordRow {
|
|
// The DDL's CHECK constraint makes the decode total for any row nxdns
|
|
// wrote; `error.Mismatch` covers a row that something else wrote, and the
|
|
// same goes for a `ttl` outside `u32`.
|
|
const rtype = model.RecordType.fromDb(stmt.columnText(2)) orelse return error.Mismatch;
|
|
const ttl = std.math.cast(u32, stmt.columnInt(4)) orelse return error.Mismatch;
|
|
const name = try stmt.columnTextAlloc(gpa, 1);
|
|
errdefer gpa.free(name);
|
|
const value = try stmt.columnTextAlloc(gpa, 3);
|
|
errdefer gpa.free(value);
|
|
return .{ .id = stmt.columnInt(0), .name = name, .rtype = rtype, .value = value, .ttl = ttl };
|
|
}
|
|
|
|
/// `error.Constraint`: `local_records` declares `UNIQUE(name, rtype, value)`, so
|
|
/// the same answer cannot be stored twice — a second TTL for one record would be
|
|
/// two truths.
|
|
pub fn insertLocalRecordRow(database: *db.Db, item: model.LocalRecord) db.Error!i64 {
|
|
var stmt = try database.prepare(insert_local_record_sql);
|
|
defer stmt.deinit();
|
|
try stmt.bindText(1, item.name);
|
|
try stmt.bindText(2, item.rtype.toDb());
|
|
try stmt.bindText(3, item.value);
|
|
try stmt.bindInt(4, item.ttl);
|
|
try stmt.exec();
|
|
return database.lastInsertRowid();
|
|
}
|
|
|
|
/// `error.NotFound`: no record holds `id`. `error.Constraint`:
|
|
/// `UNIQUE(name, rtype, value)`.
|
|
pub fn updateLocalRecord(database: *db.Db, id: i64, item: model.LocalRecord) db.Error!void {
|
|
var stmt = try database.prepare(
|
|
"UPDATE local_records SET name = ?2, rtype = ?3, value = ?4, ttl = ?5 WHERE id = ?1",
|
|
);
|
|
defer stmt.deinit();
|
|
try stmt.bindInt(1, id);
|
|
try stmt.bindText(2, item.name);
|
|
try stmt.bindText(3, item.rtype.toDb());
|
|
try stmt.bindText(4, item.value);
|
|
try stmt.bindInt(5, item.ttl);
|
|
return crud.execStrict(database, &stmt);
|
|
}
|
|
|
|
/// `error.NotFound`: no record holds `id`. Nothing references `local_records`,
|
|
/// so a delete cannot violate a constraint.
|
|
pub fn deleteLocalRecord(database: *db.Db, id: i64) db.Error!void {
|
|
var stmt = try database.prepare("DELETE FROM local_records WHERE id = ?1");
|
|
defer stmt.deinit();
|
|
try stmt.bindInt(1, id);
|
|
return crud.execStrict(database, &stmt);
|
|
}
|
|
|
|
pub const ForwardZoneRow = struct { id: i64, zone: []const u8, resolver: []const u8 };
|
|
|
|
/// Same order as `listForwardZones`; every string is a heap copy owned by `gpa`.
|
|
pub fn listForwardZoneRows(database: *db.Db, gpa: Allocator) db.Error!std.ArrayList(ForwardZoneRow) {
|
|
return crud.listRows(
|
|
ForwardZoneRow,
|
|
database,
|
|
gpa,
|
|
"SELECT id, zone, resolver FROM forward_zones ORDER BY zone",
|
|
readForwardZoneRow,
|
|
);
|
|
}
|
|
|
|
pub fn freeForwardZoneRow(gpa: Allocator, row: ForwardZoneRow) void {
|
|
crud.freeRow(ForwardZoneRow, gpa, row);
|
|
}
|
|
|
|
pub fn freeForwardZoneRows(gpa: Allocator, items: []const ForwardZoneRow) void {
|
|
crud.freeRows(ForwardZoneRow, gpa, items);
|
|
}
|
|
|
|
pub fn getForwardZone(database: *db.Db, gpa: Allocator, id: i64) db.Error!?ForwardZoneRow {
|
|
var stmt = try database.prepare("SELECT id, zone, resolver FROM forward_zones WHERE id = ?1");
|
|
defer stmt.deinit();
|
|
try stmt.bindInt(1, id);
|
|
if (!try stmt.step()) return null;
|
|
return try readForwardZoneRow(&stmt, gpa);
|
|
}
|
|
|
|
fn readForwardZoneRow(stmt: *db.Stmt, gpa: Allocator) db.Error!ForwardZoneRow {
|
|
const zone = try stmt.columnTextAlloc(gpa, 1);
|
|
errdefer gpa.free(zone);
|
|
const resolver = try stmt.columnTextAlloc(gpa, 2);
|
|
errdefer gpa.free(resolver);
|
|
return .{ .id = stmt.columnInt(0), .zone = zone, .resolver = resolver };
|
|
}
|
|
|
|
/// `error.Constraint`: `forward_zones.zone` is UNIQUE — one zone has one
|
|
/// resolver.
|
|
pub fn insertForwardZoneRow(database: *db.Db, item: model.ForwardZone) db.Error!i64 {
|
|
var stmt = try database.prepare("INSERT INTO forward_zones (zone, resolver) VALUES (?1, ?2)");
|
|
defer stmt.deinit();
|
|
try stmt.bindText(1, item.zone);
|
|
try stmt.bindText(2, item.resolver);
|
|
try stmt.exec();
|
|
return database.lastInsertRowid();
|
|
}
|
|
|
|
/// `error.NotFound`: no zone holds `id`. `error.Constraint`:
|
|
/// `forward_zones.zone` is UNIQUE.
|
|
pub fn updateForwardZone(database: *db.Db, id: i64, item: model.ForwardZone) db.Error!void {
|
|
var stmt = try database.prepare("UPDATE forward_zones SET zone = ?2, resolver = ?3 WHERE id = ?1");
|
|
defer stmt.deinit();
|
|
try stmt.bindInt(1, id);
|
|
try stmt.bindText(2, item.zone);
|
|
try stmt.bindText(3, item.resolver);
|
|
return crud.execStrict(database, &stmt);
|
|
}
|
|
|
|
/// `error.NotFound`: no zone holds `id`. Nothing references `forward_zones`, so
|
|
/// a delete cannot violate a constraint.
|
|
pub fn deleteForwardZone(database: *db.Db, id: i64) db.Error!void {
|
|
var stmt = try database.prepare("DELETE FROM forward_zones WHERE id = ?1");
|
|
defer stmt.deinit();
|
|
try stmt.bindInt(1, id);
|
|
return crud.execStrict(database, &stmt);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const testing = std.testing;
|
|
|
|
fn openMigrated() !db.Db {
|
|
var database = try db.Db.open(":memory:", .{ .mode = .memory });
|
|
errdefer database.close();
|
|
try db.applyPragmas(&database, .{});
|
|
_ = try migrations.migrate(&database);
|
|
return database;
|
|
}
|
|
|
|
fn seedLocalRecords(database: *db.Db) !void {
|
|
const ctx: InsertContext = .{};
|
|
try insertLocalRecord(database, .{
|
|
.name = "nas.home.arpa",
|
|
.rtype = .aaaa,
|
|
.value = "fd00::5",
|
|
.ttl = 60,
|
|
}, ctx);
|
|
try insertLocalRecord(database, .{
|
|
.name = "nas.home.arpa",
|
|
.rtype = .a,
|
|
.value = "192.168.1.5",
|
|
}, ctx);
|
|
try insertLocalRecord(database, .{
|
|
.name = "alias.home.arpa",
|
|
.rtype = .cname,
|
|
.value = "nas.home.arpa",
|
|
.ttl = 120,
|
|
}, ctx);
|
|
}
|
|
|
|
fn seedForwardZones(database: *db.Db) !void {
|
|
const ctx: InsertContext = .{};
|
|
try insertForwardZone(database, .{ .zone = "work.example", .resolver = "udp://10.0.0.1:53" }, ctx);
|
|
try insertForwardZone(database, .{ .zone = "home.arpa", .resolver = "udp://192.168.1.1:53" }, ctx);
|
|
try insertForwardZone(database, .{ .zone = "lab.example", .resolver = "tcp://[fd00::1]:53" }, ctx);
|
|
}
|
|
|
|
test "local_records round-trip in name, rtype, value order" {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
try seedLocalRecords(&database);
|
|
|
|
var items = try listLocalRecords(&database, testing.allocator);
|
|
defer items.deinit(testing.allocator);
|
|
defer freeLocalRecords(testing.allocator, items.items);
|
|
|
|
// `rtype` is compared as stored text, so 'A' sorts before 'AAAA'.
|
|
try testing.expectEqual(@as(usize, 3), items.items.len);
|
|
try testing.expectEqualStrings("alias.home.arpa", items.items[0].name);
|
|
try testing.expectEqual(model.RecordType.cname, items.items[0].rtype);
|
|
try testing.expectEqualStrings("nas.home.arpa", items.items[0].value);
|
|
try testing.expectEqual(@as(u32, 120), items.items[0].ttl);
|
|
try testing.expectEqualStrings("nas.home.arpa", items.items[1].name);
|
|
try testing.expectEqual(model.RecordType.a, items.items[1].rtype);
|
|
try testing.expectEqualStrings("192.168.1.5", items.items[1].value);
|
|
try testing.expectEqual(@as(u32, 300), items.items[1].ttl);
|
|
try testing.expectEqualStrings("nas.home.arpa", items.items[2].name);
|
|
try testing.expectEqual(model.RecordType.aaaa, items.items[2].rtype);
|
|
try testing.expectEqualStrings("fd00::5", items.items[2].value);
|
|
try testing.expectEqual(@as(u32, 60), items.items[2].ttl);
|
|
}
|
|
|
|
test "deleteAllLocalRecords empties the table and countLocalRecords reflects it" {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
try seedLocalRecords(&database);
|
|
|
|
try testing.expectEqual(@as(i64, 3), try countLocalRecords(&database));
|
|
try deleteAllLocalRecords(&database);
|
|
try testing.expectEqual(@as(i64, 0), try countLocalRecords(&database));
|
|
}
|
|
|
|
fn listLocalRecordsUnderFailure(gpa: Allocator) !void {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
try seedLocalRecords(&database);
|
|
|
|
var items = try listLocalRecords(&database, gpa);
|
|
defer items.deinit(gpa);
|
|
defer freeLocalRecords(gpa, items.items);
|
|
}
|
|
|
|
test "listLocalRecords is leak-safe under allocation failure" {
|
|
try testing.checkAllAllocationFailures(testing.allocator, listLocalRecordsUnderFailure, .{});
|
|
}
|
|
|
|
test "forward_zones round-trip in zone order" {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
try seedForwardZones(&database);
|
|
|
|
var items = try listForwardZones(&database, testing.allocator);
|
|
defer items.deinit(testing.allocator);
|
|
defer freeForwardZones(testing.allocator, items.items);
|
|
|
|
try testing.expectEqual(@as(usize, 3), items.items.len);
|
|
try testing.expectEqualStrings("home.arpa", items.items[0].zone);
|
|
try testing.expectEqualStrings("udp://192.168.1.1:53", items.items[0].resolver);
|
|
try testing.expectEqualStrings("lab.example", items.items[1].zone);
|
|
try testing.expectEqualStrings("tcp://[fd00::1]:53", items.items[1].resolver);
|
|
try testing.expectEqualStrings("work.example", items.items[2].zone);
|
|
try testing.expectEqualStrings("udp://10.0.0.1:53", items.items[2].resolver);
|
|
}
|
|
|
|
test "deleteAllForwardZones empties the table and countForwardZones reflects it" {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
try seedForwardZones(&database);
|
|
|
|
try testing.expectEqual(@as(i64, 3), try countForwardZones(&database));
|
|
try deleteAllForwardZones(&database);
|
|
try testing.expectEqual(@as(i64, 0), try countForwardZones(&database));
|
|
}
|
|
|
|
fn listForwardZonesUnderFailure(gpa: Allocator) !void {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
try seedForwardZones(&database);
|
|
|
|
var items = try listForwardZones(&database, gpa);
|
|
defer items.deinit(gpa);
|
|
defer freeForwardZones(gpa, items.items);
|
|
}
|
|
|
|
test "listForwardZones is leak-safe under allocation failure" {
|
|
try testing.checkAllAllocationFailures(testing.allocator, listForwardZonesUnderFailure, .{});
|
|
}
|
|
|
|
// --- REST surface ----------------------------------------------------------
|
|
|
|
test "a local record round-trips through insert, get, list, update and delete" {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
|
|
const id = try insertLocalRecordRow(&database, .{
|
|
.name = "nas.home.arpa",
|
|
.rtype = .a,
|
|
.value = "192.168.1.5",
|
|
});
|
|
|
|
const fetched = (try getLocalRecord(&database, testing.allocator, id)).?;
|
|
defer freeLocalRecordRow(testing.allocator, fetched);
|
|
try testing.expectEqual(id, fetched.id);
|
|
try testing.expectEqualStrings("nas.home.arpa", fetched.name);
|
|
try testing.expectEqual(model.RecordType.a, fetched.rtype);
|
|
try testing.expectEqualStrings("192.168.1.5", fetched.value);
|
|
try testing.expectEqual(@as(u32, 300), fetched.ttl);
|
|
|
|
try updateLocalRecord(&database, id, .{
|
|
.name = "nas.home.arpa",
|
|
.rtype = .a,
|
|
.value = "192.168.1.6",
|
|
.ttl = 60,
|
|
});
|
|
const updated = (try getLocalRecord(&database, testing.allocator, id)).?;
|
|
defer freeLocalRecordRow(testing.allocator, updated);
|
|
try testing.expectEqualStrings("192.168.1.6", updated.value);
|
|
try testing.expectEqual(@as(u32, 60), updated.ttl);
|
|
|
|
var rows = try listLocalRecordRows(&database, testing.allocator);
|
|
defer rows.deinit(testing.allocator);
|
|
defer freeLocalRecordRows(testing.allocator, rows.items);
|
|
try testing.expectEqual(@as(usize, 1), rows.items.len);
|
|
try testing.expectEqual(id, rows.items[0].id);
|
|
|
|
try deleteLocalRecord(&database, id);
|
|
try testing.expectEqual(@as(?LocalRecordRow, null), try getLocalRecord(&database, testing.allocator, id));
|
|
try testing.expectEqual(@as(i64, 0), try countLocalRecords(&database));
|
|
}
|
|
|
|
test "local record update and delete report NotFound for an id no row holds" {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
|
|
const item: model.LocalRecord = .{ .name = "x.home.arpa", .rtype = .a, .value = "10.0.0.1" };
|
|
try testing.expectError(error.NotFound, updateLocalRecord(&database, 404, item));
|
|
try testing.expectError(error.NotFound, deleteLocalRecord(&database, 404));
|
|
try testing.expectEqual(@as(?LocalRecordRow, null), try getLocalRecord(&database, testing.allocator, 404));
|
|
}
|
|
|
|
test "a duplicate name, rtype and value surfaces as error.Constraint" {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
|
|
const first: model.LocalRecord = .{ .name = "nas.home.arpa", .rtype = .a, .value = "192.168.1.5" };
|
|
_ = try insertLocalRecordRow(&database, first);
|
|
// The TTL is not part of the UNIQUE key, so a second TTL is still a clash.
|
|
try testing.expectError(error.Constraint, insertLocalRecordRow(&database, .{
|
|
.name = "nas.home.arpa",
|
|
.rtype = .a,
|
|
.value = "192.168.1.5",
|
|
.ttl = 60,
|
|
}));
|
|
|
|
const other = try insertLocalRecordRow(&database, .{
|
|
.name = "nas.home.arpa",
|
|
.rtype = .aaaa,
|
|
.value = "fd00::5",
|
|
});
|
|
try testing.expectError(error.Constraint, updateLocalRecord(&database, other, first));
|
|
try testing.expectEqual(@as(i64, 2), try countLocalRecords(&database));
|
|
}
|
|
|
|
test "a forward zone round-trips through insert, get, list, update and delete" {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
|
|
const id = try insertForwardZoneRow(&database, .{
|
|
.zone = "home.arpa",
|
|
.resolver = "udp://192.168.1.1:53",
|
|
});
|
|
|
|
const fetched = (try getForwardZone(&database, testing.allocator, id)).?;
|
|
defer freeForwardZoneRow(testing.allocator, fetched);
|
|
try testing.expectEqual(id, fetched.id);
|
|
try testing.expectEqualStrings("home.arpa", fetched.zone);
|
|
try testing.expectEqualStrings("udp://192.168.1.1:53", fetched.resolver);
|
|
|
|
try updateForwardZone(&database, id, .{ .zone = "lab.example", .resolver = "tcp://[fd00::1]:53" });
|
|
const updated = (try getForwardZone(&database, testing.allocator, id)).?;
|
|
defer freeForwardZoneRow(testing.allocator, updated);
|
|
try testing.expectEqualStrings("lab.example", updated.zone);
|
|
try testing.expectEqualStrings("tcp://[fd00::1]:53", updated.resolver);
|
|
|
|
var rows = try listForwardZoneRows(&database, testing.allocator);
|
|
defer rows.deinit(testing.allocator);
|
|
defer freeForwardZoneRows(testing.allocator, rows.items);
|
|
try testing.expectEqual(@as(usize, 1), rows.items.len);
|
|
try testing.expectEqual(id, rows.items[0].id);
|
|
|
|
try deleteForwardZone(&database, id);
|
|
try testing.expectEqual(@as(?ForwardZoneRow, null), try getForwardZone(&database, testing.allocator, id));
|
|
try testing.expectEqual(@as(i64, 0), try countForwardZones(&database));
|
|
}
|
|
|
|
test "forward zone update and delete report NotFound for an id no row holds" {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
|
|
const item: model.ForwardZone = .{ .zone = "home.arpa", .resolver = "udp://10.0.0.1:53" };
|
|
try testing.expectError(error.NotFound, updateForwardZone(&database, 404, item));
|
|
try testing.expectError(error.NotFound, deleteForwardZone(&database, 404));
|
|
try testing.expectEqual(@as(?ForwardZoneRow, null), try getForwardZone(&database, testing.allocator, 404));
|
|
}
|
|
|
|
test "a duplicate zone surfaces as error.Constraint on insert and on update" {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
|
|
_ = try insertForwardZoneRow(&database, .{ .zone = "home.arpa", .resolver = "udp://10.0.0.1:53" });
|
|
const other = try insertForwardZoneRow(&database, .{ .zone = "lab.example", .resolver = "udp://10.0.0.2:53" });
|
|
|
|
try testing.expectError(error.Constraint, insertForwardZoneRow(&database, .{
|
|
.zone = "home.arpa",
|
|
.resolver = "udp://10.0.0.3:53",
|
|
}));
|
|
try testing.expectError(error.Constraint, updateForwardZone(&database, other, .{
|
|
.zone = "home.arpa",
|
|
.resolver = "udp://10.0.0.2:53",
|
|
}));
|
|
try testing.expectEqual(@as(i64, 2), try countForwardZones(&database));
|
|
}
|
|
|
|
fn localRowsUnderFailure(gpa: Allocator) !void {
|
|
var database = try openMigrated();
|
|
defer database.close();
|
|
try seedLocalRecords(&database);
|
|
try seedForwardZones(&database);
|
|
|
|
var records = try listLocalRecordRows(&database, gpa);
|
|
defer records.deinit(gpa);
|
|
defer freeLocalRecordRows(gpa, records.items);
|
|
const record = (try getLocalRecord(&database, gpa, records.items[0].id)).?;
|
|
defer freeLocalRecordRow(gpa, record);
|
|
|
|
var zones = try listForwardZoneRows(&database, gpa);
|
|
defer zones.deinit(gpa);
|
|
defer freeForwardZoneRows(gpa, zones.items);
|
|
const zone = (try getForwardZone(&database, gpa, zones.items[0].id)).?;
|
|
defer freeForwardZoneRow(gpa, zone);
|
|
}
|
|
|
|
test "the local read surface is leak-safe under allocation failure" {
|
|
try testing.checkAllAllocationFailures(testing.allocator, localRowsUnderFailure, .{});
|
|
}
|