chore: Fixed tests

This commit is contained in:
Simeon Radivoev 2026-05-15 15:07:51 +03:00
parent 9141fb35d4
commit 5593985884
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
13 changed files with 139 additions and 66 deletions

1
.gitignore vendored
View file

@ -28,6 +28,7 @@ downloads
gameflow-deck.code-workspace
.env.local
src/tests/mock-roms/db.sqlite
src/tests/mock-roms/store
src/tests/mock-config
bin
.config/flatpak/repo

View file

@ -116,6 +116,13 @@ export async function cleanup ()
cleannedUp = true;
}
/** Reset the cleanup flags. This is mainly used by tests since they run the same app. */
export async function resetCleanup ()
{
cleaningUp = false;
cleannedUp = false;
}
export async function reloadDatabase ()
{
await ensureDir(config.get('downloadPath'));

View file

@ -454,18 +454,18 @@ export default new Elysia()
}, {
params: z.object({ id: z.string(), source: z.string() }),
})
.post('/game/:source/:id/install', async ({ params: { id, source }, body: { downloadId } }) =>
.post('/game/:source/:id/install', async ({ params: { id, source }, body }) =>
{
if (!taskQueue.findJob(InstallJob.query({ source, id }), InstallJob))
{
return taskQueue.enqueue(InstallJob.query({ source, id }), new InstallJob(id, source, { downloadId }));
return taskQueue.enqueue(InstallJob.query({ source, id }), new InstallJob(id, source, body));
} else
{
return status('Not Implemented');
}
}, {
params: z.object({ id: z.string(), source: z.string() }),
body: z.object({ downloadId: z.string().optional() }),
body: z.object({ downloadId: z.string().optional() }).optional(),
response: z.any()
})
.delete('/game/:source/:id/install', async ({ params: { id, source } }) =>

View file

@ -6,8 +6,9 @@ import { runBunPackageCommand } from "../plugins/services";
import { PluginRegistry } from "@/shared/constants";
import path from "node:path";
import sdkPkg from '@simeonradivoev/gameflow-sdk/package.json';
import { IsPluginAllowed } from "@/bun/utils";
export default class UpdateStoreJob implements IJob<never, string>
export default class EnsureStore implements IJob<never, string>
{
static id = "update-store" as const;
static dataSchema = z.never();
@ -20,7 +21,7 @@ export default class UpdateStoreJob implements IJob<never, string>
this.storeVersion = process.env.STORE_VERSION ?? "^0.1.0";
}
async start (context: JobContext<UpdateStoreJob, never, string>)
async start (context: JobContext<EnsureStore, never, string>)
{
const storeFolder = getStoreRootFolder();
await ensureDir(storeFolder);
@ -32,6 +33,8 @@ export default class UpdateStoreJob implements IJob<never, string>
const storePackage = await Bun.file(path.join(storeFolder, "package.json")).json();
if (IsPluginAllowed(sdkPkg.name))
{
if (!storePackage.dependencies?.[sdkPkg.name] || storePackage.dependencies?.[sdkPkg.name] !== sdkPkg.version)
{
let response = await runBunPackageCommand(["add", `${sdkPkg.name}@${sdkPkg.version}`, "--registry", PluginRegistry, '--omit', 'peer']);
@ -44,6 +47,10 @@ export default class UpdateStoreJob implements IJob<never, string>
let response = await runBunPackageCommand(["add", '--exact', `${sdkPkg.name}@latest`, "--registry", PluginRegistry, '--omit', 'peer']);
console.log(response);
}
} else
{
console.log("Ignoring SDK package");
}
if (process.env.CUSTOM_STORE_PATH) return;

View file

@ -3,7 +3,7 @@ import z, { _ZodType } from "zod";
import { taskQueue } from "../app";
import { LoginJob } from "./login-job";
import TwitchLoginJob from "./twitch-login-job";
import UpdateStoreJob from "./update-store";
import EnsureStore from "./ensure-store";
import { EmulatorDownloadJob } from "./emulator-download-job";
import { getErrorMessage } from "@/bun/utils";
import { BaseEvent, IJob } from "@simeonradivoev/gameflow-sdk/task-queue";
@ -184,7 +184,7 @@ export const jobs = new Elysia({ prefix: '/api/jobs' })
.use(registerJob(LaunchGameJob))
.use(registerJob(LoginJob))
.use(registerJob(TwitchLoginJob))
.use(registerJob(UpdateStoreJob))
.use(registerJob(EnsureStore))
.use(registerJob(BiosDownloadJob))
.use(registerJob(InstallJob))
.use(registerJob(ReloadPluginsJob))

View file

@ -2,14 +2,14 @@ import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-s
import desc from './package.json';
import path, { } from 'node:path';
import { buildStoreFrontendEmulatorSystems, getAllStoreEmulatorPackages, getStoreEmulatorPackage, getStoreFolder } from "@/bun/api/store/services/gamesService";
import { Glob, pathToFileURL, which } from "bun";
import { Glob, pathToFileURL, sleep, which } from "bun";
import { and, eq } from "drizzle-orm";
import * as emulatorSchema from '@schema/emulators';
import { config, emulatorsDb, taskQueue } from "@/bun/api/app";
import fs from "node:fs/promises";
import { getSourceGameDetailed } from "@/bun/api/games/services/utils";
import UpdateStoreJob from "@/bun/api/jobs/update-store";
import EnsureStore from "@/bun/api/jobs/ensure-store";
import { getEmulatorDownload, getEmulatorPath } from "@/bun/api/store/services/emulatorsService";
import { buildFilters, buildLaunchCommand, buildSaves, convertStoreEmulatorToFrontend, convertStoreToFrontend, convertStoreToFrontendDetailed, getExistingStoreEmulatorDownload, getShuffledStoreGames, getStoreGame, getValidDownloads } from "./services";
import { DownloadInfo, FrontEndEmulatorDetailed, FrontEndGameTypeWithIds } from "@simeonradivoev/gameflow-sdk/shared";
@ -20,7 +20,7 @@ import StreamZip from "node-stream-zip";
import { path7za } from "7zip-bin";
import Seven from 'node-7z';
export default class RommIntegration implements PluginType
export default class StoreIntegration implements PluginType
{
eventsNames = [{ id: 'updateStore', title: "Update Store", description: "Update the Store Manifest", action: "Update" }];
@ -29,7 +29,7 @@ export default class RommIntegration implements PluginType
switch (e)
{
case 'updateStore':
await taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob());
await taskQueue.enqueue(EnsureStore.id, new EnsureStore());
return { reload: true };
}
}
@ -38,7 +38,7 @@ export default class RommIntegration implements PluginType
{
console.log("Store Directory is ", getStoreFolder());
ctx.setProgress(0, "Updating Store");
await taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob());
await taskQueue.enqueue(EnsureStore.id, new EnsureStore());
}
async load (ctx: PluginLoadingContextType)

View file

@ -14,10 +14,12 @@ import rclone from './builtin/other/com.simeonradivoev.gameflow.rclone/package.j
import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@simeonradivoev/gameflow-sdk";
import path from 'node:path';
import { getStoreRootFolder } from "../store/services/gamesService";
import { getUpdates } from "./services";
import { getUpdates, runBunPackageCommand } from "./services";
import { PluginSourceType } from "@simeonradivoev/gameflow-sdk/shared";
import { taskQueue } from "../app";
import UpdateStoreJob from "../jobs/update-store";
import EnsureStore from "../jobs/ensure-store";
import { PluginRegistry } from "@/shared/constants";
import { IsPluginAllowed } from "@/bun/utils";
type PluginEntry = PluginDescriptionType & { load: () => Promise<any>; };
@ -58,15 +60,9 @@ export async function unregisterPlugin (id: string, pluginManager: PluginManager
export async function registerPlugin (plugin: PluginEntry, source: PluginSourceType, pluginManager: PluginManager)
{
if (process.env.PLUGIN_WHITELIST && !process.env.PLUGIN_WHITELIST.includes(plugin.name))
if (!IsPluginAllowed(plugin.name))
{
console.log("Skipping", plugin.name, "missing in whitelist");
return;
}
if (process.env.PLUGIN_BLACKLIST && process.env.PLUGIN_BLACKLIST.includes(plugin.name))
{
console.log("Skipping", plugin.name, "found in whitelist");
console.log("Skipping", plugin.name, "plugin not allowed");
return;
}
@ -101,11 +97,13 @@ export default async function register (pluginManager: PluginManager)
await Promise.all(plugins.map(p => registerPlugin(p, 'builtin', pluginManager)));
if (IsPluginAllowed('@simeonradivoev/gameflow-store'))
{
const storePackageFilePath = path.join(getStoreRootFolder(), 'package.json');
if (!await Bun.file(storePackageFilePath).exists())
{
console.log("Store is missing. Updating it.");
await taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob());
await taskQueue.enqueue(EnsureStore.id, new EnsureStore());
console.log("Store Updated");
}
const storePackage = await Bun.file(storePackageFilePath).json();
@ -124,16 +122,27 @@ export default async function register (pluginManager: PluginManager)
if (outdated)
{
validPlugins.forEach(p =>
for await (const plugin of validPlugins)
{
const newVersion = outdated[p.name];
const newVersion = outdated[plugin.name];
if (newVersion)
{
console.log("Plugin", p.name, "has update", p.version, "=>", newVersion);
console.log("Plugin", plugin.name, "has update", plugin.version, "=>", newVersion);
}
if (plugin.autoUpdate)
{
console.log("Auto Updating Plugin", plugin.name);
let response = await runBunPackageCommand(["add", `${plugin.name}@${newVersion}`, "--registry", PluginRegistry, '--omit', 'peer']);
console.log(response);
}
}
});
}
await Promise.all(validPlugins.map(p => registerPlugin(p, 'store', pluginManager)));
}
} else
{
console.log('Skipping Store Packages');
}
}

