diff --git a/apps/aelis-backend/src/session/user-session-manager.test.ts b/apps/aelis-backend/src/session/user-session-manager.test.ts index 5d08924..af0c0da 100644 --- a/apps/aelis-backend/src/session/user-session-manager.test.ts +++ b/apps/aelis-backend/src/session/user-session-manager.test.ts @@ -44,6 +44,15 @@ function getEnabledSourceIds(userId: string): string[] { */ let mockFindResult: unknown | undefined +/** + * Spy for `upsertConfig` calls. Tests can inspect calls via + * `mockUpsertConfigCalls`. + */ +const mockUpsertConfigCalls: Array<{ + sourceId: string + data: { enabled: boolean; config: unknown } +}> = [] + /** * Spy for `updateCredentials` calls. Tests can inspect calls via * `mockUpdateCredentialsCalls` or override behavior. @@ -81,6 +90,9 @@ mock.module("../sources/user-sources.ts", () => ({ updatedAt: now, } }, + async upsertConfig(sourceId: string, data: { enabled: boolean; config: unknown }) { + mockUpsertConfigCalls.push({ sourceId, data }) + }, async updateCredentials(sourceId: string, credentials: Buffer) { if (mockUpdateCredentialsError) { throw mockUpdateCredentialsError @@ -138,6 +150,7 @@ const weatherProvider: FeedSourceProvider = { beforeEach(() => { enabledByUser.clear() mockFindResult = undefined + mockUpsertConfigCalls.length = 0 mockUpdateCredentialsCalls.length = 0 mockUpdateCredentialsError = null }) @@ -824,3 +837,65 @@ describe("UserSessionManager.updateSourceCredentials", () => { expect(factory).not.toHaveBeenCalled() }) }) + +describe("UserSessionManager.upsertSourceConfig", () => { + test("persists config to DB even when feedSourceForUser throws", async () => { + setEnabledSources(["test"]) + let callCount = 0 + const factory = mock(async (_userId: string, _config: unknown, _credentials: unknown) => { + callCount++ + // Succeed on first call (session creation), throw on second (upsert refresh) + if (callCount > 1) { + throw new InvalidSourceCredentialsError("test", "credentials required") + } + return createStubSource("test") + }) + const provider: FeedSourceProvider = { sourceId: "test", feedSourceForUser: factory } + const manager = new UserSessionManager({ db: fakeDb, providers: [provider] }) + + // Create a session so the session-refresh path is exercised + await manager.getOrCreate("user-1") + + const spy = spyOn(console, "warn").mockImplementation(() => {}) + + // upsertSourceConfig with no existing credentials — provider will throw + await manager.upsertSourceConfig("user-1", "test", { + enabled: true, + config: { url: "https://example.com" }, + }) + + // Config should still have been persisted to DB + expect(mockUpsertConfigCalls).toHaveLength(1) + expect(mockUpsertConfigCalls[0]!.sourceId).toBe("test") + expect(mockUpsertConfigCalls[0]!.data.enabled).toBe(true) + + // The error should have been logged, not thrown + expect(spy).toHaveBeenCalled() + + spy.mockRestore() + }) + + test("adds source to session when feedSourceForUser succeeds", async () => { + setEnabledSources(["test"]) + const factory = mock(async () => createStubSource("test")) + const provider: FeedSourceProvider = { sourceId: "test", feedSourceForUser: factory } + const manager = new UserSessionManager({ db: fakeDb, providers: [provider] }) + + const session = await manager.getOrCreate("user-1") + await manager.upsertSourceConfig("user-1", "test", { enabled: true }) + + // Config persisted + expect(mockUpsertConfigCalls).toHaveLength(1) + // Source should be in the session (feedSourceForUser succeeded) + expect(session.getSource("test")).toBeDefined() + }) + + test("throws SourceNotFoundError for unknown provider", async () => { + setEnabledSources([]) + const manager = new UserSessionManager({ db: fakeDb, providers: [] }) + + await expect( + manager.upsertSourceConfig("user-1", "unknown", { enabled: true }), + ).rejects.toBeInstanceOf(SourceNotFoundError) + }) +}) diff --git a/apps/aelis-backend/src/session/user-session-manager.ts b/apps/aelis-backend/src/session/user-session-manager.ts index d9dcc86..822f1fa 100644 --- a/apps/aelis-backend/src/session/user-session-manager.ts +++ b/apps/aelis-backend/src/session/user-session-manager.ts @@ -210,11 +210,22 @@ export class UserSessionManager { const credentials = existingRow?.credentials ? this.decryptCredentials(existingRow.credentials) : null - const source = await provider.feedSourceForUser(userId, config, credentials) - if (session.hasSource(sourceId)) { - session.replaceSource(sourceId, source) - } else { - session.addSource(source) + try { + const source = await provider.feedSourceForUser(userId, config, credentials) + if (session.hasSource(sourceId)) { + session.replaceSource(sourceId, source) + } else { + session.addSource(source) + } + } catch (err) { + // Provider may fail when credentials are not yet available (e.g. new + // source added before updateSourceCredentials is called). The config + // is already persisted above; updateSourceCredentials will add the + // source to the session later. + console.warn( + `[UserSessionManager] feedSourceForUser("${sourceId}") failed during upsert for user ${userId}, skipping session update:`, + err, + ) } } }