@gencow/core 0.1.23 → 0.1.25
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.
- package/dist/crud.d.ts +2 -2
- package/dist/crud.js +225 -208
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -1
- package/dist/reactive.js +10 -3
- package/dist/retry.js +1 -1
- package/dist/rls-db.d.ts +2 -2
- package/dist/rls-db.js +1 -5
- package/dist/scheduler.d.ts +2 -0
- package/dist/scheduler.js +16 -6
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -1
- package/dist/storage.js +29 -22
- package/dist/v.d.ts +2 -2
- package/dist/workflow-types.d.ts +81 -0
- package/dist/workflow-types.js +12 -0
- package/dist/workflow.d.ts +30 -0
- package/dist/workflow.js +150 -0
- package/dist/workflows-api.d.ts +13 -0
- package/dist/workflows-api.js +321 -0
- package/package.json +46 -42
- package/src/__tests__/auth.test.ts +90 -86
- package/src/__tests__/crons.test.ts +69 -67
- package/src/__tests__/crud-codegen-integration.test.ts +164 -170
- package/src/__tests__/crud-owner-rls.test.ts +308 -301
- package/src/__tests__/crud.test.ts +694 -711
- package/src/__tests__/dist-exports.test.ts +120 -114
- package/src/__tests__/fixtures/basic/auth.ts +16 -16
- package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
- package/src/__tests__/fixtures/basic/index.ts +1 -1
- package/src/__tests__/fixtures/basic/schema.ts +1 -1
- package/src/__tests__/fixtures/basic/tasks.ts +4 -4
- package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
- package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
- package/src/__tests__/helpers/pglite-migrations.ts +2 -5
- package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
- package/src/__tests__/helpers/seed-like-fill.ts +50 -44
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
- package/src/__tests__/httpaction.test.ts +91 -91
- package/src/__tests__/image-optimization.test.ts +570 -574
- package/src/__tests__/load.test.ts +321 -308
- package/src/__tests__/network-sim.test.ts +238 -215
- package/src/__tests__/reactive.test.ts +380 -358
- package/src/__tests__/retry.test.ts +99 -84
- package/src/__tests__/rls-crud-basic.test.ts +172 -245
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
- package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
- package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
- package/src/__tests__/rls-session-and-policies.test.ts +181 -199
- package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
- package/src/__tests__/scheduler-durable.test.ts +117 -117
- package/src/__tests__/scheduler-exec.test.ts +258 -246
- package/src/__tests__/scheduler.test.ts +129 -111
- package/src/__tests__/storage.test.ts +282 -269
- package/src/__tests__/tsconfig.json +6 -6
- package/src/__tests__/validator.test.ts +236 -232
- package/src/__tests__/workflow.test.ts +606 -0
- package/src/__tests__/ws-integration.test.ts +223 -218
- package/src/__tests__/ws-scale.test.ts +168 -159
- package/src/auth-config.ts +18 -18
- package/src/auth.ts +106 -106
- package/src/crons.ts +77 -77
- package/src/crud.ts +523 -479
- package/src/index.ts +71 -6
- package/src/reactive.ts +357 -331
- package/src/retry.ts +51 -54
- package/src/rls-db.ts +195 -205
- package/src/rls.ts +33 -36
- package/src/scheduler.ts +237 -211
- package/src/server.ts +0 -1
- package/src/storage.ts +632 -593
- package/src/v.ts +119 -114
- package/src/workflow-types.ts +108 -0
- package/src/workflow.ts +188 -0
- package/src/workflows-api.ts +415 -0
- package/src/db.ts +0 -18
|
@@ -8,314 +8,327 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
11
|
-
import { createStorage } from "../storage";
|
|
11
|
+
import { createStorage } from "../storage.js";
|
|
12
12
|
import * as fs from "fs/promises";
|
|
13
13
|
import * as path from "path";
|
|
14
14
|
import * as os from "os";
|
|
15
15
|
|
|
16
16
|
describe("createStorage()", () => {
|
|
17
|
-
|
|
17
|
+
let tmpDir: string;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-storage-test-"));
|
|
21
|
+
});
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
25
|
+
});
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
describe("store()", () => {
|
|
30
|
-
it("File 객체를 저장하고 storageId를 반환한다", async () => {
|
|
31
|
-
const storage = createStorage(tmpDir);
|
|
32
|
-
const file = new File(["hello world"], "test.txt", { type: "text/plain" });
|
|
33
|
-
|
|
34
|
-
const id = await storage.store(file);
|
|
35
|
-
expect(typeof id).toBe("string");
|
|
36
|
-
expect(id.length).toBeGreaterThan(0);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("저장된 파일이 디스크에 존재한다", async () => {
|
|
40
|
-
const storage = createStorage(tmpDir);
|
|
41
|
-
const file = new File(["content"], "doc.txt", { type: "text/plain" });
|
|
42
|
-
|
|
43
|
-
const id = await storage.store(file);
|
|
44
|
-
const filePath = path.join(tmpDir, id);
|
|
45
|
-
const stat = await fs.stat(filePath);
|
|
46
|
-
expect(stat.isFile()).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("Blob 객체도 저장 가능하다", async () => {
|
|
50
|
-
const storage = createStorage(tmpDir);
|
|
51
|
-
const blob = new Blob(["blob content"], { type: "application/octet-stream" });
|
|
52
|
-
|
|
53
|
-
const id = await storage.store(blob);
|
|
54
|
-
expect(typeof id).toBe("string");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("50MB 초과 파일 → 에러", async () => {
|
|
58
|
-
const storage = createStorage(tmpDir);
|
|
59
|
-
// 51MB File mock — File constructor에서 size를 직접 제어
|
|
60
|
-
const bigContent = new Uint8Array(51 * 1024 * 1024);
|
|
61
|
-
const file = new File([bigContent], "big.bin");
|
|
62
|
-
|
|
63
|
-
await expect(storage.store(file)).rejects.toThrow("File too large");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("커스텀 filename 적용", async () => {
|
|
67
|
-
const storage = createStorage(tmpDir);
|
|
68
|
-
const file = new File(["data"], "original.txt");
|
|
69
|
-
|
|
70
|
-
const id = await storage.store(file, "custom-name.txt");
|
|
71
|
-
const meta = await storage.getMeta(id);
|
|
72
|
-
expect(meta?.name).toBe("custom-name.txt");
|
|
73
|
-
});
|
|
74
|
-
});
|
|
27
|
+
// ─── store + getUrl ─────────────────────────────────
|
|
75
28
|
|
|
76
|
-
|
|
29
|
+
describe("store()", () => {
|
|
30
|
+
it("File 객체를 저장하고 storageId를 반환한다", async () => {
|
|
31
|
+
const storage = createStorage(tmpDir);
|
|
32
|
+
const file = new File(["hello world"], "test.txt", { type: "text/plain" });
|
|
77
33
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
34
|
+
const id = await storage.store(file);
|
|
35
|
+
expect(typeof id).toBe("string");
|
|
36
|
+
expect(id.length).toBeGreaterThan(0);
|
|
37
|
+
});
|
|
82
38
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
39
|
+
it("저장된 파일이 디스크에 존재한다", async () => {
|
|
40
|
+
const storage = createStorage(tmpDir);
|
|
41
|
+
const file = new File(["content"], "doc.txt", { type: "text/plain" });
|
|
86
42
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
43
|
+
const id = await storage.store(file);
|
|
44
|
+
const filePath = path.join(tmpDir, id);
|
|
45
|
+
const stat = await fs.stat(filePath);
|
|
46
|
+
expect(stat.isFile()).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("Blob 객체도 저장 가능하다", async () => {
|
|
50
|
+
const storage = createStorage(tmpDir);
|
|
51
|
+
const blob = new Blob(["blob content"], { type: "application/octet-stream" });
|
|
90
52
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
expect(meta).not.toBeNull();
|
|
94
|
-
expect(meta!.name).toBe("meta.txt");
|
|
95
|
-
expect(meta!.type).toBe("text/plain");
|
|
96
|
-
expect(meta!.size).toBe(buffer.length);
|
|
97
|
-
});
|
|
53
|
+
const id = await storage.store(blob);
|
|
54
|
+
expect(typeof id).toBe("string");
|
|
98
55
|
});
|
|
99
56
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("URL 패턴이 /api/storage/{id} 형식이다", async () => {
|
|
110
|
-
const storage = createStorage(tmpDir);
|
|
111
|
-
const file = new File(["test"], "test.txt");
|
|
112
|
-
const id = await storage.store(file);
|
|
113
|
-
const url = storage.getUrl(id);
|
|
114
|
-
expect(url).toMatch(/^\/api\/storage\/.+$/);
|
|
115
|
-
});
|
|
57
|
+
it("50MB 초과 파일 → 에러", async () => {
|
|
58
|
+
const storage = createStorage(tmpDir);
|
|
59
|
+
// 51MB File mock — File constructor에서 size를 직접 제어
|
|
60
|
+
const bigContent = new Uint8Array(51 * 1024 * 1024);
|
|
61
|
+
const file = new File([bigContent], "big.bin");
|
|
62
|
+
|
|
63
|
+
await expect(storage.store(file)).rejects.toThrow("File too large");
|
|
116
64
|
});
|
|
117
65
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const meta = await storage.getMeta(id);
|
|
127
|
-
expect(meta).not.toBeNull();
|
|
128
|
-
expect(meta!.id).toBe(id);
|
|
129
|
-
expect(meta!.name).toBe("hello.txt");
|
|
130
|
-
expect(meta!.type).toContain("text/plain");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("존재하지 않는 storageId → null 반환", async () => {
|
|
134
|
-
const storage = createStorage(tmpDir);
|
|
135
|
-
const meta = await storage.getMeta("nonexistent-id");
|
|
136
|
-
expect(meta).toBeNull();
|
|
137
|
-
});
|
|
66
|
+
it("커스텀 filename 적용", async () => {
|
|
67
|
+
const storage = createStorage(tmpDir);
|
|
68
|
+
const file = new File(["data"], "original.txt");
|
|
69
|
+
|
|
70
|
+
const id = await storage.store(file, "custom-name.txt");
|
|
71
|
+
const meta = await storage.getMeta(id);
|
|
72
|
+
expect(meta?.name).toBe("custom-name.txt");
|
|
138
73
|
});
|
|
74
|
+
});
|
|
139
75
|
|
|
140
|
-
|
|
76
|
+
// ─── storeBuffer ────────────────────────────────────
|
|
141
77
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const id = await storage.store(file);
|
|
78
|
+
describe("storeBuffer()", () => {
|
|
79
|
+
it("Buffer를 저장하고 storageId를 반환한다", async () => {
|
|
80
|
+
const storage = createStorage(tmpDir);
|
|
81
|
+
const buffer = Buffer.from("buffer content");
|
|
147
82
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
expect(statBefore.isFile()).toBe(true);
|
|
83
|
+
const id = await storage.storeBuffer(buffer, "buffer.txt", "text/plain");
|
|
84
|
+
expect(typeof id).toBe("string");
|
|
85
|
+
});
|
|
152
86
|
|
|
153
|
-
|
|
154
|
-
|
|
87
|
+
it("저장된 파일의 메타데이터가 올바르다", async () => {
|
|
88
|
+
const storage = createStorage(tmpDir);
|
|
89
|
+
const buffer = Buffer.from("meta test");
|
|
155
90
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
91
|
+
const id = await storage.storeBuffer(buffer, "meta.txt", "text/plain");
|
|
92
|
+
const meta = await storage.getMeta(id);
|
|
93
|
+
expect(meta).not.toBeNull();
|
|
94
|
+
expect(meta!.name).toBe("meta.txt");
|
|
95
|
+
expect(meta!.type).toBe("text/plain");
|
|
96
|
+
expect(meta!.size).toBe(buffer.length);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
159
99
|
|
|
160
|
-
|
|
161
|
-
const meta = await storage.getMeta(id);
|
|
162
|
-
expect(meta).toBeNull();
|
|
163
|
-
});
|
|
100
|
+
// ─── getUrl ─────────────────────────────────────────
|
|
164
101
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
102
|
+
describe("getUrl()", () => {
|
|
103
|
+
it("storageId로 URL 반환", () => {
|
|
104
|
+
const storage = createStorage(tmpDir);
|
|
105
|
+
const url = storage.getUrl("some-uuid-123");
|
|
106
|
+
expect(url).toBe("/api/storage/some-uuid-123");
|
|
169
107
|
});
|
|
170
108
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (sql.includes("SUM")) return [{ total: "999999999" }]; // 쿼터에 근접
|
|
178
|
-
return [];
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const storage = createStorage(tmpDir, {
|
|
182
|
-
rawSql: mockRawSql,
|
|
183
|
-
storageQuota: 1000000000, // 1GB
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const file = new File(["x".repeat(1024)], "test.txt");
|
|
187
|
-
await expect(storage.store(file)).rejects.toThrow("Storage quota exceeded");
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("쿼터 0 = 무제한 (에러 없음)", async () => {
|
|
191
|
-
const mockRawSql = async (sql: string, _params?: unknown[]) => {
|
|
192
|
-
if (sql.includes("information_schema")) return [];
|
|
193
|
-
if (sql.includes("SUM")) return [{ total: "999999999999" }];
|
|
194
|
-
if (sql.includes("INSERT")) return [];
|
|
195
|
-
return [];
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const storage = createStorage(tmpDir, {
|
|
199
|
-
rawSql: mockRawSql,
|
|
200
|
-
storageQuota: 0, // 무제한
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
const file = new File(["small"], "small.txt");
|
|
204
|
-
const id = await storage.store(file);
|
|
205
|
-
expect(typeof id).toBe("string");
|
|
206
|
-
});
|
|
109
|
+
it("URL 패턴이 /api/storage/{id} 형식이다", async () => {
|
|
110
|
+
const storage = createStorage(tmpDir);
|
|
111
|
+
const file = new File(["test"], "test.txt");
|
|
112
|
+
const id = await storage.store(file);
|
|
113
|
+
const url = storage.getUrl(id);
|
|
114
|
+
expect(url).toMatch(/^\/api\/storage\/.+$/);
|
|
207
115
|
});
|
|
208
|
-
});
|
|
116
|
+
});
|
|
209
117
|
|
|
210
|
-
// ───
|
|
211
|
-
// 2026-04-10 WSOD 사고 후 추가: files → _system_files 리네이밍 검증
|
|
212
|
-
// 📄 참고: docs/analysis/analysis-files-page-wsod.md
|
|
118
|
+
// ─── getMeta ────────────────────────────────────────
|
|
213
119
|
|
|
214
|
-
describe("
|
|
215
|
-
it("
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const storage = createStorage(tmpDir, { rawSql: mockRawSql });
|
|
226
|
-
|
|
227
|
-
// store 트리거 (ensureFilesTable + checkQuota + recordFileToDb)
|
|
228
|
-
const file = new File(["test"], "test.txt", { type: "text/plain" });
|
|
229
|
-
try { await storage.store(file); } catch {}
|
|
230
|
-
|
|
231
|
-
// 모든 실행된 SQL에서 old 테이블명 'files'가 아닌 '_system_files'만 참조하는지 확인
|
|
232
|
-
// (files 단독 참조를 찾되, _system_files는 통과)
|
|
233
|
-
for (const sql of executedSql) {
|
|
234
|
-
// "FROM files", "INTO files", "TABLE files" 같은 old 패턴이 없어야 함
|
|
235
|
-
expect(sql).not.toMatch(/\bFROM\s+files\b(?!_)/i);
|
|
236
|
-
expect(sql).not.toMatch(/\bINTO\s+files\b(?!_)/i);
|
|
237
|
-
expect(sql).not.toMatch(/\bTABLE\s+files\b(?!_)/i);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
120
|
+
describe("getMeta()", () => {
|
|
121
|
+
it("저장된 파일의 메타데이터를 반환한다", async () => {
|
|
122
|
+
const storage = createStorage(tmpDir);
|
|
123
|
+
const file = new File(["hello"], "hello.txt", { type: "text/plain" });
|
|
124
|
+
const id = await storage.store(file);
|
|
125
|
+
|
|
126
|
+
const meta = await storage.getMeta(id);
|
|
127
|
+
expect(meta).not.toBeNull();
|
|
128
|
+
expect(meta!.id).toBe(id);
|
|
129
|
+
expect(meta!.name).toBe("hello.txt");
|
|
130
|
+
expect(meta!.type).toContain("text/plain");
|
|
241
131
|
});
|
|
242
132
|
|
|
243
|
-
it("
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
133
|
+
it("존재하지 않는 storageId → null 반환", async () => {
|
|
134
|
+
const storage = createStorage(tmpDir);
|
|
135
|
+
const meta = await storage.getMeta("nonexistent-id");
|
|
136
|
+
expect(meta).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ─── delete ─────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
describe("delete()", () => {
|
|
143
|
+
it("파일을 삭제한다", async () => {
|
|
144
|
+
const storage = createStorage(tmpDir);
|
|
145
|
+
const file = new File(["to delete"], "delete-me.txt");
|
|
146
|
+
const id = await storage.store(file);
|
|
147
|
+
|
|
148
|
+
// 파일 존재 확인
|
|
149
|
+
const filePath = path.join(tmpDir, id);
|
|
150
|
+
const statBefore = await fs.stat(filePath);
|
|
151
|
+
expect(statBefore.isFile()).toBe(true);
|
|
152
|
+
|
|
153
|
+
// 삭제
|
|
154
|
+
await storage.delete(id);
|
|
155
|
+
|
|
156
|
+
// 파일 삭제 확인
|
|
157
|
+
const exists = await fs
|
|
158
|
+
.access(filePath)
|
|
159
|
+
.then(() => true)
|
|
160
|
+
.catch(() => false);
|
|
161
|
+
expect(exists).toBe(false);
|
|
162
|
+
|
|
163
|
+
// 메타데이터 삭제 확인
|
|
164
|
+
const meta = await storage.getMeta(id);
|
|
165
|
+
expect(meta).toBeNull();
|
|
269
166
|
});
|
|
270
167
|
|
|
271
|
-
it("
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return [];
|
|
277
|
-
};
|
|
168
|
+
it("존재하지 않는 storageId 삭제 시 에러 없음", async () => {
|
|
169
|
+
const storage = createStorage(tmpDir);
|
|
170
|
+
await expect(storage.delete("nonexistent-id")).resolves.toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
278
173
|
|
|
279
|
-
|
|
280
|
-
const storage = createStorage(tmpDir, { rawSql: mockRawSql });
|
|
174
|
+
// ─── Storage Quota ──────────────────────────────────
|
|
281
175
|
|
|
282
|
-
|
|
283
|
-
|
|
176
|
+
describe("스토리지 쿼터", () => {
|
|
177
|
+
it("쿼터 초과 시 에러 (rawSql 있을 때)", async () => {
|
|
178
|
+
const mockRawSql = async (sql: string, _params?: unknown[]) => {
|
|
179
|
+
if (sql.includes("information_schema")) return []; // ensureFilesTable
|
|
180
|
+
if (sql.includes("SUM")) return [{ total: "999999999" }]; // 쿼터에 근접
|
|
181
|
+
return [];
|
|
182
|
+
};
|
|
284
183
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
184
|
+
const storage = createStorage(tmpDir, {
|
|
185
|
+
rawSql: mockRawSql,
|
|
186
|
+
storageQuota: 1000000000, // 1GB
|
|
187
|
+
});
|
|
290
188
|
|
|
291
|
-
|
|
189
|
+
const file = new File(["x".repeat(1024)], "test.txt");
|
|
190
|
+
await expect(storage.store(file)).rejects.toThrow("Storage quota exceeded");
|
|
292
191
|
});
|
|
293
192
|
|
|
294
|
-
it("
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
try { await storage.delete(storageId); } catch {}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const deleteSql = executedSql.find(s => s.includes("DELETE"));
|
|
314
|
-
if (deleteSql) {
|
|
315
|
-
expect(deleteSql).toContain("_system_files");
|
|
316
|
-
expect(deleteSql).not.toMatch(/FROM\s+files[^_]/);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
193
|
+
it("쿼터 0 = 무제한 (에러 없음)", async () => {
|
|
194
|
+
const mockRawSql = async (sql: string, _params?: unknown[]) => {
|
|
195
|
+
if (sql.includes("information_schema")) return [];
|
|
196
|
+
if (sql.includes("SUM")) return [{ total: "999999999999" }];
|
|
197
|
+
if (sql.includes("INSERT")) return [];
|
|
198
|
+
return [];
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const storage = createStorage(tmpDir, {
|
|
202
|
+
rawSql: mockRawSql,
|
|
203
|
+
storageQuota: 0, // 무제한
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const file = new File(["small"], "small.txt");
|
|
207
|
+
const id = await storage.store(file);
|
|
208
|
+
expect(typeof id).toBe("string");
|
|
320
209
|
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ─── _system_files 테이블 리네이밍 관련 테스트 ────────────
|
|
214
|
+
// 2026-04-10 WSOD 사고 후 추가: files → _system_files 리네이밍 검증
|
|
215
|
+
// 📄 참고: docs/analysis/analysis-files-page-wsod.md
|
|
216
|
+
|
|
217
|
+
describe("_system_files 시스템 테이블 네이밍", () => {
|
|
218
|
+
it("rawSql로 실행되는 모든 SQL에 old 'files' 테이블 참조가 없다", async () => {
|
|
219
|
+
const executedSql: string[] = [];
|
|
220
|
+
const mockRawSql = async (sql: string) => {
|
|
221
|
+
executedSql.push(sql);
|
|
222
|
+
if (sql.includes("SUM")) return [{ total: "0" }];
|
|
223
|
+
if (sql.includes("INSERT")) return [];
|
|
224
|
+
return [];
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-naming-"));
|
|
228
|
+
const storage = createStorage(tmpDir, { rawSql: mockRawSql });
|
|
229
|
+
|
|
230
|
+
// store 트리거 (ensureFilesTable + checkQuota + recordFileToDb)
|
|
231
|
+
const file = new File(["test"], "test.txt", { type: "text/plain" });
|
|
232
|
+
try {
|
|
233
|
+
await storage.store(file);
|
|
234
|
+
} catch {}
|
|
235
|
+
|
|
236
|
+
// 모든 실행된 SQL에서 old 테이블명 'files'가 아닌 '_system_files'만 참조하는지 확인
|
|
237
|
+
// (files 단독 참조를 찾되, _system_files는 통과)
|
|
238
|
+
for (const sql of executedSql) {
|
|
239
|
+
// "FROM files", "INTO files", "TABLE files" 같은 old 패턴이 없어야 함
|
|
240
|
+
expect(sql).not.toMatch(/\bFROM\s+files\b(?!_)/i);
|
|
241
|
+
expect(sql).not.toMatch(/\bINTO\s+files\b(?!_)/i);
|
|
242
|
+
expect(sql).not.toMatch(/\bTABLE\s+files\b(?!_)/i);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("쿼터 검증 SQL이 _system_files 테이블을 참조한다", async () => {
|
|
249
|
+
const executedSql: string[] = [];
|
|
250
|
+
const mockRawSql = async (sql: string) => {
|
|
251
|
+
executedSql.push(sql);
|
|
252
|
+
if (sql.includes("SUM")) return [{ total: "0" }];
|
|
253
|
+
if (sql.includes("INSERT")) return [];
|
|
254
|
+
return [];
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-quota-"));
|
|
258
|
+
const storage = createStorage(tmpDir, {
|
|
259
|
+
rawSql: mockRawSql,
|
|
260
|
+
storageQuota: 1000000000,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const file = new File(["small"], "small.txt");
|
|
264
|
+
try {
|
|
265
|
+
await storage.store(file);
|
|
266
|
+
} catch {}
|
|
267
|
+
|
|
268
|
+
// SUM 쿼리가 _system_files 테이블을 참조하는지 확인
|
|
269
|
+
const sumSql = executedSql.find((s) => s.includes("SUM"));
|
|
270
|
+
if (sumSql) {
|
|
271
|
+
expect(sumSql).toContain("_system_files");
|
|
272
|
+
expect(sumSql).not.toMatch(/FROM\s+files[^_]/);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("recordFileToDb SQL이 _system_files 테이블에 INSERT한다", async () => {
|
|
279
|
+
const executedSql: string[] = [];
|
|
280
|
+
const mockRawSql = async (sql: string) => {
|
|
281
|
+
executedSql.push(sql);
|
|
282
|
+
if (sql.includes("SUM")) return [{ total: "0" }];
|
|
283
|
+
return [];
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-insert-"));
|
|
287
|
+
const storage = createStorage(tmpDir, { rawSql: mockRawSql });
|
|
288
|
+
|
|
289
|
+
const file = new File(["data"], "data.txt", { type: "text/plain" });
|
|
290
|
+
try {
|
|
291
|
+
await storage.store(file);
|
|
292
|
+
} catch {}
|
|
293
|
+
|
|
294
|
+
const insertSql = executedSql.find((s) => s.includes("INSERT"));
|
|
295
|
+
if (insertSql) {
|
|
296
|
+
expect(insertSql).toContain("_system_files");
|
|
297
|
+
expect(insertSql).not.toMatch(/INTO\s+files[^_]/);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("delete SQL이 _system_files 테이블에서 삭제한다", async () => {
|
|
304
|
+
const executedSql: string[] = [];
|
|
305
|
+
const mockRawSql = async (sql: string) => {
|
|
306
|
+
executedSql.push(sql);
|
|
307
|
+
if (sql.includes("SUM")) return [{ total: "0" }];
|
|
308
|
+
return [];
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-delete-"));
|
|
312
|
+
const storage = createStorage(tmpDir, { rawSql: mockRawSql });
|
|
313
|
+
|
|
314
|
+
// store 후 delete
|
|
315
|
+
const file = new File(["delete-me"], "del.txt");
|
|
316
|
+
let storageId: string | undefined;
|
|
317
|
+
try {
|
|
318
|
+
storageId = await storage.store(file);
|
|
319
|
+
} catch {}
|
|
320
|
+
if (storageId) {
|
|
321
|
+
try {
|
|
322
|
+
await storage.delete(storageId);
|
|
323
|
+
} catch {}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const deleteSql = executedSql.find((s) => s.includes("DELETE"));
|
|
327
|
+
if (deleteSql) {
|
|
328
|
+
expect(deleteSql).toContain("_system_files");
|
|
329
|
+
expect(deleteSql).not.toMatch(/FROM\s+files[^_]/);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
333
|
+
});
|
|
321
334
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true
|
|
5
|
+
},
|
|
6
|
+
"include": ["./**/*.ts"],
|
|
7
|
+
"exclude": []
|
|
8
8
|
}
|