@rebasepro/server-core 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e
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/common/src/collections/CollectionRegistry.d.ts +8 -0
- package/dist/common/src/util/entities.d.ts +22 -0
- package/dist/common/src/util/relations.d.ts +14 -4
- package/dist/common/src/util/resolutions.d.ts +1 -1
- package/dist/index-DXVBFp5V.js +37 -0
- package/dist/index-DXVBFp5V.js.map +1 -0
- package/dist/index.es.js +49931 -55
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +28475 -34569
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/openapi-generator.d.ts +15 -1
- package/dist/server-core/src/api/types.d.ts +1 -1
- package/dist/server-core/src/auth/admin-routes.d.ts +10 -1
- package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
- package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
- package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +5 -11
- package/dist/server-core/src/auth/index.d.ts +13 -2
- package/dist/server-core/src/auth/interfaces.d.ts +45 -6
- package/dist/server-core/src/auth/jwt.d.ts +1 -0
- package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
- package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
- package/dist/server-core/src/auth/middleware.d.ts +36 -11
- package/dist/server-core/src/auth/rate-limiter.d.ts +2 -2
- package/dist/server-core/src/auth/routes.d.ts +13 -3
- package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
- package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
- package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
- package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
- package/dist/server-core/src/cron/cron-store.d.ts +32 -0
- package/dist/server-core/src/cron/index.d.ts +6 -0
- package/dist/server-core/src/email/index.d.ts +2 -2
- package/dist/server-core/src/email/templates.d.ts +9 -0
- package/dist/server-core/src/email/types.d.ts +20 -23
- package/dist/server-core/src/history/history-routes.d.ts +2 -2
- package/dist/server-core/src/index.d.ts +5 -0
- package/dist/server-core/src/init.d.ts +112 -2
- package/dist/server-core/src/singleton.d.ts +35 -0
- package/dist/server-core/src/storage/LocalStorageController.d.ts +9 -9
- package/dist/server-core/src/storage/S3StorageController.d.ts +8 -8
- package/dist/server-core/src/storage/index.d.ts +18 -11
- package/dist/server-core/src/storage/routes.d.ts +3 -3
- package/dist/server-core/src/storage/types.d.ts +28 -16
- package/dist/server-core/src/utils/dev-port.d.ts +35 -0
- package/dist/server-core/src/utils/logger.d.ts +31 -0
- package/dist/server-core/src/utils/request-logger.d.ts +19 -0
- package/dist/types/src/controllers/auth.d.ts +2 -0
- package/dist/types/src/controllers/client.d.ts +119 -7
- package/dist/types/src/controllers/collection_registry.d.ts +4 -3
- package/dist/types/src/controllers/customization_controller.d.ts +7 -1
- package/dist/types/src/controllers/data.d.ts +34 -7
- package/dist/types/src/controllers/data_driver.d.ts +20 -28
- package/dist/types/src/controllers/database_admin.d.ts +2 -2
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +1 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
- package/dist/types/src/controllers/navigation.d.ts +5 -5
- package/dist/types/src/controllers/registry.d.ts +6 -3
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
- package/dist/types/src/controllers/storage.d.ts +24 -26
- package/dist/types/src/rebase_context.d.ts +8 -4
- package/dist/types/src/types/backend.d.ts +4 -1
- package/dist/types/src/types/builders.d.ts +5 -4
- package/dist/types/src/types/chips.d.ts +1 -1
- package/dist/types/src/types/collections.d.ts +169 -125
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +1 -1
- package/dist/types/src/types/entity_actions.d.ts +8 -8
- package/dist/types/src/types/entity_callbacks.d.ts +15 -15
- package/dist/types/src/types/entity_link_builder.d.ts +1 -1
- package/dist/types/src/types/entity_overrides.d.ts +2 -1
- package/dist/types/src/types/entity_views.d.ts +8 -8
- package/dist/types/src/types/export_import.d.ts +3 -3
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/plugins.d.ts +72 -18
- package/dist/types/src/types/properties.d.ts +118 -33
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/slots.d.ts +30 -6
- package/dist/types/src/types/translations.d.ts +44 -0
- package/dist/types/src/types/user_management_delegate.d.ts +1 -0
- package/package.json +84 -84
- package/scratch.ts +6 -5
- package/src/api/ast-schema-editor.ts +11 -11
- package/src/api/collections_for_test/callbacks_test_collection.ts +5 -2
- package/src/api/errors.ts +38 -14
- package/src/api/graphql/graphql-schema-generator.ts +5 -3
- package/src/api/openapi-generator.ts +613 -58
- package/src/api/rest/api-generator.ts +17 -11
- package/src/api/rest/query-parser.ts +1 -1
- package/src/api/schema-editor-routes.ts +2 -0
- package/src/api/server.ts +8 -5
- package/src/api/types.ts +1 -1
- package/src/auth/admin-routes.ts +58 -17
- package/src/auth/apple-oauth.ts +130 -0
- package/src/auth/bitbucket-oauth.ts +82 -0
- package/src/auth/discord-oauth.ts +83 -0
- package/src/auth/facebook-oauth.ts +72 -0
- package/src/auth/github-oauth.ts +110 -0
- package/src/auth/gitlab-oauth.ts +70 -0
- package/src/auth/google-oauth.ts +34 -46
- package/src/auth/index.ts +15 -2
- package/src/auth/interfaces.ts +54 -7
- package/src/auth/jwt.ts +22 -5
- package/src/auth/linkedin-oauth.ts +81 -0
- package/src/auth/microsoft-oauth.ts +88 -0
- package/src/auth/middleware.ts +194 -45
- package/src/auth/password.ts +3 -1
- package/src/auth/rate-limiter.ts +4 -4
- package/src/auth/routes.ts +197 -139
- package/src/auth/slack-oauth.ts +71 -0
- package/src/auth/spotify-oauth.ts +67 -0
- package/src/auth/twitter-oauth.ts +120 -0
- package/src/collections/loader.ts +6 -6
- package/src/cron/cron-loader.ts +89 -0
- package/src/cron/cron-routes.test.ts +265 -0
- package/src/cron/cron-routes.ts +85 -0
- package/src/cron/cron-scheduler.test.ts +421 -0
- package/src/cron/cron-scheduler.ts +413 -0
- package/src/cron/cron-store.ts +163 -0
- package/src/cron/index.ts +6 -0
- package/src/db/interfaces.ts +2 -2
- package/src/email/index.ts +3 -2
- package/src/email/smtp-email-service.ts +5 -2
- package/src/email/templates.ts +93 -6
- package/src/email/types.ts +17 -24
- package/src/functions/function-loader.ts +34 -6
- package/src/functions/function-routes.ts +2 -2
- package/src/history/history-routes.ts +4 -3
- package/src/index.ts +10 -0
- package/src/init.ts +475 -57
- package/src/services/driver-registry.ts +1 -1
- package/src/singleton.test.ts +28 -0
- package/src/singleton.ts +70 -0
- package/src/storage/LocalStorageController.ts +50 -53
- package/src/storage/S3StorageController.ts +49 -46
- package/src/storage/index.ts +27 -16
- package/src/storage/routes.ts +76 -59
- package/src/storage/storage-registry.ts +5 -5
- package/src/storage/types.ts +44 -32
- package/src/types/index.ts +4 -4
- package/src/utils/dev-port.ts +176 -0
- package/src/utils/logger.ts +143 -0
- package/src/utils/logging.ts +5 -2
- package/src/utils/request-logger.ts +66 -0
- package/test/admin-routes.test.ts +114 -65
- package/test/api-generator.test.ts +73 -30
- package/test/ast-schema-editor.test.ts +5 -3
- package/test/auth-middleware-hono.test.ts +268 -33
- package/test/auth-routes.test.ts +325 -146
- package/test/driver-registry.test.ts +4 -2
- package/test/error-propagation.test.ts +226 -0
- package/test/errors.test.ts +12 -7
- package/test/jwt-security.test.ts +18 -9
- package/test/jwt.test.ts +26 -13
- package/test/middleware.test.ts +13 -8
- package/test/query-parser.test.ts +13 -8
- package/test/rate-limiter.test.ts +7 -7
- package/test/safe-compare.test.ts +66 -0
- package/test/singleton.test.ts +59 -0
- package/test/storage-local.test.ts +36 -43
- package/test/storage-registry.test.ts +38 -36
- package/test/storage-routes.test.ts +9 -5
- package/test/storage-s3.test.ts +51 -48
- package/test.ts +6 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +7 -5
- package/dist/index-BeMqpmfQ.js +0 -239
- package/dist/index-BeMqpmfQ.js.map +0 -1
- package/dist/index-bl4J3lNb.js +0 -55823
- package/dist/index-bl4J3lNb.js.map +0 -1
- package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +0 -46
|
@@ -9,7 +9,7 @@ describe("Rate Limiter", () => {
|
|
|
9
9
|
const limiter = createRateLimiter({
|
|
10
10
|
windowMs: options.windowMs ?? 60 * 1000, // 1 minute
|
|
11
11
|
limit: options.limit ?? 3,
|
|
12
|
-
keyGenerator: (c) => c.req.header("x-forwarded-for") || "test-ip"
|
|
12
|
+
keyGenerator: (c) => c.req.header("x-forwarded-for") || "test-ip"
|
|
13
13
|
});
|
|
14
14
|
app.use("/api/*", limiter);
|
|
15
15
|
app.get("/api/test", (c) => c.json({ ok: true }));
|
|
@@ -20,7 +20,7 @@ describe("Rate Limiter", () => {
|
|
|
20
20
|
const app = createTestApp({ limit: 5 });
|
|
21
21
|
|
|
22
22
|
const res = await app.request("/api/test", {
|
|
23
|
-
headers: { "x-forwarded-for": "1.2.3.4" }
|
|
23
|
+
headers: { "x-forwarded-for": "1.2.3.4" }
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
expect(res.status).toBe(200);
|
|
@@ -37,7 +37,7 @@ describe("Rate Limiter", () => {
|
|
|
37
37
|
|
|
38
38
|
// Third should be rate limited
|
|
39
39
|
const res = await app.request("/api/test", {
|
|
40
|
-
headers: { "x-forwarded-for": "10.0.0.1" }
|
|
40
|
+
headers: { "x-forwarded-for": "10.0.0.1" }
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
expect(res.status).toBe(429);
|
|
@@ -50,7 +50,7 @@ describe("Rate Limiter", () => {
|
|
|
50
50
|
|
|
51
51
|
await app.request("/api/test", { headers: { "x-forwarded-for": "10.0.0.2" } });
|
|
52
52
|
const res = await app.request("/api/test", {
|
|
53
|
-
headers: { "x-forwarded-for": "10.0.0.2" }
|
|
53
|
+
headers: { "x-forwarded-for": "10.0.0.2" }
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
expect(res.headers.get("Retry-After")).toBeDefined();
|
|
@@ -61,10 +61,10 @@ describe("Rate Limiter", () => {
|
|
|
61
61
|
const app = createTestApp({ limit: 1 });
|
|
62
62
|
|
|
63
63
|
const res1 = await app.request("/api/test", {
|
|
64
|
-
headers: { "x-forwarded-for": "ip-a" }
|
|
64
|
+
headers: { "x-forwarded-for": "ip-a" }
|
|
65
65
|
});
|
|
66
66
|
const res2 = await app.request("/api/test", {
|
|
67
|
-
headers: { "x-forwarded-for": "ip-b" }
|
|
67
|
+
headers: { "x-forwarded-for": "ip-b" }
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
expect(res1.status).toBe(200);
|
|
@@ -90,7 +90,7 @@ describe("Rate Limiter", () => {
|
|
|
90
90
|
const limiter = createRateLimiter({
|
|
91
91
|
limit: 0,
|
|
92
92
|
message: "Slow down!",
|
|
93
|
-
keyGenerator: () => "always-same"
|
|
93
|
+
keyGenerator: () => "always-same"
|
|
94
94
|
});
|
|
95
95
|
app.use("/api/*", limiter);
|
|
96
96
|
app.get("/api/test", (c) => c.json({ ok: true }));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the safeCompare function used in auth middleware.
|
|
3
|
+
*
|
|
4
|
+
* Since safeCompare is not exported, we test it indirectly through
|
|
5
|
+
* the createAuthMiddleware behavior with service keys. However, for
|
|
6
|
+
* unit-level verification we replicate the logic here.
|
|
7
|
+
*/
|
|
8
|
+
import { timingSafeEqual } from "crypto";
|
|
9
|
+
|
|
10
|
+
// Replicate the safeCompare implementation to test in isolation
|
|
11
|
+
function safeCompare(a: string, b: string): boolean {
|
|
12
|
+
const maxLen = Math.max(a.length, b.length);
|
|
13
|
+
const bufA = Buffer.alloc(maxLen);
|
|
14
|
+
const bufB = Buffer.alloc(maxLen);
|
|
15
|
+
bufA.write(a);
|
|
16
|
+
bufB.write(b);
|
|
17
|
+
try {
|
|
18
|
+
const isEqual = timingSafeEqual(bufA, bufB);
|
|
19
|
+
return isEqual && a.length === b.length;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("safeCompare", () => {
|
|
26
|
+
it("should return true for identical strings", () => {
|
|
27
|
+
expect(safeCompare("my-secret-key-12345678901234567890", "my-secret-key-12345678901234567890")).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should return false for different strings of same length", () => {
|
|
31
|
+
expect(safeCompare("aaaa", "bbbb")).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should return false for different length strings", () => {
|
|
35
|
+
expect(safeCompare("short", "a-much-longer-string")).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should return false when one string is a prefix of the other", () => {
|
|
39
|
+
// This is the critical case: "abc" vs "abc\0\0\0" would match
|
|
40
|
+
// without the length check, since Buffer.alloc zero-fills
|
|
41
|
+
expect(safeCompare("abc", "abc\0\0\0")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should return false for empty string vs non-empty", () => {
|
|
45
|
+
expect(safeCompare("", "something")).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should return true for two empty strings", () => {
|
|
49
|
+
expect(safeCompare("", "")).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should handle unicode strings", () => {
|
|
53
|
+
expect(safeCompare("café", "café")).toBe(true);
|
|
54
|
+
expect(safeCompare("café", "cafe")).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should not leak length information via early return", () => {
|
|
58
|
+
// Both comparisons should take similar time (constant-time)
|
|
59
|
+
// We can't easily assert timing, but we verify they both
|
|
60
|
+
// go through the same code path
|
|
61
|
+
const result1 = safeCompare("a", "bb");
|
|
62
|
+
const result2 = safeCompare("aa", "bb");
|
|
63
|
+
expect(result1).toBe(false);
|
|
64
|
+
expect(result2).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { rebase, _initRebase, _setRebaseMock, _resetRebaseMock } from "../src/singleton";
|
|
2
|
+
import type { RebaseClient } from "@rebasepro/types";
|
|
3
|
+
|
|
4
|
+
describe("rebase singleton", () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
// Reset after each test to avoid interference
|
|
7
|
+
process.env.NODE_ENV = "test";
|
|
8
|
+
_resetRebaseMock();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should throw when accessing properties before initialization", () => {
|
|
12
|
+
expect(() => rebase.data).toThrowError(/server not initialized yet/);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should allow property access after initialization", () => {
|
|
16
|
+
const mockData = { find: jest.fn() };
|
|
17
|
+
_setRebaseMock({ data: mockData } as Partial<RebaseClient>);
|
|
18
|
+
|
|
19
|
+
expect(rebase.data).toBe(mockData);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should throw on property assignment (set trap)", () => {
|
|
23
|
+
_setRebaseMock({ data: {} } as Partial<RebaseClient>);
|
|
24
|
+
|
|
25
|
+
expect(() => {
|
|
26
|
+
(rebase as Record<string, unknown>).data = "overwritten";
|
|
27
|
+
}).toThrowError(/Cannot set rebase\.data directly/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should reset properly with _resetRebaseMock", () => {
|
|
31
|
+
_setRebaseMock({ data: {} } as Partial<RebaseClient>);
|
|
32
|
+
expect(() => rebase.data).not.toThrow();
|
|
33
|
+
|
|
34
|
+
_resetRebaseMock();
|
|
35
|
+
expect(() => rebase.data).toThrowError(/server not initialized yet/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should throw if _setRebaseMock is called outside test env", () => {
|
|
39
|
+
const originalEnv = process.env.NODE_ENV;
|
|
40
|
+
process.env.NODE_ENV = "production";
|
|
41
|
+
|
|
42
|
+
expect(() => _setRebaseMock({} as Partial<RebaseClient>)).toThrowError(
|
|
43
|
+
/can only be called in a test environment/
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
process.env.NODE_ENV = originalEnv;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should throw if _resetRebaseMock is called outside test env", () => {
|
|
50
|
+
const originalEnv = process.env.NODE_ENV;
|
|
51
|
+
process.env.NODE_ENV = "production";
|
|
52
|
+
|
|
53
|
+
expect(() => _resetRebaseMock()).toThrowError(
|
|
54
|
+
/can only be called in a test environment/
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
process.env.NODE_ENV = originalEnv;
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, jest as vi } from "@jest/globals";
|
|
1
2
|
import * as fs from "fs";
|
|
2
3
|
import * as path from "path";
|
|
3
4
|
import * as os from "os";
|
|
@@ -17,7 +18,8 @@ describe("LocalStorageController", () => {
|
|
|
17
18
|
|
|
18
19
|
afterEach(async () => {
|
|
19
20
|
// Clean up temporary directory
|
|
20
|
-
await fs.promises.rm(tempDir, { recursive: true,
|
|
21
|
+
await fs.promises.rm(tempDir, { recursive: true,
|
|
22
|
+
force: true });
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
describe("constructor", () => {
|
|
@@ -30,19 +32,18 @@ describe("LocalStorageController", () => {
|
|
|
30
32
|
});
|
|
31
33
|
});
|
|
32
34
|
|
|
33
|
-
describe("
|
|
35
|
+
describe("putObject", () => {
|
|
34
36
|
it("should upload a file and return metadata", async () => {
|
|
35
37
|
const content = Buffer.from("Hello, World!");
|
|
36
38
|
const file = new File([content], "test.txt", { type: "text/plain" });
|
|
37
39
|
|
|
38
|
-
const result = await controller.
|
|
40
|
+
const result = await controller.putObject({
|
|
39
41
|
file,
|
|
40
|
-
|
|
41
|
-
path: "uploads"
|
|
42
|
+
key: "uploads/test.txt"
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
expect(result.
|
|
45
|
-
expect(result.
|
|
45
|
+
expect(result.key).toContain("uploads");
|
|
46
|
+
expect(result.key).toContain("test.txt");
|
|
46
47
|
|
|
47
48
|
// Verify file exists on disk (uses default bucket)
|
|
48
49
|
const filePath = path.join(tempDir, "default", "uploads", "test.txt");
|
|
@@ -54,10 +55,9 @@ describe("LocalStorageController", () => {
|
|
|
54
55
|
const content = Buffer.from("Nested content");
|
|
55
56
|
const file = new File([content], "nested.txt", { type: "text/plain" });
|
|
56
57
|
|
|
57
|
-
await controller.
|
|
58
|
+
await controller.putObject({
|
|
58
59
|
file,
|
|
59
|
-
|
|
60
|
-
path: "level1/level2/level3"
|
|
60
|
+
key: "level1/level2/level3/nested.txt"
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
const filePath = path.join(tempDir, "default", "level1", "level2", "level3", "nested.txt");
|
|
@@ -69,10 +69,9 @@ describe("LocalStorageController", () => {
|
|
|
69
69
|
const content = Buffer.from("Bucket content");
|
|
70
70
|
const file = new File([content], "bucket.txt", { type: "text/plain" });
|
|
71
71
|
|
|
72
|
-
await controller.
|
|
72
|
+
await controller.putObject({
|
|
73
73
|
file,
|
|
74
|
-
|
|
75
|
-
path: "files",
|
|
74
|
+
key: "files/bucket.txt",
|
|
76
75
|
bucket: "custom-bucket"
|
|
77
76
|
});
|
|
78
77
|
|
|
@@ -85,10 +84,9 @@ describe("LocalStorageController", () => {
|
|
|
85
84
|
const content = Buffer.from("With metadata");
|
|
86
85
|
const file = new File([content], "meta.txt", { type: "text/plain" });
|
|
87
86
|
|
|
88
|
-
await controller.
|
|
87
|
+
await controller.putObject({
|
|
89
88
|
file,
|
|
90
|
-
|
|
91
|
-
path: "uploads",
|
|
89
|
+
key: "uploads/meta.txt",
|
|
92
90
|
metadata: { customField: "customValue" }
|
|
93
91
|
});
|
|
94
92
|
|
|
@@ -99,39 +97,37 @@ describe("LocalStorageController", () => {
|
|
|
99
97
|
});
|
|
100
98
|
});
|
|
101
99
|
|
|
102
|
-
describe("
|
|
100
|
+
describe("getObject", () => {
|
|
103
101
|
it("should retrieve an uploaded file using local:// URL format", async () => {
|
|
104
102
|
const content = Buffer.from("Retrieve me");
|
|
105
103
|
const file = new File([content], "retrieve.txt", { type: "text/plain" });
|
|
106
104
|
|
|
107
|
-
const uploadResult = await controller.
|
|
105
|
+
const uploadResult = await controller.putObject({
|
|
108
106
|
file,
|
|
109
|
-
|
|
110
|
-
path: "uploads"
|
|
107
|
+
key: "uploads/retrieve.txt"
|
|
111
108
|
});
|
|
112
109
|
|
|
113
110
|
// Use the storageUrl from upload result (local:// format)
|
|
114
|
-
const retrieved = await controller.
|
|
111
|
+
const retrieved = await controller.getObject(uploadResult.storageUrl!);
|
|
115
112
|
|
|
116
113
|
expect(retrieved).not.toBeNull();
|
|
117
114
|
expect(retrieved?.name).toBe("retrieve.txt");
|
|
118
115
|
});
|
|
119
116
|
|
|
120
117
|
it("should return null for non-existent file", async () => {
|
|
121
|
-
const result = await controller.
|
|
118
|
+
const result = await controller.getObject("local://default/nonexistent/file.txt");
|
|
122
119
|
expect(result).toBeNull();
|
|
123
120
|
});
|
|
124
121
|
});
|
|
125
122
|
|
|
126
|
-
describe("
|
|
123
|
+
describe("deleteObject", () => {
|
|
127
124
|
it("should delete an uploaded file using local:// URL format", async () => {
|
|
128
125
|
const content = Buffer.from("Delete me");
|
|
129
126
|
const file = new File([content], "delete.txt", { type: "text/plain" });
|
|
130
127
|
|
|
131
|
-
const uploadResult = await controller.
|
|
128
|
+
const uploadResult = await controller.putObject({
|
|
132
129
|
file,
|
|
133
|
-
|
|
134
|
-
path: "uploads"
|
|
130
|
+
key: "uploads/delete.txt"
|
|
135
131
|
});
|
|
136
132
|
|
|
137
133
|
// Verify file exists
|
|
@@ -140,7 +136,7 @@ describe("LocalStorageController", () => {
|
|
|
140
136
|
expect(exists).toBe(true);
|
|
141
137
|
|
|
142
138
|
// Delete the file using local:// URL format
|
|
143
|
-
await controller.
|
|
139
|
+
await controller.deleteObject(uploadResult.storageUrl!);
|
|
144
140
|
|
|
145
141
|
// Verify file no longer exists
|
|
146
142
|
exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
@@ -148,21 +144,20 @@ describe("LocalStorageController", () => {
|
|
|
148
144
|
});
|
|
149
145
|
|
|
150
146
|
it("should not throw when deleting non-existent file", async () => {
|
|
151
|
-
await expect(controller.
|
|
147
|
+
await expect(controller.deleteObject("local://default/nonexistent/file.txt")).resolves.not.toThrow();
|
|
152
148
|
});
|
|
153
149
|
|
|
154
150
|
it("should also delete metadata file", async () => {
|
|
155
151
|
const content = Buffer.from("Delete with metadata");
|
|
156
152
|
const file = new File([content], "withmeta.txt", { type: "text/plain" });
|
|
157
153
|
|
|
158
|
-
const uploadResult = await controller.
|
|
154
|
+
const uploadResult = await controller.putObject({
|
|
159
155
|
file,
|
|
160
|
-
|
|
161
|
-
path: "uploads",
|
|
156
|
+
key: "uploads/withmeta.txt",
|
|
162
157
|
metadata: { key: "value" }
|
|
163
158
|
});
|
|
164
159
|
|
|
165
|
-
await controller.
|
|
160
|
+
await controller.deleteObject(uploadResult.storageUrl!);
|
|
166
161
|
|
|
167
162
|
const metadataPath = path.join(tempDir, "default", "uploads", "withmeta.txt.metadata.json");
|
|
168
163
|
const exists = await fs.promises.access(metadataPath).then(() => true).catch(() => false);
|
|
@@ -175,48 +170,46 @@ describe("LocalStorageController", () => {
|
|
|
175
170
|
// Upload some test files
|
|
176
171
|
for (let i = 1; i <= 5; i++) {
|
|
177
172
|
const file = new File([`Content ${i}`], `file${i}.txt`, { type: "text/plain" });
|
|
178
|
-
await controller.
|
|
173
|
+
await controller.putObject({
|
|
179
174
|
file,
|
|
180
|
-
|
|
181
|
-
path: "listtest"
|
|
175
|
+
key: `listtest/file${i}.txt`
|
|
182
176
|
});
|
|
183
177
|
}
|
|
184
178
|
});
|
|
185
179
|
|
|
186
180
|
it("should list files in a directory", async () => {
|
|
187
|
-
const result = await controller.
|
|
181
|
+
const result = await controller.listObjects("listtest", { bucket: "default" });
|
|
188
182
|
|
|
189
183
|
// Items should be the actual files (not metadata files)
|
|
190
184
|
expect(result.items.length).toBeGreaterThanOrEqual(5);
|
|
191
185
|
});
|
|
192
186
|
|
|
193
187
|
it("should return empty list for non-existent directory", async () => {
|
|
194
|
-
const result = await controller.
|
|
188
|
+
const result = await controller.listObjects("nonexistent", { bucket: "default" });
|
|
195
189
|
|
|
196
190
|
expect(result.items).toHaveLength(0);
|
|
197
191
|
});
|
|
198
192
|
});
|
|
199
193
|
|
|
200
|
-
describe("
|
|
194
|
+
describe("getSignedUrl", () => {
|
|
201
195
|
it("should return download URL for existing file", async () => {
|
|
202
196
|
const content = Buffer.from("Download me");
|
|
203
197
|
const file = new File([content], "download.txt", { type: "text/plain" });
|
|
204
198
|
|
|
205
|
-
const uploadResult = await controller.
|
|
199
|
+
const uploadResult = await controller.putObject({
|
|
206
200
|
file,
|
|
207
|
-
|
|
208
|
-
path: "uploads"
|
|
201
|
+
key: "uploads/download.txt"
|
|
209
202
|
});
|
|
210
203
|
|
|
211
204
|
// Use the storageUrl from upload result
|
|
212
|
-
const result = await controller.
|
|
205
|
+
const result = await controller.getSignedUrl(uploadResult.storageUrl!);
|
|
213
206
|
|
|
214
207
|
expect(result.url).toBeTruthy();
|
|
215
208
|
expect(result.fileNotFound).toBeFalsy();
|
|
216
209
|
});
|
|
217
210
|
|
|
218
211
|
it("should return fileNotFound for non-existent file", async () => {
|
|
219
|
-
const result = await controller.
|
|
212
|
+
const result = await controller.getSignedUrl("local://default/nonexistent/file.txt");
|
|
220
213
|
|
|
221
214
|
expect(result.fileNotFound).toBe(true);
|
|
222
215
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, jest as vi } from "@jest/globals";
|
|
1
2
|
import {
|
|
2
3
|
StorageRegistry,
|
|
3
4
|
DefaultStorageRegistry,
|
|
@@ -8,14 +9,15 @@ import { StorageController } from "../src/storage/types";
|
|
|
8
9
|
/**
|
|
9
10
|
* Mock StorageController for testing
|
|
10
11
|
*/
|
|
11
|
-
function createMockStorageController(type:
|
|
12
|
+
function createMockStorageController(type: "local" | "s3"): StorageController {
|
|
12
13
|
return {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
putObject: vi.fn().mockResolvedValue({ path: "test/file.txt" }),
|
|
15
|
+
getSignedUrl: vi.fn().mockResolvedValue({ url: "http://example.com/file.txt" }),
|
|
16
|
+
getObject: vi.fn().mockResolvedValue(null),
|
|
17
|
+
deleteObject: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
listObjects: vi.fn().mockResolvedValue({ items: [],
|
|
19
|
+
prefixes: [] }),
|
|
20
|
+
getType: vi.fn().mockReturnValue(type)
|
|
19
21
|
};
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -36,7 +38,7 @@ describe("StorageRegistry", () => {
|
|
|
36
38
|
|
|
37
39
|
it("should register a storage controller", () => {
|
|
38
40
|
const registry = new DefaultStorageRegistry();
|
|
39
|
-
const mockController = createMockStorageController(
|
|
41
|
+
const mockController = createMockStorageController("local");
|
|
40
42
|
|
|
41
43
|
registry.register("test-storage", mockController);
|
|
42
44
|
|
|
@@ -47,7 +49,7 @@ describe("StorageRegistry", () => {
|
|
|
47
49
|
|
|
48
50
|
it("should get a registered storage controller", () => {
|
|
49
51
|
const registry = new DefaultStorageRegistry();
|
|
50
|
-
const mockController = createMockStorageController(
|
|
52
|
+
const mockController = createMockStorageController("s3");
|
|
51
53
|
|
|
52
54
|
registry.register("my-storage", mockController);
|
|
53
55
|
|
|
@@ -64,7 +66,7 @@ describe("StorageRegistry", () => {
|
|
|
64
66
|
describe("default storage handling", () => {
|
|
65
67
|
it("should get default storage with get(undefined)", () => {
|
|
66
68
|
const registry = new DefaultStorageRegistry();
|
|
67
|
-
const mockController = createMockStorageController(
|
|
69
|
+
const mockController = createMockStorageController("local");
|
|
68
70
|
|
|
69
71
|
registry.register(DEFAULT_STORAGE_ID, mockController);
|
|
70
72
|
|
|
@@ -74,7 +76,7 @@ describe("StorageRegistry", () => {
|
|
|
74
76
|
|
|
75
77
|
it("should get default storage with getDefault()", () => {
|
|
76
78
|
const registry = new DefaultStorageRegistry();
|
|
77
|
-
const mockController = createMockStorageController(
|
|
79
|
+
const mockController = createMockStorageController("local");
|
|
78
80
|
|
|
79
81
|
registry.register(DEFAULT_STORAGE_ID, mockController);
|
|
80
82
|
|
|
@@ -85,7 +87,7 @@ describe("StorageRegistry", () => {
|
|
|
85
87
|
const registry = new DefaultStorageRegistry();
|
|
86
88
|
|
|
87
89
|
expect(() => registry.getDefault()).toThrow(
|
|
88
|
-
|
|
90
|
+
"[StorageRegistry] No default storage registered."
|
|
89
91
|
);
|
|
90
92
|
});
|
|
91
93
|
});
|
|
@@ -97,8 +99,8 @@ describe("StorageRegistry", () => {
|
|
|
97
99
|
|
|
98
100
|
beforeEach(() => {
|
|
99
101
|
registry = new DefaultStorageRegistry();
|
|
100
|
-
defaultController = createMockStorageController(
|
|
101
|
-
mediaController = createMockStorageController(
|
|
102
|
+
defaultController = createMockStorageController("local");
|
|
103
|
+
mediaController = createMockStorageController("s3");
|
|
102
104
|
|
|
103
105
|
registry.register(DEFAULT_STORAGE_ID, defaultController);
|
|
104
106
|
registry.register("media", mediaController);
|
|
@@ -117,7 +119,7 @@ describe("StorageRegistry", () => {
|
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
it("should fallback to default when id not found", () => {
|
|
120
|
-
const consoleSpy =
|
|
122
|
+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation();
|
|
121
123
|
|
|
122
124
|
expect(registry.getOrDefault("non-existent")).toBe(defaultController);
|
|
123
125
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
@@ -137,10 +139,10 @@ describe("StorageRegistry", () => {
|
|
|
137
139
|
describe("overwriting storages", () => {
|
|
138
140
|
it("should overwrite existing storage with same id", () => {
|
|
139
141
|
const registry = new DefaultStorageRegistry();
|
|
140
|
-
const original = createMockStorageController(
|
|
141
|
-
const replacement = createMockStorageController(
|
|
142
|
+
const original = createMockStorageController("local");
|
|
143
|
+
const replacement = createMockStorageController("s3");
|
|
142
144
|
|
|
143
|
-
const consoleSpy =
|
|
145
|
+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation();
|
|
144
146
|
|
|
145
147
|
registry.register("my-storage", original);
|
|
146
148
|
registry.register("my-storage", replacement);
|
|
@@ -158,9 +160,9 @@ describe("StorageRegistry", () => {
|
|
|
158
160
|
it("should list all registered storages", () => {
|
|
159
161
|
const registry = new DefaultStorageRegistry();
|
|
160
162
|
|
|
161
|
-
registry.register("storage-1", createMockStorageController(
|
|
162
|
-
registry.register("storage-2", createMockStorageController(
|
|
163
|
-
registry.register(DEFAULT_STORAGE_ID, createMockStorageController(
|
|
163
|
+
registry.register("storage-1", createMockStorageController("local"));
|
|
164
|
+
registry.register("storage-2", createMockStorageController("s3"));
|
|
165
|
+
registry.register(DEFAULT_STORAGE_ID, createMockStorageController("local"));
|
|
164
166
|
|
|
165
167
|
const list = registry.list();
|
|
166
168
|
expect(list).toHaveLength(3);
|
|
@@ -174,10 +176,10 @@ describe("StorageRegistry", () => {
|
|
|
174
176
|
|
|
175
177
|
expect(registry.size()).toBe(0);
|
|
176
178
|
|
|
177
|
-
registry.register("storage-1", createMockStorageController(
|
|
179
|
+
registry.register("storage-1", createMockStorageController("local"));
|
|
178
180
|
expect(registry.size()).toBe(1);
|
|
179
181
|
|
|
180
|
-
registry.register("storage-2", createMockStorageController(
|
|
182
|
+
registry.register("storage-2", createMockStorageController("s3"));
|
|
181
183
|
expect(registry.size()).toBe(2);
|
|
182
184
|
});
|
|
183
185
|
});
|
|
@@ -186,7 +188,7 @@ describe("StorageRegistry", () => {
|
|
|
186
188
|
describe("DefaultStorageRegistry.create() factory", () => {
|
|
187
189
|
describe("with single StorageController", () => {
|
|
188
190
|
it('should register single controller as "(default)"', () => {
|
|
189
|
-
const mockController = createMockStorageController(
|
|
191
|
+
const mockController = createMockStorageController("local");
|
|
190
192
|
|
|
191
193
|
const registry = DefaultStorageRegistry.create(mockController);
|
|
192
194
|
|
|
@@ -198,8 +200,8 @@ describe("StorageRegistry", () => {
|
|
|
198
200
|
|
|
199
201
|
describe("with map of StorageControllers", () => {
|
|
200
202
|
it("should register all controllers from map", () => {
|
|
201
|
-
const defaultController = createMockStorageController(
|
|
202
|
-
const mediaController = createMockStorageController(
|
|
203
|
+
const defaultController = createMockStorageController("local");
|
|
204
|
+
const mediaController = createMockStorageController("s3");
|
|
203
205
|
|
|
204
206
|
const registry = DefaultStorageRegistry.create({
|
|
205
207
|
[DEFAULT_STORAGE_ID]: defaultController,
|
|
@@ -212,10 +214,10 @@ describe("StorageRegistry", () => {
|
|
|
212
214
|
});
|
|
213
215
|
|
|
214
216
|
it("should use first entry as default if no explicit default provided", () => {
|
|
215
|
-
const local = createMockStorageController(
|
|
216
|
-
const s3 = createMockStorageController(
|
|
217
|
+
const local = createMockStorageController("local");
|
|
218
|
+
const s3 = createMockStorageController("s3");
|
|
217
219
|
|
|
218
|
-
const consoleSpy =
|
|
220
|
+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation();
|
|
219
221
|
|
|
220
222
|
const registry = DefaultStorageRegistry.create({
|
|
221
223
|
"primary": local,
|
|
@@ -243,7 +245,7 @@ describe("StorageRegistry", () => {
|
|
|
243
245
|
|
|
244
246
|
describe("type detection (isStorageController)", () => {
|
|
245
247
|
it("should correctly identify a StorageController", () => {
|
|
246
|
-
const mockController = createMockStorageController(
|
|
248
|
+
const mockController = createMockStorageController("local");
|
|
247
249
|
|
|
248
250
|
// The factory should recognize it as a single controller
|
|
249
251
|
const registry = DefaultStorageRegistry.create(mockController);
|
|
@@ -253,8 +255,8 @@ describe("StorageRegistry", () => {
|
|
|
253
255
|
|
|
254
256
|
it("should correctly identify a map of StorageControllers", () => {
|
|
255
257
|
const controllers = {
|
|
256
|
-
[DEFAULT_STORAGE_ID]: createMockStorageController(
|
|
257
|
-
"other": createMockStorageController(
|
|
258
|
+
[DEFAULT_STORAGE_ID]: createMockStorageController("local"),
|
|
259
|
+
"other": createMockStorageController("s3")
|
|
258
260
|
};
|
|
259
261
|
|
|
260
262
|
// The factory should recognize it as a map
|
|
@@ -265,16 +267,16 @@ describe("StorageRegistry", () => {
|
|
|
265
267
|
|
|
266
268
|
describe("integration with storage types", () => {
|
|
267
269
|
it("should correctly report storage types", () => {
|
|
268
|
-
const localController = createMockStorageController(
|
|
269
|
-
const s3Controller = createMockStorageController(
|
|
270
|
+
const localController = createMockStorageController("local");
|
|
271
|
+
const s3Controller = createMockStorageController("s3");
|
|
270
272
|
|
|
271
273
|
const registry = DefaultStorageRegistry.create({
|
|
272
274
|
[DEFAULT_STORAGE_ID]: localController,
|
|
273
275
|
"media": s3Controller
|
|
274
276
|
});
|
|
275
277
|
|
|
276
|
-
expect(registry.getDefault().getType()).toBe(
|
|
277
|
-
expect(registry.get("media")?.getType()).toBe(
|
|
278
|
+
expect(registry.getDefault().getType()).toBe("local");
|
|
279
|
+
expect(registry.get("media")?.getType()).toBe("s3");
|
|
278
280
|
});
|
|
279
281
|
});
|
|
280
282
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, jest as vi } from "@jest/globals";
|
|
1
2
|
/**
|
|
2
3
|
* Tests for storage routes — specifically the sub-router wildcard extraction
|
|
3
4
|
* that broke when Hono's c.req.param('*') stopped working in mounted sub-routers.
|
|
@@ -96,21 +97,23 @@ describe("Storage routes (sub-router integration)", () => {
|
|
|
96
97
|
// Upload a test file so we have something to serve
|
|
97
98
|
const content = Buffer.from("Hello test file");
|
|
98
99
|
const file = new File([content], "test.txt", { type: "text/plain" });
|
|
99
|
-
await controller.
|
|
100
|
+
await controller.putObject({ file,
|
|
101
|
+
key: "photos/test.txt" });
|
|
100
102
|
|
|
101
103
|
// Create the Hono app with storage routes mounted as a SUB-ROUTER
|
|
102
104
|
// (this is the exact pattern that caused the bug)
|
|
103
105
|
app = new Hono<HonoEnv>();
|
|
104
|
-
app.onError(errorHandler);
|
|
106
|
+
app.onError(errorHandler); // required to convert ApiError throws to proper HTTP responses
|
|
105
107
|
const storageRoutes = createStorageRoutes({
|
|
106
108
|
controller,
|
|
107
|
-
requireAuth: false
|
|
109
|
+
requireAuth: false // skip auth for tests
|
|
108
110
|
});
|
|
109
111
|
app.route("/api/storage", storageRoutes);
|
|
110
112
|
});
|
|
111
113
|
|
|
112
114
|
afterEach(async () => {
|
|
113
|
-
await fs.promises.rm(tempDir, { recursive: true,
|
|
115
|
+
await fs.promises.rm(tempDir, { recursive: true,
|
|
116
|
+
force: true });
|
|
114
117
|
});
|
|
115
118
|
|
|
116
119
|
describe("GET /metadata/*", () => {
|
|
@@ -192,7 +195,8 @@ describe("Storage routes (sub-router integration)", () => {
|
|
|
192
195
|
it("should delete an existing file", async () => {
|
|
193
196
|
// Upload another file to delete
|
|
194
197
|
const file = new File([Buffer.from("delete me")], "deleteme.txt", { type: "text/plain" });
|
|
195
|
-
await controller.
|
|
198
|
+
await controller.putObject({ file,
|
|
199
|
+
key: "photos/deleteme.txt" });
|
|
196
200
|
|
|
197
201
|
const res = await app.fetch(
|
|
198
202
|
new Request("http://localhost/api/storage/file/default/photos/deleteme.txt", { method: "DELETE" })
|