@terreno/api 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,7 @@
11
11
 
12
12
  import {afterEach, beforeAll, beforeEach, describe, expect, it, mock} from "bun:test";
13
13
  import express from "express";
14
+ import mongoose from "mongoose";
14
15
 
15
16
  import {
16
17
  emitToAuthorizedRoom,
@@ -1737,6 +1738,705 @@ describe("emitToDocumentAndQueryRooms", () => {
1737
1738
  // redactCredentials — Redis URL logging
1738
1739
  // ─────────────────────────────────────────────────────────────────────────────
1739
1740
 
1741
+ // ─────────────────────────────────────────────────────────────────────────────
1742
+ // startChangeStreamWatcher / stopChangeStreamWatcher — MongoDB change stream
1743
+ // ─────────────────────────────────────────────────────────────────────────────
1744
+
1745
+ describe("startChangeStreamWatcher", () => {
1746
+ let originalDb: typeof mongoose.connection.db;
1747
+
1748
+ beforeEach(() => {
1749
+ originalDb = mongoose.connection.db;
1750
+ clearRealtimeRegistry();
1751
+ });
1752
+
1753
+ afterEach(async () => {
1754
+ (mongoose.connection as any).db = originalDb;
1755
+ clearRealtimeRegistry();
1756
+ // Ensure the watcher is stopped between tests
1757
+ const {stopChangeStreamWatcher: stop} = await import("./changeStreamWatcher");
1758
+ await stop();
1759
+ });
1760
+
1761
+ const createMockChangeStream = () => {
1762
+ const listeners = new Map<string, (...args: any[]) => void>();
1763
+ return {
1764
+ close: mock(async () => {}),
1765
+ listeners,
1766
+ on(event: string, handler: (...args: any[]) => void) {
1767
+ listeners.set(event, handler);
1768
+ return this;
1769
+ },
1770
+ trigger(event: string, ...args: any[]) {
1771
+ const handler = listeners.get(event);
1772
+ if (handler) {
1773
+ handler(...args);
1774
+ }
1775
+ },
1776
+ };
1777
+ };
1778
+
1779
+ const createMockIo = () => {
1780
+ const rooms = new Map<string, Set<string>>();
1781
+ const sockets = new Map<string, any>();
1782
+ return {
1783
+ sockets: {
1784
+ adapter: {rooms},
1785
+ sockets,
1786
+ },
1787
+ to: (_room: string) => ({
1788
+ emit: (_event: string, _data: any) => {},
1789
+ }),
1790
+ } as unknown as import("socket.io").Server;
1791
+ };
1792
+
1793
+ it("initializes and registers change/error/close/end listeners", async () => {
1794
+ const mockStream = createMockChangeStream();
1795
+ const mockDb = {
1796
+ watch: mock(() => mockStream),
1797
+ };
1798
+ (mongoose.connection as any).db = mockDb;
1799
+
1800
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
1801
+ const io = createMockIo();
1802
+
1803
+ startChangeStreamWatcher(io, {}, true);
1804
+
1805
+ expect(mockDb.watch).toHaveBeenCalled();
1806
+ expect(mockStream.listeners.has("change")).toBe(true);
1807
+ expect(mockStream.listeners.has("error")).toBe(true);
1808
+ expect(mockStream.listeners.has("close")).toBe(true);
1809
+ expect(mockStream.listeners.has("end")).toBe(true);
1810
+ });
1811
+
1812
+ it("throws when mongoose connection db is unavailable", async () => {
1813
+ (mongoose.connection as any).db = undefined;
1814
+
1815
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
1816
+ const io = createMockIo();
1817
+
1818
+ expect(() => startChangeStreamWatcher(io)).toThrow(
1819
+ "MongoDB connection not available for change stream"
1820
+ );
1821
+ });
1822
+
1823
+ it("throws when watch returns null", async () => {
1824
+ const mockDb = {
1825
+ watch: mock(() => null),
1826
+ };
1827
+ (mongoose.connection as any).db = mockDb;
1828
+
1829
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
1830
+ const io = createMockIo();
1831
+
1832
+ expect(() => startChangeStreamWatcher(io)).toThrow("Failed to create change stream watcher");
1833
+ });
1834
+
1835
+ it("handles change events for registered models", async () => {
1836
+ const mockStream = createMockChangeStream();
1837
+ const mockDb = {
1838
+ watch: mock(() => mockStream),
1839
+ };
1840
+ (mongoose.connection as any).db = mockDb;
1841
+
1842
+ registerRealtime({
1843
+ collectionName: "todos",
1844
+ config: {
1845
+ methods: ["create", "update", "delete"],
1846
+ roomStrategy: "model",
1847
+ },
1848
+ modelName: "Todo",
1849
+ options: {
1850
+ permissions: {
1851
+ create: [() => true],
1852
+ delete: [() => true],
1853
+ list: [() => true],
1854
+ read: [() => true],
1855
+ update: [() => true],
1856
+ },
1857
+ },
1858
+ routePath: "/todos",
1859
+ });
1860
+
1861
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
1862
+ const io = createMockIo();
1863
+
1864
+ startChangeStreamWatcher(io, {}, true);
1865
+
1866
+ // Trigger an insert change event
1867
+ const changeHandler = mockStream.listeners.get("change");
1868
+ expect(changeHandler).toBeDefined();
1869
+ await changeHandler!({
1870
+ documentKey: {_id: "doc-1"},
1871
+ fullDocument: {_id: "doc-1", name: "Test Todo"},
1872
+ ns: {coll: "todos"},
1873
+ operationType: "insert",
1874
+ });
1875
+ });
1876
+
1877
+ it("skips events for unregistered collections", async () => {
1878
+ const mockStream = createMockChangeStream();
1879
+ const mockDb = {
1880
+ watch: mock(() => mockStream),
1881
+ };
1882
+ (mongoose.connection as any).db = mockDb;
1883
+
1884
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
1885
+ const io = createMockIo();
1886
+
1887
+ startChangeStreamWatcher(io, {}, true);
1888
+
1889
+ const changeHandler = mockStream.listeners.get("change");
1890
+ // Trigger for an unregistered collection — should not throw
1891
+ await changeHandler!({
1892
+ documentKey: {_id: "doc-1"},
1893
+ fullDocument: {_id: "doc-1"},
1894
+ ns: {coll: "unknown_collection"},
1895
+ operationType: "insert",
1896
+ });
1897
+ });
1898
+
1899
+ it("skips events when method is not enabled for the model", async () => {
1900
+ const mockStream = createMockChangeStream();
1901
+ const mockDb = {
1902
+ watch: mock(() => mockStream),
1903
+ };
1904
+ (mongoose.connection as any).db = mockDb;
1905
+
1906
+ registerRealtime({
1907
+ collectionName: "todos",
1908
+ config: {
1909
+ methods: ["create"], // only create enabled
1910
+ roomStrategy: "model",
1911
+ },
1912
+ modelName: "Todo",
1913
+ options: {
1914
+ permissions: {
1915
+ create: [() => true],
1916
+ delete: [],
1917
+ list: [() => true],
1918
+ read: [() => true],
1919
+ update: [],
1920
+ },
1921
+ },
1922
+ routePath: "/todos",
1923
+ });
1924
+
1925
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
1926
+ const io = createMockIo();
1927
+
1928
+ startChangeStreamWatcher(io, {}, true);
1929
+
1930
+ const changeHandler = mockStream.listeners.get("change");
1931
+ // Update event should be skipped because "update" not in methods
1932
+ await changeHandler!({
1933
+ documentKey: {_id: "doc-1"},
1934
+ fullDocument: {_id: "doc-1", name: "Updated"},
1935
+ ns: {coll: "todos"},
1936
+ operationType: "update",
1937
+ updateDescription: {updatedFields: {name: "Updated"}},
1938
+ });
1939
+ });
1940
+
1941
+ it("handles delete events for owner-strategy models", async () => {
1942
+ const mockStream = createMockChangeStream();
1943
+ const mockDb = {
1944
+ watch: mock(() => mockStream),
1945
+ };
1946
+ (mongoose.connection as any).db = mockDb;
1947
+
1948
+ registerRealtime({
1949
+ collectionName: "todos",
1950
+ config: {
1951
+ methods: ["create", "update", "delete"],
1952
+ roomStrategy: "owner",
1953
+ },
1954
+ modelName: "Todo",
1955
+ options: {
1956
+ permissions: {
1957
+ create: [() => true],
1958
+ delete: [() => true],
1959
+ list: [() => true],
1960
+ read: [() => true],
1961
+ update: [() => true],
1962
+ },
1963
+ },
1964
+ routePath: "/todos",
1965
+ });
1966
+
1967
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
1968
+ const io = createMockIo();
1969
+
1970
+ startChangeStreamWatcher(io, {}, true);
1971
+
1972
+ const changeHandler = mockStream.listeners.get("change");
1973
+ // Hard delete (no fullDocument)
1974
+ await changeHandler!({
1975
+ documentKey: {_id: "doc-1"},
1976
+ ns: {coll: "todos"},
1977
+ operationType: "delete",
1978
+ });
1979
+ });
1980
+
1981
+ it("handles delete events for broadcast-strategy models", async () => {
1982
+ const mockStream = createMockChangeStream();
1983
+ const mockDb = {
1984
+ watch: mock(() => mockStream),
1985
+ };
1986
+ (mongoose.connection as any).db = mockDb;
1987
+
1988
+ registerRealtime({
1989
+ collectionName: "broadcasts",
1990
+ config: {
1991
+ methods: ["create", "update", "delete"],
1992
+ roomStrategy: "broadcast",
1993
+ },
1994
+ modelName: "Broadcast",
1995
+ options: {
1996
+ permissions: {
1997
+ create: [() => true],
1998
+ delete: [() => true],
1999
+ list: [() => true],
2000
+ read: [() => true],
2001
+ update: [() => true],
2002
+ },
2003
+ },
2004
+ routePath: "/broadcasts",
2005
+ });
2006
+
2007
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
2008
+ const io = createMockIo();
2009
+
2010
+ startChangeStreamWatcher(io, {}, true);
2011
+
2012
+ const changeHandler = mockStream.listeners.get("change");
2013
+ // Hard delete for broadcast strategy
2014
+ await changeHandler!({
2015
+ documentKey: {_id: "doc-1"},
2016
+ ns: {coll: "broadcasts"},
2017
+ operationType: "delete",
2018
+ });
2019
+ });
2020
+
2021
+ it("includes updatedFields in event for update operations", async () => {
2022
+ const mockStream = createMockChangeStream();
2023
+ const mockDb = {
2024
+ watch: mock(() => mockStream),
2025
+ };
2026
+ (mongoose.connection as any).db = mockDb;
2027
+
2028
+ registerRealtime({
2029
+ collectionName: "todos",
2030
+ config: {
2031
+ methods: ["create", "update", "delete"],
2032
+ roomStrategy: "model",
2033
+ },
2034
+ modelName: "Todo",
2035
+ options: {
2036
+ permissions: {
2037
+ create: [() => true],
2038
+ delete: [() => true],
2039
+ list: [() => true],
2040
+ read: [() => true],
2041
+ update: [() => true],
2042
+ },
2043
+ },
2044
+ routePath: "/todos",
2045
+ });
2046
+
2047
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
2048
+ const io = createMockIo();
2049
+
2050
+ startChangeStreamWatcher(io, {}, true);
2051
+
2052
+ const changeHandler = mockStream.listeners.get("change");
2053
+ await changeHandler!({
2054
+ documentKey: {_id: "doc-1"},
2055
+ fullDocument: {_id: "doc-1", name: "Updated", status: "done"},
2056
+ ns: {coll: "todos"},
2057
+ operationType: "update",
2058
+ updateDescription: {updatedFields: {name: "Updated", status: "done"}},
2059
+ });
2060
+ });
2061
+
2062
+ it("respects ignoredCollections config", async () => {
2063
+ const mockStream = createMockChangeStream();
2064
+ const mockDb = {
2065
+ watch: mock(() => mockStream),
2066
+ };
2067
+ (mongoose.connection as any).db = mockDb;
2068
+
2069
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
2070
+ const io = createMockIo();
2071
+
2072
+ startChangeStreamWatcher(io, {ignoredCollections: ["audit_logs"]}, true);
2073
+
2074
+ // Verify the pipeline passed to watch includes the ignored collections
2075
+ const pipeline = (mockDb.watch.mock.calls[0] as any[])[0];
2076
+ const matchStage = pipeline[0].$match;
2077
+ expect(matchStage["ns.coll"].$nin).toContain("audit_logs");
2078
+ expect(matchStage["ns.coll"].$nin).toContain("socketio");
2079
+ expect(matchStage["ns.coll"].$nin).toContain("sessions");
2080
+ });
2081
+
2082
+ it("respects ignoredOperations config", async () => {
2083
+ const mockStream = createMockChangeStream();
2084
+ const mockDb = {
2085
+ watch: mock(() => mockStream),
2086
+ };
2087
+ (mongoose.connection as any).db = mockDb;
2088
+
2089
+ registerRealtime({
2090
+ collectionName: "todos",
2091
+ config: {
2092
+ methods: ["create", "update", "delete"],
2093
+ roomStrategy: "model",
2094
+ },
2095
+ modelName: "Todo",
2096
+ options: {
2097
+ permissions: {
2098
+ create: [() => true],
2099
+ delete: [() => true],
2100
+ list: [() => true],
2101
+ read: [() => true],
2102
+ update: [() => true],
2103
+ },
2104
+ },
2105
+ routePath: "/todos",
2106
+ });
2107
+
2108
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
2109
+ const io = createMockIo();
2110
+
2111
+ startChangeStreamWatcher(io, {ignoredOperations: ["insert"]}, true);
2112
+
2113
+ const changeHandler = mockStream.listeners.get("change");
2114
+ // This insert should be skipped because "insert" is ignored
2115
+ await changeHandler!({
2116
+ documentKey: {_id: "doc-1"},
2117
+ fullDocument: {_id: "doc-1"},
2118
+ ns: {coll: "todos"},
2119
+ operationType: "insert",
2120
+ });
2121
+ });
2122
+
2123
+ it("skips events with no collectionName or docId", async () => {
2124
+ const mockStream = createMockChangeStream();
2125
+ const mockDb = {
2126
+ watch: mock(() => mockStream),
2127
+ };
2128
+ (mongoose.connection as any).db = mockDb;
2129
+
2130
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
2131
+ const io = createMockIo();
2132
+
2133
+ startChangeStreamWatcher(io, {}, true);
2134
+
2135
+ const changeHandler = mockStream.listeners.get("change");
2136
+ // Missing ns.coll
2137
+ await changeHandler!({
2138
+ documentKey: {_id: "doc-1"},
2139
+ ns: {},
2140
+ operationType: "insert",
2141
+ });
2142
+ // Missing documentKey
2143
+ await changeHandler!({
2144
+ documentKey: {},
2145
+ ns: {coll: "todos"},
2146
+ operationType: "insert",
2147
+ });
2148
+ });
2149
+
2150
+ it("skips non-CRUD operation types", async () => {
2151
+ const mockStream = createMockChangeStream();
2152
+ const mockDb = {
2153
+ watch: mock(() => mockStream),
2154
+ };
2155
+ (mongoose.connection as any).db = mockDb;
2156
+
2157
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
2158
+ const io = createMockIo();
2159
+
2160
+ startChangeStreamWatcher(io, {}, true);
2161
+
2162
+ const changeHandler = mockStream.listeners.get("change");
2163
+ // "drop" is not in our pipeline filter, should be skipped
2164
+ await changeHandler!({
2165
+ operationType: "drop",
2166
+ });
2167
+ });
2168
+
2169
+ it("handles error/close/end events gracefully", async () => {
2170
+ const mockStream = createMockChangeStream();
2171
+ const mockDb = {
2172
+ watch: mock(() => mockStream),
2173
+ };
2174
+ (mongoose.connection as any).db = mockDb;
2175
+
2176
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
2177
+ const io = createMockIo();
2178
+
2179
+ startChangeStreamWatcher(io, {}, true);
2180
+
2181
+ // Trigger error, close, end — should not throw
2182
+ mockStream.trigger("error", new Error("test error"));
2183
+ mockStream.trigger("close");
2184
+ mockStream.trigger("end");
2185
+ });
2186
+
2187
+ it("uses custom batchSize and fullDocument config", async () => {
2188
+ const mockStream = createMockChangeStream();
2189
+ const mockDb = {
2190
+ watch: mock(() => mockStream),
2191
+ };
2192
+ (mongoose.connection as any).db = mockDb;
2193
+
2194
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
2195
+ const io = createMockIo();
2196
+
2197
+ startChangeStreamWatcher(io, {batchSize: 100, fullDocument: "whenAvailable"}, true);
2198
+
2199
+ const options = (mockDb.watch.mock.calls[0] as any[])[1];
2200
+ expect(options.batchSize).toBe(100);
2201
+ expect(options.fullDocument).toBe("whenAvailable");
2202
+ });
2203
+
2204
+ it("catches errors thrown in the change handler", async () => {
2205
+ const mockStream = createMockChangeStream();
2206
+ const mockDb = {
2207
+ watch: mock(() => mockStream),
2208
+ };
2209
+ (mongoose.connection as any).db = mockDb;
2210
+
2211
+ // Register with a model that will throw during permission check
2212
+ registerRealtime({
2213
+ collectionName: "todos",
2214
+ config: {
2215
+ methods: ["create", "update", "delete"],
2216
+ roomStrategy: "model",
2217
+ },
2218
+ modelName: "Todo",
2219
+ options: {
2220
+ permissions: {
2221
+ create: [() => true],
2222
+ delete: [() => true],
2223
+ list: [() => true],
2224
+ read: [
2225
+ () => {
2226
+ throw new Error("permission check error");
2227
+ },
2228
+ ],
2229
+ update: [() => true],
2230
+ },
2231
+ },
2232
+ routePath: "/todos",
2233
+ });
2234
+
2235
+ const {startChangeStreamWatcher} = await import("./changeStreamWatcher");
2236
+
2237
+ // Create an IO with a socket in the target room
2238
+ const emissions: any[] = [];
2239
+ const mockSocket = {
2240
+ decodedToken: {id: "user-1"},
2241
+ emit: (_event: string, _data: any) => {
2242
+ emissions.push({_data, _event});
2243
+ },
2244
+ id: "sock-1",
2245
+ };
2246
+ const rooms = new Map<string, Set<string>>();
2247
+ rooms.set("model:todos", new Set(["sock-1"]));
2248
+ const sockets = new Map<string, any>();
2249
+ sockets.set("sock-1", mockSocket);
2250
+ const io = {
2251
+ sockets: {
2252
+ adapter: {rooms},
2253
+ sockets,
2254
+ },
2255
+ to: () => ({emit: () => {}}),
2256
+ } as unknown as import("socket.io").Server;
2257
+
2258
+ startChangeStreamWatcher(io, {}, true);
2259
+
2260
+ const changeHandler = mockStream.listeners.get("change");
2261
+ // Should not throw even though permission check throws
2262
+ await changeHandler!({
2263
+ documentKey: {_id: "doc-1"},
2264
+ fullDocument: {_id: "doc-1", name: "Test"},
2265
+ ns: {coll: "todos"},
2266
+ operationType: "insert",
2267
+ });
2268
+ });
2269
+ });
2270
+
2271
+ describe("stopChangeStreamWatcher", () => {
2272
+ it("closes and nullifies the active watcher", async () => {
2273
+ const mockStream = {
2274
+ close: mock(async () => {}),
2275
+ on: () => {},
2276
+ };
2277
+ const mockDb = {
2278
+ watch: mock(() => mockStream),
2279
+ };
2280
+ const originalDb = mongoose.connection.db;
2281
+ (mongoose.connection as any).db = mockDb;
2282
+
2283
+ const {startChangeStreamWatcher, stopChangeStreamWatcher} = await import(
2284
+ "./changeStreamWatcher"
2285
+ );
2286
+ const io = {
2287
+ sockets: {
2288
+ adapter: {rooms: new Map()},
2289
+ sockets: new Map(),
2290
+ },
2291
+ to: () => ({emit: () => {}}),
2292
+ } as unknown as import("socket.io").Server;
2293
+
2294
+ startChangeStreamWatcher(io);
2295
+ await stopChangeStreamWatcher();
2296
+ expect(mockStream.close).toHaveBeenCalled();
2297
+
2298
+ // Calling again should be a no-op
2299
+ await stopChangeStreamWatcher();
2300
+
2301
+ (mongoose.connection as any).db = originalDb;
2302
+ });
2303
+ });
2304
+
2305
+ // ─────────────────────────────────────────────────────────────────────────────
2306
+ // RealtimeApp.onServerCreated / setupAdapter
2307
+ // ─────────────────────────────────────────────────────────────────────────────
2308
+
2309
+ describe("RealtimeApp.onServerCreated", () => {
2310
+ const servers: import("http").Server[] = [];
2311
+
2312
+ afterEach(async () => {
2313
+ for (const s of servers) {
2314
+ s.close();
2315
+ }
2316
+ servers.length = 0;
2317
+ });
2318
+
2319
+ const makeServer = (): import("http").Server => {
2320
+ const http = require("http");
2321
+ const server = http.createServer();
2322
+ servers.push(server);
2323
+ return server;
2324
+ };
2325
+
2326
+ it("throws when TOKEN_SECRET is missing", () => {
2327
+ const originalSecret = process.env.TOKEN_SECRET;
2328
+ process.env.TOKEN_SECRET = "";
2329
+
2330
+ const app = new RealtimeApp({tokenSecret: undefined});
2331
+ const server = makeServer();
2332
+
2333
+ expect(() => app.onServerCreated(server)).toThrow("TOKEN_SECRET is required");
2334
+
2335
+ process.env.TOKEN_SECRET = originalSecret;
2336
+ });
2337
+
2338
+ it("sets up Socket.io with valid config", async () => {
2339
+ const app = new RealtimeApp({
2340
+ adapter: "none",
2341
+ tokenSecret: "test-secret",
2342
+ });
2343
+ const server = makeServer();
2344
+
2345
+ // Mock mongoose.connection.db for changeStreamWatcher
2346
+ const originalDb = mongoose.connection.db;
2347
+ const mockStream = {close: async () => {}, on: () => mockStream};
2348
+ (mongoose.connection as any).db = {watch: () => mockStream};
2349
+
2350
+ app.onServerCreated(server);
2351
+ expect(app.getIo()).toBeDefined();
2352
+
2353
+ await app.close();
2354
+ (mongoose.connection as any).db = originalDb;
2355
+ });
2356
+ });
2357
+
2358
+ describe("RealtimeApp.setupAdapter (private, via onServerCreated config)", () => {
2359
+ const servers: import("http").Server[] = [];
2360
+
2361
+ afterEach(async () => {
2362
+ for (const s of servers) {
2363
+ s.close();
2364
+ }
2365
+ servers.length = 0;
2366
+ });
2367
+
2368
+ const makeServer = (): import("http").Server => {
2369
+ const http = require("http");
2370
+ const server = http.createServer();
2371
+ servers.push(server);
2372
+ return server;
2373
+ };
2374
+
2375
+ it("logs warning when redis adapter requested but no URL found", async () => {
2376
+ const originalValkey = process.env.VALKEY_URL;
2377
+ const originalRedis = process.env.REDIS_URL;
2378
+ const originalDb = mongoose.connection.db;
2379
+ process.env.VALKEY_URL = "";
2380
+ process.env.REDIS_URL = "";
2381
+
2382
+ const mockStream = {close: async () => {}, on: () => mockStream};
2383
+ (mongoose.connection as any).db = {watch: () => mockStream};
2384
+
2385
+ const app = new RealtimeApp({
2386
+ adapter: "redis",
2387
+ tokenSecret: "test-secret",
2388
+ });
2389
+ const server = makeServer();
2390
+
2391
+ app.onServerCreated(server);
2392
+ await app.close();
2393
+
2394
+ process.env.VALKEY_URL = originalValkey;
2395
+ process.env.REDIS_URL = originalRedis;
2396
+ (mongoose.connection as any).db = originalDb;
2397
+ });
2398
+
2399
+ it("logs info when redis adapter has a URL", async () => {
2400
+ const originalValkey = process.env.VALKEY_URL;
2401
+ const originalDb = mongoose.connection.db;
2402
+ process.env.VALKEY_URL = "redis://user:pass@localhost:6379/0";
2403
+
2404
+ const mockStream = {close: async () => {}, on: () => mockStream};
2405
+ (mongoose.connection as any).db = {watch: () => mockStream};
2406
+
2407
+ const app = new RealtimeApp({
2408
+ adapter: "redis",
2409
+ debug: true,
2410
+ tokenSecret: "test-secret",
2411
+ });
2412
+ const server = makeServer();
2413
+
2414
+ app.onServerCreated(server);
2415
+ await app.close();
2416
+
2417
+ process.env.VALKEY_URL = originalValkey;
2418
+ (mongoose.connection as any).db = originalDb;
2419
+ });
2420
+
2421
+ it("no-op adapter mode 'none'", async () => {
2422
+ const originalDb = mongoose.connection.db;
2423
+ const mockStream = {close: async () => {}, on: () => mockStream};
2424
+ (mongoose.connection as any).db = {watch: () => mockStream};
2425
+
2426
+ const app = new RealtimeApp({
2427
+ adapter: "none",
2428
+ tokenSecret: "test-secret",
2429
+ });
2430
+ const server = makeServer();
2431
+
2432
+ app.onServerCreated(server);
2433
+ expect(app.getIo()).toBeDefined();
2434
+
2435
+ await app.close();
2436
+ (mongoose.connection as any).db = originalDb;
2437
+ });
2438
+ });
2439
+
1740
2440
  // ─────────────────────────────────────────────────────────────────────────────
1741
2441
  // ensureApiId
1742
2442
  // ─────────────────────────────────────────────────────────────────────────────
@@ -2444,6 +3144,10 @@ describe("RealtimeApp — onServerCreated and setupAdapter", () => {
2444
3144
  });
2445
3145
  });
2446
3146
 
3147
+ // ─────────────────────────────────────────────────────────────────────────────
3148
+ // redactCredentials — Redis URL logging
3149
+ // ─────────────────────────────────────────────────────────────────────────────
3150
+
2447
3151
  describe("redactCredentials", () => {
2448
3152
  it("redacts user:password@ in a redis URL", () => {
2449
3153
  expect(redactCredentials("redis://user:secret@host:6379/0")).toBe("redis://***@host:6379/0");