diff --git a/app/Http/Controllers/ProjectNotificationsController.php b/app/Http/Controllers/ProjectNotificationsController.php new file mode 100644 index 0000000000..5d96d50ae6 --- /dev/null +++ b/app/Http/Controllers/ProjectNotificationsController.php @@ -0,0 +1,116 @@ + BitmaskNotificationPreferences::EMAIL_UPDATE, + 'configure' => BitmaskNotificationPreferences::EMAIL_CONFIGURE, + 'warning' => BitmaskNotificationPreferences::EMAIL_WARNING, + 'error' => BitmaskNotificationPreferences::EMAIL_ERROR, + 'test' => BitmaskNotificationPreferences::EMAIL_TEST, + 'dynamicanalysis' => BitmaskNotificationPreferences::EMAIL_DYNAMIC_ANALYSIS, + ]; + + public function show(int $project_id): View + { + $this->setProjectById($project_id); + + /** @var User $user */ + $user = auth()->user(); + + $preferences = self::getPreferences($user, $project_id); + $eloquentProject = Project::findOrFail($project_id); + + $emailCategories = []; + foreach (self::EMAIL_CATEGORIES as $category => $bit) { + if (($preferences->emailcategory & $bit) !== 0) { + $emailCategories[] = $category; + } + } + + return $this->vue('project-notifications-page', 'Notifications', [ + 'project-id' => $eloquentProject->id, + 'project-emails-enabled' => $eloquentProject->emailbrokensubmission, + 'can-edit-project' => $user->can('update', $eloquentProject), + 'email-type' => $preferences->emailtype, + 'email-success' => (bool) $preferences->emailsuccess, + 'email-missing-sites' => (bool) $preferences->emailmissingsites, + 'email-categories' => $emailCategories, + 'message' => session('message', ''), + ]); + } + + /** + * @throws ValidationException + */ + public function update(Request $request, int $project_id): RedirectResponse + { + $this->setProjectById($project_id); + + /** @var User $user */ + $user = $request->user(); + + // Aborts if the user is not a member of the project. + self::getPreferences($user, $project_id); + + $validated = $request->validate([ + 'emailtype' => ['required', 'integer', Rule::in([0, 1, 2, 3])], + 'emailsuccess' => ['boolean'], + 'emailmissingsites' => ['boolean'], + 'emailcategories' => ['array'], + 'emailcategories.*' => ['string', Rule::in(array_keys(self::EMAIL_CATEGORIES))], + ]); + + $emailCategory = 0; + foreach ($validated['emailcategories'] ?? [] as $category) { + $emailCategory |= self::EMAIL_CATEGORIES[$category]; + } + + $user->projects()->updateExistingPivot($project_id, [ + 'emailtype' => (int) $validated['emailtype'], + 'emailcategory' => $emailCategory, + 'emailsuccess' => $request->boolean('emailsuccess'), + 'emailmissingsites' => $request->boolean('emailmissingsites'), + ]); + + return redirect("/projects/{$project_id}/notifications") + ->with('message', 'Your notification preferences have been updated.'); + } + + /** + * Notification preferences are stored on the user's project membership, so only members have any. + * + * @return Pivot&object{emailtype: int, emailcategory: int, emailsuccess: int, emailmissingsites: int} + */ + private static function getPreferences(User $user, int $project_id): Pivot + { + /** @var (Pivot&object{emailtype: int, emailcategory: int, emailsuccess: int, emailmissingsites: int})|null $preferences */ + $preferences = $user->projects() + ->withPivot(['emailtype', 'emailcategory', 'emailsuccess', 'emailmissingsites']) + ->find($project_id) + ?->pivot; + + if ($preferences === null) { + abort(403, 'You must be a member of this project to manage its notifications.'); + } + + return $preferences; + } +} diff --git a/app/Http/Controllers/SubscribeProjectController.php b/app/Http/Controllers/SubscribeProjectController.php deleted file mode 100644 index fb8bd5dc51..0000000000 --- a/app/Http/Controllers/SubscribeProjectController.php +++ /dev/null @@ -1,179 +0,0 @@ -setProjectById((int) $_GET['projectid']); - - $db = Database::getInstance(); - - $user2project = $db->executePreparedSingleRow(' - SELECT role, emailtype, emailcategory, emailmissingsites, emailsuccess - FROM user2project - WHERE userid=? AND projectid=? - ', [$user->id, $this->project->Id]); - if (!empty($user2project)) { - $xml .= add_XML_value('role', $user2project['role']); - $xml .= add_XML_value('emailtype', $user2project['emailtype']); - $xml .= add_XML_value('emailmissingsites', $user2project['emailmissingsites']); - $xml .= add_XML_value('emailsuccess', $user2project['emailsuccess']); - $emailcategory = $user2project['emailcategory']; - $xml .= add_XML_value('emailcategory_update', self::check_email_category('update', $emailcategory)); - $xml .= add_XML_value('emailcategory_configure', self::check_email_category('configure', $emailcategory)); - $xml .= add_XML_value('emailcategory_warning', self::check_email_category('warning', $emailcategory)); - $xml .= add_XML_value('emailcategory_error', self::check_email_category('error', $emailcategory)); - $xml .= add_XML_value('emailcategory_test', self::check_email_category('test', $emailcategory)); - $xml .= add_XML_value('emailcategory_dynamicanalysis', self::check_email_category('dynamicanalysis', $emailcategory)); - } else { - // we set the default categories - $xml .= add_XML_value('emailcategory_update', 1); - $xml .= add_XML_value('emailcategory_configure', 1); - $xml .= add_XML_value('emailcategory_warning', 1); - $xml .= add_XML_value('emailcategory_error', 1); - $xml .= add_XML_value('emailcategory_test', 1); - $xml .= add_XML_value('emailcategory_dynamicanalysis', 1); - } - - // If we ask to subscribe - @$UpdateSubscription = $_POST['updatesubscription']; - @$EmailType = $_POST['emailtype']; - if (!isset($_POST['emailmissingsites'])) { - $EmailMissingSites = 0; - } else { - $EmailMissingSites = $_POST['emailmissingsites']; - } - if (!isset($_POST['emailsuccess'])) { - $EmailSuccess = 0; - } else { - $EmailSuccess = $_POST['emailsuccess']; - } - - if ($UpdateSubscription) { - $emailcategory_update = (int) ($_POST['emailcategory_update'] ?? 0); - $emailcategory_configure = (int) ($_POST['emailcategory_configure'] ?? 0); - $emailcategory_warning = (int) ($_POST['emailcategory_warning'] ?? 0); - $emailcategory_error = (int) ($_POST['emailcategory_error'] ?? 0); - $emailcategory_test = (int) ($_POST['emailcategory_test'] ?? 0); - $emailcategory_dynamicanalysis = (int) ($_POST['emailcategory_dynamicanalysis'] ?? 0); - - $EmailCategory = $emailcategory_update + $emailcategory_configure + $emailcategory_warning + $emailcategory_error + $emailcategory_test + $emailcategory_dynamicanalysis; - if (!empty($user2project)) { - $db->executePrepared(' - UPDATE user2project - SET - emailtype=?, - emailcategory=?, - emailmissingsites=?, - emailsuccess=? - WHERE - userid=? - AND projectid=? - ', [ - $EmailType, - $EmailCategory, - $EmailMissingSites, - $EmailSuccess, - $user->id, - $this->project->Id, - ]); - } - - // Redirect - return redirect('/user'); - } - - $xml .= ''; - $xml .= add_XML_value('id', $this->project->Id); - $xml .= add_XML_value('name', $this->project->Name); - $xml .= add_XML_value('emailbrokensubmission', $this->project->EmailBrokenSubmission); - - $xml .= ''; - - foreach (Project::whereRelation('users', 'id', $user->id)->orderBy('name')->get() as $project) { - $xml .= ''; - $xml .= add_XML_value('id', $project->id); - $xml .= add_XML_value('name', $project->name); - if ((int) $project->id === $this->project->Id) { - $xml .= add_XML_value('selected', '1'); - } - $xml .= ''; - } - - $xml .= ''; - - return $this->view('cdash', 'Subscription Settings') - ->with('xsl', true) - ->with('xsl_content', generate_XSLT($xml, base_path() . '/app/cdash/public/subscribeProject', true)); - } - - /** - * Check the email category - */ - private static function check_email_category(string $name, int $emailcategory): bool - { - if ($emailcategory >= 64) { - if ($name === 'dynamicanalysis') { - return true; - } - $emailcategory -= 64; - } - - if ($emailcategory >= 32) { - if ($name === 'test') { - return true; - } - $emailcategory -= 32; - } - - if ($emailcategory >= 16) { - if ($name === 'error') { - return true; - } - $emailcategory -= 16; - } - - if ($emailcategory >= 8) { - if ($name === 'warning') { - return true; - } - $emailcategory -= 8; - } - - if ($emailcategory >= 4) { - if ($name === 'configure') { - return true; - } - $emailcategory -= 4; - } - - if ($emailcategory >= 2) { - if ($name === 'update') { - return true; - } - } - return false; - } -} diff --git a/app/cdash/public/subscribeProject.xsl b/app/cdash/public/subscribeProject.xsl deleted file mode 100644 index bf5190c33f..0000000000 --- a/app/cdash/public/subscribeProject.xsl +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -
Project:
- -
- subscribeProject.php?projectid= - -
-
- - - -
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*This project has not been configured to send emails. - - - project//settings#EmailChange the project settings. - - Contact the project administrator. - -
Email me:
- - - - never (this is not recommended) -
- - - - when my checkins are causing problems in any sections of the dashboard -
- - - - when any checkins are causing problems in the Nightly section of the dashboard -
- - when any checkins are causing problems in any sections of the dashboard -
- - - - when my checkins are fixing build errors, warnings or tests -
- - - - when expected sites are not submitting -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*This project has not been configured to send emails. - - - project//settings#EmailChange the project settings. - - Contact the project administrator. - -
- - - - Update -
- - - - Configure -
- - - - Warning -
- - - - Error -
- - - - Test -
- - - - Dynamic Analysis -
-
-
- -
-
- - - - -
-
-
- -
-
-
-
diff --git a/app/cdash/tests/CMakeLists.txt b/app/cdash/tests/CMakeLists.txt index 480e06a336..54d3c9fcc5 100644 --- a/app/cdash/tests/CMakeLists.txt +++ b/app/cdash/tests/CMakeLists.txt @@ -195,6 +195,8 @@ add_feature_test_in_transaction(/Feature/ProjectInvitationAcceptanceTest) add_feature_test_in_transaction(/Feature/UpdateProjectLogoTest) +add_feature_test_in_transaction(/Feature/ProjectNotificationsTest) + add_feature_test_in_transaction(/Feature/GraphQL/FilterTest) add_feature_test_in_transaction(/Feature/GraphQL/Queries/QueryTypeTest) @@ -408,6 +410,8 @@ add_browser_test(/Browser/Pages/ProjectSitesPageTest) add_browser_test(/Browser/Pages/ProjectMembersPageTest) +add_browser_test(/Browser/Pages/ProjectNotificationsPageTest) + add_browser_test(/Browser/Pages/ProjectBuildsPageTest) add_browser_test(/Browser/Pages/CreateProjectPageTest) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 45f7a560d8..2682ccde58 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1404,42 +1404,6 @@ parameters: count: 3 path: app/Http/Controllers/SubmissionController.php - - - rawMessage: ''' - Call to deprecated method executePrepared() of class CDash\Database: - 04/22/2023 Use Laravel query builder or Eloquent instead - ''' - identifier: method.deprecated - count: 1 - path: app/Http/Controllers/SubscribeProjectController.php - - - - rawMessage: ''' - Call to deprecated method executePreparedSingleRow() of class CDash\Database: - 04/22/2023 Use Laravel query builder or Eloquent instead - ''' - identifier: method.deprecated - count: 1 - path: app/Http/Controllers/SubscribeProjectController.php - - - - rawMessage: Cannot cast mixed to int. - identifier: cast.int - count: 6 - path: app/Http/Controllers/SubscribeProjectController.php - - - - rawMessage: 'Construct empty() is not allowed. Use more strict comparison.' - identifier: empty.notAllowed - count: 2 - path: app/Http/Controllers/SubscribeProjectController.php - - - - rawMessage: 'Only booleans are allowed in an if condition, mixed given.' - identifier: if.condNotBoolean - count: 1 - path: app/Http/Controllers/SubscribeProjectController.php - - rawMessage: Cannot cast mixed to int. identifier: cast.int diff --git a/resources/js/vue/app.js b/resources/js/vue/app.js index 1852d56b5c..83da73126e 100755 --- a/resources/js/vue/app.js +++ b/resources/js/vue/app.js @@ -27,6 +27,7 @@ const app = createApp({ ProjectSitesPage: defineAsyncComponent(() => import('./components/ProjectSitesPage.vue')), SitesIdPage: defineAsyncComponent(() => import('./components/SitesIdPage.vue')), ProjectMembersPage: defineAsyncComponent(() => import('./components/ProjectMembersPage.vue')), + ProjectNotificationsPage: defineAsyncComponent(() => import('./components/ProjectNotificationsPage.vue')), UsersPage: defineAsyncComponent(() => import('./components/UsersPage.vue')), BuildFilesPage: defineAsyncComponent(() => import('./components/BuildFilesPage.vue')), BuildTargetsPage: defineAsyncComponent(() => import('./components/BuildTargetsPage.vue')), diff --git a/resources/js/vue/components/ProjectNotificationsPage.vue b/resources/js/vue/components/ProjectNotificationsPage.vue new file mode 100644 index 0000000000..79fc11e873 --- /dev/null +++ b/resources/js/vue/components/ProjectNotificationsPage.vue @@ -0,0 +1,221 @@ + + + diff --git a/resources/views/components/header.blade.php b/resources/views/components/header.blade.php old mode 100755 new mode 100644 index 189f9a8530..b6aa0d23a5 --- a/resources/views/components/header.blade.php +++ b/resources/views/components/header.blade.php @@ -210,7 +210,7 @@ @endif @if($userInProject)
  • - + Id}/notifications") }}"> Notifications
  • diff --git a/routes/web.php b/routes/web.php index 99a62b6c3f..8a756347ee 100755 --- a/routes/web.php +++ b/routes/web.php @@ -25,7 +25,6 @@ use App\Http\Controllers\ProjectSettingsController; use App\Http\Controllers\SiteController; use App\Http\Controllers\SubmissionController; -use App\Http\Controllers\SubscribeProjectController; use App\Http\Controllers\UpdateProjectLogoController; use App\Http\Controllers\UsersController; use App\Http\Controllers\ViewProjectsController; @@ -311,9 +310,17 @@ Route::match(['get', 'post'], '/profile', 'UserController@edit'); Route::permanentRedirect('/editUser.php', url('/profile')); - // TODO: (williamjallen) send the POST route to a different function - Route::get('/subscribeProject.php', SubscribeProjectController::class); - Route::post('/subscribeProject.php', SubscribeProjectController::class); + Route::get('/projects/{project_id}/notifications', 'ProjectNotificationsController@show') + ->whereNumber('project_id'); + Route::post('/projects/{project_id}/notifications', 'ProjectNotificationsController@update') + ->whereNumber('project_id'); + Route::get('/subscribeProject.php', function (Request $request) { + $projectid = $request->query('projectid'); + if (!is_numeric($projectid)) { + abort(400, 'Not a valid projectid!'); + } + return redirect("/projects/{$projectid}/notifications", 301); + }); Route::get('/manageProjectRoles.php', function (Request $request) { if (!$request->has('projectid')) { diff --git a/tests/Browser/Pages/ProjectNotificationsPageTest.php b/tests/Browser/Pages/ProjectNotificationsPageTest.php new file mode 100644 index 0000000000..9edea7c28b --- /dev/null +++ b/tests/Browser/Pages/ProjectNotificationsPageTest.php @@ -0,0 +1,130 @@ +user = User::factory()->create(); + + $this->project = $this->makePublicProject(); + $this->project->users()->attach($this->user, [ + 'role' => ProjectRole::USER, + 'emailtype' => 1, + 'emailcategory' => 2 + 4, + 'emailsuccess' => false, + 'emailmissingsites' => true, + ]); + } + + public function tearDown(): void + { + $this->project->delete(); + $this->user->delete(); + + parent::tearDown(); + } + + public function testIsProtectedByLogin(): void + { + $this->browse(function (Browser $browser): void { + $browser->visit("/projects/{$this->project->id}/notifications") + ->assertUrlIs(config('app.url') . '/login'); + }); + } + + public function testHeaderLinksToPage(): void + { + $this->browse(function (Browser $browser): void { + $browser->loginAs($this->user) + ->visit("/projects/{$this->project->id}/members") + ->waitFor('@project-members-page') + ->assertAttribute('a[href$="/notifications"]', 'href', url("/projects/{$this->project->id}/notifications")); + }); + } + + public function testShowsAndUpdatesPreferences(): void + { + $this->browse(function (Browser $browser): void { + $browser->loginAs($this->user) + ->visit("/projects/{$this->project->id}/notifications") + ->waitFor('@project-notifications-page') + ->assertMissing('@message') + ->assertMissing('@project-emails-disabled-warning') + ->assertRadioSelected('emailtype', '1') + ->assertNotChecked('@email-success') + ->assertChecked('@email-missing-sites') + ->assertChecked('@email-category-update') + ->assertChecked('@email-category-configure') + ->assertNotChecked('@email-category-warning') + ->assertNotChecked('@email-category-error') + ->assertNotChecked('@email-category-test') + ->assertNotChecked('@email-category-dynamicanalysis') + ->assertMissing('@unsaved-changes') + + ->radio('emailtype', '2') + ->check('@email-success') + ->uncheck('@email-missing-sites') + ->uncheck('@email-category-update') + ->check('@email-category-test') + ->assertVisible('@unsaved-changes') + ->click('@save-button') + + ->waitForTextIn('@message', 'Your notification preferences have been updated.') + ->assertRadioSelected('emailtype', '2') + ->assertChecked('@email-success') + ->assertNotChecked('@email-missing-sites') + ->assertNotChecked('@email-category-update') + ->assertChecked('@email-category-configure') + ->assertChecked('@email-category-test') + ->assertMissing('@unsaved-changes'); + }); + + $row = DB::table('user2project') + ->where('userid', $this->user->id) + ->where('projectid', $this->project->id) + ->first(); + self::assertNotNull($row); + self::assertSame(2, (int) $row->emailtype); + self::assertSame(4 + 32, (int) $row->emailcategory); + self::assertSame(1, (int) $row->emailsuccess); + self::assertSame(0, (int) $row->emailmissingsites); + } + + public function testShowsWarningWhenProjectEmailsDisabled(): void + { + $this->project->emailbrokensubmission = false; + $this->project->save(); + + $this->browse(function (Browser $browser): void { + $browser->loginAs($this->user) + ->visit("/projects/{$this->project->id}/notifications") + ->waitFor('@project-emails-disabled-warning') + ->assertSeeIn('@project-emails-disabled-warning', 'Contact the project administrator.'); + }); + + $this->project->users()->updateExistingPivot($this->user->id, ['role' => ProjectRole::ADMINISTRATOR]); + + $this->browse(function (Browser $browser): void { + $browser->visit("/projects/{$this->project->id}/notifications") + ->waitFor('@project-emails-disabled-warning') + ->assertSeeLink('Change the project settings.'); + }); + } +} diff --git a/tests/Feature/ProjectNotificationsTest.php b/tests/Feature/ProjectNotificationsTest.php new file mode 100644 index 0000000000..320dbf2a63 --- /dev/null +++ b/tests/Feature/ProjectNotificationsTest.php @@ -0,0 +1,205 @@ +project = $this->makePublicProject(); + $this->user = User::factory()->create(); + $this->project->users()->attach($this->user, [ + 'role' => ProjectRole::USER, + 'emailtype' => 1, + 'emailcategory' => 2 + 16 + 64, + 'emailsuccess' => true, + 'emailmissingsites' => false, + ]); + } + + /** + * @return array + */ + private function getPreferences(User $user): array + { + $row = DB::table('user2project') + ->where('userid', $user->id) + ->where('projectid', $this->project->id) + ->first(['emailtype', 'emailcategory', 'emailsuccess', 'emailmissingsites']); + self::assertNotNull($row); + + return [ + 'emailtype' => (int) $row->emailtype, + 'emailcategory' => (int) $row->emailcategory, + 'emailsuccess' => (int) $row->emailsuccess, + 'emailmissingsites' => (int) $row->emailmissingsites, + ]; + } + + /** + * @param TestResponse $response + * + * @return array + */ + private static function getProps(TestResponse $response): array + { + /** @var array $props */ + $props = $response->assertOk()->viewData('props'); + self::assertIsArray($props); + return $props; + } + + public function testRequiresLogin(): void + { + $this->get("/projects/{$this->project->id}/notifications")->assertRedirect('login'); + $this->post("/projects/{$this->project->id}/notifications", ['emailtype' => 0])->assertRedirect('login'); + } + + public function testShowsCurrentPreferences(): void + { + $this->project->emailbrokensubmission = false; + $this->project->save(); + + $props = self::getProps($this->actingAs($this->user)->get("/projects/{$this->project->id}/notifications")); + + self::assertSame($this->project->id, $props['project-id']); + self::assertFalse($props['project-emails-enabled']); + self::assertFalse($props['can-edit-project']); + self::assertSame(1, $props['email-type']); + self::assertTrue($props['email-success']); + self::assertFalse($props['email-missing-sites']); + self::assertSame(['update', 'error', 'dynamicanalysis'], $props['email-categories']); + self::assertSame('', $props['message']); + } + + public function testProjectAdminCanEditProject(): void + { + $admin = User::factory()->create(); + $this->project->users()->attach($admin, ['role' => ProjectRole::ADMINISTRATOR]); + + $props = self::getProps($this->actingAs($admin)->get("/projects/{$this->project->id}/notifications")); + + self::assertTrue($props['can-edit-project']); + self::assertTrue($props['project-emails-enabled']); + } + + public function testNonMembersCannotViewOrUpdatePreferences(): void + { + $non_member = User::factory()->create(); + + $this->actingAs($non_member) + ->get("/projects/{$this->project->id}/notifications") + ->assertForbidden(); + $this->actingAs($non_member) + ->post("/projects/{$this->project->id}/notifications", ['emailtype' => 3]) + ->assertForbidden(); + + self::assertFalse($non_member->projects()->whereKey($this->project->id)->exists()); + } + + public function testCannotViewInaccessibleProject(): void + { + $private_project = $this->makePrivateProject(); + + $this->actingAs($this->user) + ->get("/projects/{$private_project->id}/notifications") + ->assertNotFound(); + $this->actingAs($this->user) + ->post("/projects/{$private_project->id}/notifications", ['emailtype' => 3]) + ->assertNotFound(); + $this->actingAs($this->user) + ->get('/projects/123456789/notifications') + ->assertNotFound(); + } + + public function testUpdatesPreferences(): void + { + $other_user = User::factory()->create(); + $this->project->users()->attach($other_user, ['role' => ProjectRole::USER]); + $other_user_preferences = $this->getPreferences($other_user); + + $this->actingAs($this->user) + ->post("/projects/{$this->project->id}/notifications", [ + 'emailtype' => '3', + 'emailmissingsites' => '1', + 'emailcategories' => ['configure', 'warning', 'test'], + ]) + ->assertRedirect("/projects/{$this->project->id}/notifications") + ->assertSessionHas('message', 'Your notification preferences have been updated.'); + + self::assertSame([ + 'emailtype' => 3, + 'emailcategory' => 4 + 8 + 32, + 'emailsuccess' => 0, + 'emailmissingsites' => 1, + ], $this->getPreferences($this->user)); + self::assertSame($other_user_preferences, $this->getPreferences($other_user)); + + $props = self::getProps($this->actingAs($this->user)->get("/projects/{$this->project->id}/notifications")); + self::assertSame(3, $props['email-type']); + self::assertSame(['configure', 'warning', 'test'], $props['email-categories']); + self::assertSame('Your notification preferences have been updated.', $props['message']); + } + + public function testClearsAllCategories(): void + { + $this->actingAs($this->user) + ->post("/projects/{$this->project->id}/notifications", ['emailtype' => '0']) + ->assertRedirect("/projects/{$this->project->id}/notifications"); + + self::assertSame([ + 'emailtype' => 0, + 'emailcategory' => 0, + 'emailsuccess' => 0, + 'emailmissingsites' => 0, + ], $this->getPreferences($this->user)); + } + + public function testRejectsInvalidPreferences(): void + { + $original_preferences = $this->getPreferences($this->user); + + foreach ([ + [], + ['emailtype' => '4'], + ['emailtype' => 'abc'], + ['emailtype' => '1', 'emailsuccess' => 'abc'], + ['emailtype' => '1', 'emailcategories' => 'test'], + ['emailtype' => '1', 'emailcategories' => ['coverage']], + ] as $input) { + $this->actingAs($this->user) + ->post("/projects/{$this->project->id}/notifications", $input) + ->assertSessionHasErrors(); + } + + self::assertSame($original_preferences, $this->getPreferences($this->user)); + } + + public function testLegacyUrlRedirects(): void + { + $this->actingAs($this->user) + ->get("/subscribeProject.php?projectid={$this->project->id}") + ->assertRedirect("/projects/{$this->project->id}/notifications"); + $this->actingAs($this->user) + ->get('/subscribeProject.php') + ->assertStatus(400); + } +}