Skip to content

Commit ae79548

Browse files
committed
test(data-access): regression test for es:app vs app
This test was very difficult to get working because of issues initializing the context under vitest. After a lot of effort and trying to narrow down the issue in many ways that didn't work, I eventually found that manually setting the storage and calling run on the AsyncLocalStorage object got it working. I'm currently unsure if this is specific to vitest or not. We don't seem to do this in Puter's kernel but everything works fine in the typical runtime. Maybe this is side-effect of otel's own use of AsyncLocalStorage.
1 parent 3a78564 commit ae79548

File tree

1 file changed

+323
-0
lines changed

1 file changed

+323
-0
lines changed
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
import { createTestKernel } from '../../../tools/test.mjs';
2+
import { tmp_provide_services } from '../../helpers.js';
3+
import AppES from '../../om/entitystorage/AppES';
4+
import { AppLimitedES } from '../../om/entitystorage/AppLimitedES';
5+
import { ESBuilder } from '../../om/entitystorage/ESBuilder';
6+
import { MaxLimitES } from '../../om/entitystorage/MaxLimitES';
7+
import { ProtectedAppES } from '../../om/entitystorage/ProtectedAppES';
8+
import { SetOwnerES } from '../../om/entitystorage/SetOwnerES';
9+
import SQLES from '../../om/entitystorage/SQLES';
10+
import ValidationES from '../../om/entitystorage/ValidationES';
11+
import WriteByOwnerOnlyES from '../../om/entitystorage/WriteByOwnerOnlyES';
12+
import { Eq, Or } from '../../om/query/query';
13+
import { Actor, UserActorType } from '../../services/auth/Actor';
14+
import { EntityStoreService } from '../../services/EntityStoreService';
15+
import { Context } from '../../util/esmcontext.js';
16+
import { AppIconService } from '../apps/AppIconService';
17+
import { AppInformationService } from '../apps/AppInformationService';
18+
import { OldAppNameService } from '../apps/OldAppNameService';
19+
import AppService from './AppService';
20+
21+
import { describe, expect, it } from 'vitest';
22+
23+
/*
24+
// CRITICAL: Mock BOTH Context modules (CommonJS and ESM) BEFORE any other imports
25+
// CommonJS modules use '../../util/context', ESM modules use '../../util/esmcontext.js'
26+
vi.mock('../../util/context', async () => {
27+
const actual = await vi.importActual('../../util/context');
28+
const { Context: OriginalContext } = actual;
29+
30+
// Store original get method BEFORE patching
31+
const originalGet = OriginalContext.get;
32+
33+
// Store test values
34+
let testContext = null;
35+
let testActor = null;
36+
let testUser = null;
37+
38+
// Patch Context.get to use test values
39+
OriginalContext.get = function(key, options) {
40+
// Check test values FIRST
41+
if (key === 'actor' && testActor) {
42+
return testActor;
43+
}
44+
if (key === 'user' && testUser) {
45+
return testUser;
46+
}
47+
48+
// Call original get method
49+
return originalGet.call(this, key, options);
50+
};
51+
52+
// Export patched Context with setters for test values
53+
return {
54+
...actual,
55+
Context: OriginalContext,
56+
__setTestValues: (ctx, actor, user) => {
57+
testContext = ctx;
58+
testActor = actor;
59+
testUser = user;
60+
},
61+
__clearTestValues: () => {
62+
testContext = null;
63+
testActor = null;
64+
testUser = null;
65+
},
66+
};
67+
});
68+
69+
vi.mock('../../util/esmcontext.js', async () => {
70+
const actual = await vi.importActual('../../util/esmcontext.js');
71+
const { Context: OriginalContext } = actual;
72+
73+
// Store original get method BEFORE patching
74+
const originalGet = OriginalContext.get;
75+
76+
// Store test values
77+
let testContext = null;
78+
let testActor = null;
79+
let testUser = null;
80+
81+
// Patch Context.get to use test values
82+
OriginalContext.get = function(key, options) {
83+
// Check test values FIRST
84+
if (key === 'actor' && testActor) {
85+
return testActor;
86+
}
87+
if (key === 'user' && testUser) {
88+
return testUser;
89+
}
90+
91+
// Call original get method
92+
return originalGet.call(this, key, options);
93+
};
94+
95+
// Export patched Context with setters for test values
96+
return {
97+
...actual,
98+
Context: OriginalContext,
99+
__setTestValues: (ctx, actor, user) => {
100+
testContext = ctx;
101+
testActor = actor;
102+
testUser = user;
103+
},
104+
__clearTestValues: () => {
105+
testContext = null;
106+
testActor = null;
107+
testUser = null;
108+
},
109+
};
110+
});
111+
112+
// Store test values globally - this is the most reliable approach
113+
let globalTestActor = null;
114+
let globalTestUser = null;
115+
116+
// Patch Context.get directly (after imports) - this MUST work
117+
// We patch it on the actual Context class that's imported
118+
const originalContextGet = Context.get;
119+
Context.get = function(key, options) {
120+
// ALWAYS check global test values FIRST - this is the most reliable
121+
if (key === 'actor' && globalTestActor) {
122+
return globalTestActor;
123+
}
124+
if (key === 'user' && globalTestUser) {
125+
return globalTestUser;
126+
}
127+
// Call original
128+
return originalContextGet.call(this, key, options);
129+
};
130+
131+
// Also patch the Context class that CommonJS modules might have cached
132+
// Intercept require() calls to context.js and ensure they get our patched version
133+
const Module = require('module');
134+
const originalRequire = Module.prototype.require;
135+
Module.prototype.require = function(id) {
136+
const result = originalRequire.call(this, id);
137+
// If this is context.js, patch its Context.get
138+
if (id.includes('util/context') && result && result.Context) {
139+
const originalGet = result.Context.get;
140+
result.Context.get = function(key, options) {
141+
if (key === 'actor' && globalTestActor) {
142+
return globalTestActor;
143+
}
144+
if (key === 'user' && globalTestUser) {
145+
return globalTestUser;
146+
}
147+
return originalGet.call(this, key, options);
148+
};
149+
}
150+
return result;
151+
};
152+
*/
153+
154+
const ES_APP_ARGS = {
155+
entity: 'app',
156+
upstream: ESBuilder.create([
157+
SQLES, { table: 'app', debug: true },
158+
AppES,
159+
AppLimitedES, {
160+
permission_prefix: 'apps-of-user',
161+
exception: async () => {
162+
const actor = Context.get('actor');
163+
return new Or({
164+
children: [
165+
new Eq({
166+
key: 'approved_for_listing',
167+
value: 1,
168+
}),
169+
new Eq({
170+
key: 'uid',
171+
value: actor.type.app.uid,
172+
}),
173+
],
174+
});
175+
},
176+
},
177+
WriteByOwnerOnlyES,
178+
ValidationES,
179+
SetOwnerES,
180+
ProtectedAppES,
181+
MaxLimitES, { max: 5000 },
182+
]),
183+
};
184+
185+
const fixContextInitialization = async (callback) => {
186+
process.stdout.write(`contextAsyncLocalStorage:${ Context.contextAsyncLocalStorage }\n`);
187+
try {
188+
return Context.contextAsyncLocalStorage.run(Context.root, () => {
189+
Context.contextAsyncLocalStorage.getStore().set('context', Context.root);
190+
callback();
191+
});
192+
} catch (e) {
193+
process.stdout.write(e.stack);
194+
}
195+
};
196+
197+
const testWithEachService = async (fnToRunOnBoth) => {
198+
const esAppTestKernel = await createTestKernel({
199+
testCore: true,
200+
initLevelString: 'init',
201+
serviceMap: {
202+
'app-information': AppInformationService,
203+
'app-icon': AppIconService,
204+
'old-app-name': OldAppNameService,
205+
'es:app': EntityStoreService,
206+
},
207+
serviceMapArgs: {
208+
'es:app': ES_APP_ARGS,
209+
},
210+
});
211+
await tmp_provide_services(esAppTestKernel.services);
212+
213+
const appTestKernel = await createTestKernel({
214+
testCore: true,
215+
initLevelString: 'init',
216+
serviceMap: {
217+
'app-information': AppInformationService,
218+
'app-icon': AppIconService,
219+
'old-app-name': OldAppNameService,
220+
'app': AppService,
221+
},
222+
});
223+
await tmp_provide_services(appTestKernel.services);
224+
225+
await fnToRunOnBoth({ kernel: esAppTestKernel, key: 'es:app' });
226+
await fnToRunOnBoth({ kernel: appTestKernel, key: 'app' });
227+
228+
// Expect these tables to have the same values:
229+
const relevant_tables = ['apps', 'app_filetype_association'];
230+
const db_esApp = esAppTestKernel.services.get('database').get('write', 'test');
231+
const db_app = appTestKernel.services.get('database').get('write', 'test');
232+
for ( const table_name of relevant_tables ) {
233+
const rows_esApp = db_esApp.read(`SELECT * FROM ${table_name}`);
234+
const rows_app = db_app.read(`SELECT * FROM ${table_name}`);
235+
expect(rows_app).toEqual(rows_esApp);
236+
}
237+
};
238+
239+
describe('AppService Regression Prevention Tests', () => {
240+
it('should be testable with two test kernels', async () => {
241+
await testWithEachService(() => {
242+
});
243+
});
244+
it('should create the app', async () => {
245+
await fixContextInitialization(async () => {
246+
await testWithEachService(async ({ kernel, key }) => {
247+
// Context initialization fix
248+
249+
// Create a test user and context
250+
const db = kernel.services.get('database').get('write', 'test');
251+
const userId = 1;
252+
const username = 'testuser';
253+
const uuid = `user-uuid-${userId}`;
254+
255+
// Insert the user into the database if not exists
256+
const existingUser = await kernel.services.get('database')
257+
.get('read', 'test')
258+
.read('SELECT * FROM user WHERE uuid = ?', [uuid]);
259+
260+
if ( existingUser.length === 0 ) {
261+
await db.write('INSERT INTO user (uuid, username, free_storage) VALUES (?, ?, ?)',
262+
[uuid, username, 1024 * 1024 * 1024]);
263+
}
264+
265+
// Read the user back to get the actual id
266+
const users = await kernel.services.get('database')
267+
.get('read', 'test')
268+
.read('SELECT * FROM user WHERE uuid = ?', [uuid]);
269+
270+
const user = users[0];
271+
if ( ! user ) {
272+
throw new Error('Failed to create or retrieve test user');
273+
}
274+
275+
const actor = await Actor.create(UserActorType, { user });
276+
if ( !actor || !actor.type ) {
277+
throw new Error('Failed to create actor');
278+
}
279+
280+
const userContext = kernel.root_context.sub({
281+
user,
282+
actor,
283+
});
284+
285+
await userContext.arun(async () => {
286+
Context.set('actor', actor);
287+
// Set test values in BOTH mocked Context modules AND globally
288+
// globalTestActor = actor;
289+
// globalTestUser = user;
290+
291+
const contextCJS = require('../../util/context');
292+
const contextESM = await import('../../util/esmcontext.js');
293+
if ( contextCJS.__setTestValues ) contextCJS.__setTestValues(userContext, actor, user);
294+
if ( contextESM.__setTestValues ) contextESM.__setTestValues(userContext, actor, user);
295+
296+
process.stdout.write(`actor: ${actor}\n`);
297+
process.stdout.write(`context root from test: ${Context.get('rootContextUUID')}\n`);
298+
299+
try {
300+
const service = kernel.services.get(key);
301+
const crudQ = service.constructor.IMPLEMENTS['crud-q'];
302+
await crudQ.create.call(service, {
303+
object: {
304+
name: 'test-app',
305+
title: 'Test App',
306+
index_url: 'https://example.com',
307+
},
308+
});
309+
} finally {
310+
// Clear after test
311+
// globalTestActor = null;
312+
// globalTestUser = null;
313+
314+
const contextCJS = require('../../util/context');
315+
const contextESM = await import('../../util/esmcontext.js');
316+
if ( contextCJS.__clearTestValues ) contextCJS.__clearTestValues();
317+
if ( contextESM.__clearTestValues ) contextESM.__clearTestValues();
318+
}
319+
});
320+
});
321+
});
322+
});
323+
});

0 commit comments

Comments
 (0)