378 строки
12 KiB
PHP
378 строки
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
namespace OCA\Mail\Tests\Unit\Service;
|
|
|
|
use ChristophWurst\Nextcloud\Testing\TestCase;
|
|
use OCA\Mail\Account;
|
|
use OCA\Mail\AddressList;
|
|
use OCA\Mail\Contracts\IMailManager;
|
|
use OCA\Mail\Db\MailAccount;
|
|
use OCA\Mail\Db\Mailbox;
|
|
use OCA\Mail\Db\Message;
|
|
use OCA\Mail\Exception\ServiceException;
|
|
use OCA\Mail\IMAP\IMAPClientFactory;
|
|
use OCA\Mail\Model\IMAPMessage;
|
|
use OCA\Mail\Service\AiIntegrations\AiIntegrationsService;
|
|
use OCA\Mail\Service\AiIntegrations\Cache;
|
|
use OCP\AppFramework\QueryException;
|
|
use OCP\IConfig;
|
|
use OCP\TextProcessing\FreePromptTaskType;
|
|
use OCP\TextProcessing\IManager;
|
|
use OCP\TextProcessing\SummaryTaskType;
|
|
use OCP\TextProcessing\TopicsTaskType;
|
|
use PHPUnit\Framework\Exception;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\MockObject\UnknownTypeException;
|
|
use Psr\Container\ContainerInterface;
|
|
use function interface_exists;
|
|
|
|
class AiIntegrationsServiceTest extends TestCase {
|
|
|
|
/** @var ContainerInterface|MockObject */
|
|
private $container;
|
|
|
|
/** @var IManager|MockObject */
|
|
private $manager;
|
|
|
|
/** @var AiIntegrationsService */
|
|
private $aiIntegrationsService;
|
|
|
|
/** @var Cache */
|
|
private $cache;
|
|
|
|
/** @var IMAPClientFactory|MockObject */
|
|
private $clientFactory;
|
|
|
|
/** @var IMailManager|MockObject */
|
|
private $mailManager;
|
|
|
|
/** @var IConfig|MockObject */
|
|
private $config;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
$this->container = $this->createMock(ContainerInterface::class);
|
|
try {
|
|
$this->manager = $this->createMock(IManager::class);
|
|
} catch (UnknownTypeException $e) {
|
|
$this->manager = null;
|
|
}
|
|
|
|
$this->cache = $this->createMock(Cache::class);
|
|
$this->clientFactory = $this->createMock(IMAPClientFactory::class);
|
|
$this->mailManager = $this->createMock(IMailManager::class);
|
|
$this->config = $this->createMock(IConfig::class);
|
|
$this->aiIntegrationsService = new AiIntegrationsService(
|
|
$this->container,
|
|
$this->cache,
|
|
$this->clientFactory,
|
|
$this->mailManager,
|
|
$this->config,
|
|
);
|
|
}
|
|
|
|
public function testSummarizeThreadNoBackend() {
|
|
$account = new Account(new MailAccount());
|
|
$mailbox = new Mailbox();
|
|
if ($this->manager !== null) {
|
|
$this->container->method('get')->willReturn($this->manager);
|
|
$this->manager
|
|
->method('getAvailableTaskTypes')
|
|
->willReturn([]);
|
|
$this->expectException(ServiceException::class);
|
|
$this->expectExceptionMessage('No language model available for summary');
|
|
$this->aiIntegrationsService->summarizeThread($account, '', [], '');
|
|
}
|
|
$this->container->method('get')->willThrowException(new ServiceException());
|
|
$this->expectException(ServiceException::class);
|
|
$this->expectExceptionMessage('Text processing is not available in your current Nextcloud version');
|
|
$this->aiIntegrationsService->summarizeThread($account, '', [], '');
|
|
|
|
}
|
|
|
|
|
|
public function testSmartReplyNoBackend() {
|
|
$account = new Account(new MailAccount());
|
|
$mailbox = new Mailbox();
|
|
$message = new Message();
|
|
if ($this->manager !== null) {
|
|
$this->container->method('get')->willReturn($this->manager);
|
|
$this->manager
|
|
->method('getAvailableTaskTypes')
|
|
->willReturn([]);
|
|
$this->expectException(ServiceException::class);
|
|
$this->expectExceptionMessage('No language model available for smart replies');
|
|
$this->aiIntegrationsService->getSmartReply($account, $mailbox, $message, '');
|
|
}
|
|
$this->container->method('get')->willThrowException(new ServiceException());
|
|
$this->expectException(ServiceException::class);
|
|
$this->expectExceptionMessage('Text processing is not available in your current Nextcloud version');
|
|
$this->aiIntegrationsService->getSmartReply($account, $mailbox, $message, '');
|
|
|
|
}
|
|
|
|
public function testGeneratedMessage() {
|
|
$account = new Account(new MailAccount());
|
|
$mailbox = new Mailbox();
|
|
$message = new Message();
|
|
$message->setUid(1);
|
|
$imapMessage = $this->createMock(IMAPMessage::class);
|
|
$addessList = $this->createMock(AddressList::class);
|
|
$addessList->method('first')->willreturn('normal@email.com');
|
|
$this->mailManager->method('getImapMessage')->willReturn($imapMessage);
|
|
if ($this->manager !== null) {
|
|
$this->container->method('get')->willReturn($this->manager);
|
|
$this->manager
|
|
->method('getAvailableTaskTypes')
|
|
->willReturn([FreePromptTaskType::class]);
|
|
$imapMessage->method('isOneClickUnsubscribe')->willReturn(true);
|
|
$replies = $this->aiIntegrationsService->getSmartReply($account, $mailbox, $message, '');
|
|
$this->assertEquals($replies, []);
|
|
$imapMessage->method('isOneClickUnsubscribe')->willReturn(false);
|
|
$imapMessage->method('getUnsubscribeUrl')->willReturn('iAmAnUnsubscribeUrl');
|
|
$replies = $this->aiIntegrationsService->getSmartReply($account, $mailbox, $message, '');
|
|
$this->assertEquals($replies, []);
|
|
$imapMessage->method('isOneClickUnsubscribe')->willReturn(false);
|
|
$imapMessage->method('getUnsubscribeUrl')->willReturn(null);
|
|
$addessList->method('first')->willreturn('noreply@test.com');
|
|
$replies = $this->aiIntegrationsService->getSmartReply($account, $mailbox, $message, '');
|
|
$this->assertEquals($replies, []);
|
|
} else {
|
|
$this->container->method('get')->willThrowException(new ServiceException());
|
|
$this->expectException(ServiceException::class);
|
|
$this->expectExceptionMessage('Text processing is not available in your current Nextcloud version');
|
|
$this->aiIntegrationsService->getSmartReply($account, $mailbox, $message, '');
|
|
}
|
|
}
|
|
|
|
public function testLlmAvailable() {
|
|
if ($this->manager !== null) {
|
|
$this->container->method('get')->willReturn($this->manager);
|
|
$this->manager
|
|
->method('getAvailableTaskTypes')
|
|
->willReturn([SummaryTaskType::class, TopicsTaskType::class, FreePromptTaskType::class]);
|
|
$isAvailable = $this->aiIntegrationsService->isLlmAvailable(SummaryTaskType::class);
|
|
$this->assertTrue($isAvailable);
|
|
} else {
|
|
$this->container->method('get')->willThrowException(new Exception());
|
|
$isAvailable = $this->aiIntegrationsService->isLlmAvailable(SummaryTaskType::class);
|
|
$this->assertFalse($isAvailable);
|
|
}
|
|
|
|
}
|
|
|
|
public function testLlmUnavailable() {
|
|
if ($this->manager !== null) {
|
|
$this->container->method('get')->willReturn($this->manager);
|
|
$this->manager
|
|
->method('getAvailableTaskTypes')
|
|
->willReturn([TopicsTaskType::class, FreePromptTaskType::class]);
|
|
$isAvailable = $this->aiIntegrationsService->isLlmAvailable(SummaryTaskType::class);
|
|
$this->assertFalse($isAvailable);
|
|
} else {
|
|
$this->container->method('get')->willThrowException(new Exception());
|
|
$isAvailable = $this->aiIntegrationsService->isLlmAvailable(SummaryTaskType::class);
|
|
$this->assertFalse($isAvailable);
|
|
}
|
|
|
|
}
|
|
|
|
public function isLlmProcessingEnabledDataProvider(): array {
|
|
return [
|
|
['no', false],
|
|
['yes', true],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider isLlmProcessingEnabledDataProvider
|
|
*/
|
|
public function testIsLlmProcessingEnabled(string $appConfigValue, bool $expected) {
|
|
$this->config->expects(self::once())
|
|
->method('getAppValue')
|
|
->with('mail', 'llm_processing', 'no')
|
|
->willReturn($appConfigValue);
|
|
|
|
$this->assertEquals($expected, $this->aiIntegrationsService->isLlmProcessingEnabled());
|
|
}
|
|
|
|
public function testCached() {
|
|
$account = new Account(new MailAccount());
|
|
$mailbox = new Mailbox();
|
|
|
|
$message1 = new Message();
|
|
$message1->setMessageId('300');
|
|
$message1->setPreviewText('message1');
|
|
$message1->setThreadRootId('some-thread-root-id-1');
|
|
|
|
$message2 = new Message();
|
|
$message2->setMessageId('301');
|
|
$message2->setPreviewText('message2');
|
|
$message2->setThreadRootId('some-thread-root-id-1');
|
|
|
|
$message3 = new Message();
|
|
$message3->setMessageId('302');
|
|
$message3->setPreviewText('message3');
|
|
$message3->setThreadRootId('some-thread-root-id-1');
|
|
|
|
$messages = [ $message1,$message2,$message3];
|
|
if ($this->manager !== null) {
|
|
$this->container->method('get')->willReturn($this->manager);
|
|
$this->manager
|
|
->method('getAvailableTaskTypes')
|
|
->willReturn([SummaryTaskType::class]);
|
|
|
|
$messageIds = [ $message1->getMessageId(),$message2->getMessageId(),$message3->getMessageId()];
|
|
$key = $this->cache->buildUrlKey($messageIds);
|
|
$this->cache
|
|
->method('getValue')
|
|
->with($key)
|
|
->willReturn('this is a cached summary');
|
|
|
|
$this->assertEquals('this is a cached summary', $this->aiIntegrationsService->summarizeThread($account, 'some-thread-root-id-1', $messages, 'admin'));
|
|
} else {
|
|
$this->container->method('get')->willThrowException(new ServiceException());
|
|
$this->expectException(ServiceException::class);
|
|
$this->expectExceptionMessage('Text processing is not available in your current Nextcloud version');
|
|
$this->aiIntegrationsService->summarizeThread($account, 'some-thread-root-id-1', $messages, 'admin');
|
|
}
|
|
}
|
|
|
|
public function testGenerateEventDataLlmUnavailable(): void {
|
|
if (!interface_exists(IManager::class)) {
|
|
$this->markTestSkipped('Text processing APIs require Nextcloud 27+');
|
|
}
|
|
|
|
$account = $this->createMock(Account::class);
|
|
$message1 = new Message();
|
|
$message2 = new Message();
|
|
$this->container->expects(self::once())
|
|
->method('get')
|
|
->willThrowException($this->createMock(QueryException::class));
|
|
|
|
$result = $this->aiIntegrationsService->generateEventData(
|
|
$account,
|
|
'thread1',
|
|
[$message1, $message2],
|
|
'user123',
|
|
);
|
|
|
|
self::assertNull($result);
|
|
}
|
|
|
|
public function testGenerateEventDataFreePromptUnavailable(): void {
|
|
if (!interface_exists(IManager::class)) {
|
|
$this->markTestSkipped('Text processing APIs require Nextcloud 27+');
|
|
}
|
|
|
|
$account = $this->createMock(Account::class);
|
|
$message1 = new Message();
|
|
$message2 = new Message();
|
|
$manager = $this->createMock(IManager::class);
|
|
$this->container->expects(self::once())
|
|
->method('get')
|
|
->willReturn($manager);
|
|
$manager->expects(self::once())
|
|
->method('getAvailableTaskTypes')
|
|
->willReturn([]);
|
|
|
|
$result = $this->aiIntegrationsService->generateEventData(
|
|
$account,
|
|
'thread1',
|
|
[$message1, $message2],
|
|
'user123',
|
|
);
|
|
|
|
self::assertNull($result);
|
|
}
|
|
|
|
public function testGenerateEventDataInvalidJson(): void {
|
|
if (!interface_exists(IManager::class)) {
|
|
$this->markTestSkipped('Text processing APIs require Nextcloud 27+');
|
|
}
|
|
|
|
$account = $this->createMock(Account::class);
|
|
$message1 = new Message();
|
|
$message1->setUid(1);
|
|
$message1->setMailboxId(123);
|
|
$message2 = new Message();
|
|
$message2->setUid(2);
|
|
$message2->setMailboxId(456);
|
|
$manager = $this->createMock(IManager::class);
|
|
$this->container->expects(self::once())
|
|
->method('get')
|
|
->willReturn($manager);
|
|
$manager->expects(self::once())
|
|
->method('getAvailableTaskTypes')
|
|
->willReturn([FreePromptTaskType::class]);
|
|
$imapMessage = $this->createMock(IMAPMessage::class);
|
|
$this->mailManager->expects(self::exactly(2))
|
|
->method('getImapMessage')
|
|
->willReturn($imapMessage);
|
|
$imapMessage->expects(self::exactly(2))
|
|
->method('getPlainBody')
|
|
->willReturn('plain');
|
|
$manager->expects(self::once())
|
|
->method('runTask')
|
|
->willReturn('Jason');
|
|
|
|
$result = $this->aiIntegrationsService->generateEventData(
|
|
$account,
|
|
'thread1',
|
|
[$message1, $message2],
|
|
'user123',
|
|
);
|
|
|
|
self::assertNull($result);
|
|
}
|
|
|
|
public function testGenerateEventData(): void {
|
|
if (!interface_exists(IManager::class)) {
|
|
$this->markTestSkipped('Text processing APIs require Nextcloud 27+');
|
|
}
|
|
|
|
$account = $this->createMock(Account::class);
|
|
$message1 = new Message();
|
|
$message1->setUid(1);
|
|
$message1->setMailboxId(123);
|
|
$message2 = new Message();
|
|
$message2->setUid(2);
|
|
$message2->setMailboxId(456);
|
|
$manager = $this->createMock(IManager::class);
|
|
$this->container->expects(self::once())
|
|
->method('get')
|
|
->willReturn($manager);
|
|
$manager->expects(self::once())
|
|
->method('getAvailableTaskTypes')
|
|
->willReturn([FreePromptTaskType::class]);
|
|
$imapMessage = $this->createMock(IMAPMessage::class);
|
|
$this->mailManager->expects(self::exactly(2))
|
|
->method('getImapMessage')
|
|
->willReturn($imapMessage);
|
|
$imapMessage->expects(self::exactly(2))
|
|
->method('getPlainBody')
|
|
->willReturn('plain');
|
|
$manager->expects(self::once())
|
|
->method('runTask')
|
|
->willReturn('{"title":"Meeting", "agenda":"* Q&A"}');
|
|
|
|
$result = $this->aiIntegrationsService->generateEventData(
|
|
$account,
|
|
'thread1',
|
|
[$message1, $message2],
|
|
'user123',
|
|
);
|
|
|
|
self::assertNotNull($result);
|
|
self::assertSame('Meeting', $result->getSummary());
|
|
self::assertSame('* Q&A', $result->getDescription());
|
|
}
|
|
|
|
}
|