fix: auth scheduler env vars, concurrency and browser stability

This commit is contained in:
Giancarmine Salucci
2025-12-21 02:15:22 +01:00
parent 9357bd483a
commit 342a8eb259
9 changed files with 420 additions and 79 deletions

View File

@@ -7,7 +7,7 @@ import type { ServerInit } from '@sveltejs/kit';
*
* Environment variables:
* - AUTH_SCHEDULER_ENABLED: Set to 'true' to enable periodic auth renewal
* - AUTH_SCHEDULER_INTERVAL_HOURS: Hours between each renewal (default: 12)
* - AUTH_SCHEDULER_INTERVAL_MINUTES: Minutes between each renewal (default: 720)
*/
export const init: ServerInit = async () => {
console.log('[Server Init] Starting SvelteKit server...');

View File

@@ -19,7 +19,16 @@ export async function initializeBrowser(): Promise<Browser> {
}
export async function getBrowser(): Promise<Browser> {
if (!browser) {
if (!browser || !browser.isConnected()) {
if (browser) {
console.warn('Browser is disconnected. Re-initializing...');
try {
await browser.close();
} catch (e) {
/* ignore */
}
browser = null;
}
return initializeBrowser();
}
return browser;

View File

@@ -1,10 +1,11 @@
import fs from 'fs';
import path from 'path';
import { getBrowser } from './browser';
import { env } from '$env/dynamic/private';
export interface SchedulerConfig {
enabled: boolean;
intervalHours: number;
intervalMinutes: number;
}
interface SchedulerState {
@@ -23,12 +24,19 @@ const state: SchedulerState = {
* Get scheduler configuration from environment variables
*/
function getConfig(): SchedulerConfig {
const enabled = process.env.AUTH_SCHEDULER_ENABLED === 'true';
const intervalHours = parseInt(process.env.AUTH_SCHEDULER_INTERVAL_HOURS || '12', 10);
const enabled = env.AUTH_SCHEDULER_ENABLED === 'true';
let intervalMinutes = parseInt(env.AUTH_SCHEDULER_INTERVAL_MINUTES || '720', 10);
if (isNaN(intervalMinutes) || intervalMinutes < 15) {
console.warn(
`[Scheduler] Invalid or too short interval '${env.AUTH_SCHEDULER_INTERVAL_MINUTES}'. Defaulting to 720 minutes.`
);
intervalMinutes = 720;
}
return {
enabled,
intervalHours
intervalMinutes
};
}
@@ -70,14 +78,17 @@ async function renewInstagramAuth(): Promise<boolean> {
state.isRenewing = true;
let context = null;
let page = null;
try {
console.log('[Scheduler] Starting Instagram authentication renewal...');
console.log(`[Scheduler] Loading existing auth from: ${authPath}`);
const browser = await getBrowser();
// Load existing authentication state
const context = await browser.newContext({ storageState: authPath });
const page = await context.newPage();
context = await browser.newContext({ storageState: authPath });
page = await context.newPage();
// Navigate to Instagram homepage - the existing auth will be used automatically
await page.goto('https://www.instagram.com/', { waitUntil: 'domcontentloaded' });
@@ -88,9 +99,6 @@ async function renewInstagramAuth(): Promise<boolean> {
console.log('[Scheduler] Successfully authenticated with Instagram');
} catch (e) {
console.warn('[Scheduler] Home icon not found - session may be expired or invalid');
await page.close();
await context.close();
state.isRenewing = false;
return false;
}
@@ -105,9 +113,6 @@ async function renewInstagramAuth(): Promise<boolean> {
// Update auth.json with refreshed session
await context.storageState({ path: authPath });
await page.close();
await context.close();
state.lastRenewalTime = Date.now();
console.log(`[Scheduler] Instagram authentication renewed successfully at ${new Date().toISOString()}`);
console.log(`[Scheduler] Auth state updated at: ${authPath}`);
@@ -117,6 +122,12 @@ async function renewInstagramAuth(): Promise<boolean> {
console.error('[Scheduler] Instagram authentication renewal failed:', error);
return false;
} finally {
if (page) {
await page.close().catch(() => {});
}
if (context) {
await context.close().catch(() => {});
}
state.isRenewing = false;
}
}
@@ -137,9 +148,9 @@ export async function startScheduler(): Promise<void> {
return;
}
const intervalMs = config.intervalHours * 60 * 60 * 1000;
const intervalMs = config.intervalMinutes * 60 * 1000;
console.log(`[Scheduler] Starting authentication scheduler with ${config.intervalHours}h interval`);
console.log(`[Scheduler] Starting authentication scheduler with ${config.intervalMinutes}min interval`);
// Schedule periodic renewals
state.intervalId = setInterval(async () => {

View File

@@ -2,13 +2,18 @@ import { getSchedulerStatus, startScheduler, stopScheduler } from '$lib/server/s
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Mock environment variables
const setEnv = (key: string, value: string | undefined) => {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
};
const { mockEnv } = vi.hoisted(() => {
return {
mockEnv: {
AUTH_SCHEDULER_ENABLED: 'false',
AUTH_SCHEDULER_INTERVAL_MINUTES: '720'
}
};
});
vi.mock('$env/dynamic/private', () => ({
env: mockEnv
}));
// Mock the browser module
vi.mock('$lib/server/browser', () => ({
@@ -28,8 +33,8 @@ const mockFs = {
describe('Scheduler Service', () => {
beforeEach(() => {
// Reset environment variables
setEnv('AUTH_SCHEDULER_ENABLED', undefined);
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', undefined);
mockEnv.AUTH_SCHEDULER_ENABLED = 'false';
mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '720';
// Clear all mocks
vi.clearAllMocks();
@@ -48,24 +53,24 @@ describe('Scheduler Service', () => {
});
describe('Configuration', () => {
it('should use default interval when AUTH_SCHEDULER_INTERVAL_HOURS is not set', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', undefined);
it('should use default interval when AUTH_SCHEDULER_INTERVAL_MINUTES is not set', async () => {
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '';
const status = getSchedulerStatus();
expect(status.config.intervalHours).toBe(12);
expect(status.config.intervalMinutes).toBe(720);
});
it('should parse custom interval hours from environment', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', '6');
it('should parse custom interval minutes from environment', async () => {
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '30';
const status = getSchedulerStatus();
expect(status.config.intervalHours).toBe(6);
expect(status.config.intervalMinutes).toBe(30);
});
it('should disable scheduler when AUTH_SCHEDULER_ENABLED is not true', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'false');
mockEnv.AUTH_SCHEDULER_ENABLED = 'false';
const status = getSchedulerStatus();
expect(status.config.enabled).toBe(false);
@@ -73,7 +78,7 @@ describe('Scheduler Service', () => {
});
it('should parse AUTH_SCHEDULER_ENABLED as true when set to "true"', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
const status = getSchedulerStatus();
expect(status.config.enabled).toBe(true);
@@ -82,7 +87,7 @@ describe('Scheduler Service', () => {
describe('Scheduler Lifecycle', () => {
it('should not start when disabled', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'false');
mockEnv.AUTH_SCHEDULER_ENABLED = 'false';
await startScheduler();
@@ -91,7 +96,7 @@ describe('Scheduler Service', () => {
});
it('should start when enabled', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockFs.existsSync.mockReturnValue(true);
await startScheduler();
@@ -101,7 +106,7 @@ describe('Scheduler Service', () => {
});
it('should not start twice', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockFs.existsSync.mockReturnValue(true);
await startScheduler();
@@ -113,7 +118,7 @@ describe('Scheduler Service', () => {
});
it('should stop the scheduler', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockFs.existsSync.mockReturnValue(true);
await startScheduler();
@@ -132,7 +137,7 @@ describe('Scheduler Service', () => {
describe('Status Reporting', () => {
it('should return scheduler status with default values', () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'false');
mockEnv.AUTH_SCHEDULER_ENABLED = 'false';
const status = getSchedulerStatus();
@@ -142,13 +147,13 @@ describe('Scheduler Service', () => {
isRenewing: false,
config: {
enabled: false,
intervalHours: 12
intervalMinutes: 720
}
});
});
it('should report running state correctly', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockFs.existsSync.mockReturnValue(true);
await startScheduler();
@@ -159,13 +164,13 @@ describe('Scheduler Service', () => {
});
it('should track configuration', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', '24');
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '1440';
const status = getSchedulerStatus();
expect(status.config.enabled).toBe(true);
expect(status.config.intervalHours).toBe(24);
expect(status.config.intervalMinutes).toBe(1440);
});
});
@@ -187,14 +192,14 @@ describe('Scheduler Service', () => {
});
describe('Environment Variables', () => {
it('should handle empty AUTH_SCHEDULER_INTERVAL_HOURS with default', () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true');
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', '');
it('should handle empty AUTH_SCHEDULER_INTERVAL_MINUTES with default', () => {
mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '';
const status = getSchedulerStatus();
// Empty string should fall back to default due to parseInt('', 10) returning NaN
// and the || 12 fallback
expect(status.config.intervalHours).toBeDefined();
// and the || 720 fallback
expect(status.config.intervalMinutes).toBeDefined();
});
});
});