mirror of
https://github.com/kennethnym/aris.git
synced 2026-04-21 09:11:17 +01:00
fix: handle provider errors during source config upsert
Wrap feedSourceForUser + addSource/replaceSource in upsertSourceConfig with try/catch. When credentials are not yet available (e.g. new CalDAV source before updateSourceCredentials is called), the provider throws but the config is still persisted. updateSourceCredentials will add the source to the session later. Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user