@lowdefy/server-e2e 6.0.0 → 7.0.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/client/App.jsx +24 -14
- package/client/Page.jsx +53 -16
- package/client/getShownPage.js +28 -0
- package/client/getShownPage.test.mjs +52 -0
- package/lib/client/auth/Auth.jsx +30 -4
- package/lib/server/auth/{session.js → getUser.js} +8 -7
- package/lib/server/log/createLogger.js +3 -0
- package/lib/server/log/logRequest.js +1 -3
- package/lib/server/scrubSecrets.js +25 -0
- package/lowdefy/build.mjs +1 -0
- package/lowdefy/build.test.mjs +65 -0
- package/lowdefy/createCustomPluginTypesMap.mjs +3 -2
- package/lowdefy/createCustomPluginTypesMap.test.mjs +5 -0
- package/package.json +38 -39
- package/package.original.json +38 -39
- package/src/app.js +12 -5
- package/src/html/getAssets.js +3 -1
- package/src/html/getHomePath.js +29 -0
- package/src/html/renderPage.js +29 -17
- package/src/html/renderPage.test.mjs +162 -0
- package/src/html/template.js +16 -6
- package/src/index.js +23 -0
- package/src/lib/getRequestPath.js +29 -0
- package/src/middleware/apiContext.js +22 -3
- package/src/middleware/apiContext.test.mjs +120 -0
- package/src/middleware/errorHandler.js +63 -4
- package/src/middleware/errorHandler.test.mjs +402 -0
- package/src/routes/apiPage.js +14 -9
- package/src/routes/apiPage.test.mjs +92 -0
- package/src/routes/clientError.js +6 -12
- package/src/routes/e2eIdentity.js +28 -0
- package/src/routes/e2eIdentity.test.mjs +43 -0
- package/src/routes/endpoints.js +5 -3
- package/src/routes/mountPageRoutes.js +29 -0
- package/src/routes/mountPageRoutes.test.mjs +56 -0
- package/src/routes/request.js +5 -3
- package/src/routes/requestBody.test.mjs +69 -0
- package/src/routes/sessionMock.js +4 -1
- package/src/routes/usage.js +6 -2
- package/src/routes/user.js +27 -0
- package/src/routes/websocket.js +23 -3
- package/src/routes/websocket.test.mjs +73 -0
- package/vite.config.js +57 -1
- package/lib/server/apiWrapper.js +0 -73
package/client/App.jsx
CHANGED
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
import React, { useCallback, useRef } from 'react';
|
|
18
18
|
|
|
19
19
|
import { ErrorBoundary } from '@lowdefy/block-utils';
|
|
20
|
-
import { useDarkMode, useLocale } from '@lowdefy/client';
|
|
20
|
+
import { IconProvider, useDarkMode, useLocale } from '@lowdefy/client';
|
|
21
21
|
import { StyleProvider } from '@ant-design/cssinjs';
|
|
22
22
|
import { App as AntdApp, theme as antdTheme } from 'antd';
|
|
23
23
|
import { XProvider } from '@ant-design/x';
|
|
24
24
|
|
|
25
|
+
import { appStorageKey } from '@lowdefy/helpers';
|
|
26
|
+
|
|
25
27
|
import antdLocaleLoaders from '../build/i18n/antdLocales.js';
|
|
26
28
|
import antdXLocaleLoaders from '../build/i18n/antdXLocales.js';
|
|
27
29
|
import dayjsLocaleMap from '../build/i18n/dayjsLocales.js';
|
|
@@ -39,7 +41,7 @@ function ThemeTokenResolver({ lowdefyRef, children }) {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
function App({ config }) {
|
|
42
|
-
const { rootConfig,
|
|
44
|
+
const { rootConfig, user } = config;
|
|
43
45
|
|
|
44
46
|
const usageDataRef = useRef({});
|
|
45
47
|
const lowdefyRef = useRef({ eventCallback: createLogUsage({ usageDataRef }) });
|
|
@@ -47,9 +49,12 @@ function App({ config }) {
|
|
|
47
49
|
lowdefyRef.current.theme = rootConfig.theme;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
// Device preferences, kept per app (the same key the HTML template's dark mode script reads).
|
|
53
|
+
const storageScope = { basePath: config.basePath ?? '', slug: rootConfig.lowdefyApp?.slug };
|
|
50
54
|
const { algorithm, token, components } = useDarkMode({
|
|
51
55
|
antd: lowdefyRef.current.theme?.antd,
|
|
52
56
|
configDarkMode: lowdefyRef.current.theme?.darkMode,
|
|
57
|
+
storageKey: appStorageKey({ ...storageScope, name: 'lowdefy_darkMode' }),
|
|
53
58
|
});
|
|
54
59
|
|
|
55
60
|
const {
|
|
@@ -61,6 +66,7 @@ function App({ config }) {
|
|
|
61
66
|
antdLocaleLoaders,
|
|
62
67
|
antdXLocaleLoaders,
|
|
63
68
|
dayjsLocaleMap,
|
|
69
|
+
storageKey: appStorageKey({ ...storageScope, name: 'lowdefy_locale' }),
|
|
64
70
|
});
|
|
65
71
|
|
|
66
72
|
if (rootConfig?.i18n?.defaultLocale) {
|
|
@@ -108,18 +114,22 @@ function App({ config }) {
|
|
|
108
114
|
algorithm,
|
|
109
115
|
}}
|
|
110
116
|
>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
{/* Message, Notification and ConfirmModal render in the App.useApp()
|
|
118
|
+
holders here, above the Client's icon provider. */}
|
|
119
|
+
<IconProvider icons={rootConfig.theme.icons}>
|
|
120
|
+
<AntdApp>
|
|
121
|
+
<ThemeTokenResolver lowdefyRef={lowdefyRef}>
|
|
122
|
+
<ErrorBoundary fullPage onError={handleError}>
|
|
123
|
+
<Auth user={user}>
|
|
124
|
+
{(auth) => {
|
|
125
|
+
usageDataRef.current.user = auth.user?.id;
|
|
126
|
+
return <Page auth={auth} config={config} lowdefy={lowdefyRef.current} />;
|
|
127
|
+
}}
|
|
128
|
+
</Auth>
|
|
129
|
+
</ErrorBoundary>
|
|
130
|
+
</ThemeTokenResolver>
|
|
131
|
+
</AntdApp>
|
|
132
|
+
</IconProvider>
|
|
123
133
|
</XProvider>
|
|
124
134
|
</StyleProvider>
|
|
125
135
|
);
|
package/client/Page.jsx
CHANGED
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
17
|
+
import React, { useEffect, useReducer, useRef, useState } from 'react';
|
|
18
18
|
|
|
19
19
|
import Client from '@lowdefy/client';
|
|
20
20
|
import createRouter from '@lowdefy/client/adapters/createRouter.js';
|
|
21
21
|
import createLinkComponent from '@lowdefy/client/adapters/Link.js';
|
|
22
22
|
import { createUrl } from '@lowdefy/client/adapters/url.js';
|
|
23
23
|
import Head from '@lowdefy/client/adapters/Head.js';
|
|
24
|
+
import { buildPagePath } from '@lowdefy/helpers';
|
|
24
25
|
|
|
25
26
|
import actions from '../build/plugins/actions.js';
|
|
26
27
|
import blockMetas from '../build/plugins/blockMetas.json';
|
|
@@ -29,10 +30,39 @@ import icons from '../build/plugins/icons.js';
|
|
|
29
30
|
import operators from '../build/plugins/operators/client.js';
|
|
30
31
|
import jsMap from '../build/plugins/operators/clientJsMap.js';
|
|
31
32
|
|
|
33
|
+
import getShownPage from './getShownPage.js';
|
|
34
|
+
|
|
35
|
+
// The path to fetch for a navigation. The app root shows the home page, and
|
|
36
|
+
// /api/page/ with an empty path matches no page, so it fetches the home page's
|
|
37
|
+
// own path.
|
|
38
|
+
function getFetchPath({ path, rootConfig }) {
|
|
39
|
+
if (path !== '') {
|
|
40
|
+
return path;
|
|
41
|
+
}
|
|
42
|
+
const { home, pagePaths } = rootConfig;
|
|
43
|
+
return buildPagePath({
|
|
44
|
+
pageId: home.pageId,
|
|
45
|
+
path: pagePaths[home.pageId],
|
|
46
|
+
pathParams: home.pathParams,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
// Replaces lib/client/Page.js. The first page renders from the config
|
|
33
|
-
// embedded in the HTML; SPA navigations fetch /api/page
|
|
51
|
+
// embedded in the HTML; SPA navigations fetch /api/page/<path> and swap the
|
|
52
|
+
// page. The server matches the path and answers with the page it shows, the
|
|
53
|
+
// values it carries and the path it matched.
|
|
34
54
|
function Page({ auth, config, lowdefy }) {
|
|
35
|
-
const [
|
|
55
|
+
const [page, setPage] = useState({
|
|
56
|
+
matchedPath: config.matchedPath,
|
|
57
|
+
pageConfig: config.pageConfig,
|
|
58
|
+
pathParams: config.pathParams,
|
|
59
|
+
});
|
|
60
|
+
// The navigation listener is subscribed once, so it reads the shown page
|
|
61
|
+
// through a ref rather than the state its closure captured.
|
|
62
|
+
const pageRef = useRef(page);
|
|
63
|
+
// A re-render with the same config is how the page context picks up a new
|
|
64
|
+
// URL (getContext updates a memoized context on every render).
|
|
65
|
+
const [, rerender] = useReducer((count) => count + 1, 0);
|
|
36
66
|
|
|
37
67
|
const routerRef = useRef(null);
|
|
38
68
|
if (!routerRef.current) {
|
|
@@ -50,33 +80,38 @@ function Page({ auth, config, lowdefy }) {
|
|
|
50
80
|
const latestNavRef = useRef(0);
|
|
51
81
|
|
|
52
82
|
useEffect(() => {
|
|
53
|
-
const unsubscribe = router.subscribe(async ({
|
|
83
|
+
const unsubscribe = router.subscribe(async ({ path, search }) => {
|
|
54
84
|
const token = ++latestNavRef.current;
|
|
55
|
-
|
|
85
|
+
// The path the shown instance was matched on shows the same page and
|
|
86
|
+
// values whatever the query, so it needs no fetch. Another spelling of
|
|
87
|
+
// the same values fetches and lands on the same instance. Dynamic pages
|
|
88
|
+
// re-resolve per navigation.
|
|
89
|
+
if (path === pageRef.current.matchedPath && pageRef.current.pageConfig.dynamic !== true) {
|
|
90
|
+
rerender();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const fetchPath = getFetchPath({ path, rootConfig: config.rootConfig });
|
|
56
94
|
try {
|
|
57
95
|
// Forward the current query string so server-side Dynamic block
|
|
58
96
|
// resolution sees the same urlQuery as an initial HTML load.
|
|
59
|
-
const res = await fetch(
|
|
60
|
-
`${router.basePath}/api/page/${targetPageId}${window.location.search}`
|
|
61
|
-
);
|
|
97
|
+
const res = await fetch(`${router.basePath}/api/page/${fetchPath}${search}`);
|
|
62
98
|
if (!res.ok) {
|
|
63
99
|
if (token !== latestNavRef.current) return;
|
|
64
|
-
if (
|
|
100
|
+
if (path !== '404') {
|
|
65
101
|
router.replace({ pathname: '/404' });
|
|
66
102
|
}
|
|
67
103
|
return;
|
|
68
104
|
}
|
|
69
|
-
const
|
|
105
|
+
const response = await res.json();
|
|
70
106
|
if (token !== latestNavRef.current) return;
|
|
71
|
-
|
|
107
|
+
const nextPage = getShownPage({ path, response });
|
|
108
|
+
pageRef.current = nextPage;
|
|
109
|
+
setPage(nextPage);
|
|
72
110
|
} catch (error) {
|
|
73
111
|
// Network failure on SPA navigation — fall back to a full page load.
|
|
74
112
|
if (token !== latestNavRef.current) return;
|
|
75
113
|
window.location.assign(
|
|
76
|
-
createUrl({
|
|
77
|
-
basePath: router.basePath,
|
|
78
|
-
pathname: targetPageId === config.rootConfig.home.pageId ? '/' : `/${targetPageId}`,
|
|
79
|
-
})
|
|
114
|
+
createUrl({ basePath: router.basePath, pathname: `/${path}`, query: search.slice(1) })
|
|
80
115
|
);
|
|
81
116
|
}
|
|
82
117
|
});
|
|
@@ -88,11 +123,13 @@ function Page({ auth, config, lowdefy }) {
|
|
|
88
123
|
auth={auth}
|
|
89
124
|
Components={{ Head, Link }}
|
|
90
125
|
config={{
|
|
91
|
-
pageConfig,
|
|
126
|
+
pageConfig: page.pageConfig,
|
|
92
127
|
rootConfig: config.rootConfig,
|
|
93
128
|
}}
|
|
94
129
|
jsMap={jsMap}
|
|
95
130
|
lowdefy={lowdefy}
|
|
131
|
+
matchedPath={page.matchedPath}
|
|
132
|
+
pathParams={page.pathParams}
|
|
96
133
|
router={router}
|
|
97
134
|
stage="e2e"
|
|
98
135
|
types={{
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// The page a navigation to `path` shows, from the page response. At the app root the client
|
|
18
|
+
// fetches the home page's own path, but the shown instance is matched on the root, as on a first
|
|
19
|
+
// load there, so a later navigation to the root (a query change, the home link) needs no fetch.
|
|
20
|
+
function getShownPage({ path, response }) {
|
|
21
|
+
const { pageConfig, pathParams } = response;
|
|
22
|
+
if (path === '') {
|
|
23
|
+
return { matchedPath: '', pageConfig, pathParams };
|
|
24
|
+
}
|
|
25
|
+
return { matchedPath: response.matchedPath, pageConfig, pathParams };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default getShownPage;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import getShownPage from './getShownPage.js';
|
|
18
|
+
|
|
19
|
+
const pageConfig = { pageId: 'ticket', path: 'tickets/{space}/{ticket_id}' };
|
|
20
|
+
|
|
21
|
+
test('getShownPage keeps the path the server matched', () => {
|
|
22
|
+
expect(
|
|
23
|
+
getShownPage({
|
|
24
|
+
path: 'tickets/a%2Bb/1',
|
|
25
|
+
response: {
|
|
26
|
+
pageId: 'ticket',
|
|
27
|
+
pathParams: { space: 'a+b', ticket_id: '1' },
|
|
28
|
+
matchedPath: 'tickets/a%2Bb/1',
|
|
29
|
+
pageConfig,
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
).toEqual({
|
|
33
|
+
matchedPath: 'tickets/a%2Bb/1',
|
|
34
|
+
pageConfig,
|
|
35
|
+
pathParams: { space: 'a+b', ticket_id: '1' },
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('getShownPage matches the home page fetched for the app root on the root', () => {
|
|
40
|
+
const homeConfig = { pageId: 'users', path: 'admin/users' };
|
|
41
|
+
expect(
|
|
42
|
+
getShownPage({
|
|
43
|
+
path: '',
|
|
44
|
+
response: {
|
|
45
|
+
pageId: 'users',
|
|
46
|
+
pathParams: {},
|
|
47
|
+
matchedPath: 'admin/users',
|
|
48
|
+
pageConfig: homeConfig,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
).toEqual({ matchedPath: '', pageConfig: homeConfig, pathParams: {} });
|
|
52
|
+
});
|
package/lib/client/auth/Auth.jsx
CHANGED
|
@@ -24,19 +24,45 @@ function e2eNotSupported() {
|
|
|
24
24
|
throw new Error('Sign-in and sign-out are not supported in e2e testing.');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function Auth({ children,
|
|
27
|
+
function Auth({ children, user }) {
|
|
28
28
|
const auth = {
|
|
29
29
|
authConfig,
|
|
30
|
-
|
|
30
|
+
user,
|
|
31
|
+
// Matches the BetterAuth client contract: { data, error }.
|
|
31
32
|
getSession: async () => {
|
|
32
33
|
const res = await fetch('/api/auth/session');
|
|
34
|
+
if (res.ok) {
|
|
35
|
+
return { data: await res.json(), error: null };
|
|
36
|
+
}
|
|
37
|
+
return { data: null, error: null };
|
|
38
|
+
},
|
|
39
|
+
getResolvedUser: async () => {
|
|
40
|
+
const res = await fetch('/api/user');
|
|
33
41
|
if (res.ok) {
|
|
34
42
|
return res.json();
|
|
35
43
|
}
|
|
36
|
-
return null;
|
|
44
|
+
return { user: null };
|
|
37
45
|
},
|
|
38
|
-
|
|
46
|
+
acceptInvitation: e2eNotSupported,
|
|
47
|
+
addPasskey: e2eNotSupported,
|
|
48
|
+
changePassword: e2eNotSupported,
|
|
49
|
+
deletePasskey: e2eNotSupported,
|
|
50
|
+
updatePasskey: e2eNotSupported,
|
|
51
|
+
requestPasswordReset: e2eNotSupported,
|
|
52
|
+
resetPassword: e2eNotSupported,
|
|
53
|
+
revokeOtherSessions: e2eNotSupported,
|
|
54
|
+
sendVerificationEmail: e2eNotSupported,
|
|
55
|
+
setActiveOrganization: e2eNotSupported,
|
|
56
|
+
signInEmail: e2eNotSupported,
|
|
57
|
+
signInMagicLink: e2eNotSupported,
|
|
58
|
+
signInSocial: e2eNotSupported,
|
|
39
59
|
signOut: e2eNotSupported,
|
|
60
|
+
signUpEmail: e2eNotSupported,
|
|
61
|
+
twoFactorDisable: e2eNotSupported,
|
|
62
|
+
twoFactorEnable: e2eNotSupported,
|
|
63
|
+
twoFactorGenerateBackupCodes: e2eNotSupported,
|
|
64
|
+
twoFactorVerifyBackupCode: e2eNotSupported,
|
|
65
|
+
twoFactorVerifyTotp: e2eNotSupported,
|
|
40
66
|
};
|
|
41
67
|
return children(auth);
|
|
42
68
|
}
|
|
@@ -14,22 +14,23 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
// E2E test-user injection: the
|
|
17
|
+
// E2E test-user injection: the caller is read from the lowdefy_e2e_user
|
|
18
18
|
// cookie (base64-encoded JSON user object) instead of a real auth engine.
|
|
19
|
-
|
|
19
|
+
// The cookie user is pre-resolved - it substitutes for resolveAuthentication
|
|
20
|
+
// and its roles are authoritative.
|
|
21
|
+
function getUser(c) {
|
|
20
22
|
const cookieHeader = c.req.header('cookie') ?? '';
|
|
21
23
|
const match = cookieHeader.match(/lowdefy_e2e_user=([^;]+)/);
|
|
22
24
|
if (!match) {
|
|
23
|
-
return
|
|
25
|
+
return null;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
try {
|
|
27
29
|
const decoded = Buffer.from(decodeURIComponent(match[1]), 'base64').toString();
|
|
28
|
-
|
|
29
|
-
return { user };
|
|
30
|
+
return JSON.parse(decoded);
|
|
30
31
|
} catch {
|
|
31
|
-
return
|
|
32
|
+
return null;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export default
|
|
36
|
+
export default getUser;
|
|
@@ -15,11 +15,14 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { createNodeLogger } from '@lowdefy/logger/node';
|
|
18
|
+
import { scrubCredentials } from '@lowdefy/node-utils';
|
|
18
19
|
|
|
19
20
|
const logger = createNodeLogger({
|
|
20
21
|
name: 'lowdefy_server_e2e',
|
|
21
22
|
level: process.env.LOWDEFY_LOG_LEVEL ?? 'info',
|
|
22
23
|
base: { pid: undefined, hostname: undefined },
|
|
24
|
+
// Secrets stay in these logs, but a value a request marked with _credential does not.
|
|
25
|
+
hooks: { streamWrite: scrubCredentials },
|
|
23
26
|
});
|
|
24
27
|
|
|
25
28
|
function createLogger(metadata = {}) {
|
|
@@ -15,13 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
function logRequest({ context }) {
|
|
18
|
-
const
|
|
18
|
+
const user = context.user ?? {};
|
|
19
19
|
context.logger.debug({
|
|
20
20
|
user: {
|
|
21
21
|
id: user.id,
|
|
22
22
|
roles: user.roles,
|
|
23
|
-
sub: user.sub,
|
|
24
|
-
session_id: user.session_id,
|
|
25
23
|
},
|
|
26
24
|
url: context.req.url,
|
|
27
25
|
method: context.req.method,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createSecretScrubber } from '@lowdefy/node-utils';
|
|
18
|
+
|
|
19
|
+
import getE2eSecrets from './getE2eSecrets.js';
|
|
20
|
+
|
|
21
|
+
// E2E logs stay unscrubbed, but _error reads this off the context like every
|
|
22
|
+
// other server package, so a thrown message behaves identically in every mode.
|
|
23
|
+
const scrubSecrets = createSecretScrubber({ secrets: getE2eSecrets() });
|
|
24
|
+
|
|
25
|
+
export default scrubSecrets;
|
package/lowdefy/build.mjs
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { jest } from '@jest/globals';
|
|
18
|
+
|
|
19
|
+
const mockBuild = jest.fn();
|
|
20
|
+
|
|
21
|
+
jest.unstable_mockModule('@lowdefy/build', () => ({ default: mockBuild }));
|
|
22
|
+
jest.unstable_mockModule('@lowdefy/logger/node', () => ({
|
|
23
|
+
createNodeLogger: jest.fn(() => 'logger'),
|
|
24
|
+
}));
|
|
25
|
+
jest.unstable_mockModule('./createCustomPluginTypesMap.mjs', () => ({
|
|
26
|
+
default: jest.fn().mockResolvedValue({}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const savedSkip = process.env.LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS;
|
|
30
|
+
let loadCount = 0;
|
|
31
|
+
|
|
32
|
+
// build.mjs runs the build when imported, so each case loads a fresh module
|
|
33
|
+
// instance and waits for the build call.
|
|
34
|
+
async function runScript() {
|
|
35
|
+
loadCount += 1;
|
|
36
|
+
await import(`./build.mjs?case=${loadCount}`);
|
|
37
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
delete process.env.LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS;
|
|
42
|
+
mockBuild.mockReset();
|
|
43
|
+
mockBuild.mockResolvedValue();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterAll(() => {
|
|
47
|
+
if (savedSkip === undefined) {
|
|
48
|
+
delete process.env.LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS;
|
|
49
|
+
} else {
|
|
50
|
+
process.env.LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS = savedSkip;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('build passes skipPluginChecks true when LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS is true', async () => {
|
|
55
|
+
process.env.LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS = 'true';
|
|
56
|
+
await runScript();
|
|
57
|
+
expect(mockBuild).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(mockBuild.mock.calls[0][0].skipPluginChecks).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('build passes skipPluginChecks false when LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS is unset', async () => {
|
|
62
|
+
await runScript();
|
|
63
|
+
expect(mockBuild).toHaveBeenCalledTimes(1);
|
|
64
|
+
expect(mockBuild.mock.calls[0][0].skipPluginChecks).toBe(false);
|
|
65
|
+
});
|
|
@@ -42,14 +42,15 @@ async function createCustomPluginTypesMap({ directories }) {
|
|
|
42
42
|
agents: {},
|
|
43
43
|
auth: {
|
|
44
44
|
adapters: {},
|
|
45
|
-
callbacks: {},
|
|
46
|
-
events: {},
|
|
47
45
|
providers: {},
|
|
46
|
+
strategies: {},
|
|
48
47
|
},
|
|
49
48
|
blockMetas: {},
|
|
49
|
+
connectionMetas: {},
|
|
50
50
|
blocks: {},
|
|
51
51
|
connections: {},
|
|
52
52
|
icons: {},
|
|
53
|
+
iconSets: {},
|
|
53
54
|
operators: {
|
|
54
55
|
client: {},
|
|
55
56
|
server: {},
|
|
@@ -41,7 +41,12 @@ const pluginTypesEsm = {
|
|
|
41
41
|
},
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
// The module under test imports @lowdefy/build, which links against every
|
|
45
|
+
// export it uses, so the real module passes through and only file access is
|
|
46
|
+
// replaced.
|
|
47
|
+
const realNodeUtils = await import('@lowdefy/node-utils');
|
|
44
48
|
jest.unstable_mockModule('@lowdefy/node-utils', () => ({
|
|
49
|
+
...realNodeUtils,
|
|
45
50
|
cleanDirectory: jest.fn(),
|
|
46
51
|
copyFileOrDirectory: jest.fn(),
|
|
47
52
|
getFileExtension: jest.fn(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/server-e2e",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Lowdefy e2e testing server with cookie-based user injection",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -41,58 +41,57 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@ant-design/cssinjs": "2.1.2",
|
|
44
|
-
"@ant-design/x": "2.
|
|
45
|
-
"@hono/node-server": "2.0.
|
|
46
|
-
"@lowdefy/actions-core": "
|
|
47
|
-
"@lowdefy/api": "
|
|
48
|
-
"@lowdefy/block-utils": "
|
|
49
|
-
"@lowdefy/blocks-aggrid": "
|
|
50
|
-
"@lowdefy/blocks-antd": "
|
|
51
|
-
"@lowdefy/blocks-antd-x": "
|
|
52
|
-
"@lowdefy/blocks-basic": "
|
|
53
|
-
"@lowdefy/blocks-
|
|
54
|
-
"@lowdefy/blocks-
|
|
55
|
-
"@lowdefy/blocks-
|
|
56
|
-
"@lowdefy/blocks-
|
|
57
|
-
"@lowdefy/
|
|
58
|
-
"@lowdefy/
|
|
59
|
-
"@lowdefy/
|
|
60
|
-
"@lowdefy/
|
|
61
|
-
"@lowdefy/
|
|
62
|
-
"@lowdefy/
|
|
63
|
-
"@lowdefy/
|
|
64
|
-
"@lowdefy/
|
|
65
|
-
"@lowdefy/
|
|
66
|
-
"@lowdefy/
|
|
67
|
-
"@lowdefy/operators-
|
|
68
|
-
"@lowdefy/
|
|
69
|
-
"@
|
|
44
|
+
"@ant-design/x": "2.9.0",
|
|
45
|
+
"@hono/node-server": "2.0.10",
|
|
46
|
+
"@lowdefy/actions-core": "7.0.0",
|
|
47
|
+
"@lowdefy/api": "7.0.0",
|
|
48
|
+
"@lowdefy/block-utils": "7.0.0",
|
|
49
|
+
"@lowdefy/blocks-aggrid": "7.0.0",
|
|
50
|
+
"@lowdefy/blocks-antd": "7.0.0",
|
|
51
|
+
"@lowdefy/blocks-antd-x": "7.0.0",
|
|
52
|
+
"@lowdefy/blocks-basic": "7.0.0",
|
|
53
|
+
"@lowdefy/blocks-captcha": "7.0.0",
|
|
54
|
+
"@lowdefy/blocks-echarts": "7.0.0",
|
|
55
|
+
"@lowdefy/blocks-loaders": "7.0.0",
|
|
56
|
+
"@lowdefy/blocks-markdown": "7.0.0",
|
|
57
|
+
"@lowdefy/blocks-table": "7.0.0",
|
|
58
|
+
"@lowdefy/blocks-tiptap": "7.0.0",
|
|
59
|
+
"@lowdefy/client": "7.0.0",
|
|
60
|
+
"@lowdefy/connection-axios-http": "7.0.0",
|
|
61
|
+
"@lowdefy/connection-mongodb": "7.0.0",
|
|
62
|
+
"@lowdefy/errors": "7.0.0",
|
|
63
|
+
"@lowdefy/helpers": "7.0.0",
|
|
64
|
+
"@lowdefy/layout": "7.0.0",
|
|
65
|
+
"@lowdefy/logger": "7.0.0",
|
|
66
|
+
"@lowdefy/node-utils": "7.0.0",
|
|
67
|
+
"@lowdefy/operators-js": "7.0.0",
|
|
68
|
+
"@lowdefy/operators-nunjucks": "7.0.0",
|
|
69
|
+
"@lowdefy/operators-uuid": "7.0.0",
|
|
70
|
+
"@lowdefy/websockets-core": "7.0.0",
|
|
71
|
+
"@tailwindcss/postcss": "4.3.3",
|
|
70
72
|
"@vitejs/plugin-react": "6.0.2",
|
|
71
|
-
"antd": "6.
|
|
73
|
+
"antd": "6.6.5",
|
|
72
74
|
"dayjs": "1.11.20",
|
|
73
|
-
"hono": "4.
|
|
74
|
-
"
|
|
75
|
+
"hono": "4.13.7",
|
|
76
|
+
"lightningcss": "1.32.0",
|
|
75
77
|
"react": "18.2.0",
|
|
76
78
|
"react-dom": "18.2.0",
|
|
77
|
-
"
|
|
78
|
-
"tailwindcss": "4.3.1",
|
|
79
|
+
"tailwindcss": "4.3.3",
|
|
79
80
|
"uuid": "14.0.0",
|
|
80
81
|
"vite": "8.0.16",
|
|
81
|
-
"ws": "8.
|
|
82
|
+
"ws": "8.21.0"
|
|
82
83
|
},
|
|
83
84
|
"devDependencies": {
|
|
84
85
|
"@jest/globals": "28.1.3",
|
|
85
|
-
"@lowdefy/build": "
|
|
86
|
-
"@
|
|
87
|
-
"@tailwindcss/postcss": "4.3.1",
|
|
86
|
+
"@lowdefy/build": "7.0.0",
|
|
87
|
+
"@tailwindcss/postcss": "4.3.3",
|
|
88
88
|
"jest": "28.1.3",
|
|
89
|
-
"tailwindcss": "4.3.
|
|
90
|
-
"webpack": "5.94.0",
|
|
89
|
+
"tailwindcss": "4.3.3",
|
|
91
90
|
"yaml": "2.9.0",
|
|
92
91
|
"yargs": "18.0.0"
|
|
93
92
|
},
|
|
94
93
|
"engines": {
|
|
95
|
-
"node": ">=
|
|
94
|
+
"node": ">=24"
|
|
96
95
|
},
|
|
97
96
|
"publishConfig": {
|
|
98
97
|
"access": "public"
|