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 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());
}
- }
}