mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
feat: extensive file checking
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { readFile, writeFile, access } from 'fs/promises';
|
||||
import { readFile, writeFile, access, unlink } from 'fs/promises';
|
||||
import { constants } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { join, dirname, basename } from 'path';
|
||||
import { applyPatch, parsePatch, reversePatch } from 'diff';
|
||||
|
||||
export interface PatchResult {
|
||||
@@ -25,7 +25,7 @@ export abstract class FileModification {
|
||||
|
||||
private getPatchFilePath(targetFile: string): string {
|
||||
const dir = dirname(targetFile);
|
||||
const filename = `${this.id}.patch`;
|
||||
const filename = `${basename(targetFile)}.patch`;
|
||||
return join(dir, filename);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ export abstract class FileModification {
|
||||
const { targetFile, patch } = patchResult;
|
||||
const currentContent = await readFile(targetFile, 'utf8');
|
||||
const parsedPatch = parsePatch(patch)[0];
|
||||
|
||||
const results = applyPatch(currentContent, parsedPatch);
|
||||
if (results === false) {
|
||||
throw new Error(`Failed to apply patch to ${targetFile}`);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Index: text-patch-file.txt
|
||||
===================================================================
|
||||
--- text-patch-file.txt
|
||||
+++ text-patch-file.txt
|
||||
@@ -1,1 +1,1 @@
|
||||
-original
|
||||
\ No newline at end of file
|
||||
+modified
|
||||
\ No newline at end of file
|
||||
@@ -1,47 +1,71 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { resolve } from 'path';
|
||||
|
||||
|
||||
|
||||
import { applyPatch } from 'diff';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
|
||||
|
||||
import DefaultPageLayoutModification from '@app/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification';
|
||||
import NotificationsPageModification from '@app/unraid-api/unraid-file-modifier/modifications/notifications-page.modification';
|
||||
import { FileModification } from '@app/unraid-api/unraid-file-modifier/file-modification';
|
||||
import SSOFileModification from '@app/unraid-api/unraid-file-modifier/modifications/sso.modification';
|
||||
|
||||
interface ModificationTestCase {
|
||||
name: string;
|
||||
ModificationClass: new (logger: Logger) => FileModification;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
const testCases: ModificationTestCase[] = [
|
||||
{
|
||||
name: 'DefaultPageLayout.php',
|
||||
ModificationClass: DefaultPageLayoutModification,
|
||||
fileName: 'DefaultPageLayout.php'
|
||||
},
|
||||
{
|
||||
name: 'Notifications.page',
|
||||
ModificationClass: NotificationsPageModification,
|
||||
fileName: 'Notifications.page'
|
||||
},
|
||||
{
|
||||
name: '.login.php',
|
||||
ModificationClass: SSOFileModification,
|
||||
fileName: '.login.php'
|
||||
}
|
||||
];
|
||||
|
||||
async function testModification(testCase: ModificationTestCase) {
|
||||
const path = resolve(__dirname, `../__fixtures__/${testCase.fileName}`);
|
||||
const fileContent = await readFile(path, 'utf-8');
|
||||
expect(fileContent.length).toBeGreaterThan(0);
|
||||
|
||||
const logger = new Logger();
|
||||
const patcher = await new testCase.ModificationClass(logger);
|
||||
// @ts-ignore - Ignore for testing purposes
|
||||
patcher.filePath = path;
|
||||
|
||||
// @ts-ignore - Ignore for testing purposes
|
||||
const patch = await patcher.generatePatch();
|
||||
|
||||
// Test patch matches snapshot
|
||||
await expect(patch.patch).toMatchFileSnapshot(
|
||||
`snapshots/${testCase.fileName}.snapshot.patch`
|
||||
);
|
||||
|
||||
describe('DefaultPageLayout.php modifier', () => {
|
||||
test('correctly applies to fresh install', async () => {
|
||||
const fileContent = await readFile(
|
||||
resolve(__dirname, '../__fixtures__/DefaultPageLayout.php'),
|
||||
'utf-8'
|
||||
);
|
||||
const path = resolve(__dirname, '../__fixtures__/DefaultPageLayout.php');
|
||||
expect(fileContent.length).toBeGreaterThan(0);
|
||||
const logger = new Logger();
|
||||
const patcher = await new DefaultPageLayoutModification(logger);
|
||||
patcher.filePath = path;
|
||||
const patch = await patcher.generatePatch();
|
||||
// Apply patch and verify modified file
|
||||
await patcher.apply();
|
||||
await expect(await readFile(path, 'utf-8')).toMatchFileSnapshot(
|
||||
`snapshots/${testCase.fileName}.modified.snapshot.php`
|
||||
);
|
||||
|
||||
await expect(patch.patch).toMatchFileSnapshot('DefaultPageLayout.patch.php');
|
||||
// Now we need to apply the patch
|
||||
// Rollback and verify original state
|
||||
await patcher.rollback();
|
||||
const revertedContent = await readFile(path, 'utf-8');
|
||||
await expect(revertedContent).toMatchFileSnapshot(
|
||||
`snapshots/${testCase.fileName}.original.php`
|
||||
);
|
||||
}
|
||||
|
||||
const newContent = applyPatch(fileContent, patch.patch, {
|
||||
fuzzFactor: 1,
|
||||
});
|
||||
await expect(newContent).toMatchFileSnapshot('DefaultPageLayout.modified.php');
|
||||
|
||||
// Now apply the patch
|
||||
await patcher.apply();
|
||||
|
||||
// Now rollback the patch and check that the file is back to the original
|
||||
await patcher.rollback();
|
||||
const revertedContent = await readFile(path, 'utf-8');
|
||||
await expect(revertedContent).toMatchFileSnapshot('DefaultPageLayout.original.php');
|
||||
describe('File modifications', () => {
|
||||
test.each(testCases)('$name modifier correctly applies to fresh install', async (testCase) => {
|
||||
await testModification(testCase);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { readFile } from 'fs/promises';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import NotificationsPageModification from '@app/unraid-api/unraid-file-modifier/modifications/notifications-page.modification';
|
||||
|
||||
describe('Notifications.page modifier', () => {
|
||||
test('correctly applies to fresh install', async () => {
|
||||
const fileContent = await readFile(
|
||||
resolve(__dirname, '../__fixtures__/Notifications.page'),
|
||||
'utf-8'
|
||||
);
|
||||
expect(fileContent.length).toBeGreaterThan(0);
|
||||
await expect(NotificationsPageModification.applyToSource(fileContent)).toMatchFileSnapshot(
|
||||
'Notifications.modified.page'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
Menu="UserPreferences"
|
||||
Type="xmenu"
|
||||
Title="Notification Settings"
|
||||
Icon="icon-notifications"
|
||||
Tag="phone-square"
|
||||
---
|
||||
<?PHP
|
||||
/* Copyright 2005-2023, Lime Technology
|
||||
* Copyright 2012-2023, Bergware International.
|
||||
* Copyright 2012, Andrew Hamer-Adams, http://www.pixeleyes.co.nz.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License version 2,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*/
|
||||
?>
|
||||
<?
|
||||
$events = explode('|', $notify['events'] ?? '');
|
||||
$disabled = $notify['system'] ? '' : 'disabled';
|
||||
?>
|
||||
<script>
|
||||
function prepareNotify(form) {
|
||||
form.entity.value = form.normal1.checked | form.warning1.checked | form.alert1.checked;
|
||||
form.normal.value = form.normal1.checked*1 + form.normal2.checked*2 + form.normal3.checked*4;
|
||||
form.warning.value = form.warning1.checked*1 + form.warning2.checked*2 + form.warning3.checked*4;
|
||||
form.alert.value = form.alert1.checked*1 + form.alert2.checked*2 + form.alert3.checked*4;
|
||||
form.unraid.value = form.unraid1.checked*1 + form.unraid2.checked*2 + form.unraid3.checked*4;
|
||||
form.plugin.value = form.plugin1.checked*1 + form.plugin2.checked*2 + form.plugin3.checked*4;
|
||||
form.docker_notify.value = form.docker_notify1.checked*1 + form.docker_notify2.checked*2 + form.docker_notify3.checked*4;
|
||||
form.language_notify.value = form.language_notify1.checked*1 + form.language_notify2.checked*2 + form.language_notify3.checked*4;
|
||||
form.report.value = form.report1.checked*1 + form.report2.checked*2 + form.report3.checked*4;
|
||||
form.normal1.disabled = true;
|
||||
form.normal2.disabled = true;
|
||||
form.normal3.disabled = true;
|
||||
form.warning1.disabled = true;
|
||||
form.warning2.disabled = true;
|
||||
form.warning3.disabled = true;
|
||||
form.alert1.disabled = true;
|
||||
form.alert2.disabled = true;
|
||||
form.alert3.disabled = true;
|
||||
form.unraid1.disabled = true;
|
||||
form.unraid2.disabled = true;
|
||||
form.unraid3.disabled = true;
|
||||
form.plugin1.disabled = true;
|
||||
form.plugin2.disabled = true;
|
||||
form.plugin3.disabled = true;
|
||||
form.docker_notify1.disabled = true;
|
||||
form.docker_notify2.disabled = true;
|
||||
form.docker_notify3.disabled = true;
|
||||
form.language_notify1.disabled = true;
|
||||
form.language_notify2.disabled = true;
|
||||
form.language_notify3.disabled = true;
|
||||
form.report1.disabled = true;
|
||||
form.report2.disabled = true;
|
||||
form.report3.disabled = true;
|
||||
}
|
||||
function prepareSystem(index) {
|
||||
if (index==0) $('.checkbox').attr('disabled','disabled'); else $('.checkbox').removeAttr('disabled');
|
||||
}
|
||||
function prepareTitle() {
|
||||
var title = '_(Available notifications)_:';
|
||||
$('#unraidTitle,#pluginTitle,#dockerTitle,#languageTitle,#reportTitle').html(' ');
|
||||
if ($('.unraid').is(':visible')) {$('#unraidTitle').html(title); return;}
|
||||
if ($('.plugin').is(':visible')) {$('#pluginTitle').html(title); return;}
|
||||
if ($('.docker').is(':visible')) {$('#dockerTitle').html(title); return;}
|
||||
if ($('.language').is(':visible')) {$('#languageTitle').html(title); return;}
|
||||
if ($('.report').is(':visible')) {$('#reportTitle').html(title); return;}
|
||||
}
|
||||
function prepareUnraid(value) {
|
||||
if (value=='') $('.unraid').hide(); else $('.unraid').show();
|
||||
prepareTitle();
|
||||
}
|
||||
function preparePlugin(value) {
|
||||
if (value=='') $('.plugin').hide(); else $('.plugin').show();
|
||||
prepareTitle();
|
||||
}
|
||||
function prepareDocker(value) {
|
||||
if (value=='') $('.docker').hide(); else $('.docker').show();
|
||||
prepareTitle();
|
||||
}
|
||||
function prepareLanguage(value) {
|
||||
if (value=='') $('.language').hide(); else $('.language').show();
|
||||
prepareTitle();
|
||||
}
|
||||
function prepareReport(value) {
|
||||
if (value=='') $('.report').hide(); else $('.report').show();
|
||||
prepareTitle();
|
||||
}
|
||||
$(function(){
|
||||
prepareUnraid(document.notify_settings.unraidos.value);
|
||||
preparePlugin(document.notify_settings.version.value);
|
||||
prepareDocker(document.notify_settings.docker_update.value);
|
||||
prepareLanguage(document.notify_settings.language_update.value);
|
||||
prepareReport(document.notify_settings.status.value);
|
||||
});
|
||||
</script>
|
||||
<form markdown="1" name="notify_settings" method="POST" action="/update.php" target="progressFrame" onsubmit="prepareNotify(this)">
|
||||
<input type="hidden" name="#file" value="dynamix/dynamix.cfg">
|
||||
<input type="hidden" name="#section" value="notify">
|
||||
<input type="hidden" name="#command" value="/webGui/scripts/notify">
|
||||
<input type="hidden" name="#arg[1]" value="cron-init">
|
||||
<input type="hidden" name="entity">
|
||||
<input type="hidden" name="normal">
|
||||
<input type="hidden" name="warning">
|
||||
<input type="hidden" name="alert">
|
||||
<input type="hidden" name="unraid">
|
||||
<input type="hidden" name="plugin">
|
||||
<input type="hidden" name="docker_notify">
|
||||
<input type="hidden" name="language_notify">
|
||||
<input type="hidden" name="report">
|
||||
_(Notifications display)_:
|
||||
: <select class="a" name="display">
|
||||
<?=mk_option($notify['display'], "0", _("Detailed"))?>
|
||||
<?=mk_option($notify['display'], "1", _("Summarized"))?>
|
||||
</select>
|
||||
|
||||
:notifications_display_help:
|
||||
|
||||
_(Display position)_:
|
||||
: <select name="position" class="a">
|
||||
<?=mk_option($notify['position'], "top-left", _("top-left"))?>
|
||||
<?=mk_option($notify['position'], "top-right", _("top-right"))?>
|
||||
<?=mk_option($notify['position'], "bottom-left", _("bottom-left"))?>
|
||||
<?=mk_option($notify['position'], "bottom-right", _("bottom-right"))?>
|
||||
<?=mk_option($notify['position'], "center", _("center"))?>
|
||||
</select>
|
||||
|
||||
:notifications_display_position_help:
|
||||
|
||||
_(Auto-close)_ (_(seconds)_):
|
||||
: <input type="number" name="life" class="a" min="0" max="60" value="<?=$notify['life']?>"> _(a value of zero means no automatic closure)_
|
||||
|
||||
:notifications_auto_close_help:
|
||||
|
||||
_(Date format)_:
|
||||
: <select name="date" class="a">
|
||||
<?=mk_option($notify['date'], "d-m-Y", _("DD-MM-YYYY"))?>
|
||||
<?=mk_option($notify['date'], "m-d-Y", _("MM-DD-YYYY"))?>
|
||||
<?=mk_option($notify['date'], "Y-m-d", _("YYYY-MM-DD"))?>
|
||||
</select>
|
||||
|
||||
:notifications_date_format_help:
|
||||
|
||||
_(Time format)_:
|
||||
: <select name="time" class="a">
|
||||
<?=mk_option($notify['time'], "h:i A", _("12 hours"))?>
|
||||
<?=mk_option($notify['time'], "H:i", _("24 hours"))?>
|
||||
</select>
|
||||
|
||||
:notifications_time_format_help:
|
||||
|
||||
_(Store notifications to flash)_:
|
||||
: <select name="path" class="a">
|
||||
<?=mk_option($notify['path'], "/tmp/notifications", _("No"))?>
|
||||
<?=mk_option($notify['path'], "/boot/config/plugins/dynamix/notifications", _("Yes"))?>
|
||||
</select>
|
||||
|
||||
:notifications_store_flash_help:
|
||||
|
||||
_(System notifications)_:
|
||||
: <select name="system" class="a" onchange="prepareSystem(this.selectedIndex)">
|
||||
<?=mk_option($notify['system'], "", _("Disabled"))?>
|
||||
<?=mk_option($notify['system'], "*/1 * * * *", _("Enabled"))?>
|
||||
</select>
|
||||
|
||||
:notifications_system_help:
|
||||
|
||||
_(Unraid OS update notification)_:
|
||||
: <select name="unraidos" class="a" onchange="prepareUnraid(this.value)">
|
||||
<?=mk_option($notify['unraidos'], "", _("Never check"))?>
|
||||
<?=mk_option($notify['unraidos'], "11 */6 * * *", _("Check four times a day"))?>
|
||||
<?=mk_option($notify['unraidos'], "11 0,12 * * *", _("Check twice a day"))?>
|
||||
<?=mk_option($notify['unraidos'], "11 0 * * *", _("Check once a day"))?>
|
||||
<?=mk_option($notify['unraidos'], "11 0 * * 1", _("Check once a week"))?>
|
||||
<?=mk_option($notify['unraidos'], "11 0 1 * *", _("Check once a month"))?>
|
||||
</select>
|
||||
|
||||
:notifications_os_update_help:
|
||||
|
||||
_(Plugins update notification)_:
|
||||
: <select name="version" class="a" onchange="preparePlugin(this.value)">
|
||||
<?=mk_option($notify['version'], "", _("Never check"))?>
|
||||
<?=mk_option($notify['version'], "10 */6 * * *", _("Check four times a day"))?>
|
||||
<?=mk_option($notify['version'], "10 0,12 * * *", _("Check twice a day"))?>
|
||||
<?=mk_option($notify['version'], "10 0 * * *", _("Check once a day"))?>
|
||||
<?=mk_option($notify['version'], "10 0 * * 1", _("Check once a week"))?>
|
||||
<?=mk_option($notify['version'], "10 0 1 * *", _("Check once a month"))?>
|
||||
</select>
|
||||
|
||||
:notifications_plugins_update_help:
|
||||
|
||||
_(Docker update notification)_:
|
||||
: <select name="docker_update" class="a" onchange="prepareDocker(this.value)">
|
||||
<?=mk_option($notify['docker_update'], "", _("Never check"))?>
|
||||
<?=mk_option($notify['docker_update'], "10 */6 * * *", _("Check four times a day"))?>
|
||||
<?=mk_option($notify['docker_update'], "10 0,12 * * *", _("Check twice a day"))?>
|
||||
<?=mk_option($notify['docker_update'], "10 0 * * *", _("Check once a day"))?>
|
||||
<?=mk_option($notify['docker_update'], "10 0 * * 1", _("Check once a week"))?>
|
||||
<?=mk_option($notify['docker_update'], "10 0 1 * *", _("Check once a month"))?>
|
||||
</select>
|
||||
|
||||
:notifications_docker_update_help:
|
||||
|
||||
_(Language update notification)_:
|
||||
: <select name="language_update" class="a" onchange="prepareLanguage(this.value)">
|
||||
<?=mk_option($notify['language_update'], "", _("Never check"))?>
|
||||
<?=mk_option($notify['language_update'], "10 */6 * * *", _("Check four times a day"))?>
|
||||
<?=mk_option($notify['language_update'], "10 0,12 * * *", _("Check twice a day"))?>
|
||||
<?=mk_option($notify['language_update'], "10 0 * * *", _("Check once a day"))?>
|
||||
<?=mk_option($notify['language_update'], "10 0 * * 1", _("Check once a week"))?>
|
||||
<?=mk_option($notify['language_update'], "10 0 1 * *", _("Check once a month"))?>
|
||||
</select>
|
||||
|
||||
_(Array status notification)_:
|
||||
: <select name="status" class="a" onchange="prepareReport(this.value)">
|
||||
<?=mk_option($notify['status'], "", _("Never send"))?>
|
||||
<?=mk_option($notify['status'], "20 * * * *", _("Send every hour"))?>
|
||||
<?=mk_option($notify['status'], "20 */2 * * *", _("Send every two hours"))?>
|
||||
<?=mk_option($notify['status'], "20 */6 * * *", _("Send four times a day"))?>
|
||||
<?=mk_option($notify['status'], "20 */8 * * *", _("Send three times a day"))?>
|
||||
<?=mk_option($notify['status'], "20 0,12 * * *", _("Send twice a day"))?>
|
||||
<?=mk_option($notify['status'], "20 0 * * *", _("Send once a day"))?>
|
||||
<?=mk_option($notify['status'], "20 0 * * 1", _("Send once a week"))?>
|
||||
<?=mk_option($notify['status'], "20 0 1 * *", _("Send once a month"))?>
|
||||
</select>
|
||||
|
||||
:notifications_array_status_help:
|
||||
|
||||
<span id="unraidTitle" class="unraid" style="display:none"> </span>
|
||||
: <span class="unraid" style="display:none"><span class="a">_(Unraid OS update)_</span>
|
||||
<input type="checkbox" name="unraid1"<?=($notify['unraid'] & 1)==1 ? ' checked' : ''?>>_(Browser)_
|
||||
<input type="checkbox" name="unraid2"<?=($notify['unraid'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
||||
<input type="checkbox" name="unraid3"<?=($notify['unraid'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ </span>
|
||||
|
||||
<span id="pluginTitle" class="plugin" style="display:none"> </span>
|
||||
: <span class="plugin" style="display:none"><span class="a">_(Plugins update)_</span>
|
||||
<input type="checkbox" name="plugin1"<?=($notify['plugin'] & 1)==1 ? ' checked' : ''?>>_(Browser)_
|
||||
<input type="checkbox" name="plugin2"<?=($notify['plugin'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
||||
<input type="checkbox" name="plugin3"<?=($notify['plugin'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ </span>
|
||||
|
||||
<span id="dockerTitle" class="docker" style="display:none"> </span>
|
||||
: <span class="docker" style="display:none"><span class="a">_(Docker update)_</span>
|
||||
<input type="checkbox" name="docker_notify1"<?=($notify['docker_notify'] & 1)==1 ? ' checked' : ''?>>_(Browser)_
|
||||
<input type="checkbox" name="docker_notify2"<?=($notify['docker_notify'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
||||
<input type="checkbox" name="docker_notify3"<?=($notify['docker_notify'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ </span>
|
||||
|
||||
<span id="languageTitle" class="language" style="display:none"> </span>
|
||||
: <span class="language" style="display:none"><span class="a">_(Language update)_</span>
|
||||
<input type="checkbox" name="language_notify1"<?=($notify['language_notify'] & 1)==1 ? ' checked' : ''?>>_(Browser)_
|
||||
<input type="checkbox" name="language_notify2"<?=($notify['language_notify'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
||||
<input type="checkbox" name="language_notify3"<?=($notify['language_notify'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ </span>
|
||||
|
||||
<span id="reportTitle" class="report" style="display:none"> </span>
|
||||
: <span class="report" style="display:none"><span class="a">_(Array status)_</span>
|
||||
<input type="checkbox" name="report1"<?=($notify['report'] & 1)==1 ? ' checked' : ''?>>_(Browser)_
|
||||
<input type="checkbox" name="report2"<?=($notify['report'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
||||
<input type="checkbox" name="report3"<?=($notify['report'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ </span>
|
||||
|
||||
:notifications_agent_selection_help:
|
||||
|
||||
_(Notification entity)_:
|
||||
: <span class="a">_(Notices)_</span>
|
||||
<input type="checkbox" class="checkbox" name="normal1"<?=($notify['normal'] & 1)==1 ? " checked $disabled" : $disabled?>>_(Browser)_
|
||||
<input type="checkbox" class="checkbox" name="normal2"<?=($notify['normal'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_
|
||||
<input type="checkbox" class="checkbox" name="normal3"<?=($notify['normal'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_
|
||||
|
||||
|
||||
: <span class="a">_(Warnings)_</span>
|
||||
<input type="checkbox" class="checkbox" name="warning1"<?=($notify['warning'] & 1)==1 ? " checked $disabled" : $disabled?>>_(Browser)_
|
||||
<input type="checkbox" class="checkbox" name="warning2"<?=($notify['warning'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_
|
||||
<input type="checkbox" class="checkbox" name="warning3"<?=($notify['warning'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_
|
||||
|
||||
|
||||
: <span class="a">_(Alerts)_</span>
|
||||
<input type="checkbox" class="checkbox" name="alert1"<?=($notify['alert'] & 1)==1 ? " checked $disabled" : $disabled?>>_(Browser)_
|
||||
<input type="checkbox" class="checkbox" name="alert2"<?=($notify['alert'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_
|
||||
<input type="checkbox" class="checkbox" name="alert3"<?=($notify['alert'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_
|
||||
|
||||
:notifications_classification_help:
|
||||
|
||||
<input type="submit" name="#default" value="_(Default)_">
|
||||
: <input type="submit" name="#apply" value="_(Apply)_" disabled><input type="button" value="_(Done)_" onclick="done()">
|
||||
</form>
|
||||
@@ -0,0 +1,28 @@
|
||||
Index: /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/Notifications.page
|
||||
===================================================================
|
||||
--- /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/Notifications.page
|
||||
+++ /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/Notifications.page
|
||||
@@ -135,23 +135,7 @@
|
||||
|
||||
:notifications_auto_close_help:
|
||||
|
||||
-_(Date format)_:
|
||||
-: <select name="date" class="a">
|
||||
- <?=mk_option($notify['date'], "d-m-Y", _("DD-MM-YYYY"))?>
|
||||
- <?=mk_option($notify['date'], "m-d-Y", _("MM-DD-YYYY"))?>
|
||||
- <?=mk_option($notify['date'], "Y-m-d", _("YYYY-MM-DD"))?>
|
||||
- </select>
|
||||
|
||||
-:notifications_date_format_help:
|
||||
-
|
||||
-_(Time format)_:
|
||||
-: <select name="time" class="a">
|
||||
- <?=mk_option($notify['time'], "h:i A", _("12 hours"))?>
|
||||
- <?=mk_option($notify['time'], "H:i", _("24 hours"))?>
|
||||
- </select>
|
||||
-
|
||||
-:notifications_time_format_help:
|
||||
-
|
||||
_(Store notifications to flash)_:
|
||||
: <select name="path" class="a">
|
||||
<?=mk_option($notify['path'], "/tmp/notifications", _("No"))?>
|
||||
@@ -5,7 +5,7 @@ import { FileModification, PatchResult, ShouldApplyWithReason } from '@app/unrai
|
||||
|
||||
export default class SSOFileModification extends FileModification {
|
||||
id: string = 'sso';
|
||||
private loginFilePath: string = '/usr/local/emhttp/plugins/dynamix/include/.login.php';
|
||||
private readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/include/.login.php';
|
||||
|
||||
constructor(logger: Logger) {
|
||||
super(logger);
|
||||
@@ -46,7 +46,7 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
|
||||
'<?php include "$docroot/plugins/dynamix.my.servers/include/sso-login.php"; ?>';
|
||||
|
||||
// Read the file content
|
||||
const originalContent = await readFile(this.loginFilePath, 'utf-8');
|
||||
const originalContent = await readFile(this.filePath, 'utf-8');
|
||||
|
||||
// Create modified content
|
||||
let newContent = originalContent;
|
||||
@@ -64,10 +64,10 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
|
||||
newContent = newContent.replace(/<\/form>/i, `</form>\n${tagToInject}`);
|
||||
|
||||
// Create and return the patch
|
||||
const patch = createPatch(this.loginFilePath, originalContent, newContent, 'original', 'modified');
|
||||
const patch = createPatch(this.filePath, originalContent, newContent, 'original', 'modified');
|
||||
|
||||
return {
|
||||
targetFile: this.loginFilePath,
|
||||
targetFile: this.filePath,
|
||||
patch
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
|
||||
|
||||
import { createPatch } from 'diff';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
|
||||
|
||||
import { FileModification, PatchResult, ShouldApplyWithReason } from '@app/unraid-api/unraid-file-modifier/file-modification';
|
||||
import {
|
||||
FileModification,
|
||||
PatchResult,
|
||||
ShouldApplyWithReason,
|
||||
} from '@app/unraid-api/unraid-file-modifier/file-modification';
|
||||
import { UnraidFileModificationService } from '@app/unraid-api/unraid-file-modifier/unraid-file-modifier.service';
|
||||
|
||||
|
||||
|
||||
|
||||
const FIXTURE_PATH = join(__dirname, 'modifications', '__fixtures__', 'text-patch-file.txt');
|
||||
const ORIGINAL_CONTENT = 'original';
|
||||
|
||||
class TestFileModification extends FileModification {
|
||||
id = 'test';
|
||||
@@ -25,27 +25,17 @@ class TestFileModification extends FileModification {
|
||||
|
||||
protected async generatePatch(): Promise<PatchResult> {
|
||||
return {
|
||||
targetFile: join(__dirname, '__fixtures__/text-patch-file.txt'),
|
||||
patch: createPatch(
|
||||
'__fixtures__/text-patch-file.txt',
|
||||
'original',
|
||||
'modified',
|
||||
'original',
|
||||
'modified'
|
||||
),
|
||||
targetFile: FIXTURE_PATH,
|
||||
patch: createPatch('text-patch-file.txt', ORIGINAL_CONTENT, 'modified'),
|
||||
};
|
||||
}
|
||||
|
||||
apply = vi.fn();
|
||||
|
||||
rollback = vi.fn();
|
||||
|
||||
async shouldApply(): Promise<ShouldApplyWithReason> {
|
||||
return { shouldApply: true, reason: 'Always Apply this mod' };
|
||||
}
|
||||
}
|
||||
|
||||
describe('FileModificationService', () => {
|
||||
describe.sequential('FileModificationService', () => {
|
||||
let mockLogger: {
|
||||
log: ReturnType<typeof vi.fn>;
|
||||
error: ReturnType<typeof vi.fn>;
|
||||
@@ -57,6 +47,9 @@ describe('FileModificationService', () => {
|
||||
let logger: Logger;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create/reset the fixture file before each test
|
||||
await fs.writeFile(FIXTURE_PATH, ORIGINAL_CONTENT);
|
||||
|
||||
mockLogger = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
@@ -64,7 +57,7 @@ describe('FileModificationService', () => {
|
||||
debug: vi.fn(),
|
||||
verbose: vi.fn(),
|
||||
};
|
||||
// Mock the Logger constructor
|
||||
|
||||
vi.spyOn(Logger.prototype, 'log').mockImplementation(mockLogger.log);
|
||||
vi.spyOn(Logger.prototype, 'error').mockImplementation(mockLogger.error);
|
||||
vi.spyOn(Logger.prototype, 'warn').mockImplementation(mockLogger.warn);
|
||||
@@ -100,10 +93,18 @@ describe('FileModificationService', () => {
|
||||
|
||||
it('should rollback all mods', async () => {
|
||||
await service.loadModifications();
|
||||
const initialContent = await fs.readFile(FIXTURE_PATH, 'utf-8');
|
||||
expect(initialContent).toBe(ORIGINAL_CONTENT);
|
||||
|
||||
const mod = new TestFileModification(logger);
|
||||
|
||||
await service.applyModification(mod);
|
||||
const modifiedContent = await fs.readFile(FIXTURE_PATH, 'utf-8');
|
||||
expect(modifiedContent).toBe('modified');
|
||||
|
||||
await service.rollbackAll();
|
||||
const rolledBackContent = await fs.readFile(FIXTURE_PATH, 'utf-8');
|
||||
expect(rolledBackContent).toBe(ORIGINAL_CONTENT);
|
||||
|
||||
expect(mockLogger.error).not.toHaveBeenCalled();
|
||||
expect(mockLogger.log.mock.calls).toEqual([
|
||||
@@ -115,47 +116,37 @@ describe('FileModificationService', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle errors during rollback', async () => {
|
||||
// Mock the logger to track error calls
|
||||
it('should handle errors during dual application', async () => {
|
||||
await service.loadModifications();
|
||||
const initialContent = await fs.readFile(FIXTURE_PATH, 'utf-8');
|
||||
expect(initialContent).toBe(ORIGINAL_CONTENT);
|
||||
|
||||
const mod = new TestFileModification(logger);
|
||||
|
||||
await service.applyModification(mod);
|
||||
console.log(service.appliedModifications);
|
||||
|
||||
expect(mockLogger.log.mock.calls).toEqual([
|
||||
['RootTestModule dependencies initialized'],
|
||||
['Applying modification: test - Always Apply this mod'],
|
||||
['Modification applied successfully: test'],
|
||||
]);
|
||||
|
||||
service.appliedModifications[0].appliedPatch = null;
|
||||
console.log(service.appliedModifications);
|
||||
// Now break the appliedModifications array so that the rollbackAll method fails
|
||||
await service.rollbackAll();
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Failed to roll back modification')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle concurrent modifications', async () => {
|
||||
vi.mock('fs/promises', () => ({
|
||||
readFile: vi.fn().mockResolvedValue('modified'),
|
||||
writeFile: vi.fn(),
|
||||
}));
|
||||
const mods = [new TestFileModification(logger), new TestFileModification(logger)];
|
||||
|
||||
await Promise.all(mods.map((mod) => service.applyModification(mod)));
|
||||
await service.rollbackAll();
|
||||
|
||||
expect(mockLogger.error).not.toHaveBeenCalled();
|
||||
expect(mockLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Successfully rolled back modification')
|
||||
// Now apply again and ensure the contents don't change
|
||||
await service.applyModification(mod);
|
||||
const errorMessage = mockLogger.error.mock.calls[0][0];
|
||||
expect(errorMessage).toContain(
|
||||
'Failed to apply patch to /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/text-patch-file.txt'
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await service.rollbackAll();
|
||||
// Clean up the fixture file
|
||||
try {
|
||||
await fs.unlink(FIXTURE_PATH);
|
||||
} catch (error) {
|
||||
// Ignore errors if file doesn't exist
|
||||
}
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user