Skip to content

Commit

Permalink
Add tests for export endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
korridor committed Oct 29, 2024
1 parent 134a776 commit 7501e41
Show file tree
Hide file tree
Showing 11 changed files with 570 additions and 85 deletions.
22 changes: 9 additions & 13 deletions .env.ci
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ APP_URL=http://localhost
APP_FORCE_HTTPS=false
SESSION_SECURE_COOKIE=false

# Logging
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

# Database
DB_CONNECTION=pgsql_test

DB_TEST_HOST=127.0.0.1
DB_TEST_PORT=5432
DB_TEST_DATABASE=laravel
Expand All @@ -20,26 +21,21 @@ DB_TEST_PASSWORD=root

BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=database
SESSION_LIFETIME=120

MEMCACHED_HOST=127.0.0.1

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

# Mail
MAIL_MAILER=log
MAIL_FROM_ADDRESS="[email protected]"
MAIL_FROM_NAME="${APP_NAME}"

S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_REGION=us-east-1
S3_BUCKET=
S3_USE_PATH_STYLE_ENDPOINT=false
# Filesystems
FILESYSTEM_DISK=local
PUBLIC_FILESYSTEM_DISK=public

# Services
GOTENBERG_URL=http://gotenberg:3000

PUSHER_APP_ID=
PUSHER_APP_KEY=
Expand Down
18 changes: 7 additions & 11 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk=
APP_DEBUG=true
APP_URL=https://solidtime.test
AUDITING_ENABLED=true

SUPER_ADMINS=[email protected]

# Logging
LOG_CHANNEL=single
LOG_DEPRECATIONS_CHANNEL=deprecation
LOG_LEVEL=debug

# Database
DB_CONNECTION=pgsql

DB_HOST=pgsql
DB_PORT=5432
DB_DATABASE=laravel
Expand All @@ -31,14 +31,7 @@ QUEUE_CONNECTION=sync
SESSION_DRIVER=database
SESSION_LIFETIME=120

GOTENBERG_URL=http://gotenberg:3000

MEMCACHED_HOST=127.0.0.1

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

# Mail
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
Expand All @@ -56,7 +49,7 @@ PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1

# Storage
# Filesystems
FILESYSTEM_DISK=s3
PUBLIC_FILESYSTEM_DISK=s3
S3_ACCESS_KEY_ID=sail
Expand All @@ -67,6 +60,9 @@ S3_URL=http://storage.solidtime.test/local
S3_ENDPOINT=http://storage.solidtime.test
S3_USE_PATH_STYLE_ENDPOINT=true

# Services
GOTENBERG_URL=http://gotenberg:3000

VITE_HOST_NAME=vite.solidtime.test
VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@ jobs:
--health-interval 10s
--health-timeout 5s
--health-retries 5
gotenberg:
image: gotenberg/gotenberg:8
ports:
- 3000:3000
options: >-
--health-cmd "curl --silent --fail http://localhost:3000/health"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: "Checkout code"
uses: actions/checkout@v4
Expand Down
10 changes: 10 additions & 0 deletions app/Exceptions/Api/FeatureIsNotAvailableInFreePlanApiException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace App\Exceptions\Api;

class FeatureIsNotAvailableInFreePlanApiException extends ApiException
{
public const string KEY = 'feature_is_not_available_in_free_plan';
}
31 changes: 26 additions & 5 deletions app/Http/Controllers/Api/V1/TimeEntryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Http\Controllers\Api\V1;

use App\Enums\ExportFormat;
use App\Exceptions\Api\FeatureIsNotAvailableInFreePlanApiException;
use App\Exceptions\Api\PdfRendererIsNotConfiguredException;
use App\Exceptions\Api\TimeEntryCanNotBeRestartedApiException;
use App\Exceptions\Api\TimeEntryStillRunningApiException;
Expand Down Expand Up @@ -35,6 +36,7 @@
use Gotenberg\Exceptions\NoOutputFileInResponse;
use Gotenberg\Gotenberg;
use Gotenberg\Stream;
use GuzzleHttp\Client;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\File;
Expand Down Expand Up @@ -158,7 +160,7 @@ private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexR
/**
* Export time entries in organization
*
* @throws AuthorizationException|PdfRendererIsNotConfiguredException
* @throws AuthorizationException|PdfRendererIsNotConfiguredException|FeatureIsNotAvailableInFreePlanApiException
*
* @operationId exportTimeEntries
*/
Expand All @@ -171,6 +173,10 @@ public function indexExport(Organization $organization, TimeEntryIndexExportRequ
} else {
$this->checkPermission($organization, 'time-entries:view:all');
}
$format = $request->getFormatValue();
if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) {
throw new FeatureIsNotAvailableInFreePlanApiException;
}

