diff --git a/server/src/nest/feeds/feeds.controller.ts b/server/src/nest/feeds/feeds.controller.ts index ebb25cf11..f7a1db98f 100644 --- a/server/src/nest/feeds/feeds.controller.ts +++ b/server/src/nest/feeds/feeds.controller.ts @@ -16,7 +16,7 @@ import { FeedsService } from './feeds.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { CurrentUser } from '../auth/current-user.decorator'; import type { User } from '../../types'; -import { db } from '../../db/database'; +import { DatabaseService } from '../database/database.service'; // Resolve the public origin used to build feed URLs. APP_URL wins — it is the // canonical externally-reachable URL behind a reverse proxy. When it is unset @@ -77,10 +77,13 @@ export class FeedsPublicController { @Controller('api/trips/:tripId/feed') @UseGuards(JwtAuthGuard) export class TripFeedTokenController { - constructor(private readonly feeds: FeedsService) {} + constructor( + private readonly feeds: FeedsService, + private readonly dbs: DatabaseService, + ) {} private assertAccess(tripId: string, userId: number): void { - const row = db + const row = this.dbs.connection .prepare( 'SELECT id FROM trips WHERE id = ? AND (user_id = ? OR id IN (SELECT trip_id FROM trip_members WHERE user_id = ?))', ) diff --git a/server/src/nest/plugins/day-schedule.controller.ts b/server/src/nest/plugins/day-schedule.controller.ts index c3beb495b..85e85f9dc 100644 --- a/server/src/nest/plugins/day-schedule.controller.ts +++ b/server/src/nest/plugins/day-schedule.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { canAccessTrip, db } from '../../db/database'; +import { DatabaseService } from '../database/database.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { pluginsEnabled } from './kill-switch'; import { PluginRuntimeService } from './plugin-runtime.service'; @@ -78,7 +78,10 @@ function normalize(pluginId: string, tripDayIds: ReadonlySet, raw: unkno @Controller('api/day-schedule') @UseGuards(JwtAuthGuard) export class DayScheduleController { - constructor(private readonly runtime: PluginRuntimeService) {} + constructor( + private readonly runtime: PluginRuntimeService, + private readonly dbs: DatabaseService, + ) {} @Get(':tripId') async get( @@ -88,11 +91,11 @@ export class DayScheduleController { if (!pluginsEnabled()) return { items: [] }; const tripId = Number(tripIdRaw); const userId = req.user?.id; - if (!Number.isFinite(tripId) || userId == null || !canAccessTrip(tripId, userId)) return { items: [] }; + if (!Number.isFinite(tripId) || userId == null || !this.dbs.canAccessTrip(tripId, userId)) return { items: [] }; const ids = this.runtime.providersOf('dayScheduleProvider'); if (ids.length === 0) return { items: [] }; - const dayRows = db.prepare('SELECT id FROM days WHERE trip_id = ?').all(tripId) as Array<{ id: number }>; + const dayRows = this.dbs.connection.prepare('SELECT id FROM days WHERE trip_id = ?').all(tripId) as Array<{ id: number }>; const tripDayIds: ReadonlySet = new Set(dayRows.map((d) => d.id)); const perProvider = await Promise.all( diff --git a/server/src/nest/plugins/journal-entry-rows.controller.ts b/server/src/nest/plugins/journal-entry-rows.controller.ts index 2d322dff4..8353e43c5 100644 --- a/server/src/nest/plugins/journal-entry-rows.controller.ts +++ b/server/src/nest/plugins/journal-entry-rows.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { db } from '../../db/database'; +import { DatabaseService } from '../database/database.service'; import { canAccessJourney } from '../../services/journeyService'; import { isAddonEnabled } from '../../services/adminService'; import { ADDON_IDS } from '../../addons'; @@ -66,7 +66,10 @@ function normalize(raw: unknown): EntryRow[] { @Controller('api/journal-entry-rows') @UseGuards(JwtAuthGuard) export class JournalEntryRowsController { - constructor(private readonly runtime: PluginRuntimeService) {} + constructor( + private readonly runtime: PluginRuntimeService, + private readonly dbs: DatabaseService, + ) {} @Get(':entryId') async get( @@ -79,7 +82,7 @@ export class JournalEntryRowsController { if (!Number.isFinite(entryId) || userId == null) return { providers: [] }; // The entry's journey must be one the caller can access — same gate as a read. - const row = db.prepare('SELECT journey_id FROM journey_entries WHERE id = ?').get(entryId) as { journey_id: number } | undefined; + const row = this.dbs.connection.prepare('SELECT journey_id FROM journey_entries WHERE id = ?').get(entryId) as { journey_id: number } | undefined; if (!row || !canAccessJourney(row.journey_id, userId)) return { providers: [] }; const ids = this.runtime.providersOf('journalEntryProvider'); diff --git a/server/src/nest/plugins/place-details.controller.ts b/server/src/nest/plugins/place-details.controller.ts index 0b262b95c..2efc07af0 100644 --- a/server/src/nest/plugins/place-details.controller.ts +++ b/server/src/nest/plugins/place-details.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { db, canAccessTrip } from '../../db/database'; +import { DatabaseService } from '../database/database.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { pluginsEnabled } from './kill-switch'; import { PluginRuntimeService } from './plugin-runtime.service'; @@ -61,7 +61,10 @@ function normalize(raw: unknown): DetailItem[] { @Controller('api/place-details') @UseGuards(JwtAuthGuard) export class PlaceDetailsController { - constructor(private readonly runtime: PluginRuntimeService) {} + constructor( + private readonly runtime: PluginRuntimeService, + private readonly dbs: DatabaseService, + ) {} @Get(':placeId') async get( @@ -74,8 +77,8 @@ export class PlaceDetailsController { if (!Number.isFinite(placeId) || userId == null) return { providers: [] }; // The place must belong to a trip the caller can access — same gate as a read. - const row = db.prepare('SELECT trip_id FROM places WHERE id = ?').get(placeId) as { trip_id: number } | undefined; - if (!row || !canAccessTrip(row.trip_id, userId)) return { providers: [] }; + const row = this.dbs.connection.prepare('SELECT trip_id FROM places WHERE id = ?').get(placeId) as { trip_id: number } | undefined; + if (!row || !this.dbs.canAccessTrip(row.trip_id, userId)) return { providers: [] }; const ids = this.runtime.providersOf('placeDetailProvider'); const results = await Promise.all( diff --git a/server/src/nest/plugins/plugin-activity.controller.ts b/server/src/nest/plugins/plugin-activity.controller.ts index 5bdb65c0f..c9737448e 100644 --- a/server/src/nest/plugins/plugin-activity.controller.ts +++ b/server/src/nest/plugins/plugin-activity.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { db } from '../../db/database'; +import { DatabaseService } from '../database/database.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { pluginsEnabled } from './kill-switch'; import { readAuditForUser } from './host/plugin-audit'; @@ -18,6 +18,8 @@ import { readAuditForUser } from './host/plugin-audit'; @Controller('api/plugin-activity') @UseGuards(JwtAuthGuard) export class PluginActivityController { + constructor(private readonly dbs: DatabaseService) {} + @Get() mine(@Req() req: Request & { user?: { id: number } }, @Query('limit') limitRaw?: string): { activity: unknown[] } { if (!pluginsEnabled()) return { activity: [] }; @@ -26,6 +28,6 @@ export class PluginActivityController { // Math.floor so a non-integer (e.g. ?limit=2.5) can't reach SQLite's LIMIT and 500; // Math.floor(NaN) stays NaN so the `|| 200` fallback still applies. const limit = Math.min(Math.max(Math.floor(Number(limitRaw)) || 200, 1), 500); - return { activity: readAuditForUser(db, userId, limit) }; + return { activity: readAuditForUser(this.dbs.connection, userId, limit) }; } } diff --git a/server/src/nest/plugins/plugin-oauth.controller.ts b/server/src/nest/plugins/plugin-oauth.controller.ts index df78c610a..5ebaa13f1 100644 --- a/server/src/nest/plugins/plugin-oauth.controller.ts +++ b/server/src/nest/plugins/plugin-oauth.controller.ts @@ -3,7 +3,7 @@ import type { Request, Response } from 'express'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { pluginsEnabled } from './kill-switch'; import { PluginOAuthService } from './plugin-oauth.service'; -import { db } from '../../db/database'; +import { DatabaseService } from '../database/database.service'; /** * Host-brokered outbound OAuth endpoints (#plugins). All are gated by JwtAuthGuard — @@ -15,10 +15,13 @@ import { db } from '../../db/database'; @Controller('api/plugin-oauth') @UseGuards(JwtAuthGuard) export class PluginOAuthController { - constructor(private readonly oauth: PluginOAuthService) {} + constructor( + private readonly oauth: PluginOAuthService, + private readonly dbs: DatabaseService, + ) {} private isActive(id: string): boolean { - return !!db.prepare("SELECT 1 FROM plugins WHERE id = ? AND status = 'active'").get(id); + return !!this.dbs.connection.prepare("SELECT 1 FROM plugins WHERE id = ? AND status = 'active'").get(id); } @Get(':id/status') diff --git a/server/src/nest/plugins/plugin-routes.controller.ts b/server/src/nest/plugins/plugin-routes.controller.ts index 5e7327e77..fef425cfd 100644 --- a/server/src/nest/plugins/plugin-routes.controller.ts +++ b/server/src/nest/plugins/plugin-routes.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Param, Post, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { canAccessTrip, db } from '../../db/database'; +import type Database from 'better-sqlite3'; +import { DatabaseService } from '../database/database.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { pluginsEnabled } from './kill-switch'; import { PluginRuntimeService } from './plugin-runtime.service'; @@ -148,7 +149,10 @@ function normalize(pluginId: string, profile: string, waypointCount: number, raw @Controller('api/plugin-routes') @UseGuards(JwtAuthGuard) export class PluginRoutesController { - constructor(private readonly runtime: PluginRuntimeService) {} + constructor( + private readonly runtime: PluginRuntimeService, + private readonly dbs: DatabaseService, + ) {} @Post(':pluginId/:profileId') async route( @@ -160,7 +164,7 @@ export class PluginRoutesController { if (!pluginsEnabled()) return { route: null }; const userId = req.user?.id; const tripId = Number(body?.tripId); - if (userId == null || !Number.isFinite(tripId) || !canAccessTrip(tripId, userId)) return { route: null }; + if (userId == null || !Number.isFinite(tripId) || !this.dbs.canAccessTrip(tripId, userId)) return { route: null }; if (!PROFILE_RE.test(profileId)) return { route: null }; const waypoints = readWaypoints(body?.waypoints); if (!waypoints) return { route: null }; @@ -171,7 +175,7 @@ export class PluginRoutesController { // one the manifest declared — re-validated from the DB row like the plugins feed, // so a hand-edited capabilities blob can't invent profiles. if (!this.runtime.providersOf('routeProvider').includes(pluginId)) return { route: null }; - if (!declaredProfiles(pluginId).includes(profileId)) return { route: null }; + if (!declaredProfiles(this.dbs.connection, pluginId).includes(profileId)) return { route: null }; try { const raw = await this.runtime.invokeHook( @@ -189,9 +193,9 @@ export class PluginRoutesController { } } -function declaredProfiles(pluginId: string): string[] { +function declaredProfiles(conn: Database.Database, pluginId: string): string[] { try { - const row = db.prepare('SELECT capabilities FROM plugins WHERE id = ?').get(pluginId) as { capabilities?: string } | undefined; + const row = conn.prepare('SELECT capabilities FROM plugins WHERE id = ?').get(pluginId) as { capabilities?: string } | undefined; const c = JSON.parse(row?.capabilities || '{}') as { routeProfiles?: Array<{ id?: unknown }> }; if (!Array.isArray(c.routeProfiles)) return []; return c.routeProfiles diff --git a/server/src/nest/plugins/plugin-user-settings.controller.ts b/server/src/nest/plugins/plugin-user-settings.controller.ts index 408811e22..0cc284c3a 100644 --- a/server/src/nest/plugins/plugin-user-settings.controller.ts +++ b/server/src/nest/plugins/plugin-user-settings.controller.ts @@ -4,7 +4,7 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { pluginsEnabled } from './kill-switch'; import { PluginsService } from './plugins.service'; import { PluginRuntimeService } from './plugin-runtime.service'; -import { db } from '../../db/database'; +import { DatabaseService } from '../database/database.service'; /** * GET/POST /api/plugin-settings/:id — a USER's own `scope:'user'` settings for a @@ -23,10 +23,11 @@ export class PluginUserSettingsController { constructor( private readonly plugins: PluginsService, private readonly runtime: PluginRuntimeService, + private readonly dbs: DatabaseService, ) {} private activeWithUserFields(id: string): boolean { - const row = db.prepare("SELECT 1 FROM plugins WHERE id = ? AND status = 'active'").get(id); + const row = this.dbs.connection.prepare("SELECT 1 FROM plugins WHERE id = ? AND status = 'active'").get(id); return !!row; } diff --git a/server/src/nest/plugins/plugins-feed.controller.ts b/server/src/nest/plugins/plugins-feed.controller.ts index 792b858c8..2e6648ef5 100644 --- a/server/src/nest/plugins/plugins-feed.controller.ts +++ b/server/src/nest/plugins/plugins-feed.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, UseGuards } from '@nestjs/common'; -import { db } from '../../db/database'; +import { DatabaseService } from '../database/database.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { pluginsEnabled } from './kill-switch'; @@ -30,10 +30,12 @@ interface ActivePlugin { @Controller('api/plugins') @UseGuards(JwtAuthGuard) export class PluginsFeedController { + constructor(private readonly dbs: DatabaseService) {} + @Get() list(): { plugins: ActivePlugin[] } { if (!pluginsEnabled()) return { plugins: [] }; - const rows = db + const rows = this.dbs.connection .prepare("SELECT id, name, type, icon, capabilities, granted_permissions FROM plugins WHERE status = 'active' ORDER BY sort_order, name") .all() as Array & { capabilities: string; granted_permissions: string }>; const plugins = rows.map(({ capabilities, granted_permissions, ...p }) => { diff --git a/server/tests/unit/plugins/day-schedule.controller.test.ts b/server/tests/unit/plugins/day-schedule.controller.test.ts index 450243328..c7236b0fc 100644 --- a/server/tests/unit/plugins/day-schedule.controller.test.ts +++ b/server/tests/unit/plugins/day-schedule.controller.test.ts @@ -16,6 +16,8 @@ vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ all: () => tripDays.value }) }, canAccessTrip, })); +import { db as dbConn } from '../../../src/db/database'; +import { DatabaseService } from '../../../src/nest/database/database.service'; vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { DayScheduleController } from '../../../src/nest/plugins/day-schedule.controller'; @@ -28,7 +30,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { providersOf: vi.fn(() => providers), invokeHook: vi.fn(async (id: string) => invoke(id)), } as unknown as PluginRuntimeService; - return { c: new DayScheduleController(runtime), runtime }; + return { c: new DayScheduleController(runtime, new DatabaseService(dbConn)), runtime }; } const item = (over: Record = {}) => ({ id: 's1', dayId: 10, label: 'Charging', ...over }); diff --git a/server/tests/unit/plugins/journal-entry-rows.controller.test.ts b/server/tests/unit/plugins/journal-entry-rows.controller.test.ts index 4739f1214..60523cf66 100644 --- a/server/tests/unit/plugins/journal-entry-rows.controller.test.ts +++ b/server/tests/unit/plugins/journal-entry-rows.controller.test.ts @@ -17,6 +17,8 @@ vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ get: (entryId: number) => entryJourney(entryId) }) }, canAccessTrip: vi.fn(), })); +import { db as dbConn } from '../../../src/db/database'; +import { DatabaseService } from '../../../src/nest/database/database.service'; vi.mock('../../../src/services/journeyService', () => ({ canAccessJourney })); vi.mock('../../../src/services/adminService', () => ({ isAddonEnabled })); vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); @@ -31,7 +33,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { providersOf: vi.fn(() => providers), invokeHook: vi.fn(async (id: string) => invoke(id)), } as unknown as PluginRuntimeService; - return { c: new JournalEntryRowsController(runtime), runtime }; + return { c: new JournalEntryRowsController(runtime, new DatabaseService(dbConn)), runtime }; } const row = (over: Record = {}) => ({ label: 'Distance', value: '12 km', ...over }); diff --git a/server/tests/unit/plugins/place-details.controller.test.ts b/server/tests/unit/plugins/place-details.controller.test.ts index 39bfb3ab1..4f9b758a6 100644 --- a/server/tests/unit/plugins/place-details.controller.test.ts +++ b/server/tests/unit/plugins/place-details.controller.test.ts @@ -9,6 +9,8 @@ vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ get: (placeId: number) => placeTrip(placeId) }) }, canAccessTrip, })); +import { db as dbConn } from '../../../src/db/database'; +import { DatabaseService } from '../../../src/nest/database/database.service'; vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { PlaceDetailsController } from '../../../src/nest/plugins/place-details.controller'; @@ -22,7 +24,7 @@ function controller(over: Partial = {}) { invokeHook: vi.fn(async (id: string) => (id === 'p2' ? [{ label: 'Rating', value: '4.5' }] : [{ label: 'Reviews', value: '12', url: 'https://x' }])), ...over, } as unknown as PluginRuntimeService; - return { c: new PlaceDetailsController(runtime), runtime }; + return { c: new PlaceDetailsController(runtime, new DatabaseService(dbConn)), runtime }; } describe('PlaceDetailsController', () => { diff --git a/server/tests/unit/plugins/plugin-oauth.controller.test.ts b/server/tests/unit/plugins/plugin-oauth.controller.test.ts index 12cb096f1..a1b15f428 100644 --- a/server/tests/unit/plugins/plugin-oauth.controller.test.ts +++ b/server/tests/unit/plugins/plugin-oauth.controller.test.ts @@ -12,6 +12,8 @@ const { pluginsEnabled, getMock } = vi.hoisted(() => ({ })); vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ get: getMock }) } })); +import { db as dbConn } from '../../../src/db/database'; +import { DatabaseService } from '../../../src/nest/database/database.service'; import { PluginOAuthController } from '../../../src/nest/plugins/plugin-oauth.controller'; import type { PluginOAuthService } from '../../../src/nest/plugins/plugin-oauth.service'; @@ -30,7 +32,7 @@ function ctrl(over: Partial = {}) { disconnect: vi.fn(), ...over, } as unknown as PluginOAuthService; - return { c: new PluginOAuthController(svc), svc }; + return { c: new PluginOAuthController(svc, new DatabaseService(dbConn)), svc }; } describe('PluginOAuthController', () => { diff --git a/server/tests/unit/plugins/plugin-routes.controller.test.ts b/server/tests/unit/plugins/plugin-routes.controller.test.ts index 03f88ed3f..afc172d91 100644 --- a/server/tests/unit/plugins/plugin-routes.controller.test.ts +++ b/server/tests/unit/plugins/plugin-routes.controller.test.ts @@ -16,6 +16,8 @@ vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ get: () => (capabilitiesRow.value === undefined ? undefined : { capabilities: capabilitiesRow.value }) }) }, canAccessTrip, })); +import { db as dbConn } from '../../../src/db/database'; +import { DatabaseService } from '../../../src/nest/database/database.service'; vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { PluginRoutesController } from '../../../src/nest/plugins/plugin-routes.controller'; @@ -28,7 +30,7 @@ function controller(invoke: () => unknown, providers = ['ev-plug']) { providersOf: vi.fn(() => providers), invokeHook: vi.fn(async () => invoke()), } as unknown as PluginRuntimeService; - return { c: new PluginRoutesController(runtime), runtime }; + return { c: new PluginRoutesController(runtime, new DatabaseService(dbConn)), runtime }; } const wp = (n = 3) => Array.from({ length: n }, (_, i) => ({ lat: 48 + i * 0.1, lng: 2 + i * 0.1 })); const goodRoute = (n = 3, over: Record = {}) => ({ diff --git a/server/tests/unit/plugins/plugin-user-settings.controller.test.ts b/server/tests/unit/plugins/plugin-user-settings.controller.test.ts index 3a46bfabf..37d03aa70 100644 --- a/server/tests/unit/plugins/plugin-user-settings.controller.test.ts +++ b/server/tests/unit/plugins/plugin-user-settings.controller.test.ts @@ -11,6 +11,8 @@ const { pluginsEnabled, getMock } = vi.hoisted(() => ({ })); vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ get: getMock }) } })); +import { db as dbConn } from '../../../src/db/database'; +import { DatabaseService } from '../../../src/nest/database/database.service'; import { PluginUserSettingsController } from '../../../src/nest/plugins/plugin-user-settings.controller'; import type { PluginRuntimeService } from '../../../src/nest/plugins/plugin-runtime.service'; @@ -26,7 +28,7 @@ function ctrl() { } as unknown as PluginsService; // The controller now also takes the runtime (for settings-page actions). const runtime = { actionsOf: () => [], invokeAction: vi.fn() } as unknown as PluginRuntimeService; - return { c: new PluginUserSettingsController(svc, runtime), svc, runtime }; + return { c: new PluginUserSettingsController(svc, runtime, new DatabaseService(dbConn)), svc, runtime }; } describe('PluginUserSettingsController', () => { diff --git a/server/tests/unit/plugins/plugins-feed.controller.test.ts b/server/tests/unit/plugins/plugins-feed.controller.test.ts index 056144758..c6d17f4a6 100644 --- a/server/tests/unit/plugins/plugins-feed.controller.test.ts +++ b/server/tests/unit/plugins/plugins-feed.controller.test.ts @@ -13,6 +13,8 @@ const { rows, pluginsEnabled } = vi.hoisted(() => ({ pluginsEnabled: vi.fn(() => true), })); vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ all: () => rows.value }) } })); +import { db as dbConn } from '../../../src/db/database'; +import { DatabaseService } from '../../../src/nest/database/database.service'; vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { PluginsFeedController } from '../../../src/nest/plugins/plugins-feed.controller'; @@ -24,7 +26,7 @@ const row = (over: Record = {}) => ({ }); describe('PluginsFeedController', () => { - const c = new PluginsFeedController(); + const c = new PluginsFeedController(new DatabaseService(dbConn)); beforeEach(() => { pluginsEnabled.mockReturnValue(true); rows.value = []; }); it('returns an empty feed when the runtime is disabled', () => {