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

@@ -0,0 +1,174 @@
# Hexagonal Architecture (Ports & Adapters) Reference Manual
## Overview
Hexagonal Architecture, also known as **Ports and Adapters**, is a software design pattern that isolates an applications **core business logic** from external systems such as user interfaces, databases, frameworks, and third-party services.
The application is conceptually placed at the center (often drawn as a hexagon). All communication with the outside world happens through **ports** (abstract interfaces), which are implemented by **adapters**.
The key idea:
> **The domain does not depend on technology. Technology depends on the domain.**
---
## Fundamental Principles
### 1. Business Logic First
The core domain represents the real business rules and use cases. It must:
- Be independent of UI, databases, frameworks, and delivery mechanisms
- Express *what* the system does, not *how* it is delivered
### 2. Explicit Boundaries
All interactions between the core and external systems cross explicit boundaries (ports). This prevents accidental coupling.
### 3. Dependency Inversion
Dependencies always point **inward**, toward the core. External components depend on abstractions defined by the core.
---
## Core Concepts
### Domain (Core)
The domain contains:
- Business entities
- Value objects
- Use cases / application services
- Domain rules and policies
It contains **no technical concerns** such as HTTP, databases, file systems, or frameworks.
---
### Port
A **port** is an abstract interface defined by the core.
Ports describe:
- What the application *needs* from the outside world (output ports)
- What the application *offers* to the outside world (input ports)
Ports are defined in the language of the domain, not infrastructure.
Examples (conceptual):
- “Store an order”
- “Send a notification”
- “Execute a checkout use case”
---
### Adapter
An **adapter** is a concrete implementation of a port using a specific technology.
Adapters translate:
- External representations → domain concepts
- Domain requests → external system calls
Adapters are replaceable and exist at the systems edge.
#### Adapter Types
**Primary (Driving) Adapters**
- Initiate interaction with the core
- Examples: Web UI, CLI, REST controller, automated tests
**Secondary (Driven) Adapters**
- Are used by the core
- Examples: Database repositories, message brokers, email services
---
## Dependency Rule
- The **core defines ports**
- **Adapters implement ports**
- The core knows nothing about adapters
- Adapters depend on the core, never the reverse
This rule guarantees that business logic remains stable even when technologies change.
---
## Interaction Flow
1. A primary adapter receives input (e.g., user action)
2. It calls an **input port**
3. The core executes business logic
4. The core calls **output ports** as needed
5. Secondary adapters fulfill those ports using external systems
All I/O stays outside the core.
---
## Structuring a System
A conceptual structure:
- Core
- Domain entities
- Use cases
- Port interfaces
- Adapters
- Input adapters (UI, API, tests)
- Output adapters (DB, services, files)
- Composition root
- Wires ports to adapters at startup
This structure is conceptual, not tied to folders or modules.
---
## Testing Strategy
Hexagonal architecture enables strong testing practices:
- Test core logic using fake or in-memory adapters
- No need for databases or servers in unit tests
- Integration tests focus on individual adapters
Testing becomes simpler because dependencies are explicit and replaceable.
---
## Benefits
- High testability
- Clear separation of concerns
- Technology independence
- Easier maintenance and evolution
- Multiple interfaces over the same core logic
- Strong alignment with Domain-Driven Design
---
## Comparison to Layered Architecture
Layered Architecture:
- Organizes code by technical layers
- Often allows UI → DB coupling
- Business logic can leak into infrastructure
Hexagonal Architecture:
- Organizes around the domain
- Enforces strict boundaries
- Treats UI and DB as interchangeable details
---
## When to Use
Hexagonal architecture is well suited for:
- Medium to large systems
- Long-lived codebases
- Complex business domains
- Systems with multiple interfaces
- Applications that must remain adaptable
It may be unnecessary for very small or trivial applications.
---
## Summary
Hexagonal Architecture places the domain at the center and treats all external systems as replaceable plugins. By communicating exclusively through ports and adapters, it ensures long-term flexibility, maintainability, and testability.
This pattern is language-agnostic and focuses on **design principles**, not frameworks.

View File

