fix: add no extra account named after its id when Android refuses one #34

Turning device sync on retried with the app account's registry id as
the Android account name whenever adding the account failed. That
fallback is for one case only: the login name turned out to belong to
another app account's Android account, which the native module
reports with the `conflict` code. A passing AccountManager failure or
a permission error instead created a second account named after the
opaque id, which then showed in the Contacts and Calendar apps.

Only a `conflict` rejection takes the fallback now; any other failure
fails the turn-on as before.
This commit is contained in:
Linus Rath
2026-09-27 10:20:55 +02:00
parent ef96acd05f
commit 5e62cf71f4
2 changed files with 17 additions and 3 deletions
@@ -43,7 +43,8 @@ vi.mock('../../native', () => ({
ensureAndroidAccount: vi.fn(async (name: string, registryId: string) => {
h.state.log.push(`ensure ${name}`);
const existing = h.find(name);
if (existing && existing.registryId !== registryId) throw new Error('conflict');
// As the native module rejects (DeviceSyncAccounts.ensure → AccountConflictException).
if (existing && existing.registryId !== registryId) throw Object.assign(new Error(`${name} belongs to another app account`), { code: 'conflict' });
if (existing) return false;
h.state.accounts.push({ name, registryId, sync: {} });
return true;
@@ -212,6 +213,18 @@ describe('enable', () => {
expect(outcome).toEqual({ kind: 'enabled', accountName: ALICE });
});
it('adds no second account named after the registry id when adding the account fails for another reason', async () => {
for (const code of ['failed', 'permission']) {
vi.mocked(native.ensureAndroidAccount).mockClear();
vi.mocked(native.ensureAndroidAccount).mockRejectedValueOnce(Object.assign(new Error(`AccountManager said no (${code})`), { code }));
const outcome = await enableDeviceSync(ALICE, CONTACTS_AUTHORITY);
expect(outcome).toEqual({ kind: 'failed', message: `AccountManager said no (${code})` });
expect(native.ensureAndroidAccount).toHaveBeenCalledTimes(1);
expect(h.state.accounts).toEqual([]);
expect(accountDeviceSync(ALICE).androidAccountName).toBeUndefined();
}
});
it('leaves no account behind when turning sync on fails', async () => {
h.state.failSetSyncEnabled = true;
const outcome = await enableDeviceSync(ALICE, CONTACTS_AUTHORITY);
+3 -2
View File
@@ -122,8 +122,9 @@ async function ensureAccountFor(registryId: string): Promise<string> {
await ensureAndroidAccount(name, registryId);
return name;
} catch (err) {
// The name belongs to another app account after all (the list was stale).
if (name === registryId) throw err;
// The name belongs to another app account after all (the list was stale). Any
// other failure fails the turn-on: a second account would show in the apps.
if (name === registryId || (err as { code?: unknown } | null)?.code !== 'conflict') throw err;
await ensureAndroidAccount(registryId, registryId);
return registryId;
}