milestone 19: hygiene sweep - dead ecs surface, single-source constants, tls classification, frontend state hazards, docker smoke network fix
This commit is contained in:
@@ -332,10 +332,10 @@ pub fn applyPut(
|
||||
const hash_changed = password != null and !std.mem.eql(u8, previous_hash, cfg.web.password_hash);
|
||||
const replacement: ?[]u8 = if (hash_changed) try state.gpa.dupe(u8, cfg.web.password_hash) else null;
|
||||
|
||||
if (writeSettings(arena, database, cfg)) |err| {
|
||||
writeSettings(arena, database, cfg) catch |err| {
|
||||
if (replacement) |hash| state.gpa.free(hash);
|
||||
return .{ .fail = .{ .internal = err } };
|
||||
}
|
||||
};
|
||||
|
||||
if (replacement) |hash| {
|
||||
// Ruling 17, both halves: the running server must verify against the
|
||||
@@ -354,26 +354,36 @@ pub fn applyPut(
|
||||
/// costs a few dozen upserts and buys the guarantee that the table is exactly
|
||||
/// what `model.toSettings` says the merged configuration is — no key can be
|
||||
/// missed and none can be left behind.
|
||||
fn writeSettings(arena: Allocator, database: *db.Db, cfg: model.Config) ?db.Error {
|
||||
fn writeSettings(arena: Allocator, database: *db.Db, cfg: model.Config) db.Error!void {
|
||||
var pairs: std.ArrayList(model.SettingPair) = .empty;
|
||||
model.toSettings(cfg, arena, &pairs) catch return error.OutOfMemory;
|
||||
try model.toSettings(cfg, arena, &pairs);
|
||||
|
||||
var tx = db.Tx.begin(database) catch |err| return err;
|
||||
var tx = try db.Tx.begin(database);
|
||||
errdefer tx.rollback();
|
||||
|
||||
try write_fault.check();
|
||||
for (pairs.items) |pair| {
|
||||
settings_repo.putSetting(database, pair.key, pair.value) catch |err| {
|
||||
tx.rollback();
|
||||
return err;
|
||||
};
|
||||
try settings_repo.putSetting(database, pair.key, pair.value);
|
||||
}
|
||||
tx.commit() catch |err| {
|
||||
tx.rollback();
|
||||
return err;
|
||||
};
|
||||
return null;
|
||||
try tx.commit();
|
||||
}
|
||||
|
||||
/// Fails one `writeSettings` after its transaction has begun, so a test can
|
||||
/// prove the `errdefer` above rolls that transaction back rather than leaving
|
||||
/// the shared connection inside it. Test builds only, and it reduces to nothing
|
||||
/// everywhere else — the rotation seam's shape (logging.zig).
|
||||
const write_fault = if (builtin.is_test) struct {
|
||||
var armed: bool = false;
|
||||
|
||||
fn check() db.Error!void {
|
||||
if (!armed) return;
|
||||
armed = false;
|
||||
return error.Internal;
|
||||
}
|
||||
} else struct {
|
||||
fn check() db.Error!void {}
|
||||
};
|
||||
|
||||
fn problem(arena: Allocator, cfg: model.Config) error{OutOfMemory}!?[]const u8 {
|
||||
return mutations.firstProblem(arena, cfg);
|
||||
}
|
||||
@@ -581,6 +591,28 @@ test "a put that would not validate writes nothing" {
|
||||
try testing.expectEqual(@as(u16, 53), stored.dns.port);
|
||||
}
|
||||
|
||||
test "a write that fails after the transaction begins rolls it back" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
try seeded(&bench);
|
||||
|
||||
var patch: Patch = .{};
|
||||
patch.dns = .{ .port = 5353 };
|
||||
|
||||
write_fault.armed = true;
|
||||
const outcome = try applyPut(&bench.state, bench.io(), bench.arena(), patch);
|
||||
try testing.expect(outcome.fail == .internal);
|
||||
|
||||
// BEGIN IMMEDIATE inside an open transaction is an error, so a second
|
||||
// `begin` succeeding is what proves the errdefer ran.
|
||||
var tx = try db.Tx.begin(&bench.database);
|
||||
tx.rollback();
|
||||
|
||||
const stored = try mutations.loadConfig(bench.arena(), &bench.database);
|
||||
try testing.expectEqual(@as(u16, 53), stored.dns.port);
|
||||
}
|
||||
|
||||
test "an enum value the model does not know names the key it came from" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
|
||||
Reference in New Issue
Block a user