@@ -0,0 +1,20 @@
# Outcome - Fix Auth Scheduler Env Vars
## Summary
Successfully fixed the environment variable loading issue in the authentication scheduler and updated the frequency configuration to support minutes.
## Changes
- **Refactored Scheduler Logic:**
- Updated `src/lib/server/scheduler.ts` to use `$env/dynamic/private` for reliable environment variable access.
- Changed configuration from `intervalHours` to `intervalMinutes`.
- Updated `startScheduler` to calculate interval in milliseconds based on minutes.
- **Updated Documentation:**
- Updated `src/hooks.server.ts` JSDoc to reflect the new configuration.
- **Updated Configuration:**
- Updated `.env.local` to set `AUTH_SCHEDULER_INTERVAL_MINUTES=5`.
- **Verified Tests:**
- Updated `src/tests/scheduler.spec.ts` to mock `$env/dynamic/private` and verify the new logic.
- All tests passed.
## Feature Branch
`feature/FixAuthSchedulerEnvVars`

View File

@@ -0,0 +1,21 @@
# Outcome - Fix Scheduler Concurrency and Browser Stability
## Summary
Successfully implemented fixes for the scheduler concurrency issues and browser instability.
## Changes
- **Scheduler Configuration Validation:**
- Updated `src/lib/server/scheduler.ts` to validate `intervalMinutes`.
- Added a check for `NaN` and a minimum interval of 15 minutes.
- Defaults to 720 minutes if the configuration is invalid.
- **Resource Cleanup:**
- Refactored `renewInstagramAuth` in `src/lib/server/scheduler.ts` to use a `finally` block for closing `page` and `context`.
- Ensures resources are released even if an error occurs during renewal.
- **Robust Browser Management:**
- Updated `src/lib/server/browser.ts` to check `browser.isConnected()`.
- Automatically re-initializes the browser if it is disconnected or crashed.
## Verification
- The scheduler will now default to a safe interval if misconfigured, preventing console spam.
- Browser crashes will be automatically recovered from on the next scheduler run.
- Resource leaks from failed renewal attempts are prevented.

View File

@@ -0,0 +1,89 @@
# Execution Plan: Fix Scheduler Concurrency and Browser Stability
## Context
The application is experiencing two related issues with the Instagram authentication scheduler:
1. **Console Spam**: "Auth renewal already in progress" is logged repeatedly. This indicates the scheduler is triggering new renewal attempts while a previous one is still active (or perceived as active). This is likely caused by an invalid or extremely short interval configuration (e.g., `NaN` resulting from parsing failure).
2. **Browser Instability**: "Target page, context or browser has been closed" errors. This occurs when the scheduler attempts to use a cached Playwright browser instance that has crashed or disconnected.
## Goal
Ensure the authentication scheduler runs reliably at the configured interval without overlapping executions, and make the browser instance management robust against crashes.
## Exception to workflow
Do not create a dedicated branch. It's a fix on a new feature.
## Proposed Solution
### Story 1: Fix Scheduler Configuration and Resource Cleanup
**Objective**: Prevent rapid-fire execution of the scheduler and ensure browser resources are cleaned up properly even when errors occur.
**Changes**:
1. **Validate Configuration**: In `src/lib/server/scheduler.ts`, update `getConfig()` to strictly validate `intervalMinutes`.
* Handle `NaN` (parsing errors).
* Enforce a minimum interval (e.g., 15 minutes) to prevent spamming.
* Default to 720 minutes if invalid.
2. **Improve Resource Management**: Refactor `renewInstagramAuth` to ensure `page` and `context` are closed in a `finally` block (or nested `try/finally`), preventing resource leaks if an error occurs during the renewal process.
**Verification**:
* Set `AUTH_SCHEDULER_INTERVAL_MINUTES` to an invalid value (e.g., "abc") and verify it defaults to 720.
* Verify that `setInterval` is called with a valid duration.
### Story 2: Robust Browser Lifecycle Management
**Objective**: Ensure the application automatically recovers from browser crashes by detecting disconnected instances.
**Changes**:
1. **Check Connection Status**: In `src/lib/server/browser.ts`, update `getBrowser()` to check `browser.isConnected()`.
2. **Auto-Recovery**: If the cached browser instance is not connected:
* Log a warning.
* Attempt to close the dead instance (swallowing errors).
* Re-initialize a new browser instance.
**Verification**:
* Simulate a browser crash (e.g., by manually killing the chrome process if possible, or mocking `isConnected` to return false).
* Verify that the next call to `getBrowser()` creates a new instance instead of throwing.
## Implementation Details
### `src/lib/server/scheduler.ts`
```typescript
function getConfig(): SchedulerConfig {
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, intervalMinutes };
}
// In renewInstagramAuth:
let context = null;
let page = null;
try {
// ... setup ...
context = await browser.newContext(...);
page = await context.newPage();
// ... logic ...
} catch (e) {
// ... error handling ...
} finally {
if (page) await page.close().catch(() => {});
if (context) await context.close().catch(() => {});
state.isRenewing = false;
}
```
### `src/lib/server/browser.ts`
```typescript
export async function getBrowser(): Promise<Browser> {
if (!browser || !browser.isConnected()) {
if (browser) {
console.warn('Browser is disconnected. Re-initializing...');
try { await browser.close(); } catch (e) { /* ignore */ }
}
return initializeBrowser();
}
return browser;
}
```

