@digitalpaws/starr 0.0.1
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/README.rst +75 -0
- package/data/.nfs.811bbb0d.43c34.4 +0 -0
- package/data/.nfs.81ba4cd0.ffff87804.4 +0 -0
- package/data/.nfs.82d127f7.fffff5704.4 +0 -0
- package/data/.nfs.82d127fe.fffff5704.4 +0 -0
- package/data/.nfs.82d159a5.fffff5aa4.4 +0 -0
- package/data/.nfs.82d159aa.fffff5aa4.4 +0 -0
- package/data/.nfs.82d15a9c.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15aa1.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15c65.fffff5c24.4 +0 -0
- package/data/.nfs.82d1852c.fffff5e24.4 +0 -0
- package/data/.nfs.82d18536.fffff5e24.4 +0 -0
- package/data/.nfs.82d18dbe.fffff5e44.4 +0 -0
- package/data/.nfs.82d18dc6.fffff5e44.4 +0 -0
- package/data/.nfs.8309a3f5.fffff82f4.4 +0 -0
- package/data/.nfs.8309a3fe.fffff82f4.4 +0 -0
- package/data/.nfs.8314122e.fffffb044.4 +0 -0
- package/data/.nfs.83141236.fffffb044.4 +0 -0
- package/data/.nfs.833c4fe9.fffffbc34.4 +0 -0
- package/data/.nfs.833c4ff3.fffffbc34.4 +0 -0
- package/data/logs/starr.log +66244 -0
- package/data/starr.db +0 -0
- package/data/starr.db-shm +0 -0
- package/data/starr.db-wal +0 -0
- package/data/starr.jwt +1 -0
- package/data/starr.key +1 -0
- package/dist/auth/auth-service.d.ts +42 -0
- package/dist/auth/auth-service.d.ts.map +1 -0
- package/dist/auth/auth-service.js +226 -0
- package/dist/auth/auth-service.js.map +1 -0
- package/dist/auth/http-server.d.ts +16 -0
- package/dist/auth/http-server.d.ts.map +1 -0
- package/dist/auth/http-server.js +207 -0
- package/dist/auth/http-server.js.map +1 -0
- package/dist/auth/jwt-secret.d.ts +6 -0
- package/dist/auth/jwt-secret.d.ts.map +1 -0
- package/dist/auth/jwt-secret.js +51 -0
- package/dist/auth/jwt-secret.js.map +1 -0
- package/dist/auth/oidc/oidc-flow.d.ts +60 -0
- package/dist/auth/oidc/oidc-flow.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-flow.js +201 -0
- package/dist/auth/oidc/oidc-flow.js.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts +59 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.js +124 -0
- package/dist/auth/oidc/oidc-provider-service.js.map +1 -0
- package/dist/auth/oidc/user-service.d.ts +67 -0
- package/dist/auth/oidc/user-service.d.ts.map +1 -0
- package/dist/auth/oidc/user-service.js +177 -0
- package/dist/auth/oidc/user-service.js.map +1 -0
- package/dist/auth/rate-limiter.d.ts +31 -0
- package/dist/auth/rate-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limiter.js +70 -0
- package/dist/auth/rate-limiter.js.map +1 -0
- package/dist/config/config-service.d.ts +83 -0
- package/dist/config/config-service.d.ts.map +1 -0
- package/dist/config/config-service.js +309 -0
- package/dist/config/config-service.js.map +1 -0
- package/dist/config/validate.d.ts +12 -0
- package/dist/config/validate.d.ts.map +1 -0
- package/dist/config/validate.js +140 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/content-types/registry.d.ts +75 -0
- package/dist/content-types/registry.d.ts.map +1 -0
- package/dist/content-types/registry.js +171 -0
- package/dist/content-types/registry.js.map +1 -0
- package/dist/datastore/db.d.ts +9 -0
- package/dist/datastore/db.d.ts.map +1 -0
- package/dist/datastore/db.js +27 -0
- package/dist/datastore/db.js.map +1 -0
- package/dist/datastore/encrypt.d.ts +9 -0
- package/dist/datastore/encrypt.d.ts.map +1 -0
- package/dist/datastore/encrypt.js +84 -0
- package/dist/datastore/encrypt.js.map +1 -0
- package/dist/datastore/migrations.d.ts +10 -0
- package/dist/datastore/migrations.d.ts.map +1 -0
- package/dist/datastore/migrations.js +340 -0
- package/dist/datastore/migrations.js.map +1 -0
- package/dist/datastore/storage.d.ts +4 -0
- package/dist/datastore/storage.d.ts.map +1 -0
- package/dist/datastore/storage.js +60 -0
- package/dist/datastore/storage.js.map +1 -0
- package/dist/events/event-bus.d.ts +15 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +140 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/events/spine.d.ts +31 -0
- package/dist/events/spine.d.ts.map +1 -0
- package/dist/events/spine.js +68 -0
- package/dist/events/spine.js.map +1 -0
- package/dist/events/trace.d.ts +24 -0
- package/dist/events/trace.d.ts.map +1 -0
- package/dist/events/trace.js +33 -0
- package/dist/events/trace.js.map +1 -0
- package/dist/host.d.ts +65 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +94 -0
- package/dist/host.js.map +1 -0
- package/dist/library/importer.d.ts +29 -0
- package/dist/library/importer.d.ts.map +1 -0
- package/dist/library/importer.js +145 -0
- package/dist/library/importer.js.map +1 -0
- package/dist/library/library-cache.d.ts +46 -0
- package/dist/library/library-cache.d.ts.map +1 -0
- package/dist/library/library-cache.js +152 -0
- package/dist/library/library-cache.js.map +1 -0
- package/dist/library/library-config.d.ts +12 -0
- package/dist/library/library-config.d.ts.map +1 -0
- package/dist/library/library-config.js +10 -0
- package/dist/library/library-config.js.map +1 -0
- package/dist/library/library-scanner.d.ts +62 -0
- package/dist/library/library-scanner.d.ts.map +1 -0
- package/dist/library/library-scanner.js +336 -0
- package/dist/library/library-scanner.js.map +1 -0
- package/dist/library/library-service.d.ts +65 -0
- package/dist/library/library-service.d.ts.map +1 -0
- package/dist/library/library-service.js +197 -0
- package/dist/library/library-service.js.map +1 -0
- package/dist/library/metadata-matcher.d.ts +56 -0
- package/dist/library/metadata-matcher.d.ts.map +1 -0
- package/dist/library/metadata-matcher.js +207 -0
- package/dist/library/metadata-matcher.js.map +1 -0
- package/dist/library/naming.d.ts +8 -0
- package/dist/library/naming.d.ts.map +1 -0
- package/dist/library/naming.js +47 -0
- package/dist/library/naming.js.map +1 -0
- package/dist/library/scan-controller.d.ts +95 -0
- package/dist/library/scan-controller.d.ts.map +1 -0
- package/dist/library/scan-controller.js +379 -0
- package/dist/library/scan-controller.js.map +1 -0
- package/dist/library/source-resolver.d.ts +17 -0
- package/dist/library/source-resolver.d.ts.map +1 -0
- package/dist/library/source-resolver.js +75 -0
- package/dist/library/source-resolver.js.map +1 -0
- package/dist/logging/logger.d.ts +3 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +107 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/main.d.ts +36 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +229 -0
- package/dist/main.js.map +1 -0
- package/dist/plugins/context.d.ts +10 -0
- package/dist/plugins/context.d.ts.map +1 -0
- package/dist/plugins/context.js +110 -0
- package/dist/plugins/context.js.map +1 -0
- package/dist/plugins/discovery.d.ts +2 -0
- package/dist/plugins/discovery.d.ts.map +1 -0
- package/dist/plugins/discovery.js +32 -0
- package/dist/plugins/discovery.js.map +1 -0
- package/dist/plugins/loader.d.ts +9 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +245 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/state/request-intake.d.ts +26 -0
- package/dist/state/request-intake.d.ts.map +1 -0
- package/dist/state/request-intake.js +74 -0
- package/dist/state/request-intake.js.map +1 -0
- package/dist/state/state-store.d.ts +146 -0
- package/dist/state/state-store.d.ts.map +1 -0
- package/dist/state/state-store.js +465 -0
- package/dist/state/state-store.js.map +1 -0
- package/dist/state/trace-store.d.ts +12 -0
- package/dist/state/trace-store.d.ts.map +1 -0
- package/dist/state/trace-store.js +24 -0
- package/dist/state/trace-store.js.map +1 -0
- package/dist/types/plugin.d.ts +93 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +4 -0
- package/dist/types/plugin.js.map +1 -0
- package/dist/web/index.d.ts +10 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +153 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/routes/activity.d.ts +6 -0
- package/dist/web/routes/activity.d.ts.map +1 -0
- package/dist/web/routes/activity.js +31 -0
- package/dist/web/routes/activity.js.map +1 -0
- package/dist/web/routes/auth.d.ts +6 -0
- package/dist/web/routes/auth.d.ts.map +1 -0
- package/dist/web/routes/auth.js +142 -0
- package/dist/web/routes/auth.js.map +1 -0
- package/dist/web/routes/config.d.ts +7 -0
- package/dist/web/routes/config.d.ts.map +1 -0
- package/dist/web/routes/config.js +204 -0
- package/dist/web/routes/config.js.map +1 -0
- package/dist/web/routes/content-types.d.ts +6 -0
- package/dist/web/routes/content-types.d.ts.map +1 -0
- package/dist/web/routes/content-types.js +29 -0
- package/dist/web/routes/content-types.js.map +1 -0
- package/dist/web/routes/discovery.d.ts +6 -0
- package/dist/web/routes/discovery.d.ts.map +1 -0
- package/dist/web/routes/discovery.js +111 -0
- package/dist/web/routes/discovery.js.map +1 -0
- package/dist/web/routes/library-import.d.ts +6 -0
- package/dist/web/routes/library-import.d.ts.map +1 -0
- package/dist/web/routes/library-import.js +120 -0
- package/dist/web/routes/library-import.js.map +1 -0
- package/dist/web/routes/library-management.d.ts +6 -0
- package/dist/web/routes/library-management.d.ts.map +1 -0
- package/dist/web/routes/library-management.js +120 -0
- package/dist/web/routes/library-management.js.map +1 -0
- package/dist/web/routes/library.d.ts +6 -0
- package/dist/web/routes/library.d.ts.map +1 -0
- package/dist/web/routes/library.js +28 -0
- package/dist/web/routes/library.js.map +1 -0
- package/dist/web/routes/oidc.d.ts +9 -0
- package/dist/web/routes/oidc.d.ts.map +1 -0
- package/dist/web/routes/oidc.js +203 -0
- package/dist/web/routes/oidc.js.map +1 -0
- package/dist/web/routes/requests.d.ts +6 -0
- package/dist/web/routes/requests.d.ts.map +1 -0
- package/dist/web/routes/requests.js +183 -0
- package/dist/web/routes/requests.js.map +1 -0
- package/dist/web/routes/sse.d.ts +6 -0
- package/dist/web/routes/sse.d.ts.map +1 -0
- package/dist/web/routes/sse.js +54 -0
- package/dist/web/routes/sse.js.map +1 -0
- package/package.json +33 -0
- package/package.json.license +2 -0
- package/src/auth/auth-service.ts +333 -0
- package/src/auth/http-server.ts +249 -0
- package/src/auth/jwt-secret.ts +65 -0
- package/src/auth/oidc/oidc-flow.ts +266 -0
- package/src/auth/oidc/oidc-provider-service.ts +174 -0
- package/src/auth/oidc/user-service.ts +227 -0
- package/src/auth/rate-limiter.ts +85 -0
- package/src/config/config-service.ts +434 -0
- package/src/config/validate.ts +153 -0
- package/src/content-types/registry.ts +194 -0
- package/src/datastore/db.ts +38 -0
- package/src/datastore/encrypt.ts +107 -0
- package/src/datastore/migrations.ts +367 -0
- package/src/datastore/storage.ts +83 -0
- package/src/events/event-bus.ts +180 -0
- package/src/events/spine.ts +87 -0
- package/src/events/trace.ts +38 -0
- package/src/host.ts +130 -0
- package/src/library/importer.ts +174 -0
- package/src/library/library-cache.ts +212 -0
- package/src/library/library-config.ts +22 -0
- package/src/library/library-scanner.ts +404 -0
- package/src/library/library-service.ts +282 -0
- package/src/library/metadata-matcher.ts +285 -0
- package/src/library/naming.ts +59 -0
- package/src/library/scan-controller.ts +490 -0
- package/src/library/source-resolver.ts +92 -0
- package/src/logging/logger.ts +115 -0
- package/src/main.ts +364 -0
- package/src/plugins/context.ts +138 -0
- package/src/plugins/discovery.ts +29 -0
- package/src/plugins/loader.ts +309 -0
- package/src/state/request-intake.ts +101 -0
- package/src/state/state-store.ts +621 -0
- package/src/state/trace-store.ts +27 -0
- package/src/types/plugin.ts +110 -0
- package/src/web/index.ts +182 -0
- package/src/web/routes/activity.ts +41 -0
- package/src/web/routes/auth.ts +170 -0
- package/src/web/routes/config.ts +282 -0
- package/src/web/routes/content-types.ts +42 -0
- package/src/web/routes/discovery.ts +156 -0
- package/src/web/routes/library-import.ts +196 -0
- package/src/web/routes/library-management.ts +140 -0
- package/src/web/routes/library.ts +46 -0
- package/src/web/routes/oidc.ts +260 -0
- package/src/web/routes/requests.ts +250 -0
- package/src/web/routes/sse.ts +92 -0
- package/tests/auth.test.ts +510 -0
- package/tests/config-route.test.ts +345 -0
- package/tests/config.test.ts +612 -0
- package/tests/content-types.test.ts +376 -0
- package/tests/datastore.test.ts +345 -0
- package/tests/e2e.test.ts +430 -0
- package/tests/event-bus.test.ts +925 -0
- package/tests/host.test.ts +449 -0
- package/tests/library-cache.test.ts +171 -0
- package/tests/library-scanner.test.ts +177 -0
- package/tests/library.test.ts +210 -0
- package/tests/loader-context.test.ts +644 -0
- package/tests/logger-jsonl.test.ts +154 -0
- package/tests/metadata-matcher.test.ts +198 -0
- package/tests/oidc.test.ts +218 -0
- package/tests/scan-controller.test.ts +328 -0
- package/tests/source-resolver.test.ts +100 -0
- package/tests/starr-video-integration.test.ts +153 -0
- package/tests/state-store.test.ts +700 -0
- package/tests/trace-history.test.ts +356 -0
- package/tsconfig.json +8 -0
- package/tsconfig.json.license +2 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import { createLogger } from "../src/logging/logger.js";
|
|
9
|
+
|
|
10
|
+
function tmpLogPath(): string {
|
|
11
|
+
return path.join(
|
|
12
|
+
os.tmpdir(),
|
|
13
|
+
`starr-log-test-${Date.now()}-${Math.random().toString(36).slice(2)}.log`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readLines(
|
|
18
|
+
logPath: string,
|
|
19
|
+
): Array<Record<string, unknown>> {
|
|
20
|
+
if (!fs.existsSync(logPath)) return [];
|
|
21
|
+
return fs
|
|
22
|
+
.readFileSync(logPath, "utf8")
|
|
23
|
+
.trim()
|
|
24
|
+
.split("\n")
|
|
25
|
+
.filter((line) => line.length > 0)
|
|
26
|
+
.map((line) => JSON.parse(line) as Record<string, unknown>);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("JSONL file logging", () => {
|
|
30
|
+
let logPath: string;
|
|
31
|
+
let originalEnv: Record<string, string | undefined>;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
logPath = tmpLogPath();
|
|
35
|
+
originalEnv = {
|
|
36
|
+
STARR_LOG_FILE: process.env.STARR_LOG_FILE,
|
|
37
|
+
STARR_LOG_LEVEL: process.env.STARR_LOG_LEVEL,
|
|
38
|
+
STARR_LOG_FILE_LEVEL: process.env.STARR_LOG_FILE_LEVEL,
|
|
39
|
+
};
|
|
40
|
+
process.env.STARR_LOG_FILE = logPath;
|
|
41
|
+
process.env.STARR_LOG_LEVEL = "debug";
|
|
42
|
+
delete process.env.STARR_LOG_FILE_LEVEL;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
try {
|
|
47
|
+
fs.rmSync(logPath, { force: true });
|
|
48
|
+
} catch {
|
|
49
|
+
// ignore
|
|
50
|
+
}
|
|
51
|
+
process.env.STARR_LOG_FILE = originalEnv.STARR_LOG_FILE;
|
|
52
|
+
process.env.STARR_LOG_LEVEL = originalEnv.STARR_LOG_LEVEL;
|
|
53
|
+
process.env.STARR_LOG_FILE_LEVEL = originalEnv.STARR_LOG_FILE_LEVEL;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("creates the log file and parent directory on first log", () => {
|
|
57
|
+
// Use a nested tmp subdirectory to safely test directory creation
|
|
58
|
+
const nestedDir = path.join(os.tmpdir(), `starr-log-dir-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
59
|
+
const nestedLogPath = path.join(nestedDir, "sub", "test.log");
|
|
60
|
+
process.env.STARR_LOG_FILE = nestedLogPath;
|
|
61
|
+
const logger = createLogger("core", "debug");
|
|
62
|
+
logger.info("hello");
|
|
63
|
+
expect(fs.existsSync(nestedLogPath)).toBe(true);
|
|
64
|
+
const lines = readLines(nestedLogPath);
|
|
65
|
+
expect(lines.length).toBe(1);
|
|
66
|
+
expect(lines[0].message).toBe("hello");
|
|
67
|
+
expect(lines[0].level).toBe("info");
|
|
68
|
+
expect(lines[0].tag).toBe("core");
|
|
69
|
+
// Clean up
|
|
70
|
+
fs.rmSync(nestedDir, { recursive: true, force: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("appends multiple log lines", () => {
|
|
74
|
+
const logger = createLogger("core", "debug");
|
|
75
|
+
logger.info("first");
|
|
76
|
+
logger.info("second");
|
|
77
|
+
const lines = readLines(logPath);
|
|
78
|
+
expect(lines.length).toBe(2);
|
|
79
|
+
expect(lines[0].message).toBe("first");
|
|
80
|
+
expect(lines[1].message).toBe("second");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("preserves log level filtering", () => {
|
|
84
|
+
process.env.STARR_LOG_LEVEL = "warn";
|
|
85
|
+
const logger = createLogger("core", "warn");
|
|
86
|
+
logger.debug("debug line");
|
|
87
|
+
logger.info("info line");
|
|
88
|
+
logger.warn("warn line");
|
|
89
|
+
const lines = readLines(logPath);
|
|
90
|
+
expect(lines.length).toBe(1);
|
|
91
|
+
expect(lines[0].message).toBe("warn line");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("uses independent file log level", () => {
|
|
95
|
+
process.env.STARR_LOG_LEVEL = "error";
|
|
96
|
+
process.env.STARR_LOG_FILE_LEVEL = "info";
|
|
97
|
+
const logger = createLogger("core", "error");
|
|
98
|
+
logger.info("file only");
|
|
99
|
+
logger.error("both sinks");
|
|
100
|
+
const lines = readLines(logPath);
|
|
101
|
+
expect(lines.length).toBe(2);
|
|
102
|
+
expect(lines[0].message).toBe("file only");
|
|
103
|
+
expect(lines[1].message).toBe("both sinks");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("preserves plugin-style tags", () => {
|
|
107
|
+
const logger = createLogger("starr-tmdb", "debug");
|
|
108
|
+
logger.info("plugin log");
|
|
109
|
+
const lines = readLines(logPath);
|
|
110
|
+
expect(lines[0].tag).toBe("starr-tmdb");
|
|
111
|
+
expect(lines[0].message).toBe("plugin log");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("includes metadata fields", () => {
|
|
115
|
+
const logger = createLogger("core", "debug");
|
|
116
|
+
logger.info("with meta", { requestId: "req-1", count: 2 });
|
|
117
|
+
const lines = readLines(logPath);
|
|
118
|
+
expect(lines[0].requestId).toBe("req-1");
|
|
119
|
+
expect(lines[0].count).toBe(2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("does not write file when STARR_LOG_FILE is empty", () => {
|
|
123
|
+
process.env.STARR_LOG_FILE = "";
|
|
124
|
+
const logger = createLogger("core", "debug");
|
|
125
|
+
logger.info("disabled");
|
|
126
|
+
expect(fs.existsSync(logPath)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("uses the default path when STARR_LOG_FILE is not set", () => {
|
|
130
|
+
delete process.env.STARR_LOG_FILE;
|
|
131
|
+
const dataDir = path.join(os.tmpdir(), `starr-data-${Date.now()}`);
|
|
132
|
+
process.env.STARR_DATA_DIR = dataDir;
|
|
133
|
+
const expected = path.join(dataDir, "logs", "starr.log");
|
|
134
|
+
try {
|
|
135
|
+
const logger = createLogger("core", "debug");
|
|
136
|
+
logger.info("default path");
|
|
137
|
+
expect(fs.existsSync(expected)).toBe(true);
|
|
138
|
+
const lines = readLines(expected);
|
|
139
|
+
expect(lines[0].message).toBe("default path");
|
|
140
|
+
} finally {
|
|
141
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
142
|
+
delete process.env.STARR_DATA_DIR;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("does not crash on invalid directory permissions", () => {
|
|
147
|
+
// Point to a directory that cannot be created (e.g., /proc/foo/bar.log)
|
|
148
|
+
process.env.STARR_LOG_FILE = "/proc/foo/bar.log";
|
|
149
|
+
const logger = createLogger("core", "debug");
|
|
150
|
+
logger.info("should not crash");
|
|
151
|
+
// Test passes if we reach this point without throwing
|
|
152
|
+
expect(true).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect, vi } from "vitest";
|
|
5
|
+
import { deriveSearchTerms } from "../src/library/metadata-matcher.js";
|
|
6
|
+
|
|
7
|
+
describe("deriveSearchTerms", () => {
|
|
8
|
+
it("extracts title and year from 'Dune (2021) [2160p]'", () => {
|
|
9
|
+
const result = deriveSearchTerms("Dune (2021) [2160p]");
|
|
10
|
+
expect(result.cleanTitle).toBe("Dune");
|
|
11
|
+
expect(result.year).toBe("2021");
|
|
12
|
+
expect(result.title).toBe("Dune");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("extracts title and year from 'The.Matrix.1999.1080p'", () => {
|
|
16
|
+
const result = deriveSearchTerms("The.Matrix.1999.1080p");
|
|
17
|
+
expect(result.cleanTitle).toBe("The Matrix");
|
|
18
|
+
expect(result.year).toBe("1999");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("handles scene-style naming with group tag", () => {
|
|
22
|
+
const result = deriveSearchTerms("A.Movie.Name.2020.REMASTERED.1080p.BluRay.x264-GROUP");
|
|
23
|
+
expect(result.cleanTitle).toBe("A Movie Name");
|
|
24
|
+
expect(result.year).toBe("2020");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("handles folder name with no year", () => {
|
|
28
|
+
const result = deriveSearchTerms("Some Movie Title");
|
|
29
|
+
expect(result.cleanTitle).toBe("Some Movie Title");
|
|
30
|
+
expect(result.year).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("strips various quality and format tags", () => {
|
|
34
|
+
const result = deriveSearchTerms("Movie (2015) [BluRay] [1080p] [x264]");
|
|
35
|
+
expect(result.cleanTitle).toBe("Movie");
|
|
36
|
+
expect(result.year).toBe("2015");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("handles underscores instead of dots", () => {
|
|
40
|
+
const result = deriveSearchTerms("A_Good_Movie_2022_1080p");
|
|
41
|
+
expect(result.cleanTitle).toBe("A Good Movie");
|
|
42
|
+
expect(result.year).toBe("2022");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns empty cleanTitle for empty input", () => {
|
|
46
|
+
const result = deriveSearchTerms("");
|
|
47
|
+
expect(result.cleanTitle).toBe("");
|
|
48
|
+
expect(result.year).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("handles simple title with parentheses year", () => {
|
|
52
|
+
const result = deriveSearchTerms("Inception (2010)");
|
|
53
|
+
expect(result.cleanTitle).toBe("Inception");
|
|
54
|
+
expect(result.year).toBe("2010");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("handles multiple dots and spaces", () => {
|
|
58
|
+
const result = deriveSearchTerms("The Dark Knight 2008 REMUX 1080p");
|
|
59
|
+
expect(result.cleanTitle).toBe("The Dark Knight");
|
|
60
|
+
expect(result.year).toBe("2008");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("handles Director's Cut tag", () => {
|
|
64
|
+
const result = deriveSearchTerms("Blade Runner (1982) [Director's Cut] 1080p");
|
|
65
|
+
expect(result.cleanTitle).toBe("Blade Runner");
|
|
66
|
+
expect(result.year).toBe("1982");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("MetadataMatcher integration", () => {
|
|
71
|
+
it("findMatches returns empty array when no metadata plugins respond", async () => {
|
|
72
|
+
// Set up a metadata matcher with a very short timeout
|
|
73
|
+
const { MetadataMatcher } = await import("../src/library/metadata-matcher.js");
|
|
74
|
+
const { EventBusImpl } = await import("../src/events/event-bus.js");
|
|
75
|
+
const { ContentTypeRegistryImpl } = await import("../src/content-types/registry.js");
|
|
76
|
+
const { createLogger } = await import("../src/logging/logger.js");
|
|
77
|
+
|
|
78
|
+
const logger = createLogger("test", "silent");
|
|
79
|
+
const impl = new EventBusImpl(logger);
|
|
80
|
+
const coreBus = impl.coreBus();
|
|
81
|
+
const registry = new ContentTypeRegistryImpl();
|
|
82
|
+
|
|
83
|
+
const matcher = new MetadataMatcher(coreBus, registry, logger, 50); // 50ms timeout
|
|
84
|
+
|
|
85
|
+
const group = {
|
|
86
|
+
directory: "/media/movies/Dune (2021)",
|
|
87
|
+
folderName: "Dune (2021)",
|
|
88
|
+
files: ["/media/movies/Dune (2021)/Dune.mkv"],
|
|
89
|
+
subtitleFiles: [],
|
|
90
|
+
contentTypeId: "starr-video/movie@1",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// No metadata plugin will respond, so should return empty
|
|
94
|
+
const matches = await matcher.findMatches(group);
|
|
95
|
+
expect(Array.isArray(matches)).toBe(true);
|
|
96
|
+
expect(matches.length).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("findMatches returns results when metadata plugins respond", async () => {
|
|
100
|
+
const { MetadataMatcher } = await import("../src/library/metadata-matcher.js");
|
|
101
|
+
const { EventBusImpl } = await import("../src/events/event-bus.js");
|
|
102
|
+
const { ContentTypeRegistryImpl } = await import("../src/content-types/registry.js");
|
|
103
|
+
const { createLogger } = await import("../src/logging/logger.js");
|
|
104
|
+
|
|
105
|
+
const logger = createLogger("test", "silent");
|
|
106
|
+
const impl = new EventBusImpl(logger);
|
|
107
|
+
const coreBus = impl.coreBus();
|
|
108
|
+
const registry = new ContentTypeRegistryImpl();
|
|
109
|
+
registry.register({
|
|
110
|
+
id: "starr-video/movie@1",
|
|
111
|
+
name: "Movie",
|
|
112
|
+
identity: ["tmdb"],
|
|
113
|
+
fields: [],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const matcher = new MetadataMatcher(coreBus, registry, logger, 200);
|
|
117
|
+
|
|
118
|
+
// Simulate a metadata plugin responding
|
|
119
|
+
coreBus.on("discovery.search", (event: any) => {
|
|
120
|
+
coreBus.emit("discovery.results", {
|
|
121
|
+
contentTypeId: "starr-video/movie@1",
|
|
122
|
+
items: [
|
|
123
|
+
{
|
|
124
|
+
contentTypeId: "starr-video/movie@1",
|
|
125
|
+
title: "Dune",
|
|
126
|
+
releaseDate: "2021-10-22",
|
|
127
|
+
externalIds: { tmdb: "438631" },
|
|
128
|
+
images: [],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const group = {
|
|
135
|
+
directory: "/media/movies/Dune (2021)",
|
|
136
|
+
folderName: "Dune (2021)",
|
|
137
|
+
files: ["/media/movies/Dune (2021)/Dune.mkv"],
|
|
138
|
+
subtitleFiles: [],
|
|
139
|
+
contentTypeId: "starr-video/movie@1",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const matches = await matcher.findMatches(group);
|
|
143
|
+
expect(matches.length).toBeGreaterThanOrEqual(1);
|
|
144
|
+
expect(matches[0].item.title).toBe("Dune");
|
|
145
|
+
expect(matches[0].source).toBe("core");
|
|
146
|
+
expect(matches[0].confidence).toBeGreaterThan(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("searchCustom returns results for arbitrary queries", async () => {
|
|
150
|
+
const { MetadataMatcher } = await import("../src/library/metadata-matcher.js");
|
|
151
|
+
const { EventBusImpl } = await import("../src/events/event-bus.js");
|
|
152
|
+
const { ContentTypeRegistryImpl } = await import("../src/content-types/registry.js");
|
|
153
|
+
const { createLogger } = await import("../src/logging/logger.js");
|
|
154
|
+
|
|
155
|
+
const logger = createLogger("test", "silent");
|
|
156
|
+
const impl = new EventBusImpl(logger);
|
|
157
|
+
const coreBus = impl.coreBus();
|
|
158
|
+
const registry = new ContentTypeRegistryImpl();
|
|
159
|
+
|
|
160
|
+
const matcher = new MetadataMatcher(coreBus, registry, logger, 100);
|
|
161
|
+
|
|
162
|
+
// Simulate a metadata plugin responding
|
|
163
|
+
coreBus.on("discovery.search", (event: any) => {
|
|
164
|
+
coreBus.emit("discovery.results", {
|
|
165
|
+
contentTypeId: "starr-video/movie@1",
|
|
166
|
+
items: [
|
|
167
|
+
{
|
|
168
|
+
contentTypeId: "starr-video/movie@1",
|
|
169
|
+
title: "Inception",
|
|
170
|
+
releaseDate: "2010-07-16",
|
|
171
|
+
externalIds: { tmdb: "27205" },
|
|
172
|
+
images: [],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const matches = await matcher.searchCustom("Inception (2010)", "Inception 2010", "starr-video/movie@1");
|
|
179
|
+
expect(matches.length).toBeGreaterThanOrEqual(1);
|
|
180
|
+
expect(matches[0].item.title).toBe("Inception");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("searchCustom returns empty array for empty query", async () => {
|
|
184
|
+
const { MetadataMatcher } = await import("../src/library/metadata-matcher.js");
|
|
185
|
+
const { EventBusImpl } = await import("../src/events/event-bus.js");
|
|
186
|
+
const { ContentTypeRegistryImpl } = await import("../src/content-types/registry.js");
|
|
187
|
+
const { createLogger } = await import("../src/logging/logger.js");
|
|
188
|
+
|
|
189
|
+
const logger = createLogger("test", "silent");
|
|
190
|
+
const impl = new EventBusImpl(logger);
|
|
191
|
+
const coreBus = impl.coreBus();
|
|
192
|
+
const registry = new ContentTypeRegistryImpl();
|
|
193
|
+
|
|
194
|
+
const matcher = new MetadataMatcher(coreBus, registry, logger, 50);
|
|
195
|
+
const matches = await matcher.searchCustom("folder", "", "starr-video/movie@1");
|
|
196
|
+
expect(matches).toEqual([]);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { OidcProviderService } from "../src/auth/oidc/oidc-provider-service.js";
|
|
7
|
+
import { UserService } from "../src/auth/oidc/user-service.js";
|
|
8
|
+
|
|
9
|
+
// Minimal encrypt/decrypt for testing (not real encryption)
|
|
10
|
+
const testEncrypt = (s: string) => `ENC(${s})`;
|
|
11
|
+
const testDecrypt = (s: string) => s.replace(/^ENC\(/, "").replace(/\)$/, "");
|
|
12
|
+
|
|
13
|
+
describe("OidcProviderService", () => {
|
|
14
|
+
let db: Database.Database;
|
|
15
|
+
let service: OidcProviderService;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
db = new Database(":memory:");
|
|
19
|
+
db.exec(`
|
|
20
|
+
CREATE TABLE oidc_providers (
|
|
21
|
+
id TEXT PRIMARY KEY, name TEXT, issuer_url TEXT, client_id TEXT,
|
|
22
|
+
client_secret TEXT, scopes TEXT, role_attribute TEXT,
|
|
23
|
+
default_role TEXT, enabled INTEGER,
|
|
24
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
25
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
26
|
+
);
|
|
27
|
+
`);
|
|
28
|
+
service = new OidcProviderService(db, testEncrypt, testDecrypt, console as any);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("creates and retrieves a provider", () => {
|
|
32
|
+
const provider = service.create({
|
|
33
|
+
name: "Test Provider",
|
|
34
|
+
issuerUrl: "https://auth.example.com",
|
|
35
|
+
clientId: "test-client",
|
|
36
|
+
clientSecret: "super-secret",
|
|
37
|
+
});
|
|
38
|
+
expect(provider.name).toBe("Test Provider");
|
|
39
|
+
expect(provider.clientSecret).toBe("super-secret"); // decrypted
|
|
40
|
+
expect(provider.enabled).toBe(true);
|
|
41
|
+
expect(provider.id).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("masks secret in database (encrypted at rest)", () => {
|
|
45
|
+
service.create({
|
|
46
|
+
name: "Test",
|
|
47
|
+
issuerUrl: "https://auth.example.com",
|
|
48
|
+
clientId: "cid",
|
|
49
|
+
clientSecret: "my-secret",
|
|
50
|
+
});
|
|
51
|
+
const row = db.prepare("SELECT client_secret FROM oidc_providers").get() as any;
|
|
52
|
+
expect(row.client_secret).toBe("ENC(my-secret)"); // encrypted
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("updates provider", () => {
|
|
56
|
+
const created = service.create({
|
|
57
|
+
name: "Original",
|
|
58
|
+
issuerUrl: "https://auth.example.com",
|
|
59
|
+
clientId: "cid",
|
|
60
|
+
clientSecret: "secret",
|
|
61
|
+
});
|
|
62
|
+
const updated = service.update(created.id, { name: "Updated" });
|
|
63
|
+
expect(updated!.name).toBe("Updated");
|
|
64
|
+
expect(updated!.clientSecret).toBe("secret"); // unchanged
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("updates secret when provided", () => {
|
|
68
|
+
const created = service.create({
|
|
69
|
+
name: "Test",
|
|
70
|
+
issuerUrl: "https://auth.example.com",
|
|
71
|
+
clientId: "cid",
|
|
72
|
+
clientSecret: "old-secret",
|
|
73
|
+
});
|
|
74
|
+
const updated = service.update(created.id, { clientSecret: "new-secret" });
|
|
75
|
+
expect(updated!.clientSecret).toBe("new-secret");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("deletes provider", () => {
|
|
79
|
+
const created = service.create({
|
|
80
|
+
name: "Test",
|
|
81
|
+
issuerUrl: "https://auth.example.com",
|
|
82
|
+
clientId: "cid",
|
|
83
|
+
clientSecret: "secret",
|
|
84
|
+
});
|
|
85
|
+
const deleted = service.delete(created.id);
|
|
86
|
+
expect(deleted).toBe(true);
|
|
87
|
+
expect(service.get(created.id)).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("lists only enabled providers for public", () => {
|
|
91
|
+
service.create({ name: "P1", issuerUrl: "https://a.com", clientId: "c1", clientSecret: "s1", enabled: true });
|
|
92
|
+
service.create({ name: "P2", issuerUrl: "https://b.com", clientId: "c2", clientSecret: "s2", enabled: false });
|
|
93
|
+
const enabled = service.listEnabled();
|
|
94
|
+
expect(enabled).toHaveLength(1);
|
|
95
|
+
expect(enabled[0].name).toBe("P1");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("UserService", () => {
|
|
100
|
+
let db: Database.Database;
|
|
101
|
+
let service: UserService;
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
db = new Database(":memory:");
|
|
105
|
+
db.exec(`
|
|
106
|
+
CREATE TABLE users (
|
|
107
|
+
id TEXT PRIMARY KEY, username TEXT UNIQUE, display_name TEXT,
|
|
108
|
+
avatar_url TEXT, role TEXT DEFAULT 'user', provider TEXT DEFAULT 'local',
|
|
109
|
+
oidc_sub TEXT, oidc_provider_id TEXT,
|
|
110
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
111
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
112
|
+
);
|
|
113
|
+
`);
|
|
114
|
+
service = new UserService(db, console as any);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("creates and retrieves a user", () => {
|
|
118
|
+
const { user, created } = service.findOrCreateFromOidc(
|
|
119
|
+
{ sub: "sub123", preferred_username: "jdoe", name: "John Doe" },
|
|
120
|
+
"provider-1",
|
|
121
|
+
"user",
|
|
122
|
+
);
|
|
123
|
+
expect(created).toBe(true);
|
|
124
|
+
expect(user.username).toBe("jdoe");
|
|
125
|
+
expect(user.displayName).toBe("John Doe");
|
|
126
|
+
expect(user.role).toBe("user");
|
|
127
|
+
expect(user.provider).toBe("oidc");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("finds existing user by OIDC sub", () => {
|
|
131
|
+
const { user: u1 } = service.findOrCreateFromOidc(
|
|
132
|
+
{ sub: "sub456", preferred_username: "alice" },
|
|
133
|
+
"provider-1",
|
|
134
|
+
"user",
|
|
135
|
+
);
|
|
136
|
+
const { user: u2, created } = service.findOrCreateFromOidc(
|
|
137
|
+
{ sub: "sub456", preferred_username: "alice_new", name: "Alice Smith" },
|
|
138
|
+
"provider-1",
|
|
139
|
+
"user",
|
|
140
|
+
);
|
|
141
|
+
expect(created).toBe(false);
|
|
142
|
+
expect(u2.id).toBe(u1.id);
|
|
143
|
+
expect(u2.displayName).toBe("Alice Smith"); // updated
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("changes user role", () => {
|
|
147
|
+
const { user } = service.findOrCreateFromOidc(
|
|
148
|
+
{ sub: "s1", preferred_username: "user1" },
|
|
149
|
+
"p1",
|
|
150
|
+
"user",
|
|
151
|
+
);
|
|
152
|
+
const updated = service.setRole(user.id, "admin");
|
|
153
|
+
expect(updated!.role).toBe("admin");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("prevents duplicate usernames", () => {
|
|
157
|
+
service.findOrCreateFromOidc(
|
|
158
|
+
{ sub: "s1", preferred_username: "sameuser" },
|
|
159
|
+
"p1",
|
|
160
|
+
"user",
|
|
161
|
+
);
|
|
162
|
+
const { user } = service.findOrCreateFromOidc(
|
|
163
|
+
{ sub: "s2", preferred_username: "sameuser" },
|
|
164
|
+
"p2",
|
|
165
|
+
"user",
|
|
166
|
+
);
|
|
167
|
+
expect(user.username).toMatch(/^sameuser_\d+$/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("lists all users", () => {
|
|
171
|
+
service.findOrCreateFromOidc({ sub: "a", preferred_username: "A" }, "p1", "user");
|
|
172
|
+
service.findOrCreateFromOidc({ sub: "b", preferred_username: "B" }, "p1", "user");
|
|
173
|
+
expect(service.list()).toHaveLength(2);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("resolves user by id or username", () => {
|
|
177
|
+
const { user } = service.findOrCreateFromOidc(
|
|
178
|
+
{ sub: "s1", preferred_username: "resolve-me" },
|
|
179
|
+
"p1", "user",
|
|
180
|
+
);
|
|
181
|
+
expect(service.resolveUser(user.id)!.username).toBe("resolve-me");
|
|
182
|
+
expect(service.resolveUser("resolve-me")!.username).toBe("resolve-me");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("counts admin users", () => {
|
|
186
|
+
// Insert a local admin
|
|
187
|
+
db.prepare(`INSERT INTO users (id, username, display_name, role, provider) VALUES ('admin', 'admin', 'Admin', 'admin', 'local')`).run();
|
|
188
|
+
expect(service.countAdmins()).toBe(1);
|
|
189
|
+
|
|
190
|
+
// Add an OIDC admin
|
|
191
|
+
const { user } = service.findOrCreateFromOidc(
|
|
192
|
+
{ sub: "s1", preferred_username: "admin2" },
|
|
193
|
+
"p1", "admin",
|
|
194
|
+
);
|
|
195
|
+
expect(service.countAdmins()).toBe(2);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("prevents demoting the last admin", () => {
|
|
199
|
+
// Insert a non-built-in admin user
|
|
200
|
+
db.prepare(`INSERT INTO users (id, username, display_name, role, provider) VALUES ('user-1', 'admin1', 'Admin One', 'admin', 'local')`).run();
|
|
201
|
+
|
|
202
|
+
expect(() => service.setRole("user-1", "user")).toThrow("last admin");
|
|
203
|
+
|
|
204
|
+
// Add a second admin, then demoting the first should now work
|
|
205
|
+
service.findOrCreateFromOidc({ sub: "s1", preferred_username: "admin2" }, "p1", "admin");
|
|
206
|
+
const updated = service.setRole("user-1", "user");
|
|
207
|
+
expect(updated!.role).toBe("user");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("prevents demoting the built-in admin account", () => {
|
|
211
|
+
// The built-in admin has id="admin" — simulate this by inserting one
|
|
212
|
+
db.prepare(`INSERT INTO users (id, username, display_name, role, provider) VALUES ('admin', 'admin', 'Admin', 'admin', 'local')`).run();
|
|
213
|
+
// Add a second admin so we're not the last
|
|
214
|
+
service.findOrCreateFromOidc({ sub: "s2", preferred_username: "other" }, "p1", "admin");
|
|
215
|
+
|
|
216
|
+
expect(() => service.setRole("admin", "user")).toThrow("built-in admin");
|
|
217
|
+
});
|
|
218
|
+
});
|