After the DC-005 module reorganization (41 files moved into src/ subdirs),
138 test suites failed because the refactor script's path-rewrite logic
missed three categories:
1. Files inside src/ doing 'require("./src/...")' — should be 'require("../...")'
2. Files in src/X/Y/ doing 'require("../../../src/...")' — should be 'require("../../...")'
3. Test files in __tests__/ with leftover 'require("../../../src/...")' paths
Root cause: the original refactor script ran before all files were moved,
so it computed relative paths against stale filesystem state.
Result:
- 30/30 test suites pass
- 879/879 tests pass (was: 18/30 suites, 614/687 tests)
Also fixed:
- routes/apps/restore.js: wrong responses import path
- routes/*/*.js: '../../src/utilities/X' → '../src/utilities/X' (depth 2 routes)
144 lines
5.1 KiB
JavaScript
144 lines
5.1 KiB
JavaScript
const express = require('express');
|
|
const { validateURL, validateToken } = require('../../../src/security/input-validator');
|
|
const { ValidationError } = require('../../../src/utilities/errors');
|
|
const { ok, successMessage } = require('../src/utils/responses');
|
|
|
|
/**
|
|
* Arr credentials routes factory
|
|
* @param {Object} deps - Explicit dependencies
|
|
* @param {Object} deps.credentialManager - Credential manager
|
|
* @param {Object} deps.servicesStateManager - Services state manager
|
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
|
* @param {Function} deps.errorResponse - Error response helper
|
|
* @param {Object} deps.log - Logger instance
|
|
* @param {Object} deps.helpers - Arr helpers module
|
|
* @returns {express.Router}
|
|
*/
|
|
module.exports = function({ credentialManager, servicesStateManager, asyncHandler, errorResponse, log, helpers }) {
|
|
const router = express.Router();
|
|
|
|
// Store arr service credentials
|
|
router.post('/arr/credentials', asyncHandler(async (req, res) => {
|
|
const { service, apiKey, url, seedboxBaseUrl, qualityProfileId, qualityProfileName } = req.body;
|
|
|
|
if (!service || !apiKey) {
|
|
throw new ValidationError('Service name and API key required');
|
|
}
|
|
|
|
const validServices = ['radarr', 'sonarr', 'prowlarr', 'lidarr', 'plex'];
|
|
if (!validServices.includes(service)) {
|
|
return errorResponse(res, 400, `Invalid service. Must be one of: ${validServices.join(', ')}`);
|
|
}
|
|
|
|
// Validate API key format
|
|
try {
|
|
validateToken(apiKey);
|
|
} catch (e) {
|
|
throw new ValidationError('Invalid API key format');
|
|
}
|
|
|
|
// Validate URL if provided
|
|
if (url) {
|
|
try { validateURL(url); } catch (e) {
|
|
throw new ValidationError('Invalid URL format');
|
|
}
|
|
}
|
|
|
|
// Determine credential key
|
|
const credKey = service === 'plex' ? 'arr.plex.token' : `arr.${service}.apikey`;
|
|
|
|
// Build metadata
|
|
const metadata = {
|
|
service,
|
|
source: url ? 'external' : 'local',
|
|
url: url || null,
|
|
storedAt: new Date().toISOString()
|
|
};
|
|
|
|
// Test connection if URL is known
|
|
let connectionTest = null;
|
|
let resolvedUrl = url;
|
|
|
|
if (!resolvedUrl) {
|
|
// Try to resolve URL from services.json
|
|
try {
|
|
const services = await servicesStateManager.read();
|
|
const svc = Array.isArray(services) ? services : services.services || [];
|
|
const found = svc.find(s => s.id === service && s.isExternal);
|
|
if (found?.externalUrl) resolvedUrl = found.externalUrl;
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
if (resolvedUrl) {
|
|
connectionTest = await helpers.testServiceConnection(service, resolvedUrl, apiKey);
|
|
if (connectionTest.success) {
|
|
metadata.lastVerified = new Date().toISOString();
|
|
metadata.version = connectionTest.version;
|
|
metadata.url = resolvedUrl;
|
|
}
|
|
}
|
|
|
|
// Store quality profile preference if provided
|
|
if (qualityProfileId) {
|
|
metadata.qualityProfileId = qualityProfileId;
|
|
metadata.qualityProfileName = qualityProfileName || null;
|
|
}
|
|
|
|
// Store the credential
|
|
const stored = await credentialManager.store(credKey, apiKey, metadata);
|
|
if (!stored) {
|
|
return errorResponse(res, 500, 'Failed to store credential');
|
|
}
|
|
|
|
// Optionally store seedbox base URL
|
|
if (seedboxBaseUrl) {
|
|
try { validateURL(seedboxBaseUrl); } catch (e) {
|
|
throw new ValidationError('Invalid seedbox base URL');
|
|
}
|
|
await credentialManager.store('arr.seedbox.baseurl', seedboxBaseUrl, {
|
|
storedAt: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
log.info('arr', 'Stored API key', { service, verified: connectionTest?.success || false });
|
|
|
|
ok(res, { message: `${service} API key stored`, connectionTest, url: resolvedUrl });
|
|
}, 'arr-credentials-store'));
|
|
|
|
// List stored arr credentials (keys only, not values)
|
|
router.get('/arr/credentials', asyncHandler(async (req, res) => {
|
|
const services = ['radarr', 'sonarr', 'prowlarr', 'lidarr', 'plex'];
|
|
const credentials = {};
|
|
|
|
for (const service of services) {
|
|
const credKey = service === 'plex' ? 'arr.plex.token' : `arr.${service}.apikey`;
|
|
const hasKey = !!(await credentialManager.retrieve(credKey));
|
|
const metadata = await credentialManager.getMetadata(credKey);
|
|
|
|
credentials[service] = {
|
|
hasKey,
|
|
url: metadata?.url || null,
|
|
lastVerified: metadata?.lastVerified || null,
|
|
version: metadata?.version || null,
|
|
source: metadata?.source || null
|
|
};
|
|
}
|
|
|
|
// Get seedbox base URL
|
|
const seedboxBaseUrl = await credentialManager.retrieve('arr.seedbox.baseurl');
|
|
|
|
ok(res, { credentials, seedboxBaseUrl: seedboxBaseUrl || null });
|
|
}, 'arr-credentials-list'));
|
|
|
|
// Delete stored arr credentials
|
|
router.delete('/arr/credentials/:service', asyncHandler(async (req, res) => {
|
|
const { service } = req.params;
|
|
const credKey = service === 'plex' ? 'arr.plex.token' : `arr.${service}.apikey`;
|
|
await credentialManager.delete(credKey);
|
|
log.info('arr', 'Deleted credentials', { service });
|
|
successMessage(res, `${service} credentials removed`);
|
|
}, 'arr-credentials-delete'));
|
|
|
|
return router;
|
|
};
|