$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member);
$timeEntriesQuery->with([
Expand All @@ -180,7 +186,6 @@ public function indexExport(Organization $organization, TimeEntryIndexExportRequ
'user',
'tagsRelation',
]);
$format = $request->getFormatValue();
$filename = 'time-entries-export-'.now()->format('Y-m-d_H-i-s').'.'.$format->getFileExtension();
$folderPath = 'exports';
$path = $folderPath.'/'.$filename;
Expand All @@ -201,14 +206,20 @@ public function indexExport(Organization $organization, TimeEntryIndexExportRequ
throw new \LogicException('View file not found');
}
$footerHtml = Blade::render($footerViewFile);
$client = new Client([
'auth' => config('services.gotenberg.basic_auth_username') !== null && config('services.gotenberg.basic_auth_password') !== null ? [
config('services.gotenberg.basic_auth_username'),
config('services.gotenberg.basic_auth_password'),
] : null,
]);
$request = Gotenberg::chromium(config('services.gotenberg.url'))
->pdf()
->pdfa('PDF/A-3b')
->paperSize('8.27', '11.7') // A4
->footer(Stream::string('footer', $footerHtml))
->html(Stream::string('body', $html));
$tempFolder = TemporaryDirectory::make();
$filenameTemp = Gotenberg::save($request, $tempFolder->path());
$filenameTemp = Gotenberg::save($request, $tempFolder->path(), $client);
Storage::disk(config('filesystems.private'))
->putFileAs($folderPath, new File($tempFolder->path($filenameTemp)), $filename);
} else {
Expand Down Expand Up @@ -301,6 +312,7 @@ public function aggregate(Organization $organization, TimeEntryAggregateRequest
* @throws PdfRendererIsNotConfiguredException
* @throws GotenbergApiErrored
* @throws NoOutputFileInResponse
* @throws FeatureIsNotAvailableInFreePlanApiException
*/
public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $request, TimeEntryAggregationService $timeEntryAggregationService): JsonResponse
{
Expand All @@ -311,6 +323,10 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx
} else {
$this->checkPermission($organization, 'time-entries:view:all');
}
$format = $request->getFormatValue();
if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) {
throw new FeatureIsNotAvailableInFreePlanApiException;
}
$user = $this->user();

$group = $request->getGroup();
Expand Down Expand Up @@ -340,7 +356,6 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx
$currency = $organization->currency;
$timezone = app(TimezoneService::class)->getTimezoneFromUser($this->user());

