Look for Stalwart's capability where Stalwart advertises it
Self-service credentials, the About page and Files all keyed off `urn:stalwart:jmap`, and all three looked for it in the session-level `capabilities`. Stalwart has never put it there. `Session::new` builds that list from a fixed set the capability is not part of, in any 0.16.x from 0.16.0 to 0.16.19; it is handed out per-account instead, so it arrives in `primaryAccounts` and in each account's `accountCapabilities`. So every real 0.16 server read as pre-0.16. Password changes, 2FA and app passwords fell back to `POST /api/account/auth`, which 0.16 removed, and reported that the server offers no self-service credential management. About named the wrong generation. Files ran the pre-0.16 path, omitting `nodeType` and listing the tree through get. Look in all three places, on both sides. Two nearby soft spots go with it: a transport error while probing the registry no longer downgrades a server to the legacy path -- which would have posted the current password to an endpoint that is not there -- and a locale request that is merely refused no longer discards a generation the capability had already settled. The mock advertised the capability in the session, which is why no test ever caught this; it now advertises it where the real server does, and validates `using` by the urn rather than by the session, as Stalwart does. Put the old lookup back and nine tests fail. Stalwart still publishes no version number to clients -- VERSION_PUBLIC is a fixed "1.0.0" -- so About continues to report the generation and edition, which are now the right ones.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { getAccountInfo, interpretAccountInfo } from "./upstream.js";
|
||||
import { getAccountInfo, hasStalwartRegistry, interpretAccountInfo } from "./upstream.js";
|
||||
|
||||
/**
|
||||
* The account locale used to be read only from `x:Account/get`, which needs
|
||||
@@ -65,3 +65,67 @@ test("no capabilities at all leaves the generation unknown", async () => {
|
||||
const info = await getAccountInfo("session-no-caps", "Basic x", { accounts: {}, primaryAccounts: {} } as never);
|
||||
assert.equal(info.generation, null);
|
||||
});
|
||||
|
||||
/**
|
||||
* Where Stalwart actually advertises `urn:stalwart:jmap`.
|
||||
*
|
||||
* Not in the session-level `capabilities`: `Session::new` builds those from a
|
||||
* fixed list that has never carried this capability, in any 0.16.x. It is
|
||||
* handed out per-account instead, so it lands in `primaryAccounts` and in each
|
||||
* account's `accountCapabilities`. Looking only at the session level called
|
||||
* every real 0.16 server pre-0.16, which sent self-service credentials to a
|
||||
* REST endpoint 0.16 had removed and made the About page report the wrong
|
||||
* generation.
|
||||
*/
|
||||
const STALWART = "urn:stalwart:jmap";
|
||||
const baseCaps = { "urn:ietf:params:jmap:core": {}, "urn:ietf:params:jmap:mail": {} };
|
||||
|
||||
test("a 0.16 server is recognised from primaryAccounts, where it advertises itself", () => {
|
||||
assert.equal(
|
||||
hasStalwartRegistry({ capabilities: baseCaps, accounts: {}, primaryAccounts: { [STALWART]: "a1" } }),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("a 0.16 server is recognised from an account's capabilities", () => {
|
||||
assert.equal(
|
||||
hasStalwartRegistry({
|
||||
capabilities: baseCaps,
|
||||
accounts: { a1: { accountCapabilities: { "urn:ietf:params:jmap:mail": {}, [STALWART]: {} } } },
|
||||
primaryAccounts: {},
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("the session level still counts, for a server that ever advertises it there", () => {
|
||||
assert.equal(hasStalwartRegistry({ capabilities: { ...baseCaps, [STALWART]: {} }, accounts: {}, primaryAccounts: {} }), true);
|
||||
});
|
||||
|
||||
test("a server that advertises it nowhere is pre-0.16", () => {
|
||||
assert.equal(hasStalwartRegistry({ capabilities: baseCaps, accounts: { a1: { accountCapabilities: baseCaps } }, primaryAccounts: { "urn:ietf:params:jmap:mail": "a1" } }), false);
|
||||
assert.equal(hasStalwartRegistry(undefined), false);
|
||||
});
|
||||
|
||||
test("a shared account carrying the capability is enough to recognise the server", () => {
|
||||
assert.equal(
|
||||
hasStalwartRegistry({
|
||||
capabilities: baseCaps,
|
||||
accounts: { a1: { accountCapabilities: baseCaps }, a2: { accountCapabilities: { [STALWART]: {} } } },
|
||||
primaryAccounts: {},
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("a locale request that fails does not talk us out of a generation we proved", () => {
|
||||
// The capability settled it. A forbidden reply costs the locale, nothing more.
|
||||
const info = interpretAccountInfo([failed("s", "forbidden"), failed("a", "forbidden")], "0.16+");
|
||||
assert.equal(info.generation, "0.16+");
|
||||
assert.equal(info.locale, null);
|
||||
});
|
||||
|
||||
test("a server that disowns the method is still older, whatever we came in believing", () => {
|
||||
const info = interpretAccountInfo([failed("s", "unknownMethod")], "0.16+");
|
||||
assert.equal(info.generation, "pre-0.16");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user