Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/host/app/services/matrix-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2100,7 +2100,12 @@ export default class MatrixService extends Service {
this._systemCard = undefined;
this._systemCardLoadFailed = false;
this.startedAtTs = -1;
this.#clientReadyDeferred = new Deferred<void>();
// `#clientReadyDeferred` is deliberately left alone. It gates on "the SDK
// has loaded and a client exists" — a condition this reset preserves, since
// the client is recreated synchronously above whenever the SDK is loaded.
// Replacing it with a fresh deferred would strand every post-logout
// `createRealmSession()` await: `loadSDK()`, the only fulfiller, runs once
// at boot and never re-runs.
}

private teardownClient() {
Expand Down
74 changes: 74 additions & 0 deletions packages/host/tests/integration/matrix-service-relogin-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { RenderingTestContext } from '@ember/test-helpers';

import { getService } from '@universal-ember/test-support';
import { rawTimeout } from 'ember-concurrency';
import { module, test } from 'qunit';

import { baseRealm } from '@cardstack/runtime-common';

import type MatrixService from '@cardstack/host/services/matrix-service';

import {
testRealmURL,
setupIntegrationTestRealm,
setupLocalIndexing,
} from '../helpers';

import { setupBaseRealm } from '../helpers/base-realm';

import { setupMockMatrix } from '../helpers/mock-matrix';

import { setupRenderingTest } from '../helpers/setup';

// `MatrixService` is a singleton whose client-ready barrier
// (`#clientReadyDeferred`) is only fulfilled once, at boot, inside `loadSDK()`.
// Logout stays in-app (a router transition, not a page reload) and its
// `finally` runs `resetState()`, so the barrier must survive that reset: if it
// were replaced with a fresh, never-fulfilled deferred, the next login's
// `createRealmSession()` would await it forever and the auth page would never
// progress. This test locks down the invariant that `createRealmSession()`
// still resolves after a `resetState()`.
module(
'Integration | matrix-service | re-login after logout',
function (hooks) {
setupRenderingTest(hooks);
setupLocalIndexing(hooks);

let mockMatrixUtils = setupMockMatrix(hooks, {
loggedInAs: '@testuser:localhost',
activeRealms: [baseRealm.url, testRealmURL],
autostart: true,
});

setupBaseRealm(hooks);

hooks.beforeEach(async function (this: RenderingTestContext) {
await setupIntegrationTestRealm({
mockMatrixUtils,
contents: {},
});
});

test('createRealmSession resolves after resetState (logout) instead of hanging', async function (assert) {
let matrixService = getService('matrix-service') as MatrixService;
await matrixService.ready;

// `logout()` runs `resetState()` in its `finally`. Drive that reset
// directly so the test doesn't depend on the full logout network
// roundtrip / router transition.
matrixService.resetState();

const TIMEOUT = Symbol('timeout');
let result = await Promise.race([
matrixService.createRealmSession(new URL(testRealmURL)),
rawTimeout(2000).then(() => TIMEOUT),
]);

assert.notStrictEqual(
result,
TIMEOUT,
'createRealmSession settles after resetState — the clientReadyDeferred barrier survives the reset rather than being left pending',
);
});
},
);
32 changes: 32 additions & 0 deletions packages/matrix/tests/login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,38 @@ test.describe('Login', () => {
await expect(page.locator('[data-test-login-btn]')).toBeVisible();
});

test('it can log back in after logout without a page reload', async ({
page,
}) => {
await login(page, username, password, { url: appURL });
await assertLoggedIn(page, {
displayName: username,
userId: credentials.userId,
});

await logout(page);
await assertLoggedOut(page);
// The login form renders as soon as auth is cleared, but logout finishes
// asynchronously with a router transition to index-root that remounts the
// form. Wait for that transition to land (the path leaves the realm route)
// before filling, or the fields get wiped mid-fill — faster than any real
// user could type.
await page.waitForURL((url) => url.pathname === '/');

// Logout stays in-app (a router transition, not a page reload), so this
// second login runs against the matrix client that logout's `resetState()`
// recreated rather than a freshly booted one.
await page.locator('[data-test-username-field]').fill(username);
await page.locator('[data-test-password-field]').fill(password);
await page.locator('[data-test-login-btn]').click();

await expect(page.locator('[data-test-workspace-chooser]')).toHaveCount(1);
await assertLoggedIn(page, {
displayName: username,
userId: credentials.userId,
});
});

test('it shows an error when invalid credentials are provided', async ({
page,
}) => {
Expand Down
Loading