refactor(nest): inject DatabaseService in db-using services

This commit is contained in:
jubnl
2026-07-25 18:15:04 +02:00
parent def6b84938
commit fbe200424d
36 changed files with 345 additions and 223 deletions
@@ -14,6 +14,8 @@ vi.mock('../../../src/services/notifications', () => ({ getAppUrl: () => 'https:
const { getDb } = vi.hoisted(() => ({ getDb: { current: null as unknown } }));
vi.mock('../../../src/db/database', () => ({ get db() { return getDb.current; } }));
import { db as dbConn } from '../../../src/db/database';
import { DatabaseService } from '../../../src/nest/database/database.service';
// The token POST now runs through the SSRF guard (ssrfGuard.safeFetchLlm), which
// resolves the host before fetching. Stub DNS so the fake provider.example host
@@ -50,12 +52,12 @@ const NOW = 1_700_000_000_000;
describe('PluginOAuthService', () => {
let svc: PluginOAuthService;
beforeEach(() => { getDb.current = freshDb(); svc = new PluginOAuthService(); vi.restoreAllMocks(); dnsState.address = '93.184.216.34'; dnsState.family = 4; });
beforeEach(() => { getDb.current = freshDb(); svc = new PluginOAuthService(new DatabaseService(dbConn)); vi.restoreAllMocks(); dnsState.address = '93.184.216.34'; dnsState.family = 4; });
it('providerConfig returns null unless every piece is present, decrypting the secrets', () => {
expect(svc.providerConfig('p')).toMatchObject({ clientId: 'client-123', clientSecret: 'secret-abc', scopes: 'read write' });
getDb.current = freshDb({ ...CFG, oauth_client_secret: '' });
expect(new PluginOAuthService().providerConfig('p')).toBeNull();
expect(new PluginOAuthService(new DatabaseService(dbConn)).providerConfig('p')).toBeNull();
});
it('startConnect builds a PKCE authorize URL + persists a single fresh state per user', () => {
@@ -76,18 +78,18 @@ describe('PluginOAuthService', () => {
it('rejects a non-https / loopback / metadata / internal authorize endpoint', () => {
getDb.current = freshDb({ ...CFG, oauth_authorize_url: 'http://provider.example/authorize' });
expect(() => new PluginOAuthService().startConnect('p', 42, NOW)).toThrow(/https/);
expect(() => new PluginOAuthService(new DatabaseService(dbConn)).startConnect('p', 42, NOW)).toThrow(/https/);
getDb.current = freshDb({ ...CFG, oauth_token_url: 'https://127.0.0.1/token' });
expect(() => new PluginOAuthService().startConnect('p', 42, NOW)).toThrow(/loopback|private/);
expect(() => new PluginOAuthService(new DatabaseService(dbConn)).startConnect('p', 42, NOW)).toThrow(/loopback|private/);
// IPv6-literal loopback must not slip past the fast-fail
getDb.current = freshDb({ ...CFG, oauth_token_url: 'https://[::1]/token' });
expect(() => new PluginOAuthService().startConnect('p', 42, NOW)).toThrow(/loopback/);
expect(() => new PluginOAuthService(new DatabaseService(dbConn)).startConnect('p', 42, NOW)).toThrow(/loopback/);
// cloud-metadata by literal is refused too
getDb.current = freshDb({ ...CFG, oauth_token_url: 'https://169.254.169.254/token' });
expect(() => new PluginOAuthService().startConnect('p', 42, NOW)).toThrow(/loopback|metadata/);
expect(() => new PluginOAuthService(new DatabaseService(dbConn)).startConnect('p', 42, NOW)).toThrow(/loopback|metadata/);
// an internal name suffix is refused
getDb.current = freshDb({ ...CFG, oauth_token_url: 'https://idp.internal/token' });
expect(() => new PluginOAuthService().startConnect('p', 42, NOW)).toThrow(/local/);
expect(() => new PluginOAuthService(new DatabaseService(dbConn)).startConnect('p', 42, NOW)).toThrow(/local/);
});
it('completeCallback verifies state (single-use, user-bound, TTL), exchanges the code, encrypts tokens', async () => {