@qwickapps/server 1.3.1 → 1.4.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.
- package/README.md +157 -0
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +114 -0
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/types.d.ts +19 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
- package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.js +166 -0
- package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
- package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
- package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
- package/dist/plugins/auth/config-store.d.ts +11 -0
- package/dist/plugins/auth/config-store.d.ts.map +1 -0
- package/dist/plugins/auth/config-store.js +232 -0
- package/dist/plugins/auth/config-store.js.map +1 -0
- package/dist/plugins/auth/config-store.test.d.ts +7 -0
- package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
- package/dist/plugins/auth/config-store.test.js +299 -0
- package/dist/plugins/auth/config-store.test.js.map +1 -0
- package/dist/plugins/auth/env-config.d.ts +51 -1
- package/dist/plugins/auth/env-config.d.ts.map +1 -1
- package/dist/plugins/auth/env-config.js +640 -7
- package/dist/plugins/auth/env-config.js.map +1 -1
- package/dist/plugins/auth/index.d.ts +6 -2
- package/dist/plugins/auth/index.d.ts.map +1 -1
- package/dist/plugins/auth/index.js +5 -1
- package/dist/plugins/auth/index.js.map +1 -1
- package/dist/plugins/auth/types.d.ts +106 -0
- package/dist/plugins/auth/types.d.ts.map +1 -1
- package/dist/plugins/index.d.ts +4 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
- package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
- package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
- package/dist/plugins/rate-limit/cleanup.js +72 -0
- package/dist/plugins/rate-limit/cleanup.js.map +1 -0
- package/dist/plugins/rate-limit/env-config.d.ts +91 -0
- package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
- package/dist/plugins/rate-limit/env-config.js +318 -0
- package/dist/plugins/rate-limit/env-config.js.map +1 -0
- package/dist/plugins/rate-limit/index.d.ts +76 -0
- package/dist/plugins/rate-limit/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/index.js +79 -0
- package/dist/plugins/rate-limit/index.js.map +1 -0
- package/dist/plugins/rate-limit/middleware.d.ts +40 -0
- package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
- package/dist/plugins/rate-limit/middleware.js +169 -0
- package/dist/plugins/rate-limit/middleware.js.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
- package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
- package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
- package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
- package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
- package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
- package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
- package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/index.js +8 -0
- package/dist/plugins/rate-limit/stores/index.js.map +1 -0
- package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
- package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
- package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
- package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/index.js +27 -0
- package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
- package/dist/plugins/rate-limit/types.d.ts +265 -0
- package/dist/plugins/rate-limit/types.d.ts.map +1 -0
- package/dist/plugins/rate-limit/types.js +9 -0
- package/dist/plugins/rate-limit/types.js.map +1 -0
- package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
- package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +141 -0
- package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
- package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
- package/dist-ui-lib/index.js +3332 -2343
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
- package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/control-panel.ts +128 -0
- package/src/core/types.ts +17 -0
- package/src/index.ts +38 -0
- package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
- package/src/plugins/auth/adapter-wrapper.ts +205 -0
- package/src/plugins/auth/config-store.test.ts +417 -0
- package/src/plugins/auth/config-store.ts +305 -0
- package/src/plugins/auth/env-config.ts +714 -7
- package/src/plugins/auth/index.ts +22 -1
- package/src/plugins/auth/types.ts +138 -0
- package/src/plugins/index.ts +49 -0
- package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
- package/src/plugins/rate-limit/cleanup.ts +117 -0
- package/src/plugins/rate-limit/env-config.ts +400 -0
- package/src/plugins/rate-limit/index.ts +128 -0
- package/src/plugins/rate-limit/middleware.ts +212 -0
- package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
- package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
- package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
- package/src/plugins/rate-limit/stores/index.ts +8 -0
- package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
- package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
- package/src/plugins/rate-limit/strategies/index.ts +30 -0
- package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
- package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
- package/src/plugins/rate-limit/types.ts +338 -0
- package/ui/src/App.tsx +32 -14
- package/ui/src/api/controlPanelApi.ts +226 -0
- package/ui/src/dashboard/builtInWidgets.tsx +5 -1
- package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
- package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
- package/ui/src/dashboard/widgets/index.ts +2 -0
- package/ui/src/pages/AuthPage.tsx +986 -142
- package/ui/src/pages/IntegrationsPage.tsx +288 -0
- package/ui/src/pages/RateLimitPage.tsx +292 -0
- package/dist-ui/assets/index-BY8OxNgO.js +0 -465
- package/dist-ui/assets/index-BY8OxNgO.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function IntegrationsPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function RateLimitPage(): import("react/jsx-runtime").JSX.Element;
|
package/package.json
CHANGED
|
@@ -320,6 +320,22 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
// Serve landing page at root (/) when control panel is mounted elsewhere
|
|
324
|
+
if (mountPath !== '/' && config.landingPage !== false) {
|
|
325
|
+
const landingConfig = config.landingPage || {};
|
|
326
|
+
app.get('/', (_req: Request, res: Response) => {
|
|
327
|
+
const html = generateLandingPageHtml({
|
|
328
|
+
productName: config.productName,
|
|
329
|
+
title: landingConfig.title || config.productName,
|
|
330
|
+
heading: landingConfig.heading || `Welcome to ${config.productName}`,
|
|
331
|
+
description: landingConfig.description || `${config.productName} is running.`,
|
|
332
|
+
controlPanelPath: mountPath,
|
|
333
|
+
links: landingConfig.links,
|
|
334
|
+
});
|
|
335
|
+
res.type('html').send(html);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
323
339
|
// Start a plugin with the registry
|
|
324
340
|
const startPlugin = async (plugin: Plugin, pluginConfig: PluginConfig = {}): Promise<boolean> => {
|
|
325
341
|
return pluginRegistry.startPlugin(plugin, pluginConfig);
|
|
@@ -564,3 +580,115 @@ function generateDashboardHtml(
|
|
|
564
580
|
</body>
|
|
565
581
|
</html>`;
|
|
566
582
|
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Generate landing page HTML for root path
|
|
586
|
+
*/
|
|
587
|
+
function generateLandingPageHtml(options: {
|
|
588
|
+
productName: string;
|
|
589
|
+
title: string;
|
|
590
|
+
heading: string;
|
|
591
|
+
description: string;
|
|
592
|
+
controlPanelPath: string;
|
|
593
|
+
links?: Array<{ label: string; url: string }>;
|
|
594
|
+
}): string {
|
|
595
|
+
const { productName, title, heading, description, controlPanelPath, links = [] } = options;
|
|
596
|
+
|
|
597
|
+
const linksHtml = [
|
|
598
|
+
{ label: 'Control Panel', url: controlPanelPath },
|
|
599
|
+
...links,
|
|
600
|
+
]
|
|
601
|
+
.map(
|
|
602
|
+
(link) =>
|
|
603
|
+
`<a href="${link.url}" class="link">${link.label}</a>`
|
|
604
|
+
)
|
|
605
|
+
.join('');
|
|
606
|
+
|
|
607
|
+
return `<!DOCTYPE html>
|
|
608
|
+
<html lang="en">
|
|
609
|
+
<head>
|
|
610
|
+
<meta charset="UTF-8">
|
|
611
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
612
|
+
<title>${title}</title>
|
|
613
|
+
<style>
|
|
614
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
615
|
+
body {
|
|
616
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
617
|
+
background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
|
|
618
|
+
color: #e2e8f0;
|
|
619
|
+
min-height: 100vh;
|
|
620
|
+
display: flex;
|
|
621
|
+
align-items: center;
|
|
622
|
+
justify-content: center;
|
|
623
|
+
}
|
|
624
|
+
.container {
|
|
625
|
+
text-align: center;
|
|
626
|
+
max-width: 600px;
|
|
627
|
+
padding: 2rem;
|
|
628
|
+
}
|
|
629
|
+
.logo {
|
|
630
|
+
width: 80px;
|
|
631
|
+
height: 80px;
|
|
632
|
+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
633
|
+
border-radius: 20px;
|
|
634
|
+
margin: 0 auto 2rem;
|
|
635
|
+
display: flex;
|
|
636
|
+
align-items: center;
|
|
637
|
+
justify-content: center;
|
|
638
|
+
font-size: 2rem;
|
|
639
|
+
font-weight: bold;
|
|
640
|
+
}
|
|
641
|
+
h1 {
|
|
642
|
+
font-size: 2.5rem;
|
|
643
|
+
margin-bottom: 1rem;
|
|
644
|
+
background: linear-gradient(135deg, #6366f1 0%, #a78bfa 100%);
|
|
645
|
+
-webkit-background-clip: text;
|
|
646
|
+
-webkit-text-fill-color: transparent;
|
|
647
|
+
background-clip: text;
|
|
648
|
+
}
|
|
649
|
+
p {
|
|
650
|
+
color: #94a3b8;
|
|
651
|
+
font-size: 1.125rem;
|
|
652
|
+
margin-bottom: 2rem;
|
|
653
|
+
line-height: 1.6;
|
|
654
|
+
}
|
|
655
|
+
.links {
|
|
656
|
+
display: flex;
|
|
657
|
+
flex-wrap: wrap;
|
|
658
|
+
gap: 1rem;
|
|
659
|
+
justify-content: center;
|
|
660
|
+
}
|
|
661
|
+
.link {
|
|
662
|
+
display: inline-block;
|
|
663
|
+
padding: 0.875rem 2rem;
|
|
664
|
+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
665
|
+
color: white;
|
|
666
|
+
text-decoration: none;
|
|
667
|
+
border-radius: 0.75rem;
|
|
668
|
+
font-weight: 600;
|
|
669
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
670
|
+
}
|
|
671
|
+
.link:hover {
|
|
672
|
+
transform: translateY(-2px);
|
|
673
|
+
box-shadow: 0 10px 25px -5px rgba(99, 102, 241, 0.4);
|
|
674
|
+
}
|
|
675
|
+
.footer {
|
|
676
|
+
margin-top: 3rem;
|
|
677
|
+
color: #475569;
|
|
678
|
+
font-size: 0.875rem;
|
|
679
|
+
}
|
|
680
|
+
</style>
|
|
681
|
+
</head>
|
|
682
|
+
<body>
|
|
683
|
+
<div class="container">
|
|
684
|
+
<div class="logo">${productName.charAt(0)}</div>
|
|
685
|
+
<h1>${heading}</h1>
|
|
686
|
+
<p>${description}</p>
|
|
687
|
+
<div class="links">${linksHtml}</div>
|
|
688
|
+
<div class="footer">
|
|
689
|
+
Powered by QwickApps Server
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
</body>
|
|
693
|
+
</html>`;
|
|
694
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -175,6 +175,23 @@ export interface ControlPanelConfig {
|
|
|
175
175
|
|
|
176
176
|
/** Optional: Custom path to a dist-ui folder for serving a custom React UI */
|
|
177
177
|
customUiPath?: string;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Optional: Landing page configuration for root path (/).
|
|
181
|
+
* Only used when mountPath is not '/'.
|
|
182
|
+
* If not provided, a default landing page is generated.
|
|
183
|
+
* Set to false to disable the landing page.
|
|
184
|
+
*/
|
|
185
|
+
landingPage?: {
|
|
186
|
+
/** Page title */
|
|
187
|
+
title?: string;
|
|
188
|
+
/** Main heading */
|
|
189
|
+
heading?: string;
|
|
190
|
+
/** Description text */
|
|
191
|
+
description?: string;
|
|
192
|
+
/** Additional links */
|
|
193
|
+
links?: Array<{ label: string; url: string }>;
|
|
194
|
+
} | false;
|
|
178
195
|
}
|
|
179
196
|
|
|
180
197
|
// Plugin types are now in plugin-registry.ts
|
package/src/index.ts
CHANGED
|
@@ -91,6 +91,8 @@ export {
|
|
|
91
91
|
createAuthPlugin,
|
|
92
92
|
createAuthPluginFromEnv,
|
|
93
93
|
getAuthStatus,
|
|
94
|
+
setAuthConfigStore,
|
|
95
|
+
postgresAuthConfigStore,
|
|
94
96
|
isAuthenticated,
|
|
95
97
|
getAuthenticatedUser,
|
|
96
98
|
getAccessToken,
|
|
@@ -140,6 +142,28 @@ export {
|
|
|
140
142
|
requireAnyEntitlement,
|
|
141
143
|
requireAllEntitlements,
|
|
142
144
|
postgresEntitlementSource,
|
|
145
|
+
// Rate Limit plugin
|
|
146
|
+
createRateLimitPlugin,
|
|
147
|
+
createRateLimitPluginFromEnv,
|
|
148
|
+
getRateLimitConfigStatus,
|
|
149
|
+
postgresRateLimitStore,
|
|
150
|
+
createRateLimitCache,
|
|
151
|
+
createNoOpCache,
|
|
152
|
+
createSlidingWindowStrategy,
|
|
153
|
+
createFixedWindowStrategy,
|
|
154
|
+
createTokenBucketStrategy,
|
|
155
|
+
getStrategy,
|
|
156
|
+
rateLimitMiddleware,
|
|
157
|
+
rateLimitStatusMiddleware,
|
|
158
|
+
RateLimitService,
|
|
159
|
+
getRateLimitService,
|
|
160
|
+
isLimited,
|
|
161
|
+
checkLimit,
|
|
162
|
+
incrementLimit,
|
|
163
|
+
getRemainingRequests,
|
|
164
|
+
getLimitStatus,
|
|
165
|
+
clearLimit,
|
|
166
|
+
createCleanupJob,
|
|
143
167
|
} from './plugins/index.js';
|
|
144
168
|
export type {
|
|
145
169
|
HealthPluginConfig,
|
|
@@ -166,6 +190,8 @@ export type {
|
|
|
166
190
|
AuthPluginState,
|
|
167
191
|
AuthEnvPluginOptions,
|
|
168
192
|
AuthConfigStatus,
|
|
193
|
+
AuthConfigStore,
|
|
194
|
+
PostgresAuthConfigStoreConfig,
|
|
169
195
|
// Users plugin types
|
|
170
196
|
UsersPluginConfig,
|
|
171
197
|
UserStore,
|
|
@@ -198,4 +224,16 @@ export type {
|
|
|
198
224
|
UserEntitlement,
|
|
199
225
|
CachedEntitlements,
|
|
200
226
|
EntitlementStats,
|
|
227
|
+
// Rate Limit plugin types
|
|
228
|
+
RateLimitPluginConfig,
|
|
229
|
+
RateLimitEnvPluginOptions,
|
|
230
|
+
RateLimitStrategy,
|
|
231
|
+
LimitStatus,
|
|
232
|
+
StoredLimit,
|
|
233
|
+
IncrementOptions,
|
|
234
|
+
RateLimitStore,
|
|
235
|
+
PostgresRateLimitStoreConfig,
|
|
236
|
+
RateLimitCache,
|
|
237
|
+
RateLimitCacheConfig,
|
|
238
|
+
RateLimitMiddlewareOptions,
|
|
201
239
|
} from './plugins/index.js';
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Adapter Wrapper Tests
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the hot-reload adapter wrapper.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
9
|
+
import { createAdapterWrapper } from './adapter-wrapper.js';
|
|
10
|
+
import type { AuthAdapter, AuthenticatedUser } from './types.js';
|
|
11
|
+
|
|
12
|
+
// Helper to create a mock adapter
|
|
13
|
+
function createMockAdapter(name: string, overrides: Partial<AuthAdapter> = {}): AuthAdapter {
|
|
14
|
+
return {
|
|
15
|
+
name,
|
|
16
|
+
initialize: vi.fn().mockReturnValue((_req: Request, _res: Response, next: NextFunction) => next()),
|
|
17
|
+
isAuthenticated: vi.fn().mockReturnValue(false),
|
|
18
|
+
getUser: vi.fn().mockReturnValue(null),
|
|
19
|
+
hasRoles: vi.fn().mockReturnValue(false),
|
|
20
|
+
getAccessToken: vi.fn().mockReturnValue(null),
|
|
21
|
+
onUnauthorized: vi.fn(),
|
|
22
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Helper to create mock request/response
|
|
28
|
+
function createMockReq(): Request {
|
|
29
|
+
return {} as Request;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createMockRes(): Response {
|
|
33
|
+
return {
|
|
34
|
+
status: vi.fn().mockReturnThis(),
|
|
35
|
+
json: vi.fn().mockReturnThis(),
|
|
36
|
+
} as unknown as Response;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('createAdapterWrapper', () => {
|
|
40
|
+
describe('initialization', () => {
|
|
41
|
+
it('should create wrapper with no-op adapter by default', () => {
|
|
42
|
+
const wrapper = createAdapterWrapper();
|
|
43
|
+
|
|
44
|
+
expect(wrapper.name).toBe('none');
|
|
45
|
+
expect(wrapper.hasAdapter()).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should create wrapper with initial adapter', () => {
|
|
49
|
+
const mockAdapter = createMockAdapter('test');
|
|
50
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
51
|
+
|
|
52
|
+
expect(wrapper.name).toBe('test');
|
|
53
|
+
expect(wrapper.hasAdapter()).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return adapter info', () => {
|
|
57
|
+
const mockAdapter = createMockAdapter('test');
|
|
58
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
59
|
+
|
|
60
|
+
const info = wrapper.getAdapterInfo();
|
|
61
|
+
|
|
62
|
+
expect(info.name).toBe('test');
|
|
63
|
+
expect(info.initialized).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('initialize', () => {
|
|
68
|
+
it('should call underlying adapter initialize', () => {
|
|
69
|
+
const mockAdapter = createMockAdapter('test');
|
|
70
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
71
|
+
|
|
72
|
+
wrapper.initialize();
|
|
73
|
+
|
|
74
|
+
expect(mockAdapter.initialize).toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should mark wrapper as initialized', () => {
|
|
78
|
+
const mockAdapter = createMockAdapter('test');
|
|
79
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
80
|
+
|
|
81
|
+
wrapper.initialize();
|
|
82
|
+
|
|
83
|
+
expect(wrapper.getAdapterInfo().initialized).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return middleware from underlying adapter', () => {
|
|
87
|
+
const mockMiddleware = vi.fn();
|
|
88
|
+
const mockAdapter = createMockAdapter('test', {
|
|
89
|
+
initialize: vi.fn().mockReturnValue(mockMiddleware),
|
|
90
|
+
});
|
|
91
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
92
|
+
|
|
93
|
+
const result = wrapper.initialize();
|
|
94
|
+
|
|
95
|
+
expect(result).toBe(mockMiddleware);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('isAuthenticated', () => {
|
|
100
|
+
it('should delegate to current adapter', () => {
|
|
101
|
+
const mockAdapter = createMockAdapter('test', {
|
|
102
|
+
isAuthenticated: vi.fn().mockReturnValue(true),
|
|
103
|
+
});
|
|
104
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
105
|
+
const req = createMockReq();
|
|
106
|
+
|
|
107
|
+
const result = wrapper.isAuthenticated(req);
|
|
108
|
+
|
|
109
|
+
expect(mockAdapter.isAuthenticated).toHaveBeenCalledWith(req);
|
|
110
|
+
expect(result).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should use new adapter after swap', async () => {
|
|
114
|
+
const oldAdapter = createMockAdapter('old', {
|
|
115
|
+
isAuthenticated: vi.fn().mockReturnValue(false),
|
|
116
|
+
});
|
|
117
|
+
const newAdapter = createMockAdapter('new', {
|
|
118
|
+
isAuthenticated: vi.fn().mockReturnValue(true),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const wrapper = createAdapterWrapper(oldAdapter);
|
|
122
|
+
await wrapper.setAdapter(newAdapter);
|
|
123
|
+
|
|
124
|
+
const req = createMockReq();
|
|
125
|
+
const result = wrapper.isAuthenticated(req);
|
|
126
|
+
|
|
127
|
+
expect(result).toBe(true);
|
|
128
|
+
expect(newAdapter.isAuthenticated).toHaveBeenCalledWith(req);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('getUser', () => {
|
|
133
|
+
it('should delegate to current adapter', () => {
|
|
134
|
+
const mockUser: AuthenticatedUser = {
|
|
135
|
+
id: '123',
|
|
136
|
+
email: 'test@example.com',
|
|
137
|
+
name: 'Test User',
|
|
138
|
+
};
|
|
139
|
+
const mockAdapter = createMockAdapter('test', {
|
|
140
|
+
getUser: vi.fn().mockReturnValue(mockUser),
|
|
141
|
+
});
|
|
142
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
143
|
+
const req = createMockReq();
|
|
144
|
+
|
|
145
|
+
const result = wrapper.getUser(req);
|
|
146
|
+
|
|
147
|
+
expect(mockAdapter.getUser).toHaveBeenCalledWith(req);
|
|
148
|
+
expect(result).toEqual(mockUser);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return null for no-op adapter', () => {
|
|
152
|
+
const wrapper = createAdapterWrapper();
|
|
153
|
+
const req = createMockReq();
|
|
154
|
+
|
|
155
|
+
const result = wrapper.getUser(req);
|
|
156
|
+
|
|
157
|
+
expect(result).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('hasRoles', () => {
|
|
162
|
+
it('should delegate to current adapter if method exists', () => {
|
|
163
|
+
const mockAdapter = createMockAdapter('test', {
|
|
164
|
+
hasRoles: vi.fn().mockReturnValue(true),
|
|
165
|
+
});
|
|
166
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
167
|
+
const req = createMockReq();
|
|
168
|
+
|
|
169
|
+
// Wrapper always implements hasRoles
|
|
170
|
+
const result = wrapper.hasRoles!(req, ['admin']);
|
|
171
|
+
|
|
172
|
+
expect(mockAdapter.hasRoles).toHaveBeenCalledWith(req, ['admin']);
|
|
173
|
+
expect(result).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should return false if adapter has no hasRoles method', () => {
|
|
177
|
+
const mockAdapter = createMockAdapter('test');
|
|
178
|
+
delete (mockAdapter as Partial<AuthAdapter>).hasRoles;
|
|
179
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
180
|
+
const req = createMockReq();
|
|
181
|
+
|
|
182
|
+
// Wrapper always implements hasRoles, even if underlying adapter doesn't
|
|
183
|
+
const result = wrapper.hasRoles!(req, ['admin']);
|
|
184
|
+
|
|
185
|
+
expect(result).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('getAccessToken', () => {
|
|
190
|
+
it('should delegate to current adapter if method exists', () => {
|
|
191
|
+
const mockAdapter = createMockAdapter('test', {
|
|
192
|
+
getAccessToken: vi.fn().mockReturnValue('token123'),
|
|
193
|
+
});
|
|
194
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
195
|
+
const req = createMockReq();
|
|
196
|
+
|
|
197
|
+
// Wrapper always implements getAccessToken
|
|
198
|
+
const result = wrapper.getAccessToken!(req);
|
|
199
|
+
|
|
200
|
+
expect(mockAdapter.getAccessToken).toHaveBeenCalledWith(req);
|
|
201
|
+
expect(result).toBe('token123');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should return null if adapter has no getAccessToken method', () => {
|
|
205
|
+
const mockAdapter = createMockAdapter('test');
|
|
206
|
+
delete (mockAdapter as Partial<AuthAdapter>).getAccessToken;
|
|
207
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
208
|
+
const req = createMockReq();
|
|
209
|
+
|
|
210
|
+
// Wrapper always implements getAccessToken, returns null when adapter doesn't have it
|
|
211
|
+
const result = wrapper.getAccessToken!(req);
|
|
212
|
+
|
|
213
|
+
expect(result).toBeNull();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('onUnauthorized', () => {
|
|
218
|
+
it('should delegate to current adapter if method exists', () => {
|
|
219
|
+
const mockAdapter = createMockAdapter('test');
|
|
220
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
221
|
+
const req = createMockReq();
|
|
222
|
+
const res = createMockRes();
|
|
223
|
+
|
|
224
|
+
// Wrapper always implements onUnauthorized
|
|
225
|
+
wrapper.onUnauthorized!(req, res);
|
|
226
|
+
|
|
227
|
+
expect(mockAdapter.onUnauthorized).toHaveBeenCalledWith(req, res);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should return 401 JSON response if adapter has no onUnauthorized', () => {
|
|
231
|
+
const mockAdapter = createMockAdapter('test');
|
|
232
|
+
delete (mockAdapter as Partial<AuthAdapter>).onUnauthorized;
|
|
233
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
234
|
+
const req = createMockReq();
|
|
235
|
+
const res = createMockRes();
|
|
236
|
+
|
|
237
|
+
// Wrapper always implements onUnauthorized with default behavior
|
|
238
|
+
wrapper.onUnauthorized!(req, res);
|
|
239
|
+
|
|
240
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
241
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
242
|
+
error: 'Unauthorized',
|
|
243
|
+
message: 'Authentication required',
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('setAdapter (hot-reload)', () => {
|
|
249
|
+
it('should swap adapter successfully', async () => {
|
|
250
|
+
const oldAdapter = createMockAdapter('old');
|
|
251
|
+
const newAdapter = createMockAdapter('new');
|
|
252
|
+
const wrapper = createAdapterWrapper(oldAdapter);
|
|
253
|
+
|
|
254
|
+
await wrapper.setAdapter(newAdapter);
|
|
255
|
+
|
|
256
|
+
expect(wrapper.name).toBe('new');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should shutdown old adapter before swap', async () => {
|
|
260
|
+
const oldAdapter = createMockAdapter('old');
|
|
261
|
+
const newAdapter = createMockAdapter('new');
|
|
262
|
+
const wrapper = createAdapterWrapper(oldAdapter);
|
|
263
|
+
|
|
264
|
+
await wrapper.setAdapter(newAdapter);
|
|
265
|
+
|
|
266
|
+
expect(oldAdapter.shutdown).toHaveBeenCalled();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should initialize new adapter if wrapper was initialized', async () => {
|
|
270
|
+
const oldAdapter = createMockAdapter('old');
|
|
271
|
+
const newAdapter = createMockAdapter('new');
|
|
272
|
+
const wrapper = createAdapterWrapper(oldAdapter);
|
|
273
|
+
|
|
274
|
+
// Initialize wrapper
|
|
275
|
+
wrapper.initialize();
|
|
276
|
+
|
|
277
|
+
// Swap adapter
|
|
278
|
+
await wrapper.setAdapter(newAdapter);
|
|
279
|
+
|
|
280
|
+
expect(newAdapter.initialize).toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should not initialize new adapter if wrapper was not initialized', async () => {
|
|
284
|
+
const oldAdapter = createMockAdapter('old');
|
|
285
|
+
const newAdapter = createMockAdapter('new');
|
|
286
|
+
const wrapper = createAdapterWrapper(oldAdapter);
|
|
287
|
+
|
|
288
|
+
// Don't initialize wrapper
|
|
289
|
+
|
|
290
|
+
// Swap adapter
|
|
291
|
+
await wrapper.setAdapter(newAdapter);
|
|
292
|
+
|
|
293
|
+
expect(newAdapter.initialize).not.toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should prevent concurrent adapter swaps', async () => {
|
|
297
|
+
const oldAdapter = createMockAdapter('old', {
|
|
298
|
+
shutdown: vi.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100))),
|
|
299
|
+
});
|
|
300
|
+
const newAdapter1 = createMockAdapter('new1');
|
|
301
|
+
const newAdapter2 = createMockAdapter('new2');
|
|
302
|
+
const wrapper = createAdapterWrapper(oldAdapter);
|
|
303
|
+
|
|
304
|
+
// Start first swap
|
|
305
|
+
const swap1 = wrapper.setAdapter(newAdapter1);
|
|
306
|
+
|
|
307
|
+
// Try second swap immediately (should fail)
|
|
308
|
+
await expect(wrapper.setAdapter(newAdapter2)).rejects.toThrow('Adapter swap already in progress');
|
|
309
|
+
|
|
310
|
+
// Wait for first swap to complete
|
|
311
|
+
await swap1;
|
|
312
|
+
|
|
313
|
+
// Now second swap should work
|
|
314
|
+
await wrapper.setAdapter(newAdapter2);
|
|
315
|
+
expect(wrapper.name).toBe('new2');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should handle shutdown errors gracefully', async () => {
|
|
319
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
320
|
+
|
|
321
|
+
const oldAdapter = createMockAdapter('old', {
|
|
322
|
+
shutdown: vi.fn().mockRejectedValue(new Error('Shutdown failed')),
|
|
323
|
+
});
|
|
324
|
+
const newAdapter = createMockAdapter('new');
|
|
325
|
+
const wrapper = createAdapterWrapper(oldAdapter);
|
|
326
|
+
|
|
327
|
+
// Should not throw
|
|
328
|
+
await wrapper.setAdapter(newAdapter);
|
|
329
|
+
|
|
330
|
+
expect(wrapper.name).toBe('new');
|
|
331
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
332
|
+
'[AdapterWrapper] Error shutting down old adapter:',
|
|
333
|
+
expect.any(Error)
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
consoleErrorSpy.mockRestore();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('shutdown', () => {
|
|
341
|
+
it('should shutdown current adapter', async () => {
|
|
342
|
+
const mockAdapter = createMockAdapter('test');
|
|
343
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
344
|
+
wrapper.initialize();
|
|
345
|
+
|
|
346
|
+
// Wrapper always implements shutdown
|
|
347
|
+
await wrapper.shutdown!();
|
|
348
|
+
|
|
349
|
+
expect(mockAdapter.shutdown).toHaveBeenCalled();
|
|
350
|
+
expect(wrapper.getAdapterInfo().initialized).toBe(false);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should handle adapters without shutdown method', async () => {
|
|
354
|
+
const mockAdapter = createMockAdapter('test');
|
|
355
|
+
delete (mockAdapter as Partial<AuthAdapter>).shutdown;
|
|
356
|
+
const wrapper = createAdapterWrapper(mockAdapter);
|
|
357
|
+
wrapper.initialize();
|
|
358
|
+
|
|
359
|
+
// Should not throw - wrapper handles missing shutdown gracefully
|
|
360
|
+
await wrapper.shutdown!();
|
|
361
|
+
|
|
362
|
+
expect(wrapper.getAdapterInfo().initialized).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('no-op adapter', () => {
|
|
367
|
+
it('should return false for isAuthenticated', () => {
|
|
368
|
+
const wrapper = createAdapterWrapper();
|
|
369
|
+
const req = createMockReq();
|
|
370
|
+
|
|
371
|
+
expect(wrapper.isAuthenticated(req)).toBe(false);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should return null for getUser', () => {
|
|
375
|
+
const wrapper = createAdapterWrapper();
|
|
376
|
+
const req = createMockReq();
|
|
377
|
+
|
|
378
|
+
expect(wrapper.getUser(req)).toBeNull();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should return middleware that calls next', () => {
|
|
382
|
+
const wrapper = createAdapterWrapper();
|
|
383
|
+
const middleware = wrapper.initialize();
|
|
384
|
+
|
|
385
|
+
const req = createMockReq();
|
|
386
|
+
const res = createMockRes();
|
|
387
|
+
const next = vi.fn();
|
|
388
|
+
|
|
389
|
+
if (typeof middleware === 'function') {
|
|
390
|
+
middleware(req, res, next);
|
|
391
|
+
expect(next).toHaveBeenCalled();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|