View file

@ -3,6 +3,7 @@ import os from 'node:os';
import { getStoreRootFolder } from '../store/services/gamesService';
import { PluginDescriptionType } from '@simeonradivoev/gameflow-sdk';
import { run } from 'npm-check-updates';
import { existsSync } from 'node:fs';
export function canDisable (description: PluginDescriptionType)
{
@ -15,6 +16,7 @@ export function canDisable (description: PluginDescriptionType)
export async function getUpdates ()
{
if (!existsSync(getStoreRootFolder())) return {};
const updated = await run({ packageManager: 'bun', peer: true, cwd: getStoreRootFolder(), jsonUpgraded: true, reject: ['@simeonradivoev/gameflow-sdk'] });
return updated as Record<string, string>;
}

View file

@ -188,16 +188,16 @@ export const store = new Elysia({ prefix: '/api/store' })
emulator.integrations = integrations;
return emulator;
}, { params: z.object({ id: z.string() }) })
.post('/install/emulator/:id/:source', async ({ params: { source, id }, body: { isUpdate } }) =>
.post('/install/emulator/:id/:source', async ({ params: { source, id }, body }) =>
{
if (taskQueue.hasActiveOfType(EmulatorDownloadJob))
{
return status("Conflict", "Installation already running");
}
const job = new EmulatorDownloadJob(id, source, { isUpdate });
const job = new EmulatorDownloadJob(id, source, body);
return taskQueue.enqueue(EmulatorDownloadJob.id, job);
}, {
body: z.object({ isUpdate: z.boolean().optional() })
body: z.object({ isUpdate: z.boolean().optional() }).optional()
})
.delete('/emulator/:id', async ({ params: { id } }) =>
{

View file

@ -186,3 +186,18 @@ export function isArchive (path: string)
{
return archiveRegex.test(path);
}
export function IsPluginAllowed (id: string)
{
if (process.env.PLUGIN_WHITELIST && !process.env.PLUGIN_WHITELIST.includes(id))
{
return false;
}
if (process.env.PLUGIN_BLACKLIST && process.env.PLUGIN_BLACKLIST.includes(id))
{
return false;
}
return true;
}

View file

@ -41,7 +41,8 @@ export const PluginDescriptionSchema = z.object({
peerDependencies: z.record(z.string(), z.string()).optional(),
category: z.string().default("other"),
main: z.string().describe("The main entry. It must export a default class implementing PluginType"),
canDisable: z.boolean().default(true).optional().describe("Can the plugin be disabled or enabled by the user")
canDisable: z.boolean().default(true).optional().describe("Can the plugin be disabled or enabled by the user"),
autoUpdate: z.boolean().optional().describe("Should the plugin auto update to latest version")
});
export const PluginSchema = z.object({

View file

@ -91,6 +91,11 @@ export class TaskQueue
return this.activeQueue.length > 0;
}
public hasQueued ()
{
return this.queue && this.queue.length > 0;
}
public hasActiveOfType (type: any)
{
for (const entry of this.activeQueue)
@ -109,6 +114,30 @@ export class TaskQueue
return job?.promise.promise ?? Promise.resolve();
}
public waitForAll ()
{
return new Promise((resolve) =>
{
if (!this.hasActive())
{
resolve(true);
return;
}
const handleEnded = () =>
{
if (!this.hasActive() && !this.hasQueued())
{
resolve(true);
this.events?.removeListener('ended', handleEnded);
this.events?.removeListener('abort', handleEnded);
}
};
this.events?.on('ended', handleEnded);
this.events?.on('abort', handleEnded);
});
}
public cancelJob (id: string)
{
const job = this.queue?.find(j => j.id === id)

View file

@ -1,18 +1,20 @@
import { beforeAll, beforeEach, afterEach } from 'bun:test';
import { resolve } from 'node:path';
import * as app from '@/bun/api/app';
import { remove } from 'fs-extra';
import { ensureDir, remove } from 'fs-extra';
export async function LoadApp ()
{
console.log("Loading App");
await app.load();
await app.taskQueue.waitForAll();
}
export async function CleanupApp ()
{
console.log("Cleaning Up App");
await app.cleanup();
await app.resetCleanup();
}
beforeAll(async () =>
@ -20,7 +22,7 @@ beforeAll(async () =>
process.env.CUSTOM_STORE_PATH = resolve('./src/tests/mock-store');
process.env.CONFIG_CWD = resolve('./src/tests/mock-config');
process.env.DEFAULT_DOWNLOAD_PATH = resolve('./src/tests/mock-roms');
process.env.PLUGIN_BLACKLIST = 'com.simeonradivoev.gameflow.rclone';
process.env.PLUGIN_BLACKLIST = 'com.simeonradivoev.gameflow.rclone,@simeonradivoev/gameflow-store,com.simeonradivoev.gameflow.romm,com.simeonradivoev.gameflow.igdb,@simeonradivoev/gameflow-sdk';
});
async function FileCleanup ()