opsdash-app/opsdash/tests/php/Service/NotesServiceTest.php
blade34242 b723d9f3d0 security: store notes raw, escape at render time
htmlspecialchars was applied on write which caused literal entities to
appear in the editor and could mask double-escaping in future render
paths. Content is now stored as-is; the Vue component must use text
interpolation (not v-html) to display it safely.
2026-05-06 10:22:16 +07:00

114 lines
4.7 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\Opsdash\Tests\Service;
use OCA\Opsdash\Service\CalendarAccessService;
use OCA\Opsdash\Service\NotesService;
use OCP\IConfig;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class NotesServiceTest extends TestCase {
public function testGetNotesBuildsHistoryAndFiltersEmpty(): void {
$config = $this->createMock(IConfig::class);
$calendar = $this->createMock(CalendarAccessService::class);
$logger = $this->createMock(LoggerInterface::class);
$dates = [
1 => new \DateTimeImmutable('2025-03-10'),
0 => new \DateTimeImmutable('2025-03-03'),
-1 => new \DateTimeImmutable('2025-02-24'),
-2 => new \DateTimeImmutable('2025-02-17'),
-3 => new \DateTimeImmutable('2025-02-10'),
-4 => new \DateTimeImmutable('2025-02-03'),
];
$calendar->method('resolveUserTimezone')->willReturn(new \DateTimeZone('UTC'));
$calendar->method('resolveUserWeekStart')->willReturn(1);
$calendar->method('rangeBounds')->willReturnCallback(
fn (string $range, int $offset) => [$dates[$offset] ?? $dates[0], $dates[$offset] ?? $dates[0]]
);
$calendar->method('notesKey')->willReturnCallback(
fn (string $range, \DateTimeInterface $from) => sprintf('%s-%s', $range, $from->format('Y-m-d'))
);
$config->method('getUserValue')->willReturnCallback(
fn (string $uid, string $app, string $key) => match ($key) {
'week-2025-03-10' => 'current note',
'week-2025-03-03' => 'previous note',
'week-2025-02-24' => 'history 1',
'week-2025-02-17' => '',
'week-2025-02-10' => 'history 3',
'week-2025-02-03' => '',
default => '',
}
);
$service = new NotesService($config, $calendar, $logger);
$result = $service->getNotes('admin', 'week', 1);
$this->assertSame('week', $result['period']['type']);
$this->assertSame('2025-03-10', $result['period']['current_from']);
$this->assertSame('2025-03-03', $result['period']['previous_from']);
$this->assertSame('current note', $result['notes']['current']);
$this->assertSame('previous note', $result['notes']['previous']);
$this->assertCount(3, $result['notes']['history']);
$this->assertSame(1, $result['notes']['history'][0]['offset']);
$this->assertSame('previous note', $result['notes']['history'][0]['content']);
$this->assertSame(2, $result['notes']['history'][1]['offset']);
$this->assertSame('history 1', $result['notes']['history'][1]['content']);
$this->assertSame(4, $result['notes']['history'][2]['offset']);
$this->assertSame('history 3', $result['notes']['history'][2]['content']);
}
public function testSaveNotesTruncatesAndStoresRaw(): void {
$config = $this->createMock(IConfig::class);
$calendar = $this->createMock(CalendarAccessService::class);
$logger = $this->createMock(LoggerInterface::class);
$calendar->method('resolveUserTimezone')->willReturn(new \DateTimeZone('UTC'));
$calendar->method('resolveUserWeekStart')->willReturn(1);
$calendar->method('rangeBounds')->willReturn([new \DateTimeImmutable('2025-03-10'), new \DateTimeImmutable('2025-03-10')]);
$calendar->method('notesKey')->willReturn('week-2025-03-10');
$config->expects($this->once())
->method('setUserValue')
->with(
'admin',
'opsdash',
'week-2025-03-10',
$this->callback(function (string $value): bool {
// Content must be stored raw (no entity encoding) and truncated to 32768 bytes.
return str_contains($value, '<script>')
&& !str_contains($value, '&lt;')
&& strlen($value) === 32768;
})
);
$service = new NotesService($config, $calendar, $logger);
$payload = '<script>' . str_repeat('a', 40000);
$this->assertTrue($service->saveNotes('admin', 'week', 0, $payload));
}
public function testSaveNotesReturnsFalseOnFailure(): void {
$config = $this->createMock(IConfig::class);
$calendar = $this->createMock(CalendarAccessService::class);
$logger = $this->createMock(LoggerInterface::class);
$calendar->method('resolveUserTimezone')->willReturn(new \DateTimeZone('UTC'));
$calendar->method('resolveUserWeekStart')->willReturn(1);
$calendar->method('rangeBounds')->willReturn([new \DateTimeImmutable('2025-03-10'), new \DateTimeImmutable('2025-03-10')]);
$calendar->method('notesKey')->willReturn('week-2025-03-10');
$config->method('setUserValue')->willThrowException(new \RuntimeException('boom'));
$logger->expects($this->once())
->method('error')
->with($this->stringContains('notes save failed'));
$service = new NotesService($config, $calendar, $logger);
$this->assertFalse($service->saveNotes('admin', 'week', 0, 'note'));
}
}