diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 1b9f271d7..09de003ed 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -98,7 +98,7 @@ jobs: composer i - name: Perform PhpUnit tests with coverage - if: ${{ matrix.databases == 'sqlite' && matrix.php-versions == '8.0' && matrix.server-versions == 'master' }} + if: ${{ matrix.databases == 'sqlite' && matrix.php-versions == '8.3' && matrix.server-versions == 'master' }} run: | mkdir /tmp/coverage cd /tmp @@ -117,21 +117,21 @@ jobs: anybadge -l coverage -v `cat /tmp/coverage/cov.value.txt` -m "%.2f%%" -f /tmp/coverage/coverage.svg 50=red 70=orange 80=yellow 90=green - name: Perform PhpUnit tests - if: ${{ !(matrix.databases == 'sqlite' && matrix.php-versions == '8.0' && matrix.server-versions == 'master') }} + if: ${{ !(matrix.databases == 'sqlite' && matrix.php-versions == '8.3' && matrix.server-versions == 'master') }} run: | ~/html/nextcloud/apps/${{ env.APP_ID }}/vendor/bin/phpunit --configuration ~/html/nextcloud/apps/${{ env.APP_ID }}/tests/phpunit.xml && SUCCESS=yes || SUCCESS=no if [ $SUCCESS = "yes" ]; then echo "TESTS PASSED"; else echo "TESTS FAILED"; exit 1; fi - - name: Upload coverage - if: ${{ github.ref == 'refs/heads/master' && matrix.databases == 'sqlite' && matrix.php-versions == '8.0' && matrix.server-versions == 'master' }} - uses: actions/upload-artifact@v2 - with: - name: coverage - path: /tmp/coverage + #- name: Upload coverage + # if: ${{ github.ref == 'refs/heads/master' && matrix.databases == 'sqlite' && matrix.php-versions == '8.0' && matrix.server-versions == 'master' }} + # uses: actions/upload-artifact@v2 + # with: + # name: coverage + # path: /tmp/coverage - - name: Deploy - if: ${{ github.ref == 'refs/heads/master' && matrix.databases == 'sqlite' && matrix.php-versions == '8.0' && matrix.server-versions == 'master' }} - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: /tmp/coverage + #- name: Deploy + # if: ${{ github.ref == 'refs/heads/master' && matrix.databases == 'sqlite' && matrix.php-versions == '8.0' && matrix.server-versions == 'master' }} + # uses: peaceiris/actions-gh-pages@v3 + # with: + # github_token: ${{ secrets.GITHUB_TOKEN }} + # publish_dir: /tmp/coverage diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index d9b49168d..9c6b00f97 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -18,6 +18,7 @@ use OCA\Cospend\Attribute\SupportFederatedProject; use OCA\Cospend\Attribute\CospendUserPermissions; use OCA\Cospend\Db\BillMapper; +use OCA\Cospend\Db\ProjectMapper; use OCA\Cospend\Exception\CospendBasicException; use OCA\Cospend\ResponseDefinitions; use OCA\Cospend\Service\CospendService; @@ -70,6 +71,7 @@ public function __construct( private IManager $shareManager, private IL10N $trans, private BillMapper $billMapper, + private ProjectMapper $projectMapper, private LocalProjectService $localProjectService, private CospendService $cospendService, private ActivityManager $activityManager, @@ -96,7 +98,9 @@ private static function getResponseFromClientException(ClientException $e): Data * @param string $id * @param string $name * @return DataResponse|DataResponse, array{}> + * @throws DoesNotExistException * @throws Exception + * @throws MultipleObjectsReturnedException * * 200: Project successfully created * 400: Failed to create project @@ -104,13 +108,20 @@ private static function getResponseFromClientException(ClientException $e): Data #[NoAdminRequired] #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT, tags: ['Projects'])] public function createProject(string $id, string $name): DataResponse { - $result = $this->localProjectService->createProject($name, $id, null, $this->userId); - if (isset($result['id'])) { - $projInfo = $this->localProjectService->getProjectInfo($result['id']); + try { + $this->projectMapper->getById($id); + return new DataResponse('project already exists', Http::STATUS_BAD_REQUEST); + } catch (DoesNotExistException $e) { + } + try { + $jsonProject = $this->localProjectService->createProject($name, $id, null, $this->userId); + $projInfo = $this->localProjectService->getProjectInfo($jsonProject['id']); $projInfo['myaccesslevel'] = Application::ACCESS_LEVEL_ADMIN; return new DataResponse($projInfo); - } else { - return new DataResponse($result, Http::STATUS_BAD_REQUEST); + } catch (CospendBasicException $e) { + return new DataResponse($e->data, $e->getCode()); + } catch (Exception $e) { + return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST); } } diff --git a/lib/Controller/OldApiController.php b/lib/Controller/OldApiController.php index 8f8906693..f1cb25d2e 100644 --- a/lib/Controller/OldApiController.php +++ b/lib/Controller/OldApiController.php @@ -17,9 +17,11 @@ use OCA\Cospend\Attribute\CospendPublicAuth; use OCA\Cospend\Attribute\CospendUserPermissions; use OCA\Cospend\Db\BillMapper; +use OCA\Cospend\Db\ProjectMapper; use OCA\Cospend\Exception\CospendBasicException; use OCA\Cospend\Service\LocalProjectService; use OCP\AppFramework\ApiController; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\BruteForceProtection; @@ -46,6 +48,7 @@ public function __construct( IRequest $request, private IL10N $trans, private BillMapper $billMapper, + private ProjectMapper $projectMapper, private LocalProjectService $localProjectService, private ActivityManager $activityManager, public ?string $userId, @@ -98,11 +101,20 @@ public function apiPrivSetProjectInfo(string $projectId, ?string $name = null, ? #[CORS] #[NoCSRFRequired] public function apiPrivCreateProject(string $name, string $id, ?string $contact_email = null): DataResponse { - $result = $this->localProjectService->createProject($name, $id, $contact_email, $this->userId); - if (isset($result['id'])) { - return new DataResponse($result['id']); - } else { - return new DataResponse($result, Http::STATUS_BAD_REQUEST); + try { + $this->projectMapper->getById($id); + return new DataResponse('project already exists', Http::STATUS_BAD_REQUEST); + } catch (DoesNotExistException $e) { + } + try { + $jsonProject = $this->localProjectService->createProject($name, $id, $contact_email, $this->userId); + $projInfo = $this->localProjectService->getProjectInfo($jsonProject['id']); + $projInfo['myaccesslevel'] = Application::ACCESS_LEVEL_ADMIN; + return new DataResponse($projInfo); + } catch (CospendBasicException $e) { + return new DataResponse($e->data, $e->getCode()); + } catch (Exception $e) { + return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST); } } diff --git a/lib/Db/ProjectMapper.php b/lib/Db/ProjectMapper.php index 595c2181a..b33cb1ab4 100644 --- a/lib/Db/ProjectMapper.php +++ b/lib/Db/ProjectMapper.php @@ -13,9 +13,11 @@ namespace OCA\Cospend\Db; use DateTime; +use OCA\Cospend\Exception\CospendBasicException; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\QBMapper; +use OCP\AppFramework\Http; use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -25,16 +27,16 @@ * @extends QBMapper */ class ProjectMapper extends QBMapper { - public const TABLE_NAME = 'cospend_projects'; - public const ARCHIVED_TS_UNSET = -1; public const ARCHIVED_TS_NOW = 0; public function __construct( IDBConnection $db, private IL10N $l10n, + private CategoryMapper $categoryMapper, + private PaymentModeMapper $paymentModeMapper, ) { - parent::__construct($db, self::TABLE_NAME, Project::class); + parent::__construct($db, 'cospend_projects', Project::class); } /** @@ -54,88 +56,61 @@ public function getById(string $projectId): Project { return $this->findEntity($qb); } + /** + * @param string $name + * @param string $id + * @param string|null $contact_email + * @param array $defaultCategories + * @param array $defaultPaymentModes + * @param string $userid + * @param bool $createDefaultCategories + * @param bool $createDefaultPaymentModes + * @return Project + * @throws CospendBasicException + * @throws Exception + */ public function createProject( string $name, string $id, ?string $contact_email, array $defaultCategories, array $defaultPaymentModes, string $userid = '', bool $createDefaultCategories = true, bool $createDefaultPaymentModes = true - ): array { - $qb = $this->db->getQueryBuilder(); - - $qb->select('id') - ->from($this->getTableName(), 'p') - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR)) - ); - $req = $qb->executeQuery(); - - $dbId = null; - while ($row = $req->fetch()) { - $dbId = $row['id']; - break; + ): Project { + // check if id is valid + if (str_contains($id, '/')) { + throw new CospendBasicException('', Http::STATUS_BAD_REQUEST, ['message' => $this->l10n->t('Invalid project id')]); } - $req->closeCursor(); - $qb = $this->db->getQueryBuilder(); - if ($dbId === null) { - // check if id is valid - if (strpos($id, '/') !== false) { - return ['message' => $this->l10n->t('Invalid project id')]; - } - if ($contact_email === null) { - $contact_email = ''; - } - $ts = (new DateTime())->getTimestamp(); - $qb->insert($this->getTableName()) - ->values([ - 'userid' => $qb->createNamedParameter($userid, IQueryBuilder::PARAM_STR), - 'id' => $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR), - 'name' => $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR), - 'email' => $qb->createNamedParameter($contact_email, IQueryBuilder::PARAM_STR), - 'lastchanged' => $qb->createNamedParameter($ts, IQueryBuilder::PARAM_INT) - ]); - $qb->executeStatement(); - $qb = $this->db->getQueryBuilder(); - - // create default categories - if ($createDefaultCategories) { - foreach ($defaultCategories as $category) { - $icon = urlencode($category['icon']); - $color = $category['color']; - $name = $category['name']; - $qb->insert('cospend_categories') - ->values([ - 'projectid' => $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR), - 'encoded_icon' => $qb->createNamedParameter($icon, IQueryBuilder::PARAM_STR), - 'color' => $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR), - 'name' => $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR), - ]); - $qb->executeStatement(); - $qb = $this->db->getQueryBuilder(); - } - } - // create default payment modes - if ($createDefaultPaymentModes) { - foreach ($defaultPaymentModes as $pm) { - $icon = urlencode($pm['icon']); - $color = $pm['color']; - $name = $pm['name']; - $oldId = $pm['old_id']; - $qb->insert('cospend_paymentmodes') - ->values([ - 'projectid' => $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR), - 'encoded_icon' => $qb->createNamedParameter($icon, IQueryBuilder::PARAM_STR), - 'color' => $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR), - 'name' => $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR), - 'old_id' => $qb->createNamedParameter($oldId, IQueryBuilder::PARAM_STR), - ]); - $qb->executeStatement(); - $qb = $this->db->getQueryBuilder(); - } + $ts = (new DateTime())->getTimestamp(); + $project = new Project(); + $project->setUserid($userid); + $project->setId($id); + $project->setName($name); + $project->setEmail($contact_email === null ? '' : $contact_email); + $project->setLastchanged($ts); + $insertedProject = $this->insert($project); + + if ($createDefaultCategories) { + foreach ($defaultCategories as $defaultCategory) { + $category = new Category(); + $category->setProjectid($insertedProject->getId()); + $category->setName($defaultCategory['name']); + $category->setColor($defaultCategory['color']); + $category->setEncodedIcon(urlencode($defaultCategory['icon'])); + $this->categoryMapper->insert($category); } + } - return ['id' => $id]; - } else { - return ['message' => $this->l10n->t('A project with id "%1$s" already exists', [$id])]; + if ($createDefaultPaymentModes) { + foreach ($defaultPaymentModes as $defaultPm) { + $paymentMode = new PaymentMode(); + $paymentMode->setProjectid($insertedProject->getId()); + $paymentMode->setName($defaultPm['name']); + $paymentMode->setColor($defaultPm['color']); + $paymentMode->setEncodedIcon(urlencode($defaultPm['icon'])); + $paymentMode->setOldId($defaultPm['old_id']); + $this->paymentModeMapper->insert($paymentMode); + } } + + return $insertedProject; } /** diff --git a/lib/Service/LocalProjectService.php b/lib/Service/LocalProjectService.php index 1042e0c95..1e040afa2 100644 --- a/lib/Service/LocalProjectService.php +++ b/lib/Service/LocalProjectService.php @@ -273,15 +273,18 @@ public function getShareAccessLevel(string $projectId, int $shId): int { * @param bool $createDefaultCategories * @param bool $createDefaultPaymentModes * @return array + * @throws CospendBasicException + * @throws \OCP\DB\Exception */ public function createProject( string $name, string $id, ?string $contact_email, string $userId = '', bool $createDefaultCategories = true, bool $createDefaultPaymentModes = true ): array { - return $this->projectMapper->createProject( + $newProject = $this->projectMapper->createProject( $name, $id, $contact_email, $this->defaultCategories, $this->defaultPaymentModes, $userId, $createDefaultCategories, $createDefaultPaymentModes ); + return $newProject->jsonSerialize(); } public function deleteProject(string $projectId): void { @@ -318,6 +321,9 @@ public function deleteProject(string $projectId): void { * * @param string $projectId * @return CospendProjectInfoPlusExtra|null + * @throws CospendBasicException + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException * @throws \OCP\DB\Exception */ public function getProjectInfo(string $projectId): ?array { diff --git a/src/App.vue b/src/App.vue index 417c69471..89b971850 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1040,7 +1040,7 @@ export default { console.error(error) showError( t('cospend', 'Failed to create project') - + ': ' + (error.response?.data?.ocs?.meta?.message || error.response?.data?.ocs?.data?.message || error.response?.request?.responseText), + + ': ' + error.response?.data?.ocs?.data, ) }) }, diff --git a/tests/php/controller/ApiControllerTest.php b/tests/php/service/LocalProjectServiceTest.php similarity index 94% rename from tests/php/controller/ApiControllerTest.php rename to tests/php/service/LocalProjectServiceTest.php index f2249cc79..74831306f 100644 --- a/tests/php/controller/ApiControllerTest.php +++ b/tests/php/service/LocalProjectServiceTest.php @@ -15,19 +15,21 @@ * License along with this library. If not, see . * */ -namespace OCA\Cospend\Controller; +namespace OCA\Cospend\Service; use OCA\Cospend\Activity\ActivityManager; use OCA\Cospend\AppInfo\Application; use OCA\Cospend\Db\BillMapper; use OCA\Cospend\Db\MemberMapper; use OCA\Cospend\Db\ProjectMapper; +use OCA\Cospend\Exception\CospendBasicException; use OCA\Cospend\Service\LocalProjectService; use OCA\Cospend\Service\UserService; use OCP\App\IAppManager; use OCP\AppFramework\Http; use OCP\Files\IRootFolder; use OCP\IConfig; +use OCP\IContainer; use OCP\IGroupManager; use OCP\IL10N; @@ -38,14 +40,9 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; -class ApiControllerTest extends TestCase { +class LocalProjectServiceTest extends TestCase { - private ApiController $apiController; - private ApiController $apiController2; - private BillMapper $billMapper; - private ProjectMapper $projectMapper; private LocalProjectService $localProjectService; - private MemberMapper $memberMapper; public static function setUpBeforeClass(): void { $app = new Application(); @@ -86,80 +83,7 @@ protected function setUp(): void { $app = new Application(); $c = $app->getContainer(); - $sc = $c->get(IServerContainer::class); - // $sc = $c->get(ContainerInterface::class); - $l10n = $c->get(IL10N::class); - $this->billMapper = new BillMapper($sc->getDatabaseConnection()); - $this->memberMapper = new MemberMapper($sc->getDatabaseConnection()); - $this->projectMapper = new ProjectMapper($sc->getDatabaseConnection(), $l10n); - - $activityManager = new ActivityManager( - $sc->getActivityManager(), - new UserService( - $this->projectMapper, - $c->get(IGroupManager::class), - $sc->getDatabaseConnection() - ), - $this->projectMapper, - $this->billMapper, - $sc->getL10N($c->get('AppName')), - $c->get(LoggerInterface::class), - 'test' - ); - - $activityManager2 = new ActivityManager( - $sc->getActivityManager(), - new UserService( - $this->projectMapper, - $c->get(IGroupManager::class), - $sc->getDatabaseConnection() - ), - $this->projectMapper, - $this->billMapper, - $sc->getL10N($c->get('AppName')), - $c->get(LoggerInterface::class), - 'test2' - ); - - $this->localProjectService = new LocalProjectService( - $sc->getL10N($c->get('AppName')), - $sc->getConfig(), - $this->projectMapper, - $this->billMapper, - $this->memberMapper, - $activityManager, - $c->get(IUserManager::class), - $c->get(IAppManager::class), - $c->get(IGroupManager::class), - $sc->getDateTimeZone(), - $c->get(IRootFolder::class), - $c->get(INotificationManager::class), - $sc->getDatabaseConnection() - ); - - $this->apiController = new ApiController( - $appName, - $request, - $c->get(IShareManager::class), - $sc->getL10N($c->get('AppName')), - $this->billMapper, - $this->localProjectService, - $activityManager, - $c->get(IRootFolder::class), - 'test' - ); - - $this->apiController2 = new ApiController( - $appName, - $request, - $c->get(IShareManager::class), - $sc->getL10N($c->get('AppName')), - $this->billMapper, - $this->localProjectService, - $activityManager2, - $c->get(IRootFolder::class), - 'test2' - ); + $this->localProjectService = $c->get(LocalProjectService::class); } public static function tearDownAfterClass(): void { @@ -179,67 +103,36 @@ public static function tearDownAfterClass(): void { protected function tearDown(): void { // in case there was a failure and something was not deleted - $this->apiController->deleteProject('superproj'); - $this->apiController->deleteProject('projtodel'); - $this->apiController->deleteProject('original'); - $this->apiController->deleteProject('newproject'); - } - - public function testUtils() { - // DELETE OPTIONS VALUES - $resp = $this->apiController->deleteOptionsValues(); - $data = $resp->getData(); - $this->assertEquals('', $data); - - // SET OPTIONS - $resp = $this->apiController->saveOptionValues(['lala' => 'lolo']); - $data = $resp->getData(); - $this->assertEquals('', $data); - - // GET OPTIONS - $resp = $this->apiController->getOptionsValues(); - $data = $resp->getData(); - $values = $data['values']; - $this->assertEquals('lolo', $values['lala']); + try { + $this->localProjectService->deleteProject('superproj'); + $this->localProjectService->deleteProject('projtodel'); + $this->localProjectService->deleteProject('original'); + $this->localProjectService->deleteProject('newproject'); + } catch (\Throwable $t) { + } } public function testPage() { - // CLEAR OPTIONS - $resp = $this->apiController->deleteOptionsValues(); - $data = $resp->getData(); - $this->assertEquals('', $data); - // CREATE PROJECT - $resp = $this->apiController->createProject('superproj', 'SuperProj', 'toto'); - $status = $resp->getStatus(); - $this->assertEquals(Http::STATUS_OK, $status); - $data = $resp->getData(); - $this->assertEquals('superproj', $data['id']); + $result = $this->localProjectService->createProject('SuperProj', 'superproj', null, 'toto'); + $this->assertEquals('superproj', $result['id']); - $resp = $this->apiController->createProject('superproj', 'SuperProj', 'toto'); - $status = $resp->getStatus(); - $this->assertEquals(Http::STATUS_BAD_REQUEST, $status); + $this->expectException(\OCP\DB\Exception::class); + $this->localProjectService->createProject('SuperProj', 'superproj', null, 'toto'); - $resp = $this->apiController->createProject('super/proj', 'SuperProj', 'toto'); - $status = $resp->getStatus(); - $this->assertEquals(Http::STATUS_BAD_REQUEST, $status); + $this->expectException(CospendBasicException::class); + $resp = $this->localProjectService->createProject('super/proj', 'SuperProj', 'toto'); // get project names $res = $this->localProjectService->getProjectNames(null); $this->assertEquals(0, count($res)); // create members - $resp = $this->apiController->createMember('superproj', 'bobby', null, 1, 1, ''); - $status = $resp->getStatus(); - $this->assertEquals(Http::STATUS_OK, $status); - $data = $resp->getData(); - $idMember1 = $data['id']; + $member = $this->localProjectService->createMember('superproj', 'bobby', 1, true, '', null); + $idMember1 = $member['id']; - $resp = $this->apiController->createMember('superproj', 'robert'); - $status = $resp->getStatus(); - $this->assertEquals(Http::STATUS_OK, $status); - $data = $resp->getData(); - $idMember2 = $data['id']; + $member = $this->localProjectService->createMember('superproj', 'robert'); + $idMember2 = $member['id']; $resp = $this->apiController->createMember('superproj', 'robert3'); $status = $resp->getStatus();