mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-06 05:55:42 +00:00
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Pipeline - CORS preflights send Access-Control-Max-Age: browsers stop re-preflighting authed calls every 5 s. - `req.ua` is parsed on first read instead of on every request. - JSON `rawBody` reuses the parsed buffer instead of copying it. - `emitAndWait` returns early when nothing listens: no span or closure per unlistened driver call. - SQLBatcher sends a lone query on the next tick while fewer than 8 flushes are in flight, and waits `maxTimeInQueue` as before otherwise. Lone reads skip the 10/30 ms wait; batching under load is unchanged. Auth and permissions - Token-to-actor auth starts the user, session and app lookups together and checks them in the same order: fewer sequential Redis round trips per authed request. - get-user-app-token and check-app reuse the resolved app row. getOrCreateApp skips the re-select after its own insert and doesn't wait on the cache write for replica rows. - getOrCreateApp reads the race winner from the primary when its insert loses, so it no longer hands out a session that was never written. - whoami runs the email check and the OIDC/taskbar/team lookups in parallel; key order unchanged. - OIDC providers are memoized once every discovery succeeds, with one discovery fetch in flight per URL, and the Apple key is parsed once. - gui-token and sync-cookie use `req.actor.user` instead of re-reading the user. - listSessions batches app lookups. App icon probes run together, and the original-icon probe skips the lineage fallback. - Default-granted permissions are answered from config before the scan cache, and exploded options are deduped: one less Redis read per /drivers/call. - ACL resolves ancestors lazily (owners and full-access tokens skip them) and scans the node first, then the ancestors concurrently on a miss. Filesystem - Missing-path lineage lookups run 10 at a time: batch-upload path resolution drops from about 1.3 s to about 40 ms per 100 new files. - MySQL single-file inserts return the row built from the written values, falling back to the primary read: one less primary round trip per new file. - Batch thumbnails are stored with one UPDATE per chunk (200 rows or 4M chars), with a per-row fallback. - storage.quota.bonus listeners run concurrently next to the usage SUM, and the payload carries `userUuid`: quota checks and /df no longer serialize the listener reads. - readdir cache backfill is one pipeline instead of 3xN SETEX. - Profile reads skip the lineage fallback; the own profile is read alongside the user lookup. - Legacy /stat runs is_empty (new `hasChildrenByParentUid`), owner and thumbnail concurrently. - Downloads and the video proxy pipe through `pipeToResponse`: aborts release the storage stream at once with no extra 'close' listeners, and the video proxy no longer crashes on a mid-stream upstream error. - mkdir looks up target and parent together. - Upload completion reuses the session from the access check (status is still re-read before completing), and multipart signing reads it once. - Moving a shared file to Trash skips two lookups that always returned the entry itself. Drivers and events - Subscription resolvers run concurrently and are folded in registration order. - SubdomainDriver and WorkerDriver overlap independent lookups. - The events "anyone subscribed?" check is one Redis pipeline when the region is warm. - Handler publish overlaps the count with the size check. - Broadcast flushes peers concurrently over a keep-alive agent, so a slow peer no longer delays cross-region invalidations to the others. Sites, share and AI - Hosted sites resolve the owner first, then the app and root together. Site config loads only on 404s. The hosted-app query selects only the columns it uses and moves into AppStore. - shareFlags asks about the plan only when an item is shared by link only. - Share listings run the count, teams and plan reads alongside their page query. - Claude attachment cleanup no longer blocks the response. - New util `awaitLater` for promises started early and awaited later.
141 lines
4.9 KiB
TypeScript
141 lines
4.9 KiB
TypeScript
/*
|
|
* Copyright (C) 2024-present Puter Technologies Inc.
|
|
*
|
|
* This file is part of Puter.
|
|
*
|
|
* Puter is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import type { Request, Response } from 'express';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import {
|
|
afterAll,
|
|
afterEach,
|
|
beforeAll,
|
|
describe,
|
|
expect,
|
|
it,
|
|
vi,
|
|
} from 'vitest';
|
|
import type { PuterServer } from '../../server.js';
|
|
import type { UserRow } from '../../stores/user/UserStore.js';
|
|
import { setupTestServer } from '../../testUtil.js';
|
|
import type { ProfileController } from './ProfileController.js';
|
|
|
|
// GET /profile without a username: the caller's own profile, read alongside
|
|
// their account row.
|
|
describe('ProfileController own profile', () => {
|
|
let server: PuterServer;
|
|
let controller: ProfileController;
|
|
|
|
beforeAll(async () => {
|
|
server = await setupTestServer();
|
|
controller = server.controllers.profile as unknown as ProfileController;
|
|
}, 120_000);
|
|
|
|
afterAll(async () => {
|
|
await server?.shutdown();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const makeUser = async (): Promise<UserRow> => {
|
|
const username = `pc-${Math.random().toString(36).slice(2, 10)}`;
|
|
return server.stores.user.create({
|
|
username,
|
|
uuid: uuidv4(),
|
|
password: null,
|
|
email: `${username}@test.local`,
|
|
requires_email_confirmation: false,
|
|
} as Parameters<typeof server.stores.user.create>[0]);
|
|
};
|
|
|
|
const getOwn = async (uuid: string) => {
|
|
let body: unknown;
|
|
const res = {
|
|
json: (value: unknown) => {
|
|
body = value;
|
|
},
|
|
} as unknown as Response;
|
|
await controller.get(
|
|
{ query: {}, actor: { user: { uuid } } } as unknown as Request,
|
|
res,
|
|
);
|
|
return body;
|
|
};
|
|
|
|
const isProfilePath = (path: unknown) =>
|
|
String(path).startsWith('/system/profiles/');
|
|
|
|
it('starts reading the profile before the account lookup returns', async () => {
|
|
const user = await makeUser();
|
|
await server.services.profile.updateProfile(user, { bio: 'hi' });
|
|
|
|
const order: string[] = [];
|
|
const getByUuid = server.stores.user.getByUuid.bind(server.stores.user);
|
|
vi.spyOn(server.stores.user, 'getByUuid').mockImplementation(
|
|
async (...args: Parameters<typeof getByUuid>) => {
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
order.push('account');
|
|
return getByUuid(...args);
|
|
},
|
|
);
|
|
const getEntryByPath = server.stores.fsEntry.getEntryByPath.bind(
|
|
server.stores.fsEntry,
|
|
);
|
|
vi.spyOn(server.stores.fsEntry, 'getEntryByPath').mockImplementation(
|
|
async (...args: Parameters<typeof getEntryByPath>) => {
|
|
if (isProfilePath(args[0])) order.push('profile');
|
|
return getEntryByPath(...args);
|
|
},
|
|
);
|
|
|
|
await expect(getOwn(user.uuid)).resolves.toEqual({
|
|
picture: null,
|
|
displayName: null,
|
|
bio: 'hi',
|
|
});
|
|
expect(order).toEqual(['profile', 'account']);
|
|
});
|
|
|
|
it('404s a caller with no account, whatever the profile read did', async () => {
|
|
vi.spyOn(server.stores.fsEntry, 'getEntryByPath').mockRejectedValue(
|
|
new Error('profile read failed'),
|
|
);
|
|
await expect(getOwn(uuidv4())).rejects.toMatchObject({
|
|
statusCode: 404,
|
|
});
|
|
});
|
|
|
|
it('surfaces the account lookup failure before the profile read failure', async () => {
|
|
vi.spyOn(server.stores.user, 'getByUuid').mockRejectedValue(
|
|
new Error('account lookup failed'),
|
|
);
|
|
vi.spyOn(server.stores.fsEntry, 'getEntryByPath').mockRejectedValue(
|
|
new Error('profile read failed'),
|
|
);
|
|
await expect(getOwn(uuidv4())).rejects.toThrow('account lookup failed');
|
|
});
|
|
|
|
it('surfaces a failed profile read for an existing account', async () => {
|
|
const user = await makeUser();
|
|
vi.spyOn(server.stores.fsEntry, 'getEntryByPath').mockRejectedValue(
|
|
new Error('profile read failed'),
|
|
);
|
|
await expect(getOwn(user.uuid)).rejects.toThrow('profile read failed');
|
|
});
|
|
});
|