Merge branch 'feat/modules-name-no-node' into bulk-c

This commit is contained in:
jochen
2026-10-22 21:43:10 +02:00
32 changed files with 130 additions and 130 deletions
@@ -105,7 +105,7 @@ export function hasField(schema: readonly SchemaField[] | null | undefined, name
// **Nothing loses its connection without someone seeing it.** Before Home Assistant is touched the
// broker itself is asked whether it takes the delivered login (the provisioner creates it within
// seconds of the grant): if not, nothing is written and the step fails saying why, and Home
// Assistant keeps the login it has — the carried `luffy` on ace, which mosquitto keeps. If Home
// Assistant keeps the login it has — the carried `luffy` on node-a, which mosquitto keeps. If Home
// Assistant's own connection test refuses the new settings, the flow saves nothing, and the step
// fails with Home Assistant's reason. A login that may not subscribe to the discovery topics is said
// as a warning: discovery would find nothing.
@@ -299,8 +299,8 @@ export function describe(r: { type: string; reason?: string; errors?: Record<str
// is the bound one and which is loaded: already as the mesh says. The key needs no digest here:
// a Servarr app has one key, so an entry loaded against the app holds the key the app took.
// 5. A working entry at a different URL that reaches **the same app** — the same process, by the
// app's own status (start time, data folder, version) — is left as it is and said: ace's entries
// say `127.0.0.1:<port>` and the binding says `ace.internal:<port>`, one Sonarr either way.
// app's own status (start time, data folder, version) — is left as it is and said: node-a's entries
// say `127.0.0.1:<port>` and the binding says `node-a.internal:<port>`, one Sonarr either way.
// 6. Anything else is refused, loudly, with what the operator can do; nothing is removed.
+26 -26
View File
@@ -6,7 +6,7 @@
// minted value before the operator accepts the app's) is never written.
//
// Home Assistant and the apps are fakes answering as the real ones do (flow shapes checked against
// ghcr.io/home-assistant/home-assistant 2026.9.3, the build ace runs).
// ghcr.io/home-assistant/home-assistant 2026.9.3, the build node-a runs).
import { test } from "node:test";
import assert from "node:assert/strict";
@@ -21,7 +21,7 @@ const PWD_NOT_CHANGED = "__**password_not_changed**__";
const MINTED = "mesh-minted-password";
function mqttBinding(): Binding {
return { provision: "mqtt-topic", from: "ace", at: "ace.internal", as: "mesh_ace_hass", serves: { scheme: "mqtt", port: 1883 } };
return { provision: "mqtt-topic", from: "node-a", at: "node-a.internal", as: "mesh_node-a_hass", serves: { scheme: "mqtt", port: 1883 } };
}
/** The MQTT reconfigure form as Home Assistant serializes it, pre-filled from an entry. */
@@ -138,29 +138,29 @@ function memoryMarks(): Marks & { store: Map<string, string> } {
return { store, get: async (k) => store.get(k), set: async (k, v) => void store.set(k, v) };
}
const takes = (suback = 0): Probe => async (_h, _p, user, pass) => ({ connack: user === "mesh_ace_hass" && pass === MINTED ? 0 : 5, suback });
const takes = (suback = 0): Probe => async (_h, _p, user, pass) => ({ connack: user === "mesh_node-a_hass" && pass === MINTED ? 0 : 5, suback });
const aceMqttEntry = () => ({
const nodeAMqttEntry = () => ({
entry_id: "7d1e", domain: "mqtt", state: "loaded",
data: { broker: "127.0.0.1", port: 1883, protocol: "5", username: "luffy", password: "luffys-password" },
});
test("mqtt: the broker is asked first; a login it does not take is never written", async () => {
const f = fakeHass({ entries: { mqtt: [aceMqttEntry()] } });
const f = fakeHass({ entries: { mqtt: [nodeAMqttEntry()] } });
const out = await reconcileMqtt({ hass: f.hass, probe: async () => ({ connack: 5 }), marks: memoryMarks() }, mqttBinding(), MINTED);
assert.equal(out.result, "refused");
assert.match((out as { problem: string }).problem, /does not \(yet\) take the login mesh_ace_hass/);
assert.match((out as { problem: string }).problem, /does not \(yet\) take the login mesh_node-a_hass/);
assert.deepEqual(f.calls, []); // Home Assistant not even asked
assert.equal(f.entries.mqtt[0].data.username, "luffy");
});
test("mqtt: ace's entry (127.0.0.1, luffy) is moved to the bound broker and login, every other setting kept", async () => {
const f = fakeHass({ entries: { mqtt: [aceMqttEntry()] } });
test("mqtt: node-a's entry (127.0.0.1, luffy) is moved to the bound broker and login, every other setting kept", async () => {
const f = fakeHass({ entries: { mqtt: [nodeAMqttEntry()] } });
const marks = memoryMarks();
const out = await reconcileMqtt({ hass: f.hass, probe: takes(), marks }, mqttBinding(), `${MINTED}\n`);
assert.deepEqual(out, { what: "mqtt", result: "written", fields: ["broker", "username", "password"] });
assert.deepEqual(f.entries.mqtt[0].data, {
broker: "ace.internal", port: 1883, protocol: "5", username: "mesh_ace_hass", password: MINTED,
broker: "node-a.internal", port: 1883, protocol: "5", username: "mesh_node-a_hass", password: MINTED,
other_settings: { keepalive: 60, transport: "tcp", set_ca_cert: "off", set_client_cert: false },
});
assert.ok(marks.store.get("mqtt"));
@@ -175,7 +175,7 @@ test("mqtt: ace's entry (127.0.0.1, luffy) is moved to the bound broker and logi
});
test("mqtt: a new password alone is written (the digest tells)", async () => {
const f = fakeHass({ entries: { mqtt: [aceMqttEntry()] } });
const f = fakeHass({ entries: { mqtt: [nodeAMqttEntry()] } });
const marks = memoryMarks();
await reconcileMqtt({ hass: f.hass, probe: takes(), marks }, mqttBinding(), MINTED);
const rotated: Probe = async () => ({ connack: 0, suback: 0 });
@@ -185,7 +185,7 @@ test("mqtt: a new password alone is written (the digest tells)", async () => {
});
test("mqtt: Home Assistant's own connection test refusing saves nothing and fails loudly", async () => {
const f = fakeHass({ entries: { mqtt: [aceMqttEntry()] }, accepts: () => false });
const f = fakeHass({ entries: { mqtt: [nodeAMqttEntry()] }, accepts: () => false });
const marks = memoryMarks();
const out = await reconcileMqtt({ hass: f.hass, probe: takes(), marks }, mqttBinding(), MINTED);
assert.equal(out.result, "refused");
@@ -199,11 +199,11 @@ test("mqtt: a fresh Home Assistant gets an entry; a grant without the discovery
const out = await reconcileMqtt({ hass: f.hass, probe: takes(0x80), marks: memoryMarks() }, mqttBinding(), MINTED);
assert.equal(out.result, "written");
assert.match((out as { note?: string }).note ?? "", /may not subscribe to homeassistant\/#/);
assert.equal(f.entries.mqtt[0].data.broker, "ace.internal");
assert.equal(f.entries.mqtt[0].data.broker, "node-a.internal");
});
test("mqtt: two entries, or a binding without a port, are refused rather than guessed", async () => {
const f = fakeHass({ entries: { mqtt: [aceMqttEntry(), { ...aceMqttEntry(), entry_id: "other" }] } });
const f = fakeHass({ entries: { mqtt: [nodeAMqttEntry(), { ...nodeAMqttEntry(), entry_id: "other" }] } });
assert.equal((await reconcileMqtt({ hass: f.hass, probe: takes(), marks: memoryMarks() }, mqttBinding(), MINTED)).result, "refused");
const noPort = { ...mqttBinding(), serves: {} };
assert.match(((await reconcileMqtt({ hass: f.hass, probe: takes(), marks: memoryMarks() }, noPort, MINTED)) as { problem: string }).problem, /no usable port/);
@@ -214,9 +214,9 @@ test("mqtt: two entries, or a binding without a port, are refused rather than gu
const SONARR = APPS.find((a) => a.domain === "sonarr") as ServarrApp;
const RADARR = APPS.find((a) => a.domain === "radarr") as ServarrApp;
const KEY = "the-apps-own-key";
const servarrBinding = (port: number, at = "ace.internal"): Binding => ({ provision: "sonarr-api", from: "ace", at, as: "mesh_ace_hass", serves: { scheme: "http", port, "url-base": "" } });
const servarrBinding = (port: number, at = "node-a.internal"): Binding => ({ provision: "sonarr-api", from: "node-a", at, as: "mesh_node-a_hass", serves: { scheme: "http", port, "url-base": "" } });
/** One running Sonarr, answering on several addresses (127.0.0.1 and ace.internal are one host). */
/** One running Sonarr, answering on several addresses (127.0.0.1 and node-a.internal are one host). */
function apps(instances: Record<string, { startTime: string }>): Http & { asked: string[] } {
const asked: string[] = [];
return {
@@ -231,18 +231,18 @@ function apps(instances: Record<string, { startTime: string }>): Http & { asked:
},
};
}
const oneSonarr = () => apps({ "ace.internal:8989": { startTime: "t1" }, "127.0.0.1:8989": { startTime: "t1" } });
const oneSonarr = () => apps({ "node-a.internal:8989": { startTime: "t1" }, "127.0.0.1:8989": { startTime: "t1" } });
const sonarrEntry = (state = "loaded") => ({ entry_id: "5a1d", domain: "sonarr", state, data: { url: "http://127.0.0.1:8989", api_key: KEY } });
test("servarr: the mesh's minted key is never written; the remedy names the accept", async () => {
const f = fakeHass({ entries: { sonarr: [sonarrEntry()] } });
const out = await reconcileApp({ hass: f.hass, http: oneSonarr() }, SONARR, servarrBinding(8989), "minted-by-the-mesh");
assert.equal(out.result, "refused");
assert.match((out as { problem: string }).problem, /secret accept <this node> home-assistant sonarr-api --provider ace/);
assert.match((out as { problem: string }).problem, /secret accept <this node> home-assistant sonarr-api --provider node-a/);
assert.deepEqual(f.calls, []);
});
test("servarr: ace's entry at 127.0.0.1 reaches the same Sonarr the mesh bound at ace.internal — left, and said", async () => {
test("servarr: node-a's entry at 127.0.0.1 reaches the same Sonarr the mesh bound at node-a.internal — left, and said", async () => {
const f = fakeHass({ entries: { sonarr: [sonarrEntry()] }, devices: [{ id: "d", config_entries: ["5a1d"], configuration_url: "http://127.0.0.1:8989" }] });
const out = await reconcileApp({ hass: f.hass, http: oneSonarr() }, SONARR, servarrBinding(8989), KEY);
assert.equal(out.result, "equivalent");
@@ -250,13 +250,13 @@ test("servarr: ace's entry at 127.0.0.1 reaches the same Sonarr the mesh bound a
});
test("servarr: an entry already at the bound URL is unchanged", async () => {
const f = fakeHass({ entries: { sonarr: [sonarrEntry()] }, devices: [{ id: "d", config_entries: ["5a1d"], configuration_url: "http://ace.internal:8989" }] });
const f = fakeHass({ entries: { sonarr: [sonarrEntry()] }, devices: [{ id: "d", config_entries: ["5a1d"], configuration_url: "http://node-a.internal:8989" }] });
assert.deepEqual(await reconcileApp({ hass: f.hass, http: oneSonarr() }, SONARR, servarrBinding(8989), KEY), { what: "sonarr", result: "unchanged" });
});
test("servarr: a working entry that reaches a different app is refused, and nothing is removed", async () => {
const f = fakeHass({ entries: { sonarr: [sonarrEntry()] }, devices: [{ id: "d", config_entries: ["5a1d"], configuration_url: "http://127.0.0.1:8989" }] });
const two = apps({ "ace.internal:8989": { startTime: "t1" }, "127.0.0.1:8989": { startTime: "another" } });
const two = apps({ "node-a.internal:8989": { startTime: "t1" }, "127.0.0.1:8989": { startTime: "another" } });
const out = await reconcileApp({ hass: f.hass, http: two }, SONARR, servarrBinding(8989), KEY);
assert.equal(out.result, "refused");
assert.match((out as { problem: string }).problem, /never does/);
@@ -267,7 +267,7 @@ test("servarr: no entry — one is made at the bound URL through the user flow",
const f = fakeHass({ entries: {} });
const out = await reconcileApp({ hass: f.hass, http: oneSonarr() }, SONARR, servarrBinding(8989), KEY);
assert.deepEqual(out, { what: "sonarr", result: "written", fields: ["entry"] });
assert.deepEqual(f.submitted[0], { url: "http://ace.internal:8989", api_key: KEY, more_options: { verify_ssl: false } });
assert.deepEqual(f.submitted[0], { url: "http://node-a.internal:8989", api_key: KEY, more_options: { verify_ssl: false } });
});
test("servarr: a reauth Home Assistant started is finished with the bound URL and key", async () => {
@@ -276,10 +276,10 @@ test("servarr: a reauth Home Assistant started is finished with the bound URL an
entries: { radarr: [entry] },
reauth: [{ flow_id: "r1", handler: "radarr", step_id: "reauth_confirm", context: { source: "reauth", entry_id: "1955" } }],
});
const radarr = apps({ "ace.internal:7878": { startTime: "t" } });
const radarr = apps({ "node-a.internal:7878": { startTime: "t" } });
const out = await reconcileApp({ hass: f.hass, http: radarr }, RADARR, { ...servarrBinding(7878), provision: "radarr-api" }, KEY);
assert.deepEqual(out, { what: "radarr", result: "written", fields: ["api_key", "url"] });
assert.deepEqual(f.submitted[0], { url: "http://ace.internal:7878", api_key: KEY, verify_ssl: false });
assert.deepEqual(f.submitted[0], { url: "http://node-a.internal:7878", api_key: KEY, verify_ssl: false });
});
test("form values: suggested first, then default, sections nested", () => {
@@ -287,7 +287,7 @@ test("form values: suggested first, then default, sections nested", () => {
broker: "b", port: 1, protocol: "5", username: "u", password: PWD_NOT_CHANGED,
other_settings: { keepalive: 60, transport: "tcp", set_ca_cert: "off", set_client_cert: false },
});
assert.ok(sameUrl("http://ace.internal:8989/", "http://ace.internal:8989"));
assert.ok(sameUrl("http://ACE.internal", "http://ace.internal:80"));
assert.ok(!sameUrl("http://127.0.0.1:8989", "http://ace.internal:8989"));
assert.ok(sameUrl("http://node-a.internal:8989/", "http://node-a.internal:8989"));
assert.ok(sameUrl("http://NODE-A.internal", "http://node-a.internal:80"));
assert.ok(!sameUrl("http://127.0.0.1:8989", "http://node-a.internal:8989"));
});
+9 -9
View File
@@ -14,13 +14,13 @@ import (
"testing"
)
// The environment mailu gives automx on novox when the fixtures were captured (files/mailu.env): the mail domain,
// The environment mailu gives automx on node-c when the fixtures were captured (files/mailu.env): the mail domain,
// the route name for both servers, the ports.
var capturedEnvironment = map[string]string{
"AMX_MAIL_DOMAINS": "novox.be",
"AMX_IMAP_ADDRESS": "mail.novox.be",
"AMX_MAIL_DOMAINS": "node-c.be",
"AMX_IMAP_ADDRESS": "mail.node-c.be",
"AMX_IMAP_PORT": "143",
"AMX_SMTP_ADDRESS": "mail.novox.be",
"AMX_SMTP_ADDRESS": "mail.node-c.be",
"AMX_SMTP_PORT": "587",
"AUTOMX_PORT": "4243",
}
@@ -176,7 +176,7 @@ func TestTheOnlyCorrectedAnswerIsTheImapServersName(t *testing.T) {
}
for _, c := range corrected {
orig := readFile(t, strings.TrimSuffix(c, ".corrected.body")+".body")
want := strings.Replace(orig, "<hostname />", "<hostname>mail.novox.be</hostname>", 1)
want := strings.Replace(orig, "<hostname />", "<hostname>mail.node-c.be</hostname>", 1)
if strings.Contains(want, "<hostname />") || want == orig {
t.Fatalf("%s: the capture has no single empty hostname to correct", c)
}
@@ -198,12 +198,12 @@ func TestTheImapServerIsNamedFromTheImapAddress(t *testing.T) {
t.Fatal(err)
}
rec := httptest.NewRecorder()
newHandler(cfg).ServeHTTP(rec, httptest.NewRequest("GET", "/mail/config-v1.1.xml?emailaddress=a@novox.be", nil))
newHandler(cfg).ServeHTTP(rec, httptest.NewRequest("GET", "/mail/config-v1.1.xml?emailaddress=a@node-c.be", nil))
body := rec.Body.String()
if !strings.Contains(body, `<incomingServer type="imap"><hostname>imap.example.test</hostname><port>993</port>`) {
t.Errorf("incoming server not named from AMX_IMAP_ADDRESS and AMX_IMAP_PORT: %s", body)
}
if !strings.Contains(body, `<outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port>`) {
if !strings.Contains(body, `<outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port>`) {
t.Errorf("outgoing server changed: %s", body)
}
}
@@ -213,13 +213,13 @@ func TestEveryDomainOfTheEnvironmentIsServed(t *testing.T) {
for k, v := range capturedEnvironment {
env[k] = v
}
env["AMX_MAIL_DOMAINS"] = "novox.be, example.test"
env["AMX_MAIL_DOMAINS"] = "node-c.be, example.test"
cfg, err := configFrom(getenvOf(env))
if err != nil {
t.Fatal(err)
}
h := newHandler(cfg)
for _, d := range []string{"novox.be", "example.test"} {
for _, d := range []string{"node-c.be", "example.test"} {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/mail/config-v1.1.xml?emailaddress=a@"+d, nil))
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "<domain>"+d+"</domain>") {
@@ -1 +1 @@
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
@@ -1 +1 @@
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname>mail.novox.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname>mail.node-c.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
@@ -1,3 +1,3 @@
GET https://autoconfig.novox.be/mail/config-v1.1.xml?emailaddress=jochen@novox.be
GET https://autoconfig.node-c.be/mail/config-v1.1.xml?emailaddress=jochen@node-c.be
User-Agent: Thunderbird
Accept: */*
@@ -1 +1 @@
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
@@ -1 +1 @@
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname>mail.novox.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname>mail.node-c.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
@@ -1,2 +1,2 @@
GET https://automx.novox.be/mail/config-v1.1.xml?emailaddress=jochen@novox.be
GET https://automx.node-c.be/mail/config-v1.1.xml?emailaddress=jochen@node-c.be
User-Agent: Thunderbird
@@ -1 +1 @@
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
@@ -1 +1 @@
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname>mail.novox.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname>mail.node-c.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
@@ -1,2 +1,2 @@
GET https://autodiscover.novox.be/mail/config-v1.1.xml?emailaddress=jochen@novox.be
GET https://autodiscover.node-c.be/mail/config-v1.1.xml?emailaddress=jochen@node-c.be
User-Agent: Thunderbird
@@ -1,2 +1,2 @@
GET https://autoconfig.novox.be/mail/config-v1.1.xml?emailaddress=nobody@unknown.example
GET https://autoconfig.node-c.be/mail/config-v1.1.xml?emailaddress=nobody@unknown.example
User-Agent: Thunderbird
@@ -1,2 +1,2 @@
GET https://autoconfig.novox.be/mail/config-v1.1.xml
GET https://autoconfig.node-c.be/mail/config-v1.1.xml
User-Agent: Thunderbird
@@ -1,2 +1,2 @@
GET https://autoconfig.novox.be/
GET https://autoconfig.node-c.be/
User-Agent: Mozilla/5.0
@@ -1 +1 @@
GET https://autoconfig.novox.be/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=jochen@novox.be
GET https://autoconfig.node-c.be/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=jochen@node-c.be
@@ -1,4 +1,4 @@
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
POST https://autodiscover.node-c.be/autodiscover/autodiscover.xml
User-Agent: Microsoft Office/16.0 (Windows NT 10.0; Microsoft Outlook 16.0.14326; Pro)
Content-Type: text/xml
Accept: */*
@@ -6,7 +6,7 @@ Accept: */*
<?xml version="1.0" encoding="utf-8"?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
<Request>
<EMailAddress>jochen@novox.be</EMailAddress>
<EMailAddress>jochen@node-c.be</EMailAddress>
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
</Request>
</Autodiscover>
@@ -1,4 +1,4 @@
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
POST https://autodiscover.node-c.be/autodiscover/autodiscover.xml
User-Agent: Microsoft Office/16.0
Content-Type: text/xml
@@ -1,4 +1,4 @@
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
POST https://autodiscover.node-c.be/autodiscover/autodiscover.xml
User-Agent: Microsoft Office/16.0
Content-Type: text/xml
@@ -1,4 +1,4 @@
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
POST https://autodiscover.node-c.be/autodiscover/autodiscover.xml
User-Agent: Microsoft Office/16.0
Content-Type: text/xml
@@ -1,2 +1,2 @@
GET https://autodiscover.novox.be/autodiscover/autodiscover.xml
GET https://autodiscover.node-c.be/autodiscover/autodiscover.xml
User-Agent: Microsoft Office/16.0
@@ -1,6 +1,6 @@
POST https://autodiscover.novox.be/Autodiscover/Autodiscover.xml
POST https://autodiscover.node-c.be/Autodiscover/Autodiscover.xml
User-Agent: Microsoft Office/16.0
Content-Type: text/xml
<?xml version="1.0" encoding="utf-8"?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"><Request><EMailAddress>jochen@novox.be</EMailAddress><AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema></Request></Autodiscover>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"><Request><EMailAddress>jochen@node-c.be</EMailAddress><AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema></Request></Autodiscover>
@@ -2,4 +2,4 @@
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be">https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be</a>. If not, click the link.
<p>You should be redirected automatically to the target URL: <a href="https://automx.node-c.be/mobileconfig/?emailaddress=jochen@node-c.be">https://automx.node-c.be/mobileconfig/?emailaddress=jochen@node-c.be</a>. If not, click the link.
@@ -1,4 +1,4 @@
Content-Length: 319
Content-Length: 323
Content-Type: text/html; charset=utf-8
Location: https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be
Location: https://automx.node-c.be/mobileconfig/?emailaddress=jochen@node-c.be
Server: Werkzeug/3.1.9 Python/3.9.5
@@ -1,3 +1,3 @@
GET https://automx.novox.be/mobileconfig?emailaddress=jochen@novox.be
GET https://automx.node-c.be/mobileconfig?emailaddress=jochen@node-c.be
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Safari
Accept: */*
@@ -1,3 +1,3 @@
GET https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be
GET https://automx.node-c.be/mobileconfig/?emailaddress=jochen@node-c.be
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Safari
Accept: */*
@@ -1,2 +1,2 @@
GET https://automx.novox.be/mobileconfig/?emailaddress=nobody@unknown.example
GET https://automx.node-c.be/mobileconfig/?emailaddress=nobody@unknown.example
User-Agent: Mozilla/5.0 (iPhone)
@@ -1,2 +1,2 @@
GET https://automx.novox.be/mobileconfig/
GET https://automx.node-c.be/mobileconfig/
User-Agent: Mozilla/5.0 (iPhone)
+5 -5
View File
@@ -1,10 +1,10 @@
What mailu's autoconfiguration module (automx2 2021.6, container mailu-automx on novox, Werkzeug/3.1.9 Python/3.9.5)
What mailu's autoconfiguration module (automx2 2021.6, container mailu-automx on node-c, Werkzeug/3.1.9 Python/3.9.5)
answers to the requests mail clients make (R4d step AMX-1, issue 1630, ADR 0480).
Captured 2026-10-21 between 13:58:25 and 13:58:52 (CEST; the Date headers said 11:58 GMT) with the mesh tool
shanks/netcheck.netcheck_request, called from shanks, through the names clients use (autoconfig.novox.be,
autodiscover.novox.be, automx.novox.be: HTTPS, the mesh's route), no ssh. A redirect is reported, never followed.
Mail domain served: novox.be. Mailbox used: jochen@novox.be; unknown domain: nobody@unknown.example.
node-d/netcheck.netcheck_request, called from node-d, through the names clients use (autoconfig.node-c.be,
autodiscover.node-c.be, automx.node-c.be: HTTPS, the mesh's route), no ssh. A redirect is reported, never followed.
Mail domain served: node-c.be. Mailbox used: jochen@node-c.be; unknown domain: nobody@unknown.example.
Each capture is four files with the same stem:
<stem>.request method and URL, then the request headers sent, then (after a blank line) the body sent
@@ -24,7 +24,7 @@ A fixture is not the bytes on the wire in four ways (issue 1701):
What the answers show (read, not guessed):
- GET /mail/config-v1.1.xml?emailaddress=... answers 200 application/xml on all three names; same body on each.
- The incoming (IMAP) server has an EMPTY hostname (<hostname />), port 143, STARTTLS. The outgoing server is
mail.novox.be:587 STARTTLS. Cause (from reading the module, not from the database): automx/files/add-domains:35
mail.node-c.be:587 STARTTLS. Cause (from reading the module, not from the database): automx/files/add-domains:35
reads AMX_IMAP_SERVER, files/mailu.env:58 sets AMX_IMAP_ADDRESS, so the IMAP name was never written.
A client given this answer cannot find its IMAP server.
- POST /autodiscover/autodiscover.xml with a valid request for the served domain answers 500 (the Outlook
+1 -1
View File
@@ -6,7 +6,7 @@ whole list of where the two differ.
server because its setup script read AMX_IMAP_SERVER, which mailu's environment does not set (it sets
AMX_IMAP_ADDRESS). The program names it from AMX_IMAP_ADDRESS (and the port from AMX_IMAP_PORT).
Fixtures 01, 02 and 03 each have a <stem>.corrected.body: the capture with <hostname /> replaced by
<hostname>mail.novox.be</hostname> and nothing else (a test checks that). Their Content-Length is that file's
<hostname>mail.node-c.be</hostname> and nothing else (a test checks that). Their Content-Length is that file's
size, 634, not the captured 612.
2. The Server header. The capture has "Server: Werkzeug/3.1.9 Python/3.9.5". The program sends no Server header, and
@@ -170,15 +170,15 @@ func clientWrites(t *testing.T) *memStore {
if _, _, err := Publish(s); err != nil {
t.Fatal(err)
}
s.write(t, Needs, "g14.dunst.config.font-size", `{"at":"2026-10-14T20:00:00Z"}`)
s.write(t, Needs, "g14.dunst.secret.token", `{}`)
s.write(t, Needs, "shanks.dunst.config.font-size", `{}`)
s.write(t, Needs, "shanks.gitea.bound.database.user", `{}`)
s.write(t, Applied, "g14.dunst", `{"build":"b1","outcome":"applied"}`)
s.write(t, Applied, "shanks.dunst", `{"build":"b1","outcome":"failed","reason":"waits for config font-size"}`)
s.write(t, Collect, "g14.zsh-snippets.git", `{}`)
s.write(t, Collect, "g14.bar-blocks.disk-load", `{}`)
s.write(t, Collect, "g14.shell=3Azsh=3Anormal.mesh-cli", `{}`)
s.write(t, Needs, "node-b.dunst.config.font-size", `{"at":"2026-10-14T20:00:00Z"}`)
s.write(t, Needs, "node-b.dunst.secret.token", `{}`)
s.write(t, Needs, "node-d.dunst.config.font-size", `{}`)
s.write(t, Needs, "node-d.gitea.bound.database.user", `{}`)
s.write(t, Applied, "node-b.dunst", `{"build":"b1","outcome":"applied"}`)
s.write(t, Applied, "node-d.dunst", `{"build":"b1","outcome":"failed","reason":"waits for config font-size"}`)
s.write(t, Collect, "node-b.zsh-snippets.git", `{}`)
s.write(t, Collect, "node-b.bar-blocks.disk-load", `{}`)
s.write(t, Collect, "node-b.shell=3Azsh=3Anormal.mesh-cli", `{}`)
return s
}
@@ -199,16 +199,16 @@ func TestEachBucketIsReadByItsShapeAndNarrowed(t *testing.T) {
}{
{Query{}, []string{"access", "backup", "bound", "collected", "config", "consumers", "contribution", "dir", "environment",
"machine", "port", "seat", "secret", "shell"}},
{Query{What: Needs}, []string{"g14.dunst.config.font-size", "g14.dunst.secret.token",
"shanks.dunst.config.font-size", "shanks.gitea.bound.database.user"}},
{Query{What: Needs, Node: "g14"}, []string{"g14.dunst.config.font-size", "g14.dunst.secret.token"}},
{Query{What: Needs, Module: "dunst", Node: "shanks"}, []string{"shanks.dunst.config.font-size"}},
{Query{What: Applied, Module: "dunst"}, []string{"g14.dunst", "shanks.dunst"}},
{Query{What: Applied, Node: "ace"}, nil},
{Query{What: Collect, Collection: "zsh-snippets"}, []string{"g14.zsh-snippets.git"}},
{Query{What: Collect, Module: "disk-load"}, []string{"g14.bar-blocks.disk-load"}},
{Query{What: Needs}, []string{"node-b.dunst.config.font-size", "node-b.dunst.secret.token",
"node-d.dunst.config.font-size", "node-d.gitea.bound.database.user"}},
{Query{What: Needs, Node: "node-b"}, []string{"node-b.dunst.config.font-size", "node-b.dunst.secret.token"}},
{Query{What: Needs, Module: "dunst", Node: "node-d"}, []string{"node-d.dunst.config.font-size"}},
{Query{What: Applied, Module: "dunst"}, []string{"node-b.dunst", "node-d.dunst"}},
{Query{What: Applied, Node: "node-a"}, nil},
{Query{What: Collect, Collection: "zsh-snippets"}, []string{"node-b.zsh-snippets.git"}},
{Query{What: Collect, Module: "disk-load"}, []string{"node-b.bar-blocks.disk-load"}},
// An old namespace's collection, by its name, not its key's token (design 57 L1).
{Query{What: Collect, Collection: "shell:zsh:normal"}, []string{"g14.shell=3Azsh=3Anormal.mesh-cli"}},
{Query{What: Collect, Collection: "shell:zsh:normal"}, []string{"node-b.shell=3Azsh=3Anormal.mesh-cli"}},
} {
r, err := Do(s, c.q)
if err != nil {
@@ -218,15 +218,15 @@ func TestEachBucketIsReadByItsShapeAndNarrowed(t *testing.T) {
t.Errorf("%+v: read %v (%d), want %v", c.q, got, r.Count, c.want)
}
}
r, _ := Do(s, Query{What: Needs, Node: "shanks", Module: "gitea"})
if row := r.Rows[0]; row.Node != "shanks" || row.Module != "gitea" || row.Kind != "bound" || row.Name != "database.user" {
r, _ := Do(s, Query{What: Needs, Node: "node-d", Module: "gitea"})
if row := r.Rows[0]; row.Node != "node-d" || row.Module != "gitea" || row.Kind != "bound" || row.Name != "database.user" {
t.Errorf("a need's parts: %+v", row)
}
r, _ = Do(s, Query{What: Collect, Collection: "bar-blocks"})
if row := r.Rows[0]; row.Node != "g14" || row.Collection != "bar-blocks" || row.Module != "disk-load" {
if row := r.Rows[0]; row.Node != "node-b" || row.Collection != "bar-blocks" || row.Module != "disk-load" {
t.Errorf("an item's parts: %+v", row)
}
r, _ = Do(s, Query{What: Applied, Node: "shanks"})
r, _ = Do(s, Query{What: Applied, Node: "node-d"})
if string(r.Rows[0].Value) != `{"build":"b1","outcome":"failed","reason":"waits for config font-size"}` {
t.Errorf("what is applied is read as the client wrote it: %s", r.Rows[0].Value)
}
@@ -235,12 +235,12 @@ func TestEachBucketIsReadByItsShapeAndNarrowed(t *testing.T) {
// A key out of its bucket's shape is said, never dropped quietly.
func TestAKeyOutOfItsBucketsShapeIsSaid(t *testing.T) {
s := clientWrites(t)
s.write(t, Applied, "g14.dunst.extra", `{}`)
s.write(t, Applied, "node-b.dunst.extra", `{}`)
r, err := Do(s, Query{What: Applied})
if err != nil {
t.Fatal(err)
}
if r.Count != 2 || !strings.Contains(r.Said, "g14.dunst.extra") || !strings.Contains(r.Said, "<node>.<module>") {
if r.Count != 2 || !strings.Contains(r.Said, "node-b.dunst.extra") || !strings.Contains(r.Said, "<node>.<module>") {
t.Errorf("an unshaped key: %d rows, said %q", r.Count, r.Said)
}
}
@@ -253,21 +253,21 @@ func TestTheVerbsArgumentsAreReadStrictly(t *testing.T) {
t.Fatalf("the verb is served as %s", Tools(s)[0].Name)
}
for _, args := range []map[string]any{
{"what": "needs", "caller": "g14.dunst"},
{"what": "needs", "caller": "node-b.dunst"},
{"node": 3},
{"what": "secrets"},
{"node": "g14"},
{"node": "node-b"},
{"what": "needs", "collection": "zsh-snippets"},
} {
if _, err := run(args); err == nil {
t.Errorf("%v was taken", args)
}
}
got, err := run(map[string]any{"what": "applied", "node": " g14 "})
got, err := run(map[string]any{"what": "applied", "node": " node-b "})
if err != nil {
t.Fatal(err)
}
if r := got.(Read); r.Count != 1 || r.Said != "1 in applied for node g14" {
if r := got.(Read); r.Count != 1 || r.Said != "1 in applied for node node-b" {
t.Errorf("a read: %+v", r)
}
}
+30 -30
View File
@@ -18,19 +18,19 @@ const grantFile = `{
"contributions": 1,
"requirement": "s3-bucket",
"given": [
{"from": "brother", "node": "shanks", "as": "brother-key",
{"from": "brother", "node": "node-d", "as": "brother-key",
"values": {"shares": [{"to": "seat:node-files", "methods": ["put"]}, {"to": "print-and-scan", "methods": ["get"]}]},
"derived": {"bucket": "mesh-shanks-brother"}},
{"from": "nextcloud", "node": "novox", "as": "nextcloud-key", "derived": {"bucket": "mesh-novox-nextcloud"}}
"derived": {"bucket": "mesh-node-d-brother"}},
{"from": "nextcloud", "node": "node-c", "as": "nextcloud-key", "derived": {"bucket": "mesh-node-c-nextcloud"}}
]
}`
// The seats answer is the controller's (mesh-controller.seats read on 2026-10-21): seats[].holders[] of {node, module},
// and outside[] of {Claim, Node, Module}.
const seatsAnswer = `{"answer": {"seats": [
{"seat": "node-files", "scope": "node", "holders": [{"node": "g14", "module": "mesh-files"}, {"node": "shanks", "module": "mesh-files"}]},
{"seat": "node-backup", "scope": "node", "holders": [{"node": "g14", "module": "restic"}]}
], "outside": [{"Claim": "printer", "Scope": "mesh", "Node": "shanks", "Module": "brother", "Site": "home", "Kind": ""}]}}`
{"seat": "node-files", "scope": "node", "holders": [{"node": "node-b", "module": "mesh-files"}, {"node": "node-d", "module": "mesh-files"}]},
{"seat": "node-backup", "scope": "node", "holders": [{"node": "node-b", "module": "restic"}]}
], "outside": [{"Claim": "printer", "Scope": "mesh", "Node": "node-d", "Module": "brother", "Site": "home", "Kind": ""}]}}`
type rig struct {
s *server
@@ -54,10 +54,10 @@ func newRig(t *testing.T) *rig {
}
return p
}
// What the mesh renders from files/object-store.json for a store on novox.internal, port 20001.
cfg := write("object-store.json", `{"scheme":"http","at":"novox.internal","port":"20001","region":"eu-west"}`)
write("links/mesh-shanks-brother.secret", "brother-link-secret\n")
write("links/mesh-novox-nextcloud.secret", "nextcloud-link-secret\n")
// What the mesh renders from files/object-store.json for a store on node-c.internal, port 20001.
cfg := write("object-store.json", `{"scheme":"http","at":"node-c.internal","port":"20001","region":"eu-west"}`)
write("links/mesh-node-d-brother.secret", "brother-link-secret\n")
write("links/mesh-node-c-nextcloud.secret", "nextcloud-link-secret\n")
grants := write("grants/mesh.json", grantFile)
s, err := newServer(cfg, filepath.Join(dir, "links"), grants)
if err != nil {
@@ -96,16 +96,16 @@ func linkOf(t *testing.T, got any) *url.URL {
// 1516/AC1: brother's own ask gets a link signed with the bucket's link key, for the store's host and port, 300 s.
func TestLinkIsSignedWithTheBucketsLinkKey(t *testing.T) {
r := newRig(t)
got, err := r.s.link("shanks.brother", ask("mesh-shanks-brother", "get", float64(300)))
got, err := r.s.link("node-d.brother", ask("mesh-node-d-brother", "get", float64(300)))
if err != nil {
t.Fatal(err)
}
u := linkOf(t, got)
if u.Host != "novox.internal:20001" || u.Scheme != "http" || u.Path != "/mesh-shanks-brother/doc.pdf" {
if u.Host != "node-c.internal:20001" || u.Scheme != "http" || u.Path != "/mesh-node-d-brother/doc.pdf" {
t.Fatalf("link goes to %s", u)
}
q := u.Query()
if cred := q.Get("X-Amz-Credential"); !strings.HasPrefix(cred, linkKeyID("mesh-shanks-brother")+"/20261021/eu-west/s3/") {
if cred := q.Get("X-Amz-Credential"); !strings.HasPrefix(cred, linkKeyID("mesh-node-d-brother")+"/20261021/eu-west/s3/") {
t.Fatalf("credential %q is not the bucket's link key", cred)
}
if strings.Contains(u.String(), "meshroot") {
@@ -124,18 +124,18 @@ func TestLinkKeyIDMatchesTheProvisioners(t *testing.T) {
}
}
// 1516/AC2: a share to seat:node-files for put, mesh-files holding node-files on g14: put given, get refused naming the method.
// 1516/AC2: a share to seat:node-files for put, mesh-files holding node-files on node-b: put given, get refused naming the method.
func TestSeatShareGivesTheHoldersTheSharedMethodOnly(t *testing.T) {
r := newRig(t)
if _, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "put", nil)); err != nil {
if _, err := r.s.link("node-b.mesh-files", ask("mesh-node-d-brother", "put", nil)); err != nil {
t.Fatalf("put: %v", err)
}
_, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "get", nil))
_, err := r.s.link("node-b.mesh-files", ask("mesh-node-d-brother", "get", nil))
if err == nil || !strings.Contains(err.Error(), "put, not get") {
t.Fatalf("get was not refused naming the method: %v", err)
}
// A module share is judged by name alone.
if _, err := r.s.link("novox.print-and-scan", ask("mesh-shanks-brother", "get", nil)); err != nil {
if _, err := r.s.link("node-c.print-and-scan", ask("mesh-node-d-brother", "get", nil)); err != nil {
t.Fatalf("print-and-scan get: %v", err)
}
}
@@ -148,14 +148,14 @@ func TestRefusals(t *testing.T) {
args map[string]any
want string
}{
{"a stranger", "g14.restic", ask("mesh-shanks-brother", "get", nil), "not the bucket's consumer"},
{"a seat it does not hold", "g14.restic", ask("mesh-shanks-brother", "put", nil), "not the bucket's consumer"},
{"unproven", "unproven", ask("mesh-shanks-brother", "get", nil), "did not prove"},
{"no grant", "shanks.brother", ask("no-such-bucket", "get", nil), "no grant names bucket"},
{"seconds over 600", "shanks.brother", ask("mesh-shanks-brother", "get", float64(601)), "at most ten minutes"},
{"seconds zero", "shanks.brother", ask("mesh-shanks-brother", "get", float64(0)), "outside 1 to 600"},
{"method delete", "shanks.brother", ask("mesh-shanks-brother", "delete", nil), "not get or put"},
{"another consumer's bucket", "shanks.brother", ask("mesh-novox-nextcloud", "get", nil), "not the bucket's consumer"},
{"a stranger", "node-b.restic", ask("mesh-node-d-brother", "get", nil), "not the bucket's consumer"},
{"a seat it does not hold", "node-b.restic", ask("mesh-node-d-brother", "put", nil), "not the bucket's consumer"},
{"unproven", "unproven", ask("mesh-node-d-brother", "get", nil), "did not prove"},
{"no grant", "node-d.brother", ask("no-such-bucket", "get", nil), "no grant names bucket"},
{"seconds over 600", "node-d.brother", ask("mesh-node-d-brother", "get", float64(601)), "at most ten minutes"},
{"seconds zero", "node-d.brother", ask("mesh-node-d-brother", "get", float64(0)), "outside 1 to 600"},
{"method delete", "node-d.brother", ask("mesh-node-d-brother", "delete", nil), "not get or put"},
{"another consumer's bucket", "node-d.brother", ask("mesh-node-c-nextcloud", "get", nil), "not the bucket's consumer"},
}
for _, c := range cases {
_, err := r.s.link(c.caller, c.args)
@@ -170,14 +170,14 @@ func TestRefusals(t *testing.T) {
func TestSeatsUnreadableAndCached(t *testing.T) {
r := newRig(t)
r.seatsErr = errors.New("the controller did not answer")
_, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "put", nil))
_, err := r.s.link("node-b.mesh-files", ask("mesh-node-d-brother", "put", nil))
if err == nil || !strings.Contains(err.Error(), "the controller did not answer") || !strings.Contains(err.Error(), "node-files") {
t.Fatalf("not refused naming why: %v", err)
}
r.seatsErr = nil
put := func() {
t.Helper()
if _, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "put", nil)); err != nil {
if _, err := r.s.link("node-b.mesh-files", ask("mesh-node-d-brother", "put", nil)); err != nil {
t.Fatal(err)
}
}
@@ -201,17 +201,17 @@ func TestSeatsUnreadableAndCached(t *testing.T) {
// 1516/AC5: the grant file is read at each call.
func TestGrantFileIsReadAtEachCall(t *testing.T) {
r := newRig(t)
if _, err := r.s.link("novox.print-and-scan", ask("mesh-shanks-brother", "get", nil)); err != nil {
if _, err := r.s.link("node-c.print-and-scan", ask("mesh-node-d-brother", "get", nil)); err != nil {
t.Fatal(err)
}
changed := strings.Replace(grantFile, `{"to": "print-and-scan", "methods": ["get"]}`, `{"to": "someone-else", "methods": ["get"]}`, 1)
if err := os.WriteFile(r.s.receives, []byte(changed), 0o600); err != nil {
t.Fatal(err)
}
if _, err := r.s.link("novox.print-and-scan", ask("mesh-shanks-brother", "get", nil)); err == nil {
if _, err := r.s.link("node-c.print-and-scan", ask("mesh-node-d-brother", "get", nil)); err == nil {
t.Fatal("the second call judged by the old shares")
}
if _, err := r.s.link("novox.someone-else", ask("mesh-shanks-brother", "get", nil)); err != nil {
if _, err := r.s.link("node-c.someone-else", ask("mesh-node-d-brother", "get", nil)); err != nil {
t.Fatal(err)
}
}