Files
gitea-mirror/src/lib/utils/duration-parser.test.ts
Arunavo Ray 698eb0b507 fix: Complete Issue #72 - Fix automatic mirroring and repository cleanup
Major fixes for Docker environment variable issues and cleanup functionality:

🔧 **Duration Parser & Scheduler Fixes**
- Add comprehensive duration parser supporting "8h", "30m", "24h" formats
- Fix GITEA_MIRROR_INTERVAL environment variable mapping to scheduler
- Auto-enable scheduler when GITEA_MIRROR_INTERVAL is set
- Improve scheduler logging to clarify timing behavior (from last run, not startup)

🧹 **Repository Cleanup Service**
- Complete repository cleanup service for orphaned repos (unstarred, deleted)
- Fix cleanup configuration logic - now works with CLEANUP_DELETE_IF_NOT_IN_GITHUB=true
- Auto-enable cleanup when deleteIfNotInGitHub is enabled
- Add manual cleanup trigger API endpoint (/api/cleanup/trigger)
- Support archive/delete actions with dry-run mode and protected repos

🐛 **Environment Variable Integration**
- Fix scheduler not recognizing GITEA_MIRROR_INTERVAL=8h
- Fix cleanup requiring both CLEANUP_DELETE_FROM_GITEA and CLEANUP_DELETE_IF_NOT_IN_GITHUB
- Auto-enable services when relevant environment variables are set
- Better error logging and debugging information

📚 **Documentation Updates**
- Update .env.example with auto-enabling behavior notes
- Update ENVIRONMENT_VARIABLES.md with clarified functionality
- Add comprehensive tests for duration parsing

This resolves the core issues where:
1. GITEA_MIRROR_INTERVAL=8h was not working for automatic mirroring
2. Repository cleanup was not working despite CLEANUP_DELETE_IF_NOT_IN_GITHUB=true
3. Users had no visibility into why scheduling/cleanup wasn't working

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-20 11:06:21 +05:30

94 lines
3.3 KiB
TypeScript

import { test, expect } from 'bun:test';
import { parseDuration, parseInterval, formatDuration, parseCronInterval } from './duration-parser';
test('parseDuration - handles duration strings correctly', () => {
// Hours
expect(parseDuration('8h')).toBe(8 * 60 * 60 * 1000);
expect(parseDuration('1h')).toBe(60 * 60 * 1000);
expect(parseDuration('24h')).toBe(24 * 60 * 60 * 1000);
// Minutes
expect(parseDuration('30m')).toBe(30 * 60 * 1000);
expect(parseDuration('5m')).toBe(5 * 60 * 1000);
// Seconds
expect(parseDuration('45s')).toBe(45 * 1000);
expect(parseDuration('1s')).toBe(1000);
// Days
expect(parseDuration('1d')).toBe(24 * 60 * 60 * 1000);
expect(parseDuration('7d')).toBe(7 * 24 * 60 * 60 * 1000);
// Numbers (treated as seconds)
expect(parseDuration(3600)).toBe(3600 * 1000);
expect(parseDuration('3600')).toBe(3600 * 1000);
});
test('parseDuration - handles edge cases', () => {
// Case insensitive
expect(parseDuration('8H')).toBe(8 * 60 * 60 * 1000);
expect(parseDuration('30M')).toBe(30 * 60 * 1000);
// With spaces
expect(parseDuration('8 h')).toBe(8 * 60 * 60 * 1000);
expect(parseDuration('30 minutes')).toBe(30 * 60 * 1000);
// Fractional values
expect(parseDuration('1.5h')).toBe(1.5 * 60 * 60 * 1000);
expect(parseDuration('2.5m')).toBe(2.5 * 60 * 1000);
});
test('parseDuration - throws on invalid input', () => {
expect(() => parseDuration('')).toThrow();
expect(() => parseDuration('invalid')).toThrow();
expect(() => parseDuration('8x')).toThrow();
expect(() => parseDuration('-1h')).toThrow();
});
test('parseInterval - handles cron expressions', () => {
// Every 2 hours
expect(parseInterval('0 */2 * * *')).toBe(2 * 60 * 60 * 1000);
// Every 15 minutes
expect(parseInterval('*/15 * * * *')).toBe(15 * 60 * 1000);
// Daily at 2 AM
expect(parseInterval('0 2 * * *')).toBe(24 * 60 * 60 * 1000);
});
test('parseInterval - prioritizes duration strings over cron', () => {
expect(parseInterval('8h')).toBe(8 * 60 * 60 * 1000);
expect(parseInterval('30m')).toBe(30 * 60 * 1000);
expect(parseInterval(3600)).toBe(3600 * 1000);
});
test('formatDuration - converts milliseconds back to readable format', () => {
expect(formatDuration(1000)).toBe('1s');
expect(formatDuration(60 * 1000)).toBe('1m');
expect(formatDuration(60 * 60 * 1000)).toBe('1h');
expect(formatDuration(24 * 60 * 60 * 1000)).toBe('1d');
expect(formatDuration(8 * 60 * 60 * 1000)).toBe('8h');
expect(formatDuration(500)).toBe('500ms');
});
test('parseCronInterval - handles common cron patterns', () => {
expect(parseCronInterval('0 */8 * * *')).toBe(8 * 60 * 60 * 1000);
expect(parseCronInterval('*/30 * * * *')).toBe(30 * 60 * 1000);
expect(parseCronInterval('0 2 * * *')).toBe(24 * 60 * 60 * 1000);
expect(parseCronInterval('0 0 * * 0')).toBe(7 * 24 * 60 * 60 * 1000); // Weekly
});
test('Integration test - Issue #72 scenario', () => {
// User sets GITEA_MIRROR_INTERVAL=8h
const userInterval = '8h';
const parsedMs = parseInterval(userInterval);
expect(parsedMs).toBe(8 * 60 * 60 * 1000); // 8 hours in milliseconds
expect(formatDuration(parsedMs)).toBe('8h');
// Should work from container startup time
const startTime = new Date();
const nextRun = new Date(startTime.getTime() + parsedMs);
expect(nextRun.getTime() - startTime.getTime()).toBe(8 * 60 * 60 * 1000);
});