Repository navigation
Expand file tree
/
Copy pathfactory-singleton.test.ts
More file actions
162 lines (129 loc) · 5.64 KB
/
Copy pathfactory-singleton.test.ts
File metadata and controls
162 lines (129 loc) · 5.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* Factory singleton tests — isolated process required.
* Mocks provider modules to test getStorageProvider() and closeStorageProvider()
* without real database connections.
*/
import { describe, test, expect, beforeEach, mock } from "bun:test";
// ── Mock providers ──────────────────────────────────────────────────────────
const mockInitialize = mock(async () => {});
const mockClose = mock(async () => {});
const mockGetAllData = mock(async () => ({}));
function makeMockProvider() {
return {
initialize: mockInitialize,
close: mockClose,
getAllData: mockGetAllData,
getCollection: mock(async () => null),
setCollection: mock(async () => {}),
mergeData: mock(async () => {}),
isHealthy: mock(async () => true),
};
}
const mockSQLiteInstance = makeMockProvider();
const mockPostgresInstance = makeMockProvider();
mock.module("@/lib/storage/providers/sqlite", () => ({
SQLiteStorageProvider: mock(() => mockSQLiteInstance),
}));
mock.module("@/lib/storage/providers/postgres", () => ({
PostgresStorageProvider: mock(() => mockPostgresInstance),
}));
// Import factory AFTER mocking providers
import { getStorageProvider, closeStorageProvider, getStorageProviderType } from "@/lib/storage/factory";
// ── Tests ───────────────────────────────────────────────────────────────────
describe("factory: getStorageProvider", () => {
beforeEach(async () => {
// Reset singleton state between tests
await closeStorageProvider();
mockInitialize.mockClear();
mockClose.mockClear();
delete process.env.STORAGE_PROVIDER;
});
test("returns null when STORAGE_PROVIDER is local", async () => {
process.env.STORAGE_PROVIDER = "local";
const provider = await getStorageProvider();
expect(provider).toBeNull();
});
test("returns null when STORAGE_PROVIDER is not set", async () => {
const provider = await getStorageProvider();
expect(provider).toBeNull();
});
test("creates SQLite provider when STORAGE_PROVIDER=sqlite", async () => {
process.env.STORAGE_PROVIDER = "sqlite";
const provider = await getStorageProvider();
expect(provider).not.toBeNull();
expect(mockInitialize).toHaveBeenCalledTimes(1);
});
test("creates Postgres provider when STORAGE_PROVIDER=postgres", async () => {
process.env.STORAGE_PROVIDER = "postgres";
const provider = await getStorageProvider();
expect(provider).not.toBeNull();
expect(mockInitialize).toHaveBeenCalledTimes(1);
});
test("returns same instance on second call (singleton)", async () => {
process.env.STORAGE_PROVIDER = "sqlite";
const first = await getStorageProvider();
const second = await getStorageProvider();
expect(first).toBe(second);
// initialize called only once, not twice
expect(mockInitialize).toHaveBeenCalledTimes(1);
});
test("calls initialize() on first creation", async () => {
process.env.STORAGE_PROVIDER = "sqlite";
await getStorageProvider();
expect(mockInitialize).toHaveBeenCalledTimes(1);
});
test("propagates error when initialize() throws", async () => {
process.env.STORAGE_PROVIDER = "sqlite";
mockInitialize.mockRejectedValueOnce(new Error("DB init failed"));
await expect(getStorageProvider()).rejects.toThrow("DB init failed");
});
test("the provider it hands out encrypts credentials before the backend ever sees them", async () => {
// Wiring, not crypto: if the factory ever returns the bare provider, every other test in the
// suite still passes and every credential silently goes to disk in the clear.
process.env.STORAGE_PROVIDER = "sqlite";
process.env.JWT_SECRET = "factory-singleton-test-jwt-secret-32ch";
const provider = await getStorageProvider();
await provider?.setCollection("u@example.org", "connections", [
{ id: "c1", name: "Prod", type: "postgres", password: "FACTORY-CANARY", createdAt: new Date(0) },
] as never);
const written = JSON.stringify(mockSQLiteInstance.setCollection.mock.calls);
expect(written).not.toContain("FACTORY-CANARY");
expect(written).toContain("v1:");
});
});
describe("factory: closeStorageProvider", () => {
beforeEach(async () => {
await closeStorageProvider();
mockInitialize.mockClear();
mockClose.mockClear();
delete process.env.STORAGE_PROVIDER;
});
test("closes and resets singleton", async () => {
process.env.STORAGE_PROVIDER = "sqlite";
await getStorageProvider();
await closeStorageProvider();
expect(mockClose).toHaveBeenCalledTimes(1);
});
test("creates new instance after close + re-get", async () => {
process.env.STORAGE_PROVIDER = "sqlite";
await getStorageProvider();
await closeStorageProvider();
mockInitialize.mockClear();
const provider = await getStorageProvider();
expect(provider).not.toBeNull();
// New initialize call — fresh instance
expect(mockInitialize).toHaveBeenCalledTimes(1);
});
test("does not throw when called without active provider", async () => {
await expect(closeStorageProvider()).resolves.toBeUndefined();
expect(mockClose).not.toHaveBeenCalled();
});
test("double close does not throw", async () => {
process.env.STORAGE_PROVIDER = "sqlite";
await getStorageProvider();
await closeStorageProvider();
await expect(closeStorageProvider()).resolves.toBeUndefined();
// close called only once (second call has no provider)
expect(mockClose).toHaveBeenCalledTimes(1);
});
});