mirror of
https://github.com/Drop-OSS/drop
synced 2026-08-27 14:23:05 -04:00
test(server): add metadata provider chain and plugin init-order tests
F2: 5 tests for provider chain fallthrough (parallel Promise.allSettled), timeout handling, and fuzzy sort correctness. F5: 10 tests for plugin ordering, circular dependency detection, and postcondition verification. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
7a469e6006
commit
bb98e4faf4
2 changed files with 655 additions and 0 deletions
321
server/test/unit/metadata/provider-chain.test.ts
Normal file
321
server/test/unit/metadata/provider-chain.test.ts
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
/**
|
||||
* F2: Metadata Provider Chain Fallthrough
|
||||
*
|
||||
* Tests MetadataHandler.search() which:
|
||||
* - Runs ALL providers in PARALLEL via Promise.allSettled (not sequential)
|
||||
* - Each provider has a per-provider timeout via setTimeout
|
||||
* - Failed/timeout providers are filtered out
|
||||
* - Results from all successful providers are merged and sorted by fuzzy score desc
|
||||
* - Providers implement MetadataProvider abstract class
|
||||
*
|
||||
* Key behaviors verified:
|
||||
* 1. Failures don't block other providers' results
|
||||
* 2. All providers fail → empty array (not crash)
|
||||
* 3. Timeout excludes slow providers
|
||||
* 4. Multi-provider results sorted correctly by fuzzy match
|
||||
* 5. Empty results from providers don't add noise
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import {
|
||||
MetadataHandler,
|
||||
MetadataProvider,
|
||||
} from "../../../server/internal/metadata/index";
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import type { GameMetadataSearchResult } from "../../../server/internal/metadata/types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module mocks - hoisted by vitest before all imports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Mock systemConfig so its singleton doesn't call useRuntimeConfig() at import
|
||||
vi.mock("../../../server/internal/config/sys-conf", () => ({
|
||||
systemConfig: {
|
||||
getMetadataTimeout: () => 100,
|
||||
getDropVersion: () => "test",
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock database to prevent Prisma client init from connecting to a real DB
|
||||
vi.mock("../../../server/internal/db/database", () => ({
|
||||
default: {},
|
||||
}));
|
||||
|
||||
// Mock objects module to prevent useStorage() call during module load
|
||||
// (ObjectTransactionalHandler imports objects/index.ts which triggers FsHashStore → useStorage)
|
||||
vi.mock("../../../server/internal/objects", () => ({
|
||||
default: {},
|
||||
}));
|
||||
|
||||
// Mock tasks module to prevent acls→session→cacheHandler→useStorage chain
|
||||
vi.mock("../../../server/internal/tasks", () => ({
|
||||
default: { create: vi.fn() },
|
||||
wrapTaskContext: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock library module to prevent gamesize→manifest→cacheHandler→useStorage chain
|
||||
vi.mock("../../../server/internal/library", () => ({
|
||||
createGameImportTaskId: vi.fn().mockReturnValue("test-task-id"),
|
||||
}));
|
||||
|
||||
// Mock fast-fuzzy so fuzzy scores are deterministic for sorting tests
|
||||
vi.mock("fast-fuzzy", () => ({
|
||||
fuzzy: vi.fn((_query: string, name: string) => {
|
||||
const scores: Record<string, number> = {
|
||||
"Exact Match": 1,
|
||||
"Close Match": 0.85,
|
||||
"Mediocre Match": 0.5,
|
||||
"Distant Match": 0.2,
|
||||
};
|
||||
return scores[name] ?? 0.3;
|
||||
}),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock Provider Classes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class FailingProvider extends MetadataProvider {
|
||||
name(): string {
|
||||
return "FailingProvider";
|
||||
}
|
||||
source(): MetadataSource {
|
||||
return MetadataSource.IGDB;
|
||||
}
|
||||
async search(): Promise<GameMetadataSearchResult[]> {
|
||||
throw new Error("Provider simulated failure");
|
||||
}
|
||||
async fetchGame(): Promise<never> {
|
||||
throw new Error("not used in search test");
|
||||
}
|
||||
async fetchCompany(): Promise<undefined> {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class MockSuccessProvider extends MetadataProvider {
|
||||
readonly name: () => string;
|
||||
readonly source: () => MetadataSource;
|
||||
readonly results: GameMetadataSearchResult[];
|
||||
|
||||
constructor(
|
||||
label: string,
|
||||
src: MetadataSource,
|
||||
results: GameMetadataSearchResult[],
|
||||
) {
|
||||
super();
|
||||
// Use arrow properties so `this` is captured from constructor scope,
|
||||
// working around PriorityListIndexed.getIndex calling index() without binding
|
||||
this.name = () => label;
|
||||
this.source = () => src;
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
async search(): Promise<GameMetadataSearchResult[]> {
|
||||
return this.results;
|
||||
}
|
||||
async fetchGame(): Promise<never> {
|
||||
throw new Error("not used in search test");
|
||||
}
|
||||
async fetchCompany(): Promise<undefined> {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class TimeoutProvider extends MetadataProvider {
|
||||
name(): string {
|
||||
return "TimeoutProvider";
|
||||
}
|
||||
source(): MetadataSource {
|
||||
return MetadataSource.GiantBomb;
|
||||
}
|
||||
async search(): Promise<GameMetadataSearchResult[]> {
|
||||
// Delay longer than the 100ms mock timeout
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
return [
|
||||
{
|
||||
id: "slow",
|
||||
name: "Slow Result",
|
||||
icon: "",
|
||||
description: "",
|
||||
year: 2024,
|
||||
},
|
||||
];
|
||||
}
|
||||
async fetchGame(): Promise<never> {
|
||||
throw new Error("not used in search test");
|
||||
}
|
||||
async fetchCompany(): Promise<undefined> {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyProvider extends MetadataProvider {
|
||||
name(): string {
|
||||
return "EmptyProvider";
|
||||
}
|
||||
source(): MetadataSource {
|
||||
return MetadataSource.Manual;
|
||||
}
|
||||
async search(): Promise<GameMetadataSearchResult[]> {
|
||||
return [];
|
||||
}
|
||||
async fetchGame(): Promise<never> {
|
||||
throw new Error("not used in search test");
|
||||
}
|
||||
async fetchCompany(): Promise<undefined> {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("MetadataHandler provider chain (search)", () => {
|
||||
let handler: MetadataHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new MetadataHandler();
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// F2.1: Provider failure + success mix
|
||||
// -----------------------------------------------------------------------
|
||||
it("returns merged results from all successful providers when some fail", async () => {
|
||||
handler.addProvider(new FailingProvider(), 10);
|
||||
handler.addProvider(
|
||||
new MockSuccessProvider("ProvB", MetadataSource.Steam, [
|
||||
{ id: "b1", name: "Close Match", icon: "", description: "", year: 2024 },
|
||||
{ id: "b2", name: "Exact Match", icon: "", description: "", year: 2023 },
|
||||
]),
|
||||
5,
|
||||
);
|
||||
handler.addProvider(
|
||||
new MockSuccessProvider("ProvC", MetadataSource.GiantBomb, [
|
||||
{ id: "c1", name: "Mediocre Match", icon: "", description: "", year: 2022 },
|
||||
]),
|
||||
0,
|
||||
);
|
||||
|
||||
const results = await handler.search("test");
|
||||
|
||||
// All 3 results from the 2 successful providers should be present
|
||||
expect(results).toHaveLength(3);
|
||||
const ids = results.map((r) => r.id);
|
||||
expect(ids).toContain("b1");
|
||||
expect(ids).toContain("b2");
|
||||
expect(ids).toContain("c1");
|
||||
// Each result should have source metadata attached
|
||||
expect(results.every((r) => "sourceId" in r && "sourceName" in r)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// F2.2: All providers fail → empty result
|
||||
// -----------------------------------------------------------------------
|
||||
it("returns empty array when all providers fail (no crash)", async () => {
|
||||
handler.addProvider(new FailingProvider(), 10);
|
||||
handler.addProvider(new FailingProvider(), 5);
|
||||
|
||||
const results = await handler.search("test query");
|
||||
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty array when all providers return empty results", async () => {
|
||||
handler.addProvider(new EmptyProvider(), 10);
|
||||
handler.addProvider(new EmptyProvider(), 5);
|
||||
|
||||
const results = await handler.search("anything");
|
||||
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// F2.3: Provider timeout interleaving
|
||||
// -----------------------------------------------------------------------
|
||||
it("excludes timed-out providers but keeps fast providers' results", async () => {
|
||||
handler.addProvider(
|
||||
new MockSuccessProvider("FastA", MetadataSource.Steam, [
|
||||
{
|
||||
id: "fast1",
|
||||
name: "Fast Result",
|
||||
icon: "",
|
||||
description: "",
|
||||
year: 2024,
|
||||
},
|
||||
]),
|
||||
10,
|
||||
);
|
||||
handler.addProvider(new TimeoutProvider(), 5);
|
||||
handler.addProvider(
|
||||
new MockSuccessProvider("FastB", MetadataSource.IGDB, [
|
||||
{
|
||||
id: "fast2",
|
||||
name: "Quick Result",
|
||||
icon: "",
|
||||
description: "",
|
||||
year: 2025,
|
||||
},
|
||||
]),
|
||||
0,
|
||||
);
|
||||
|
||||
const results = await handler.search("test");
|
||||
|
||||
// Only the 2 fast providers' results, not the timed-out one
|
||||
expect(results).toHaveLength(2);
|
||||
const ids = results.map((r) => r.id);
|
||||
expect(ids).toContain("fast1");
|
||||
expect(ids).toContain("fast2");
|
||||
expect(ids).not.toContain("slow");
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// F2.4: Fuzzy sort correctness across multi-provider results
|
||||
// -----------------------------------------------------------------------
|
||||
it("sorts merged results by fuzzy match score in descending order", async () => {
|
||||
handler.addProvider(
|
||||
new MockSuccessProvider("ProvA", MetadataSource.Steam, [
|
||||
{
|
||||
id: "distant",
|
||||
name: "Distant Match",
|
||||
icon: "",
|
||||
description: "",
|
||||
year: 2020,
|
||||
},
|
||||
]),
|
||||
5,
|
||||
);
|
||||
handler.addProvider(
|
||||
new MockSuccessProvider("ProvB", MetadataSource.IGDB, [
|
||||
{
|
||||
id: "exact",
|
||||
name: "Exact Match",
|
||||
icon: "",
|
||||
description: "",
|
||||
year: 2024,
|
||||
},
|
||||
{
|
||||
id: "mediocre",
|
||||
name: "Mediocre Match",
|
||||
icon: "",
|
||||
description: "",
|
||||
year: 2022,
|
||||
},
|
||||
]),
|
||||
10,
|
||||
);
|
||||
|
||||
const results = await handler.search("query");
|
||||
|
||||
// Sorted by fuzzy score descending: Exact (1) > Mediocre (0.5) > Distant (0.2)
|
||||
expect(results.map((r) => r.id)).toEqual(["exact", "mediocre", "distant"]);
|
||||
// Verify scores are monotonically non-increasing
|
||||
for (let i = 1; i < results.length; i++) {
|
||||
expect(results[i - 1].fuzzy).toBeGreaterThanOrEqual(results[i].fuzzy);
|
||||
}
|
||||
});
|
||||
});
|
||||
334
server/test/unit/plugins/init-order.test.ts
Normal file
334
server/test/unit/plugins/init-order.test.ts
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
// F5: Plugin Init Order — verify Nitro plugin ordering (01- through 09-)
|
||||
//
|
||||
// Tests at the structural + behavioral level without Nitro runtime or Prisma:
|
||||
// 1. File prefix ordering matches dependency graph
|
||||
// 2. No circular imports between plugins
|
||||
// 3. Wrong-prefix-position detection logic works
|
||||
// 4. MetadataHandler addProvider → non-empty postcondition (isolated)
|
||||
// 5. AuthManager.getEnabledAuthProviders() postcondition (isolated)
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Resolve from actual test file location, NOT process.cwd()
|
||||
// (vitest Nuxt env has multiple CWD contexts)
|
||||
const pluginsDir = join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
"../../../server/plugins",
|
||||
);
|
||||
|
||||
/** Return the numbered plugin filenames sorted by their numeric prefix. */
|
||||
function numberedPluginFiles(dir: string): string[] {
|
||||
return readdirSync(dir)
|
||||
.filter((f) => /^\d{2}/.test(f) && f.endsWith(".ts"))
|
||||
.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect misordered plugins: if any plugin file imports from a plugin that
|
||||
* has a higher numeric prefix (i.e. runs later), the ordering is wrong.
|
||||
* Returns the offending plugin filename or null.
|
||||
*/
|
||||
function detectMisorderedPlugin(dir: string): string | null {
|
||||
const files = numberedPluginFiles(dir);
|
||||
const prefixToFile = new Map<string, string>();
|
||||
const prefixOrder = new Map<string, number>();
|
||||
files.forEach((f, i) => {
|
||||
const p = f.slice(0, 2);
|
||||
prefixToFile.set(p, f);
|
||||
prefixOrder.set(p, i);
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
const content = readFileSync(join(dir, file), "utf-8");
|
||||
const imports = content.match(
|
||||
/from\s+["'][^"']*\/plugins\/(\d{2})[^"']*["']/g,
|
||||
);
|
||||
if (!imports) continue;
|
||||
|
||||
const filePrefix = file.slice(0, 2);
|
||||
const fileIdx = prefixOrder.get(filePrefix)!;
|
||||
|
||||
for (const imp of imports) {
|
||||
const match = imp.match(/\/plugins\/(\d{2})/);
|
||||
if (!match) continue;
|
||||
const refPrefix = match[1];
|
||||
const refIdx = prefixOrder.get(refPrefix);
|
||||
if (refIdx === undefined) continue;
|
||||
if (refIdx > fileIdx) return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal PriorityListIndexed replica for isolated postcondition testing.
|
||||
* Mirrors the ordering behavior of the real
|
||||
* PriorityListIndexed from server/internal/utils/prioritylist.
|
||||
*/
|
||||
class PriorityListIndexed<T extends { source: () => string }> {
|
||||
private items: Array<{ obj: T; priority: number }> = [];
|
||||
private sorted: T[] | null = null;
|
||||
|
||||
push(obj: T, priority = 0): void {
|
||||
this.items.push({ obj, priority });
|
||||
this.sorted = null;
|
||||
}
|
||||
|
||||
values(): T[] {
|
||||
if (!this.sorted) {
|
||||
// Stable sort: higher priority first, then insertion order
|
||||
const indexed = this.items.map((item, i) => ({ ...item, i }));
|
||||
indexed.sort((a, b) => {
|
||||
if (b.priority !== a.priority) return b.priority - a.priority;
|
||||
return a.i - b.i;
|
||||
});
|
||||
this.sorted = indexed.map((i) => i.obj);
|
||||
}
|
||||
return [...this.sorted];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal MetadataHandler replica for isolated postcondition testing.
|
||||
*/
|
||||
class MetadataHandler {
|
||||
private readonly providers = new PriorityListIndexed<{
|
||||
source: () => string;
|
||||
}>();
|
||||
|
||||
addProvider(
|
||||
provider: { source: () => string; name: () => string },
|
||||
priority = 0,
|
||||
): void {
|
||||
this.providers.push(provider, priority);
|
||||
}
|
||||
|
||||
fetchProviderIdsInOrder(): string[] {
|
||||
return this.providers
|
||||
.values()
|
||||
.map((e) => e.source())
|
||||
.filter((e) => e !== "Manual");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal AuthManager replica for isolated postcondition testing.
|
||||
* Mirrors the logic in server/server/internal/auth/index.ts.
|
||||
*/
|
||||
class AuthManager {
|
||||
private authProviders: Record<string, boolean | undefined> = {};
|
||||
|
||||
async init(): Promise<void> {
|
||||
// Simulate OIDC: fails in test env (no env vars) → caught silently
|
||||
try {
|
||||
if (process.env.OIDC_WELLKNOWN) {
|
||||
this.authProviders["OpenID"] = true;
|
||||
} else {
|
||||
throw new Error("OIDC not configured");
|
||||
}
|
||||
} catch {
|
||||
// OIDC init failed — expected in test env without env vars
|
||||
}
|
||||
|
||||
// Simple auth: enabled unless DISABLE_SIMPLE_AUTH is set
|
||||
const disabled = process.env.DISABLE_SIMPLE_AUTH;
|
||||
if (!disabled) {
|
||||
this.authProviders["Simple"] = true;
|
||||
}
|
||||
|
||||
// Fallback: if no OpenID, ensure Simple is on
|
||||
if (!this.authProviders["OpenID"]) {
|
||||
this.authProviders["Simple"] = true;
|
||||
}
|
||||
}
|
||||
|
||||
getEnabledAuthProviders(): string[] {
|
||||
return Object.entries(this.authProviders)
|
||||
.filter(([, v]) => !!v)
|
||||
.map(([k]) => k);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Nitro Plugin Init Order", () => {
|
||||
describe("plugin file ordering (structural)", () => {
|
||||
it("has sequential numeric prefixes 01 through 07", () => {
|
||||
const files = numberedPluginFiles(pluginsDir);
|
||||
expect(files.length).toBeGreaterThanOrEqual(7);
|
||||
|
||||
const prefixes = files.map((f) => f.slice(0, 2));
|
||||
for (let i = 0; i < prefixes.length; i++) {
|
||||
expect(prefixes[i]).toBe(
|
||||
String(i + 1).padStart(2, "0"),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("has no circular imports between plugins", () => {
|
||||
const files = numberedPluginFiles(pluginsDir);
|
||||
|
||||
for (const file of files) {
|
||||
const content = readFileSync(join(pluginsDir, file), "utf-8");
|
||||
// No plugin should import from another plugin file
|
||||
const pluginImports = content.match(
|
||||
/from\s+["'][^"']*\/plugins\/[^"']*["']/g,
|
||||
);
|
||||
expect(pluginImports).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("dependency order matches prefix order (earlier deps → later plugins)", () => {
|
||||
const files = numberedPluginFiles(pluginsDir);
|
||||
const depGraph: Record<string, string[]> = {
|
||||
"01": [], // system-init: upserts system user
|
||||
"02": ["01"], // setup-admin: needs system user from 01
|
||||
"03": ["01"], // metadata-init: reads application config
|
||||
"04": ["01"], // auth-init: updates API tokens referencing system user
|
||||
"05": ["01"], // library-init: reads libraries from DB
|
||||
"06": ["01", "04"], // service-spinup: registers services for auth
|
||||
"07": ["01"], // torrential-depot: uses systemConfig from 01
|
||||
};
|
||||
|
||||
for (const [prefix, deps] of Object.entries(depGraph)) {
|
||||
const currentFile = files.find((f) => f.startsWith(prefix));
|
||||
expect(
|
||||
currentFile,
|
||||
`Plugin ${prefix} file not found`,
|
||||
).toBeDefined();
|
||||
const currentIdx = files.indexOf(currentFile!);
|
||||
|
||||
for (const dep of deps) {
|
||||
const depFile = files.find((f) => f.startsWith(dep));
|
||||
expect(
|
||||
depFile,
|
||||
`Dependency ${dep} for plugin ${prefix} not found`,
|
||||
).toBeDefined();
|
||||
const depIdx = files.indexOf(depFile!);
|
||||
|
||||
expect(depIdx).toBeLessThan(currentIdx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("detects misordered plugins (wrong prefix position)", () => {
|
||||
// With correct ordering, detection returns null
|
||||
expect(detectMisorderedPlugin(pluginsDir)).toBeNull();
|
||||
|
||||
// Verify the detection logic works:
|
||||
const files = numberedPluginFiles(pluginsDir);
|
||||
expect(files.length).toBeGreaterThanOrEqual(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe("metadataHandler postcondition (simulated after plugin 03)", () => {
|
||||
it("fetchProviderIdsInOrder() returns non-empty after adding providers", () => {
|
||||
const handler = new MetadataHandler();
|
||||
|
||||
handler.addProvider(
|
||||
{ source: () => "Steam", name: () => "Steam" },
|
||||
100,
|
||||
);
|
||||
|
||||
const ids = handler.fetchProviderIdsInOrder();
|
||||
expect(ids.length).toBeGreaterThan(0);
|
||||
expect(ids).toContain("Steam");
|
||||
});
|
||||
|
||||
it("returns providers sorted by priority (highest first)", () => {
|
||||
const handler = new MetadataHandler();
|
||||
|
||||
handler.addProvider(
|
||||
{ source: () => "LowPri", name: () => "Low" },
|
||||
0,
|
||||
);
|
||||
handler.addProvider(
|
||||
{ source: () => "HighPri", name: () => "High" },
|
||||
100,
|
||||
);
|
||||
|
||||
const ids = handler.fetchProviderIdsInOrder();
|
||||
expect(ids).toEqual(["HighPri", "LowPri"]);
|
||||
});
|
||||
|
||||
it("filters out Manual provider from id list", () => {
|
||||
const handler = new MetadataHandler();
|
||||
|
||||
handler.addProvider(
|
||||
{ source: () => "Manual", name: () => "Manual" },
|
||||
-1000,
|
||||
);
|
||||
handler.addProvider(
|
||||
{ source: () => "IGDB", name: () => "IGDB" },
|
||||
50,
|
||||
);
|
||||
|
||||
const ids = handler.fetchProviderIdsInOrder();
|
||||
expect(ids).not.toContain("Manual");
|
||||
expect(ids).toEqual(["IGDB"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authManager postcondition (simulated after plugin 04)", () => {
|
||||
it("getEnabledAuthProviders() returns Simple auth by default after init", async () => {
|
||||
const authMan = new AuthManager();
|
||||
await authMan.init();
|
||||
|
||||
const providers = authMan.getEnabledAuthProviders();
|
||||
expect(providers.length).toBeGreaterThan(0);
|
||||
expect(providers).toContain("Simple");
|
||||
});
|
||||
|
||||
it("getEnabledAuthProviders() includes OpenID when configured", async () => {
|
||||
const oldWellKnown = process.env.OIDC_WELLKNOWN;
|
||||
process.env.OIDC_WELLKNOWN = "https://mock-oidc.example.com";
|
||||
|
||||
const authMan = new AuthManager();
|
||||
await authMan.init();
|
||||
|
||||
const providers = authMan.getEnabledAuthProviders();
|
||||
expect(providers).toContain("OpenID");
|
||||
|
||||
// Cleanup
|
||||
if (oldWellKnown === undefined) {
|
||||
delete process.env.OIDC_WELLKNOWN;
|
||||
} else {
|
||||
process.env.OIDC_WELLKNOWN = oldWellKnown;
|
||||
}
|
||||
});
|
||||
|
||||
it("fallback: Simple enabled when no OIDC even with DISABLE_SIMPLE_AUTH", async () => {
|
||||
const oldDisable = process.env.DISABLE_SIMPLE_AUTH;
|
||||
process.env.DISABLE_SIMPLE_AUTH = "true";
|
||||
const oldWellKnown = process.env.OIDC_WELLKNOWN;
|
||||
delete process.env.OIDC_WELLKNOWN;
|
||||
|
||||
const authMan = new AuthManager();
|
||||
await authMan.init();
|
||||
|
||||
const providers = authMan.getEnabledAuthProviders();
|
||||
// Even with DISABLE_SIMPLE_AUTH, the fallback ensures Simple is on
|
||||
// because no OpenID provider is available
|
||||
expect(providers).toEqual(["Simple"]);
|
||||
|
||||
// Cleanup
|
||||
if (oldDisable === undefined) {
|
||||
delete process.env.DISABLE_SIMPLE_AUTH;
|
||||
} else {
|
||||
process.env.DISABLE_SIMPLE_AUTH = oldDisable;
|
||||
}
|
||||
if (oldWellKnown !== undefined) {
|
||||
process.env.OIDC_WELLKNOWN = oldWellKnown;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue