diff --git a/.env.example b/.env.example index bb5bb70..f7595b8 100644 --- a/.env.example +++ b/.env.example @@ -53,9 +53,11 @@ REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 -MAIL_MAILER=log -MAIL_HOST=127.0.0.1 -MAIL_PORT=2525 +# change accordingly if you are not using Laravel Sail +FORWARD_MAILPIT_PORT=2525 +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT="${FORWARD_MAILPIT_PORT:-2525}" MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index b4f7735..dad1c32 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -1,14 +1,18 @@ name: Build and deploy Application +# on: +# push: +# branches: +# - prod +# workflow_dispatch: on: - push: - branches: - - prod - workflow_dispatch: + workflow_run: + workflows: [Tests] + types: [completed] jobs: build: - runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'prod' }} # Run only if the first workflow succeeded and push was to prod steps: - uses: actions/checkout@v4 - name: Setup PHP diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..8c23161 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Tests + +on: [push] +jobs: + tests: + runs-on: ubuntu-latest + env: + APP_URL: "http://127.0.0.1:8000" + DB_USERNAME: root + DB_DATABASE: tasktango + DB_HOST: 127.0.0.1 + DB_PASSWORD: root + MAIL_MAILER: log + SUPER_ADMIN_USERNAME: super_admin + SUPER_ADMIN_EMAIL: admin@admin.com + SUPER_ADMIN_PASSWORD: super-secure-password + steps: + - uses: actions/checkout@v4 + - name: Prepare The Environment + run: cp .env.example .env + - name: Create Database + run: | + sudo systemctl start mysql + mysql --user="root" --password="root" -e "CREATE DATABASE \`tasktango\` character set UTF8mb4 collate utf8mb4_bin;" + - name: Install Composer Dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + - name: Generate Application Key + run: php artisan key:generate + - name: Run Laravel Server + run: php artisan serve --no-reload & + - name: Run Tests + run: php artisan test diff --git a/.gitignore b/.gitignore index ca55740..28b8f39 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ justfile .env.testing .env.production .phpunit.result.cache +.phpunit* /public/hot /public/storage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..961f0ce --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - repo: https://github.com/gitleaks/gitleaks + rev: v8.24.0 + hooks: + - id: gitleaks diff --git a/README.md b/README.md index 39fa594..362051b 100644 --- a/README.md +++ b/README.md @@ -93,4 +93,3 @@ Inbox View ![Search](.screenshots/search-functionality.png) Search Functionality - diff --git a/phpunit.xml b/phpunit.xml index 81be799..303f4ee 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -12,7 +12,7 @@ tests/Feature - tests/Feature/Main + tests/Main diff --git a/resources/views/components/avatar-or-icon.blade.php b/resources/views/components/avatar-or-icon.blade.php index 7180fd9..5eaa7d8 100644 --- a/resources/views/components/avatar-or-icon.blade.php +++ b/resources/views/components/avatar-or-icon.blade.php @@ -12,7 +12,11 @@ $showAvatar = false; @endphp @if ($showAvatar && !$forceToShowIcon) - + except(['class', 'icon-class', 'avatar-class']) }} + class="{{$attributes->get('avatar-class')}}" /> @else - + except(['class', 'icon-class', 'avatar-class','name']) }} + class="{{$attributes->get('icon-class')}}" /> @endif diff --git a/resources/views/livewire/layout/sidebar.blade.php b/resources/views/livewire/layout/sidebar.blade.php index 1971ac3..f8c810d 100644 --- a/resources/views/livewire/layout/sidebar.blade.php +++ b/resources/views/livewire/layout/sidebar.blade.php @@ -16,7 +16,7 @@ - + diff --git a/resources/views/livewire/segments/search.blade.php b/resources/views/livewire/segments/search.blade.php index c93af0e..05e68b3 100644 --- a/resources/views/livewire/segments/search.blade.php +++ b/resources/views/livewire/segments/search.blade.php @@ -399,4 +399,3 @@ public function updatedNoDueDate($value): void {{-- task modal ready --}} - diff --git a/resources/views/livewire/set-profile-picture.blade.php b/resources/views/livewire/set-profile-picture.blade.php index 4e76390..a438ecc 100644 --- a/resources/views/livewire/set-profile-picture.blade.php +++ b/resources/views/livewire/set-profile-picture.blade.php @@ -1,16 +1,21 @@ true])] class extends Component { + use WithFileUploads; use Toast; + public $avatar; + function mount() { if (!auth()->user()->profile_picture && !auth()->user()->has_asked_for_profile_picture) { @@ -20,18 +25,56 @@ function mount() return redirect()->route('index'); } - function continue() + function setPicture() { if (auth()->user()->profile_picture) { auth()->user()->has_asked_for_profile_picture = true; return redirect()->route('index'); } - $this->warning( - title: 'Profile Picture Required', - position: 'toast-bottom toast-end text-wrap', - description: "Please select your profile picture before proceeding, if you don't want to click Skip for now. If you have changed your profile picture right now but can't move on wait a few seconds and click Next again", - icon: 'o-exclamation-triangle' - ); + try { + $validated = $this->validate( + ['avatar' => 'image|max:2048'], + [ + 'avatar.image' => 'The avatar must be an image.', + 'avatar.max' => 'Your profile picture may not be greater than 1MB.', + ] + ); + + // Validation passed, proceed with logic + } catch (ValidationException $e) { + $errors = $e->validator->errors(); // Get all validation errors + + // Example: Get all error messages as an array + $errorMessages = $errors->all(); + + // Example: Get errors for a specific field + $avatarErrors = $errors->get('avatar'); + + // Handle errors (store them in session, return JSON, etc.) + $this->warning( + title: $avatarErrors[0], + position: 'toast-bottom toast-end text-wrap', + icon: 'o-exclamation-triangle' + ); + return; + } + + $user = auth()->user(); + $diskToStore = config('filesystems.default') === 'local' ? 'public' : config('filesystems.default'); + $path = $this->avatar->store('profile_pictures', $diskToStore); + + if ($user->profile_picture) { + Storage::delete($user->profile_picture); + } + if ($diskToStore === 'public') { + $user->profile_picture = $path; + } else { + // making the temporaryUrl not that temporary + $user->profile_picture = Storage::disk($diskToStore)->temporaryUrl($path, now()->addYears(100)); + } + + $user->save(); + return redirect()->route('index'); } function skipForNow() @@ -46,13 +89,25 @@ function skipForNow()
- - + + @csrf + +
+
+ + +
+
+
+ +
+
+ + + + +
- - - - -
diff --git a/resources/views/livewire/task/partials/breadcrumbs.blade.php b/resources/views/livewire/task/partials/breadcrumbs.blade.php index 2ec8b93..c232416 100644 --- a/resources/views/livewire/task/partials/breadcrumbs.blade.php +++ b/resources/views/livewire/task/partials/breadcrumbs.blade.php @@ -32,4 +32,3 @@ - diff --git a/resources/views/test.blade.php b/resources/views/test.blade.php index 4d9f8b6..8d3903c 100644 --- a/resources/views/test.blade.php +++ b/resources/views/test.blade.php @@ -4,4 +4,3 @@ $user->is_super_admin = true; @endphp - diff --git a/tests/Browser/Components/Sidebar.php b/tests/Browser/Components/Sidebar.php index 59722c0..bf52ba3 100644 --- a/tests/Browser/Components/Sidebar.php +++ b/tests/Browser/Components/Sidebar.php @@ -31,6 +31,7 @@ public function assert(Browser $browser): void public function elements(): array { return [ + '@profile-avatar' => '@profile-avatar', '@theme-controller' => '@theme-controller', '@next-7-days' => '@next-7-days', '@logout' => '@logout', diff --git a/tests/Browser/RegisterTest.php b/tests/Browser/RegisterTest.php index f902a65..06339f6 100644 --- a/tests/Browser/RegisterTest.php +++ b/tests/Browser/RegisterTest.php @@ -2,34 +2,94 @@ namespace Tests\Browser; +use App\Models\User; use Illuminate\Foundation\Testing\DatabaseTruncation; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\URL; use Laravel\Dusk\Browser; +use Tests\Browser\Components\Sidebar; use Tests\DuskTestCase; class RegisterTest extends DuskTestCase { use DatabaseTruncation; + protected $userEmail = 'marcos@marcos.com'; // Store email as a class property + /** - * A basic browser test example. + * Common registration method */ - public function test_user_can_register(): void + protected function register_user_and_get_verification_url(): string { $this->browse(function (Browser $browser) { $browser ->visit('/register') ->type('@register-user-name', 'randomUser239') ->type('@register-full-name', 'Marcos Aparicio') - ->type('@register-email', 'marcos@marcos.com') + ->type('@register-email', $this->userEmail) // Use class-level email ->type('@register-password', 'password') ->type('@register-confirm-password', 'password') ->press('Register') ->pause(2000); $browser->assertSee('Resend Verification Email'); $this->assertDatabaseHas('users', [ - 'email' => 'marcos@marcos.com', + 'email' => $this->userEmail, // Use class-level email 'email_verified_at' => null, ]); }); + + // Retrieve the newly created user + $user = User::where('email', $this->userEmail)->firstOrFail(); // Use class-level email + + // Generate the email verification URL directly from the user + $verificationUrl = URL::signedRoute('verification.verify', [ + 'id' => $user->id, + 'hash' => sha1($user->email), + ]); + + // Ensure we got the verification URL + $this->assertNotEmpty($verificationUrl, 'Failed to generate verification URL.'); + + return $verificationUrl; + } + + public function test_user_can_register_without_setting_profile_picture(): void + { + $verificationUrl = $this->register_user_and_get_verification_url(); + // Now visit the email verification link, which will redirect to the set profile picture component + // skip the selection + $this->browse(function (Browser $browser) use ($verificationUrl) { + $browser + ->visit($verificationUrl) + ->press('Skip for Now') + ->pause(1000) + ->assertUrlIs(route('inbox')); + }); + } + + public function test_user_can_register_while_setting_profile_picture(): void + { + $verificationUrl = $this->register_user_and_get_verification_url(); + $user = User::where('email', $this->userEmail)->firstOrFail(); // Use class-level email + + $fakePic = UploadedFile::fake()->image('test_image.jpg', 200, 200); + $this->browse(function (Browser $browser) use ($verificationUrl, $fakePic, $user) { + $browser + ->visit($verificationUrl) + ->attach( + 'profile-picture', + $fakePic->getPathname(), + ) + ->pause(1400) + ->assertVisible('img') + ->press('Set Picture') + ->pause(1000) + ->assertUrlIs(route('inbox')) + ->within(new Sidebar, function (Browser $browser) use ($user) { + $user = User::where('email', $user->email)->firstOrFail(); + $browser + ->assertAttribute('@profile-avatar img', 'src', $user->getProfilePictureUrl()); + }); + }); } } diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index 8364a84..0000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,19 +0,0 @@ -get('/'); - - $response->assertStatus(200); - } -} diff --git a/tests/Feature/Livewire/Task/AddCommentTest.php b/tests/Feature/Livewire/Task/AddCommentTest.php deleted file mode 100644 index 59780f6..0000000 --- a/tests/Feature/Livewire/Task/AddCommentTest.php +++ /dev/null @@ -1,16 +0,0 @@ -assertSee(''); - // } -} diff --git a/tests/Feature/Main/BasicDeletingTasksTest.php b/tests/Main/BasicDeletingTasksTest.php similarity index 99% rename from tests/Feature/Main/BasicDeletingTasksTest.php rename to tests/Main/BasicDeletingTasksTest.php index 08db2da..fe5989c 100644 --- a/tests/Feature/Main/BasicDeletingTasksTest.php +++ b/tests/Main/BasicDeletingTasksTest.php @@ -1,6 +1,6 @@ 'google', @@ -22,5 +22,4 @@ public function test_to_fail_password_filled_with_provider_details_too (): void // Assert that the user is recognized as an OAuth account $this->assertFalse($userOAuth->isOauthAccount()); } - } }