mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-08 05:43:18 +01:00
Compare commits
4 Commits
feature/su
...
feature/st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95bd15864a | ||
|
|
8f446306ef | ||
|
|
e74af760dd | ||
|
|
425175c43e |
@@ -51,7 +51,8 @@ class TimeEntrySendStillRunningMailsCommand extends Command
|
||||
])
|
||||
->whereHas('user', function (Builder $query): void {
|
||||
/** @var Builder<User> $query */
|
||||
$query->where('is_placeholder', '=', false);
|
||||
$query->where('is_placeholder', '=', false)
|
||||
->where('send_time_entry_still_running_email', '=', true);
|
||||
})
|
||||
->orderBy('created_at', 'asc')
|
||||
->chunk(500, function (Collection $timeEntries) use ($dryRun, &$sentMails): void {
|
||||
|
||||
@@ -60,6 +60,7 @@ abstract class ApiException extends Exception
|
||||
*/
|
||||
public function report(): bool
|
||||
{
|
||||
// TODO: temporary activated
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers extends ApiException
|
||||
{
|
||||
public const string KEY = 'can_not_delete_user_who_is_owner_of_organization_with_multiple_members';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class CanNotRemoveOwnerFromOrganization extends ApiException
|
||||
{
|
||||
public const string KEY = 'can_not_remove_owner_from_organization';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class ChangingRoleOfPlaceholderIsNotAllowed extends ApiException
|
||||
{
|
||||
public const string KEY = 'changing_role_of_placeholder_is_not_allowed';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class ChangingRoleToPlaceholderIsNotAllowed extends ApiException
|
||||
{
|
||||
public const string KEY = 'changing_role_to_placeholder_is_not_allowed';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,16 +19,6 @@ class EntityStillInUseApiException extends ApiException
|
||||
|
||||
public const string KEY = 'entity_still_in_use';
|
||||
|
||||
/**
|
||||
* Report the exception.
|
||||
*
|
||||
* @return bool true means the exception handler will not report it again
|
||||
*/
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translated message for the exception.
|
||||
*/
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class FeatureIsNotAvailableInFreePlanApiException extends ApiException
|
||||
{
|
||||
public const string KEY = 'feature_is_not_available_in_free_plan';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class InactiveUserCanNotBeUsedApiException extends ApiException
|
||||
{
|
||||
public const string KEY = 'inactive_user_can_not_be_used';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class InvitationForTheEmailAlreadyExistsApiException extends ApiException
|
||||
{
|
||||
public const string KEY = 'invitation_for_the_email_already_exists';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class OnlyOwnerCanChangeOwnership extends ApiException
|
||||
{
|
||||
public const string KEY = 'only_owner_can_change_ownership';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class OnlyPlaceholdersCanBeMergedIntoAnotherMember extends ApiException
|
||||
{
|
||||
public const string KEY = 'only_placeholders_can_be_merged_into_another_member';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class OrganizationHasNoSubscriptionButMultipleMembersException extends ApiException
|
||||
{
|
||||
public const string KEY = 'organization_has_no_subscription_but_multiple_members';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class OrganizationNeedsAtLeastOneOwner extends ApiException
|
||||
{
|
||||
public const string KEY = 'organization_needs_at_least_one_owner';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class OverlappingTimeEntryApiException extends ApiException
|
||||
{
|
||||
public const string KEY = 'overlapping_time_entry';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException extends ApiException
|
||||
{
|
||||
public const string KEY = 'this_placeholder_can_not_be_invited_use_the_merge_tool_instead_api_exception';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class TimeEntryCanNotBeRestartedApiException extends ApiException
|
||||
{
|
||||
public const string KEY = 'time_entry_can_not_be_restarted';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class TimeEntryStillRunningApiException extends ApiException
|
||||
{
|
||||
public const string KEY = 'time_entry_still_running';
|
||||
|
||||
/**
|
||||
* Report the exception.
|
||||
*
|
||||
* @return bool true means the exception handler will not report it again
|
||||
*/
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class UserIsAlreadyMemberOfOrganizationApiException extends ApiException
|
||||
{
|
||||
public const string KEY = 'user_is_already_member_of_organization';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class UserIsAlreadyMemberOfProjectApiException extends ApiException
|
||||
{
|
||||
public const string KEY = 'user_is_already_member_of_project';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class UserNotPlaceholderApiException extends ApiException
|
||||
{
|
||||
public const string KEY = 'user_not_placeholder';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,4 @@ namespace App\Exceptions\Api;
|
||||
class UserResendEmailVerificationNoPendingEmailApiException extends ApiException
|
||||
{
|
||||
public const string KEY = 'user_resend_email_verification_no_pending_email';
|
||||
|
||||
public function report(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace App\Exceptions;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
@@ -35,10 +34,6 @@ class Handler extends ExceptionHandler
|
||||
//
|
||||
});
|
||||
|
||||
$this->dontReportWhen(fn (Throwable $e): bool => $e instanceof OAuthServerException
|
||||
&& $e->getErrorType() === 'access_denied'
|
||||
&& $e->getHttpStatusCode() === 401);
|
||||
|
||||
// A request on an untrusted host (see App\Http\Middleware\TrustHosts)
|
||||
// otherwise renders as a bare "Bad request." 400. Show a message that
|
||||
// says how to fix it instead. The framework has already converted the
|
||||
|
||||
@@ -124,6 +124,10 @@ class UserController extends Controller
|
||||
$user->week_start = $request->getWeekStart();
|
||||
}
|
||||
|
||||
if ($request->getSendTimeEntryStillRunningEmail() !== null) {
|
||||
$user->send_time_entry_still_running_email = $request->getSendTimeEntryStillRunningEmail();
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
if ($emailToVerify !== null) {
|
||||
|
||||
@@ -58,6 +58,9 @@ class UserUpdateRequest extends BaseFormRequest
|
||||
'week_start' => [
|
||||
Rule::enum(Weekday::class),
|
||||
],
|
||||
'send_time_entry_still_running_email' => [
|
||||
'boolean',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -81,6 +84,13 @@ class UserUpdateRequest extends BaseFormRequest
|
||||
return $this->has('week_start') ? Weekday::from($this->input('week_start')) : null;
|
||||
}
|
||||
|
||||
public function getSendTimeEntryStillRunningEmail(): ?bool
|
||||
{
|
||||
return $this->has('send_time_entry_still_running_email')
|
||||
? $this->boolean('send_time_entry_still_running_email')
|
||||
: null;
|
||||
}
|
||||
|
||||
public function hasPhotoKey(): bool
|
||||
{
|
||||
return $this->has('photo');
|
||||
|
||||
@@ -36,6 +36,8 @@ class UserResource extends BaseResource
|
||||
'timezone' => $this->resource->timezone,
|
||||
/** @var Weekday $week_start Starting day of the week */
|
||||
'week_start' => $this->resource->week_start->value,
|
||||
/** @var bool $send_time_entry_still_running_email Whether to email the user when a time entry has been running for more than 8 hours */
|
||||
'send_time_entry_still_running_email' => $this->resource->send_time_entry_still_running_email,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
* @property string $timezone
|
||||
* @property bool $is_placeholder
|
||||
* @property Weekday $week_start
|
||||
* @property bool $send_time_entry_still_running_email
|
||||
* @property string|null $profile_photo_path
|
||||
* @property-read Organization|null $currentOrganization
|
||||
* @property-read string $profile_photo_url
|
||||
@@ -108,6 +109,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
'is_admin' => 'boolean',
|
||||
'is_placeholder' => 'boolean',
|
||||
'week_start' => Weekday::class,
|
||||
'send_time_entry_still_running_email' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -117,6 +119,7 @@ class User extends Authenticatable implements AuditableContract, FilamentUser, M
|
||||
*/
|
||||
protected $attributes = [
|
||||
'week_start' => Weekday::Monday,
|
||||
'send_time_entry_still_running_email' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
$table->boolean('send_time_entry_still_running_email')->default(true)->after('week_start');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
$table->dropColumn('send_time_entry_still_running_email');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -20,6 +20,12 @@ function profileInformationForm(page: Page) {
|
||||
.locator('xpath=ancestor::*[descendant::form][1]');
|
||||
}
|
||||
|
||||
function notificationSettingsForm(page: Page) {
|
||||
return page
|
||||
.getByRole('heading', { name: 'Notifications', exact: true })
|
||||
.locator('xpath=ancestor::*[descendant::form][1]');
|
||||
}
|
||||
|
||||
async function saveProfileForm(page: Page): Promise<void> {
|
||||
const form = profileInformationForm(page);
|
||||
await form.getByRole('button', { name: 'Save' }).click();
|
||||
@@ -50,6 +56,22 @@ test('week-start change persists across reload', async ({ page }) => {
|
||||
await expect(page.getByLabel('Start of the week')).toHaveValue('sunday');
|
||||
});
|
||||
|
||||
test('still-running email notification setting persists across reload', async ({ page }) => {
|
||||
await goToProfilePage(page);
|
||||
const form = notificationSettingsForm(page);
|
||||
const checkbox = form.getByLabel('Still-running time entry reminders');
|
||||
|
||||
await expect(checkbox).toBeChecked();
|
||||
await checkbox.uncheck();
|
||||
await form.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(form.getByText('Saved.', { exact: true })).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
await expect(
|
||||
notificationSettingsForm(page).getByLabel('Still-running time entry reminders')
|
||||
).not.toBeChecked();
|
||||
});
|
||||
|
||||
test('profile photo can be uploaded, persists across reload, and can be removed', async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
103
resources/js/Pages/Profile/Partials/NotificationSettingsForm.vue
Normal file
103
resources/js/Pages/Profile/Partials/NotificationSettingsForm.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import ActionMessage from '@/Components/ActionMessage.vue';
|
||||
import FormSection from '@/Components/FormSection.vue';
|
||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||
import { Checkbox } from '@/packages/ui/src';
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
} from '@/packages/ui/src/field';
|
||||
import { getApiValidationFieldErrors } from '@/utils/apiValidation';
|
||||
import { useUpdateUserMutation, useUserQuery } from '@/utils/useUserQuery';
|
||||
|
||||
const { user } = useUserQuery();
|
||||
const updateUser = useUpdateUserMutation();
|
||||
|
||||
const sendTimeEntryStillRunningEmail = ref(true);
|
||||
const recentlySaved = ref(false);
|
||||
|
||||
watch(
|
||||
user,
|
||||
(currentUser, previousUser) => {
|
||||
if (currentUser && previousUser === undefined) {
|
||||
sendTimeEntryStillRunningEmail.value = currentUser.send_time_entry_still_running_email;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const isUserLoaded = computed(() => user.value !== undefined);
|
||||
const isSaveDisabled = computed(() => !isUserLoaded.value || updateUser.isPending.value);
|
||||
const fieldErrors = computed<Record<string, string>>(() =>
|
||||
getApiValidationFieldErrors(updateUser.error.value)
|
||||
);
|
||||
|
||||
async function save() {
|
||||
if (isSaveDisabled.value || !user.value) return;
|
||||
|
||||
if (sendTimeEntryStillRunningEmail.value === user.value.send_time_entry_still_running_email) {
|
||||
flashSaved();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedUser = await updateUser.mutateAsync({
|
||||
userId: user.value.id,
|
||||
body: {
|
||||
send_time_entry_still_running_email: sendTimeEntryStillRunningEmail.value,
|
||||
},
|
||||
});
|
||||
sendTimeEntryStillRunningEmail.value = updatedUser.send_time_entry_still_running_email;
|
||||
flashSaved();
|
||||
} catch {
|
||||
// 422: field errors render below. Other errors: toast handled in the mutation.
|
||||
}
|
||||
}
|
||||
|
||||
function flashSaved() {
|
||||
recentlySaved.value = true;
|
||||
setTimeout(() => (recentlySaved.value = false), 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormSection @submitted="save">
|
||||
<template #title>Notifications</template>
|
||||
|
||||
<template #description>Choose which email notifications you want to receive.</template>
|
||||
|
||||
<template #form>
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="send_time_entry_still_running_email"
|
||||
v-model:checked="sendTimeEntryStillRunningEmail"
|
||||
:disabled="!isUserLoaded" />
|
||||
<FieldContent>
|
||||
<FieldLabel for="send_time_entry_still_running_email">
|
||||
Still-running time entry reminders
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Email me when a time entry has been running for more than 8 hours.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
<FieldError v-if="fieldErrors.send_time_entry_still_running_email">
|
||||
{{ fieldErrors.send_time_entry_still_running_email }}
|
||||
</FieldError>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<ActionMessage :on="recentlySaved" class="me-3">Saved.</ActionMessage>
|
||||
|
||||
<PrimaryButton :class="{ 'opacity-25': isSaveDisabled }" :disabled="isSaveDisabled">
|
||||
Save
|
||||
</PrimaryButton>
|
||||
</template>
|
||||
</FormSection>
|
||||
</template>
|
||||
@@ -9,6 +9,7 @@ import UpdateProfileInformationForm from '@/Pages/Profile/Partials/UpdateProfile
|
||||
import type { Session } from '@/types/jetstream';
|
||||
import ApiTokensForm from '@/Pages/Profile/Partials/ApiTokensForm.vue';
|
||||
import ThemeForm from '@/Pages/Profile/Partials/ThemeForm.vue';
|
||||
import NotificationSettingsForm from '@/Pages/Profile/Partials/NotificationSettingsForm.vue';
|
||||
|
||||
defineProps<{
|
||||
confirmsTwoFactorAuthentication: boolean;
|
||||
@@ -36,6 +37,12 @@ defineProps<{
|
||||
<SectionBorder />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<NotificationSettingsForm />
|
||||
|
||||
<SectionBorder />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<UpdatePasswordForm class="mt-10 sm:mt-0" />
|
||||
|
||||
|
||||
@@ -705,6 +705,7 @@ const UserResource = z
|
||||
profile_photo_url: z.string(),
|
||||
timezone: z.string(),
|
||||
week_start: Weekday,
|
||||
send_time_entry_still_running_email: z.boolean(),
|
||||
})
|
||||
.passthrough();
|
||||
const UserUpdateRequest = z
|
||||
@@ -714,6 +715,7 @@ const UserUpdateRequest = z
|
||||
photo: z.union([z.string(), z.null()]),
|
||||
timezone: z.string(),
|
||||
week_start: Weekday,
|
||||
send_time_entry_still_running_email: z.boolean(),
|
||||
})
|
||||
.partial()
|
||||
.passthrough();
|
||||
|
||||
1
resources/js/types/models.d.ts
vendored
1
resources/js/types/models.d.ts
vendored
@@ -76,6 +76,7 @@ export interface User {
|
||||
two_factor_confirmed_at: string | null;
|
||||
timezone: string;
|
||||
week_start: string;
|
||||
send_time_entry_still_running_email: boolean;
|
||||
profile_photo_url: string;
|
||||
organizations: Organization[];
|
||||
clients: Client[];
|
||||
|
||||
@@ -159,4 +159,26 @@ class TimeEntrySendStillRunningMailsCommandTest extends TestCaseWithDatabase
|
||||
$this->assertSame("Sending still running time entry emails...\n".
|
||||
"Finished sending 0 still running time entry emails...\n", $output);
|
||||
}
|
||||
|
||||
public function test_does_not_send_emails_to_users_who_disabled_them(): void
|
||||
{
|
||||
// Arrange
|
||||
$user = $this->createUserWithPermission();
|
||||
$user->user->send_time_entry_still_running_email = false;
|
||||
$user->user->save();
|
||||
$timeEntry = TimeEntry::factory()->forMember($user->member)->create([
|
||||
'start' => Carbon::now()->subHours(8)->subSecond(),
|
||||
'end' => null,
|
||||
]);
|
||||
|
||||
// Act
|
||||
$exitCode = $this->withoutMockingConsoleOutput()->artisan('time-entry:send-still-running-mails');
|
||||
|
||||
// Assert
|
||||
Mail::assertNothingOutgoing();
|
||||
$this->assertNull($timeEntry->fresh()->still_active_email_sent_at);
|
||||
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||
$this->assertSame("Sending still running time entry emails...\n".
|
||||
"Finished sending 0 still running time entry emails...\n", Artisan::output());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
||||
'profile_photo_url' => $data->user->profile_photo_url,
|
||||
'timezone' => $data->user->timezone,
|
||||
'week_start' => $data->user->week_start->value,
|
||||
'send_time_entry_still_running_email' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -131,7 +132,7 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
||||
$response->assertJsonValidationErrors('organization_id');
|
||||
}
|
||||
|
||||
public function test_update_changes_user_name_timezone_and_week_start(): void
|
||||
public function test_update_changes_user_profile_settings(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission();
|
||||
@@ -142,6 +143,7 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
||||
'name' => 'Updated Name',
|
||||
'timezone' => 'America/New_York',
|
||||
'week_start' => Weekday::Sunday->value,
|
||||
'send_time_entry_still_running_email' => false,
|
||||
]);
|
||||
|
||||
// Assert
|
||||
@@ -152,6 +154,7 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
||||
'name' => 'Updated Name',
|
||||
'timezone' => 'America/New_York',
|
||||
'week_start' => Weekday::Sunday->value,
|
||||
'send_time_entry_still_running_email' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -159,6 +162,7 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
||||
$this->assertSame('Updated Name', $user->name);
|
||||
$this->assertSame('America/New_York', $user->timezone);
|
||||
$this->assertSame(Weekday::Sunday, $user->week_start);
|
||||
$this->assertFalse($user->send_time_entry_still_running_email);
|
||||
}
|
||||
|
||||
public function test_update_does_not_change_user_fields_that_are_not_given(): void
|
||||
@@ -503,6 +507,22 @@ class UserEndpointTest extends ApiEndpointTestAbstract
|
||||
$response->assertJsonValidationErrors(['week_start']);
|
||||
}
|
||||
|
||||
public function test_update_fails_if_send_time_entry_still_running_email_is_not_boolean(): void
|
||||
{
|
||||
// Arrange
|
||||
$data = $this->createUserWithPermission();
|
||||
Passport::actingAs($data->user);
|
||||
|
||||
// Act
|
||||
$response = $this->putJson(route('api.v1.users.update', $data->user->getKey()), [
|
||||
'send_time_entry_still_running_email' => 'not-a-boolean',
|
||||
]);
|
||||
|
||||
// Assert
|
||||
$response->assertUnprocessable();
|
||||
$response->assertJsonValidationErrors(['send_time_entry_still_running_email']);
|
||||
}
|
||||
|
||||
public function test_update_fails_if_photo_is_not_a_string(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Exceptions\Api;
|
||||
|
||||
use App\Exceptions\Api\ApiException;
|
||||
use App\Exceptions\Api\CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers;
|
||||
use App\Exceptions\Api\CanNotRemoveOwnerFromOrganization;
|
||||
use App\Exceptions\Api\ChangingRoleOfPlaceholderIsNotAllowed;
|
||||
use App\Exceptions\Api\ChangingRoleToPlaceholderIsNotAllowed;
|
||||
use App\Exceptions\Api\EntityStillInUseApiException;
|
||||
use App\Exceptions\Api\FeatureIsNotAvailableInFreePlanApiException;
|
||||
use App\Exceptions\Api\InactiveUserCanNotBeUsedApiException;
|
||||
use App\Exceptions\Api\InvitationForTheEmailAlreadyExistsApiException;
|
||||
use App\Exceptions\Api\OnlyOwnerCanChangeOwnership;
|
||||
use App\Exceptions\Api\OnlyPlaceholdersCanBeMergedIntoAnotherMember;
|
||||
use App\Exceptions\Api\OrganizationHasNoSubscriptionButMultipleMembersException;
|
||||
use App\Exceptions\Api\OrganizationNeedsAtLeastOneOwner;
|
||||
use App\Exceptions\Api\OverlappingTimeEntryApiException;
|
||||
use App\Exceptions\Api\PdfRendererIsNotConfiguredException;
|
||||
use App\Exceptions\Api\PersonalAccessClientIsNotConfiguredException;
|
||||
use App\Exceptions\Api\ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException;
|
||||
use App\Exceptions\Api\TimeEntryCanNotBeRestartedApiException;
|
||||
use App\Exceptions\Api\TimeEntryStillRunningApiException;
|
||||
use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
|
||||
use App\Exceptions\Api\UserIsAlreadyMemberOfProjectApiException;
|
||||
use App\Exceptions\Api\UserNotPlaceholderApiException;
|
||||
use App\Exceptions\Api\UserResendEmailVerificationNoPendingEmailApiException;
|
||||
use App\Service\Export\ExportException;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ApiExceptionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return iterable<string, array{ApiException}>
|
||||
*/
|
||||
public static function expectedApiExceptionProvider(): iterable
|
||||
{
|
||||
yield CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers::class => [new CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers];
|
||||
yield CanNotRemoveOwnerFromOrganization::class => [new CanNotRemoveOwnerFromOrganization];
|
||||
yield ChangingRoleOfPlaceholderIsNotAllowed::class => [new ChangingRoleOfPlaceholderIsNotAllowed];
|
||||
yield ChangingRoleToPlaceholderIsNotAllowed::class => [new ChangingRoleToPlaceholderIsNotAllowed];
|
||||
yield EntityStillInUseApiException::class => [new EntityStillInUseApiException('member', 'time_entry')];
|
||||
yield FeatureIsNotAvailableInFreePlanApiException::class => [new FeatureIsNotAvailableInFreePlanApiException];
|
||||
yield InactiveUserCanNotBeUsedApiException::class => [new InactiveUserCanNotBeUsedApiException];
|
||||
yield InvitationForTheEmailAlreadyExistsApiException::class => [new InvitationForTheEmailAlreadyExistsApiException];
|
||||
yield OnlyOwnerCanChangeOwnership::class => [new OnlyOwnerCanChangeOwnership];
|
||||
yield OnlyPlaceholdersCanBeMergedIntoAnotherMember::class => [new OnlyPlaceholdersCanBeMergedIntoAnotherMember];
|
||||
yield OrganizationHasNoSubscriptionButMultipleMembersException::class => [new OrganizationHasNoSubscriptionButMultipleMembersException];
|
||||
yield OrganizationNeedsAtLeastOneOwner::class => [new OrganizationNeedsAtLeastOneOwner];
|
||||
yield OverlappingTimeEntryApiException::class => [new OverlappingTimeEntryApiException];
|
||||
yield ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException::class => [new ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException];
|
||||
yield TimeEntryCanNotBeRestartedApiException::class => [new TimeEntryCanNotBeRestartedApiException];
|
||||
yield TimeEntryStillRunningApiException::class => [new TimeEntryStillRunningApiException];
|
||||
yield UserIsAlreadyMemberOfOrganizationApiException::class => [new UserIsAlreadyMemberOfOrganizationApiException];
|
||||
yield UserIsAlreadyMemberOfProjectApiException::class => [new UserIsAlreadyMemberOfProjectApiException];
|
||||
yield UserNotPlaceholderApiException::class => [new UserNotPlaceholderApiException];
|
||||
yield UserResendEmailVerificationNoPendingEmailApiException::class => [new UserResendEmailVerificationNoPendingEmailApiException];
|
||||
}
|
||||
|
||||
#[DataProvider('expectedApiExceptionProvider')]
|
||||
public function test_expected_api_exceptions_are_not_reported(ApiException $exception): void
|
||||
{
|
||||
// Arrange: exception supplied by the data provider
|
||||
|
||||
// Act
|
||||
$reportingHandled = $exception->report();
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($reportingHandled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{ApiException}>
|
||||
*/
|
||||
public static function operationalApiExceptionProvider(): iterable
|
||||
{
|
||||
yield PdfRendererIsNotConfiguredException::class => [new PdfRendererIsNotConfiguredException];
|
||||
yield PersonalAccessClientIsNotConfiguredException::class => [new PersonalAccessClientIsNotConfiguredException];
|
||||
yield ExportException::class => [new ExportException];
|
||||
}
|
||||
|
||||
#[DataProvider('operationalApiExceptionProvider')]
|
||||
public function test_operational_api_exceptions_are_reported(ApiException $exception): void
|
||||
{
|
||||
// Arrange: exception supplied by the data provider
|
||||
|
||||
// Act
|
||||
$reportingHandled = $exception->report();
|
||||
|
||||
// Assert
|
||||
$this->assertFalse($reportingHandled);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Exceptions;
|
||||
|
||||
use App\Exceptions\Handler;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use RuntimeException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class HandlerTest extends TestCase
|
||||
{
|
||||
public function test_oauth_access_denied_exceptions_are_not_reported(): void
|
||||
{
|
||||
// Arrange
|
||||
$exception = OAuthServerException::accessDenied(
|
||||
'Access token could not be verified',
|
||||
previous: new RuntimeException('The token is expired')
|
||||
);
|
||||
|
||||
// Act
|
||||
$shouldReport = app(Handler::class)->shouldReport($exception);
|
||||
|
||||
// Assert
|
||||
$this->assertFalse($shouldReport);
|
||||
}
|
||||
|
||||
public function test_operational_oauth_exceptions_are_reported(): void
|
||||
{
|
||||
// Arrange
|
||||
$exception = OAuthServerException::serverError('Signing key could not be read');
|
||||
|
||||
// Act
|
||||
$shouldReport = app(Handler::class)->shouldReport($exception);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($shouldReport);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user