View File

@@ -5,7 +5,7 @@
"value": "SDRORLyWEsWWty2ZoVGdER", "value": "SDRORLyWEsWWty2ZoVGdER",
"domain": ".instagram.com", "domain": ".instagram.com",
"path": "/", "path": "/",
"expires": 1799232681.423721, "expires": 1800839244.918688,
"httpOnly": false, "httpOnly": false,
"secure": true, "secure": true,
"sameSite": "Lax" "sameSite": "Lax"
@@ -40,39 +40,39 @@
"secure": true, "secure": true,
"sameSite": "None" "sameSite": "None"
}, },
{
"name": "sessionid",
"value": "59661903731%3AbekaIlo4nn7x2n%3A29%3AAYgUJJp9m0KL9O319kXeeujlUYhEn2vNb-kd0dD9Rg",
"domain": ".instagram.com",
"path": "/",
"expires": 1796208680.65293,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
},
{ {
"name": "ds_user_id", "name": "ds_user_id",
"value": "59661903731", "value": "59661903731",
"domain": ".instagram.com", "domain": ".instagram.com",
"path": "/", "path": "/",
"expires": 1772448681.423801, "expires": 1774055244.918777,
"httpOnly": false, "httpOnly": false,
"secure": true, "secure": true,
"sameSite": "None" "sameSite": "None"
}, },
{
"name": "sessionid",
"value": "59661903731%3AbekaIlo4nn7x2n%3A29%3AAYiHJx9fnG7GZcaJ-BL1hIYE91xYvk2h_5n6NjpiBg",
"domain": ".instagram.com",
"path": "/",
"expires": 1797815010.233987,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
},
{ {
"name": "wd", "name": "wd",
"value": "1280x720", "value": "1280x720",
"domain": ".instagram.com", "domain": ".instagram.com",
"path": "/", "path": "/",
"expires": 1765277481, "expires": 1766884045,
"httpOnly": false, "httpOnly": false,
"secure": true, "secure": true,
"sameSite": "Lax" "sameSite": "Lax"
}, },
{ {
"name": "rur", "name": "rur",
"value": "\"CLN\\05459661903731\\0541796208682:01fede18d45d4fee86d4f2a276c8a844cb9172b89b51e56eba94c8b8cefaf1f08c566656\"", "value": "\"CLN\\05459661903731\\0541797815244:01fe3220c89f7ce57e28ead6feec8aed351b809536b4729e55496018e38ea6a7ca601a89\"",
"domain": ".instagram.com", "domain": ".instagram.com",
"path": "/", "path": "/",
"expires": -1, "expires": -1,
@@ -85,29 +85,41 @@
{ {
"origin": "https://www.instagram.com", "origin": "https://www.instagram.com",
"localStorage": [ "localStorage": [
{
"name": "signal_flush_timestamp",
"value": "1764672681393"
},
{
"name": "Session",
"value": "grovm1:1764672716275"
},
{ {
"name": "chatd-deviceid", "name": "chatd-deviceid",
"value": "b74919e2-1922-4616-b903-e51f41ac4efe" "value": "1b416b56-d780-40db-b542-2a24ed66c77f"
},
{
"name": "has_interop_upgraded",
"value": "{\"lastCheckedAt\":1764672681430,\"status\":false}"
}, },
{ {
"name": "hb_timestamp", "name": "hb_timestamp",
"value": "1764672679702" "value": "1766279010726"
}, },
{ {
"name": "IGSession", "name": "IGSession",
"value": "4ulad7:1764674481275" "value": "6m2tlb:1766281044259"
},
{
"name": "mutex_polaris_banzai",
"value": "t9hvzg:1766279244136"
},
{
"name": "signal_flush_timestamp",
"value": "1766279010762"
},
{
"name": "Session",
"value": "dicivj:1766279279259"
},
{
"name": "has_interop_upgraded",
"value": "{\"lastCheckedAt\":1766279008975,\"status\":false}"
},
{
"name": "mutex_banzai",
"value": "t9hvzg:1766279244136"
},
{
"name": "banzai:last_storage_flush",
"value": "1766279009540.7998"
} }
] ]
} }

View File

@@ -7,7 +7,7 @@ import type { ServerInit } from '@sveltejs/kit';
* *
* Environment variables: * Environment variables:
* - AUTH_SCHEDULER_ENABLED: Set to 'true' to enable periodic auth renewal * - 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 () => { export const init: ServerInit = async () => {
console.log('[Server Init] Starting SvelteKit server...'); 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> { 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 initializeBrowser();
} }
return browser; return browser;

View File

@@ -1,10 +1,11 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { getBrowser } from './browser'; import { getBrowser } from './browser';
import { env } from '$env/dynamic/private';
export interface SchedulerConfig { export interface SchedulerConfig {
enabled: boolean; enabled: boolean;
intervalHours: number; intervalMinutes: number;
} }
interface SchedulerState { interface SchedulerState {
@@ -23,12 +24,19 @@ const state: SchedulerState = {
* Get scheduler configuration from environment variables * Get scheduler configuration from environment variables
*/ */
function getConfig(): SchedulerConfig { function getConfig(): SchedulerConfig {
const enabled = process.env.AUTH_SCHEDULER_ENABLED === 'true'; const enabled = env.AUTH_SCHEDULER_ENABLED === 'true';
const intervalHours = parseInt(process.env.AUTH_SCHEDULER_INTERVAL_HOURS || '12', 10); 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 { return {
enabled, enabled,
intervalHours intervalMinutes
}; };
} }
@@ -70,14 +78,17 @@ async function renewInstagramAuth(): Promise<boolean> {
state.isRenewing = true; state.isRenewing = true;
let context = null;
let page = null;
try { try {
console.log('[Scheduler] Starting Instagram authentication renewal...'); console.log('[Scheduler] Starting Instagram authentication renewal...');
console.log(`[Scheduler] Loading existing auth from: ${authPath}`); console.log(`[Scheduler] Loading existing auth from: ${authPath}`);
const browser = await getBrowser(); const browser = await getBrowser();
// Load existing authentication state // Load existing authentication state
const context = await browser.newContext({ storageState: authPath }); context = await browser.newContext({ storageState: authPath });
const page = await context.newPage(); page = await context.newPage();
// Navigate to Instagram homepage - the existing auth will be used automatically // Navigate to Instagram homepage - the existing auth will be used automatically
await page.goto('https://www.instagram.com/', { waitUntil: 'domcontentloaded' }); 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'); console.log('[Scheduler] Successfully authenticated with Instagram');
} catch (e) { } catch (e) {
console.warn('[Scheduler] Home icon not found - session may be expired or invalid'); console.warn('[Scheduler] Home icon not found - session may be expired or invalid');
await page.close();
await context.close();
state.isRenewing = false;
return false; return false;
} }
@@ -105,9 +113,6 @@ async function renewInstagramAuth(): Promise<boolean> {
// Update auth.json with refreshed session // Update auth.json with refreshed session
await context.storageState({ path: authPath }); await context.storageState({ path: authPath });
await page.close();
await context.close();
state.lastRenewalTime = Date.now(); state.lastRenewalTime = Date.now();
console.log(`[Scheduler] Instagram authentication renewed successfully at ${new Date().toISOString()}`); console.log(`[Scheduler] Instagram authentication renewed successfully at ${new Date().toISOString()}`);
console.log(`[Scheduler] Auth state updated at: ${authPath}`); 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); console.error('[Scheduler] Instagram authentication renewal failed:', error);
return false; return false;
} finally { } finally {
if (page) {
await page.close().catch(() => {});
}
if (context) {
await context.close().catch(() => {});
}
state.isRenewing = false; state.isRenewing = false;
} }
} }
@@ -137,9 +148,9 @@ export async function startScheduler(): Promise<void> {
return; 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 // Schedule periodic renewals
state.intervalId = setInterval(async () => { 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'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Mock environment variables // Mock environment variables
const setEnv = (key: string, value: string | undefined) => { const { mockEnv } = vi.hoisted(() => {
if (value === undefined) { return {
delete process.env[key]; mockEnv: {
} else { AUTH_SCHEDULER_ENABLED: 'false',
process.env[key] = value; AUTH_SCHEDULER_INTERVAL_MINUTES: '720'
} }
}; };
});
vi.mock('$env/dynamic/private', () => ({
env: mockEnv
}));
// Mock the browser module // Mock the browser module
vi.mock('$lib/server/browser', () => ({ vi.mock('$lib/server/browser', () => ({
@@ -28,8 +33,8 @@ const mockFs = {
describe('Scheduler Service', () => { describe('Scheduler Service', () => {
beforeEach(() => { beforeEach(() => {
// Reset environment variables // Reset environment variables
setEnv('AUTH_SCHEDULER_ENABLED', undefined); mockEnv.AUTH_SCHEDULER_ENABLED = 'false';
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', undefined); mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '720';
// Clear all mocks // Clear all mocks
vi.clearAllMocks(); vi.clearAllMocks();
@@ -48,24 +53,24 @@ describe('Scheduler Service', () => {
}); });
describe('Configuration', () => { describe('Configuration', () => {
it('should use default interval when AUTH_SCHEDULER_INTERVAL_HOURS is not set', async () => { it('should use default interval when AUTH_SCHEDULER_INTERVAL_MINUTES is not set', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true'); mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', undefined); mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '';
const status = getSchedulerStatus(); const status = getSchedulerStatus();
expect(status.config.intervalHours).toBe(12); expect(status.config.intervalMinutes).toBe(720);
}); });
it('should parse custom interval hours from environment', async () => { it('should parse custom interval minutes from environment', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true'); mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', '6'); mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '30';
const status = getSchedulerStatus(); 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 () => { 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(); const status = getSchedulerStatus();
expect(status.config.enabled).toBe(false); 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 () => { 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(); const status = getSchedulerStatus();
expect(status.config.enabled).toBe(true); expect(status.config.enabled).toBe(true);
@@ -82,7 +87,7 @@ describe('Scheduler Service', () => {
describe('Scheduler Lifecycle', () => { describe('Scheduler Lifecycle', () => {
it('should not start when disabled', async () => { it('should not start when disabled', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'false'); mockEnv.AUTH_SCHEDULER_ENABLED = 'false';
await startScheduler(); await startScheduler();
@@ -91,7 +96,7 @@ describe('Scheduler Service', () => {
}); });
it('should start when enabled', async () => { it('should start when enabled', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true'); mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockFs.existsSync.mockReturnValue(true); mockFs.existsSync.mockReturnValue(true);
await startScheduler(); await startScheduler();
@@ -101,7 +106,7 @@ describe('Scheduler Service', () => {
}); });
it('should not start twice', async () => { it('should not start twice', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true'); mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockFs.existsSync.mockReturnValue(true); mockFs.existsSync.mockReturnValue(true);
await startScheduler(); await startScheduler();
@@ -113,7 +118,7 @@ describe('Scheduler Service', () => {
}); });
it('should stop the scheduler', async () => { it('should stop the scheduler', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true'); mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockFs.existsSync.mockReturnValue(true); mockFs.existsSync.mockReturnValue(true);
await startScheduler(); await startScheduler();
@@ -132,7 +137,7 @@ describe('Scheduler Service', () => {
describe('Status Reporting', () => { describe('Status Reporting', () => {
it('should return scheduler status with default values', () => { it('should return scheduler status with default values', () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'false'); mockEnv.AUTH_SCHEDULER_ENABLED = 'false';
const status = getSchedulerStatus(); const status = getSchedulerStatus();
@@ -142,13 +147,13 @@ describe('Scheduler Service', () => {
isRenewing: false, isRenewing: false,
config: { config: {
enabled: false, enabled: false,
intervalHours: 12 intervalMinutes: 720
} }
}); });
}); });
it('should report running state correctly', async () => { it('should report running state correctly', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true'); mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
mockFs.existsSync.mockReturnValue(true); mockFs.existsSync.mockReturnValue(true);
await startScheduler(); await startScheduler();
@@ -159,13 +164,13 @@ describe('Scheduler Service', () => {
}); });
it('should track configuration', async () => { it('should track configuration', async () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true'); mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', '24'); mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '1440';
const status = getSchedulerStatus(); const status = getSchedulerStatus();
expect(status.config.enabled).toBe(true); 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', () => { describe('Environment Variables', () => {
it('should handle empty AUTH_SCHEDULER_INTERVAL_HOURS with default', () => { it('should handle empty AUTH_SCHEDULER_INTERVAL_MINUTES with default', () => {
setEnv('AUTH_SCHEDULER_ENABLED', 'true'); mockEnv.AUTH_SCHEDULER_ENABLED = 'true';
setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', ''); mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '';
const status = getSchedulerStatus(); const status = getSchedulerStatus();
// Empty string should fall back to default due to parseInt('', 10) returning NaN // Empty string should fall back to default due to parseInt('', 10) returning NaN
// and the || 12 fallback // and the || 720 fallback
expect(status.config.intervalHours).toBeDefined(); expect(status.config.intervalMinutes).toBeDefined();
}); });
}); });
}); });