$format = $request->getFormatValue();
$filename = 'time-entries-report-'.now()->format('Y-m-d_H-i-s').'.'.$format->getFileExtension();
$folderPath = 'exports';
$path = $folderPath.'/'.$filename;
Expand All @@ -349,6 +364,12 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx
if (config('services.gotenberg.url') === null) {
throw new PdfRendererIsNotConfiguredException;
}
$client = new Client([
'auth' => config('services.gotenberg.basic_auth_username') !== null && config('services.gotenberg.basic_auth_password') !== null ? [
config('services.gotenberg.basic_auth_username'),
config('services.gotenberg.basic_auth_password'),
] : null,
]);
$viewFile = file_get_contents(resource_path('views/reports/time-entry-aggregate-index.blade.php'));
if ($viewFile === false) {
throw new \LogicException('View file not found');
Expand All @@ -374,7 +395,7 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx
->footer(Stream::string('footer', $footerHtml))
->html(Stream::string('body', $html));
$tempFolder = TemporaryDirectory::make();
$filenameTemp = Gotenberg::save($request, $tempFolder->path());
$filenameTemp = Gotenberg::save($request, $tempFolder->path(), $client);
Storage::disk(config('filesystems.private'))
->putFileAs($folderPath, new File($tempFolder->path($filenameTemp)), $filename);
} else {
Expand Down
37 changes: 23 additions & 14 deletions app/Service/TimeEntryAggregationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,20 +184,6 @@ public function getAggregatedTimeEntriesWithDescriptions(Builder $timeEntriesQue
$descriptionMapGroup2 = $group2Type !== null ? $this->loadDescriptionMap($keysGroup2, $group2Type) : [];

if ($aggregatedTimeEntries['grouped_data'] !== null) {
/*
$aggregatedTimeEntries['grouped_data'] = array_map(function (array $value) use ($descriptionMapGroup1, $descriptionMapGroup2): array {
$value['description'] = $value['key'] !== null ? ($descriptionMapGroup1[$value['key']] ?? null) : null;
if ($value['grouped_data'] !== null) {
$value['grouped_data'] = array_map(function (array $value) use ($descriptionMapGroup2): array {
$value['description'] = $value['key'] !== null ? ($descriptionMapGroup2[$value['key']] ?? null) : null;
return $value;
}, $value['grouped_data']);
}
return $value;
}, $aggregatedTimeEntries['grouped_data']);
*/
foreach ($aggregatedTimeEntries['grouped_data'] as $keyGroup1 => $group1) {
$aggregatedTimeEntries['grouped_data'][$keyGroup1]['description'] = $group1['key'] !== null ? ($descriptionMapGroup1[$group1['key']] ?? null) : null;
if ($aggregatedTimeEntries['grouped_data'][$keyGroup1]['grouped_data'] !== null) {
Expand All @@ -208,6 +194,29 @@ public function getAggregatedTimeEntriesWithDescriptions(Builder $timeEntriesQue
}
}

/**
* @var array{
* grouped_type: string|null,
* grouped_data: null|array<array{
* key: string|null,
* description: string|null,
* seconds: int,
* cost: int,
* grouped_type: string|null,
* grouped_data: null|array<array{
* key: string|null,
* description: string|null,
* seconds: int,
* cost: int,
* grouped_type: null,
* grouped_data: null
* }>
* }>,
* seconds: int,
* cost: int
* } $aggregatedTimeEntries
*/

return $aggregatedTimeEntries;
}

Expand Down
2 changes: 2 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
return [
'gotenberg' => [
'url' => env('GOTENBERG_URL'),
'basic_auth_username' => env('GOTENBERG_BASIC_AUTH_USERNAME'),
'basic_auth_password' => env('GOTENBERG_BASIC_AUTH_PASSWORD'),
],
];
2 changes: 2 additions & 0 deletions lang/en/exceptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Exceptions\Api\CanNotRemoveOwnerFromOrganization;
use App\Exceptions\Api\ChangingRoleToPlaceholderIsNotAllowed;
use App\Exceptions\Api\EntityStillInUseApiException;
use App\Exceptions\Api\FeatureIsNotAvailableInFreePlanApiException;
use App\Exceptions\Api\InactiveUserCanNotBeUsedApiException;
use App\Exceptions\Api\OnlyOwnerCanChangeOwnership;
use App\Exceptions\Api\OrganizationHasNoSubscriptionButMultipleMembersException;
Expand Down Expand Up @@ -35,6 +36,7 @@
ExportException::KEY => 'Export failed, please try again later or contact support',
OrganizationHasNoSubscriptionButMultipleMembersException::KEY => 'Organization has no subscription but multiple members',
PdfRendererIsNotConfiguredException::KEY => 'PDF renderer is not configured',
FeatureIsNotAvailableInFreePlanApiException::KEY => 'Feature is not available in free plan',
],
'unknown_error_in_admin_panel' => 'An unknown error occurred. Please check the logs.',
];
17 changes: 11 additions & 6 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,7 @@ protected function setUp(): void
parent::setUp();
Mail::fake();
LogFake::bind();
$this->mock(BillingContract::class, function (MockInterface $mock): void {
$mock->shouldReceive('hasSubscription')->andReturn(false);
$mock->shouldReceive('hasTrial')->andReturn(false);
$mock->shouldReceive('getTrialUntil')->andReturn(null);
$mock->shouldReceive('isBlocked')->andReturn(false);
});
$this->actAsOrganizationWithoutSubscriptionAndWithoutTrial();
// Note: The following line can be used to test timezone edge cases.
// $this->travelTo(Carbon::now()->timezone('Europe/Vienna')->setHour(0)->setMinute(59)->setSecond(0));
}
Expand Down Expand Up @@ -89,4 +84,14 @@ protected function actAsOrganizationWithSubscription(): void
$mock->shouldReceive('isBlocked')->andReturn(false);
});
}

protected function actAsOrganizationWithoutSubscriptionAndWithoutTrial(): void
{
$this->mock(BillingContract::class, function (MockInterface $mock): void {
$mock->shouldReceive('hasSubscription')->andReturn(false);
$mock->shouldReceive('hasTrial')->andReturn(false);
$mock->shouldReceive('getTrialUntil')->andReturn(null);
$mock->shouldReceive('isBlocked')->andReturn(false);
});
}
}
12 changes: 11 additions & 1 deletion tests/Unit/Endpoint/Api/V1/ApiEndpointTestAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@

namespace Tests\Unit\Endpoint\Api\V1;

use Illuminate\Testing\TestResponse;
use Tests\TestCaseWithDatabase;

class ApiEndpointTestAbstract extends TestCaseWithDatabase {}
class ApiEndpointTestAbstract extends TestCaseWithDatabase
{
protected function assertResponseCode(TestResponse $response, int $statusCode): void
{
if ($response->getStatusCode() !== $statusCode) {
dump($response->getContent());
}
$response->assertStatus($statusCode);
}
}
Loading

0 comments on commit 7501e41

Please sign in to comment.