Skip to content

Commit cb04222

Browse files
committed
Adds AuthController tests
1 parent 3eaf696 commit cb04222

File tree

16 files changed

+227
-37
lines changed

16 files changed

+227
-37
lines changed

.github/workflows/run-tests.yml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@ name: run-tests
22

33
on:
44
push:
5-
branches:
6-
- main
5+
branches: [main, next]
76
pull_request:
8-
branches:
9-
- main
7+
branches: [main, next]
108

119
jobs:
1210
test:
1311
runs-on: ubuntu-latest
1412

1513
steps:
16-
- uses: actions/checkout@v3
14+
- uses: actions/checkout@v4
1715

1816
- name: Setup PHP
1917
uses: shivammathur/setup-php@v2
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace App\Exceptions;
4+
5+
class InvalidAccessTokenException extends \RuntimeException
6+
{
7+
}

app/Http/Controllers/AuthController.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use App\Models\User;
88
use Illuminate\Http\Request;
9+
use Illuminate\Validation\Rule;
910
use Socialite;
1011

1112
class AuthController extends Controller
@@ -17,6 +18,10 @@ public function show()
1718

1819
public function redirectToProvider(Request $request)
1920
{
21+
$request->validate([
22+
'scope' => ['nullable', 'string', Rule::in(['read:user', 'public_repo'])],
23+
]);
24+
2025
$scope = $request->input('scope', 'read:user');
2126
$request->session()->put(['auth_scope' => $scope]);
2227

@@ -61,9 +66,8 @@ public function logout(Request $request)
6166
auth()->logout();
6267

6368
$request->session()->invalidate();
64-
6569
$request->session()->regenerateToken();
6670

67-
// return redirect(route('auth.show'));
71+
return hybridly()->external(route('auth.show'));
6872
}
6973
}

app/Http/Controllers/UserController.php

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,6 @@
66

77
class UserController extends Controller
88
{
9-
public function revokeGrant()
10-
{
11-
auth()->user()->revokeGrant();
12-
13-
return hybridly()->external(route('auth.destroy'));
14-
}
15-
169
public function destroy()
1710
{
1811
$user = auth()->user();

app/Models/User.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,12 @@ public function revokeGrant(): self
9090
->withHeaders(['Accept' => 'application/vnd.github.v3+json'])
9191
->delete("https://api.github.com/applications/{$clientId}/grant", ['access_token' => $this->access_token]);
9292

93-
$this->update(['access_token' => null]);
94-
9593
if ($response->getStatusCode() == 404) {
9694
throw new InvalidAccessTokenException();
9795
}
9896

97+
$this->update(['access_token' => null]);
98+
9999
return $this;
100100
}
101101

app/Providers/RouteServiceProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class RouteServiceProvider extends ServiceProvider
1919
*
2020
* @var string
2121
*/
22-
public const HOME = '/home';
22+
public const HOME = '/';
2323

2424
/**
2525
* Define your route model bindings, pattern filters, and other route configuration.

phpunit.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
<env name="APP_ENV" value="testing"/>
2222
<env name="BCRYPT_ROUNDS" value="4"/>
2323
<env name="CACHE_DRIVER" value="array"/>
24-
<env name="DB_DATABASE" value="testing"/>
24+
<env name="DB_CONNECTION" value="sqlite"/>
25+
<env name="DB_DATABASE" value=":memory:"/>
2526
<env name="MAIL_MAILER" value="array"/>
2627
<env name="QUEUE_CONNECTION" value="sync"/>
2728
<env name="SESSION_DRIVER" value="array"/>

routes/web.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@
3030
|
3131
*/
3232

33-
Route::get('auth', [AuthController::class, 'show'])->name('auth.show');
34-
Route::get('auth/github', [AuthController::class, 'redirectToProvider'])->name('github.auth');
35-
Route::get('auth/github/callback', [AuthController::class, 'handleProviderCallback'])->name('github.callback');
33+
Route::group(['middleware' => 'guest'], function () {
34+
Route::get('auth', [AuthController::class, 'show'])->name('auth.show');
35+
Route::get('auth/github', [AuthController::class, 'redirectToProvider'])->name('github.auth');
36+
Route::get('auth/github/callback', [AuthController::class, 'handleProviderCallback'])->name('github.callback');
37+
});
38+
3639
Route::get('logout', [AuthController::class, 'logout'])
3740
->middleware('auth')
3841
->name('auth.destroy');
@@ -66,6 +69,6 @@
6669
Route::put('openai-token', OpenAiTokenController::class)->name('openai-token.update');
6770
Route::post('openai-summary', OpenAiReadmeSummaryController::class)->name('openai-summary.fetch');
6871

69-
Route::post('/revoke-grant', [UserController::class, 'revokeGrant'])->name('revoke-grant');
72+
Route::post('/revoke-grant', [AuthController::class, 'revokeGrant'])->name('revoke-grant');
7073
Route::delete('/user', [UserController::class, 'destroy'])->name('user.destroy');
7174
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use App\Models\User;
6+
use App\Providers\RouteServiceProvider;
7+
use Laravel\Socialite\Facades\Socialite;
8+
9+
it('creates a new user if the user doesn\'t exist and logs them in', function () {
10+
mockSocialiteFacade();
11+
12+
$this->assertDatabaseMissing(User::class, [
13+
'github_id' => 1234567890,
14+
]);
15+
16+
session()->put('auth_scope', 'read:user');
17+
18+
$this->get('/auth/github/callback')->assertRedirect(RouteServiceProvider::HOME);
19+
20+
$this->assertAuthenticated();
21+
22+
$this->assertDatabaseHas(User::class, [
23+
'github_id' => 1234567890,
24+
'username' => 'JaneDoe',
25+
'name' => 'Jane Doe',
26+
'avatar' => 'https://en.gravatar.com/userimage',
27+
'scope' => 'read:user',
28+
]);
29+
});
30+
31+
it('updates the user\'s info and logins them in if they already exist', function () {
32+
mockSocialiteFacade();
33+
34+
$user = User::factory()->create([
35+
'github_id' => 1234567890,
36+
'username' => 'OldUsername',
37+
'name' => 'Old Name',
38+
'avatar' => 'https://old.gravatar.com/userimage',
39+
'scope' => 'read:user',
40+
]);
41+
42+
session()->put('auth_scope', 'read:user');
43+
44+
$this->get('/auth/github/callback')->assertRedirect(RouteServiceProvider::HOME);
45+
46+
$this->assertAuthenticated();
47+
48+
$this->assertDatabaseHas(User::class, [
49+
'github_id' => 1234567890,
50+
'username' => 'JaneDoe',
51+
'name' => 'Jane Doe',
52+
'avatar' => 'https://en.gravatar.com/userimage',
53+
'scope' => 'read:user',
54+
]);
55+
56+
expect(User::count())->toBe(1);
57+
});
58+
59+
it('redirects authenticated users back to the dashboard')
60+
->login()
61+
->get('/auth/github/callback')
62+
->assertRedirect(RouteServiceProvider::HOME);
63+
64+
// Helpers
65+
function mockSocialiteFacade()
66+
{
67+
$abstractUser = Mockery::mock(Laravel\Socialite\Two\User::class);
68+
$abstractUser->shouldReceive('getId')
69+
->andReturn(1234567890)
70+
->shouldReceive('getNickname')
71+
->andReturn('JaneDoe')
72+
->shouldReceive('getName')
73+
->andReturn('Jane Doe')
74+
->shouldReceive('getAvatar')
75+
->andReturn('https://en.gravatar.com/userimage');
76+
$abstractUser->token = 'abcde12345';
77+
78+
$provider = Mockery::mock(Laravel\Socialite\Contracts\Provider::class);
79+
$provider->shouldReceive('user')->andReturn($abstractUser);
80+
81+
Socialite::shouldReceive('driver')->with('github')->andReturn($provider);
82+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
it('logs out an authenticated user', function() {
4+
$this->login()
5+
->get('/logout')
6+
->assertRedirect(route('auth.show'));
7+
8+
$this->assertGuest();
9+
});
10+
11+
it('redirects guest users back to the login page')
12+
->get('/logout')
13+
->assertRedirect('/login');
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use App\Providers\RouteServiceProvider;
6+
7+
it('validates the scope if present', function (array $badData, array|string $errors) {
8+
$this
9+
->get(route('github.auth', $badData))
10+
->assertInvalid($errors);
11+
})->with([
12+
[['scope' => 'admin:org'], 'scope'],
13+
[['scope' => 'repo'], 'scope'],
14+
[['scope' => 'user'], 'scope'],
15+
]);
16+
17+
it('stores a valid scope in the current session', function (string $scope) {
18+
$this
19+
->get(route('github.auth', ['scope' => $scope]))
20+
->assertSessionHas('auth_scope', $scope);
21+
})->with(['read:user', 'public_repo']);
22+
23+
it('defaults to the `read:user` scope if no scope is provided', function () {
24+
$this
25+
->get(route('github.auth', ['scope' => null]))
26+
->assertSessionHas('auth_scope', 'read:user');
27+
});
28+
29+
it('redirects to the auth provider when a valid scope is present', function (?string $scope) {
30+
// TODO: Can we perform some assertions on what we passed to Socialite?
31+
$this->get(route('github.auth', ['scope' => $scope]))->assertRedirect();
32+
})->with(['read:user', 'public_repo', null]);
33+
34+
it('redirects authenticated users back to the dashboard')
35+
->login()
36+
->get('/auth/github')
37+
->assertRedirect(RouteServiceProvider::HOME);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Support\Facades\Http;
6+
use App\Exceptions\InvalidAccessTokenException;
7+
8+
it('sends an API reqest to GitHub to revoke the user\'s access token', function () {
9+
Http::fake([
10+
'api.github.com/*' => Http::response('ok', 200),
11+
]);
12+
13+
$this
14+
->login()
15+
->post('/revoke-grant')->assertRedirect(route('auth.destroy'));
16+
17+
expect(auth()->user()->access_token)->toBeNull();
18+
});
19+
20+
it('throws an InvalidAccessTokenException if the api request comes back with a 404', function () {
21+
$this->withoutExceptionHandling();
22+
23+
Http::fake([
24+
'api.github.com/*' => Http::response('not-found', 404),
25+
]);
26+
27+
$this
28+
->login()
29+
->post('/revoke-grant');
30+
31+
expect(auth()->user()->access_token)->not->toBeNull();
32+
})->throws(InvalidAccessTokenException::class);
33+
34+
it('redirects guest users back to the login page')
35+
->post('/revoke-grant')
36+
->assertRedirect('/login');
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use App\Providers\RouteServiceProvider;
6+
7+
it('renders the login page for unauthenticated users')
8+
->get('/auth')
9+
->assertStatus(200)
10+
->assertHybridView('auth');
11+
12+
it('redirects authenticated users back to the dashboard')
13+
->login()
14+
->get('/auth')
15+
->assertRedirect(RouteServiceProvider::HOME);

tests/Feature/ExampleTest.php

Lines changed: 0 additions & 9 deletions
This file was deleted.

tests/Pest.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
declare(strict_types=1);
44

5+
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
6+
57
/*
68
|--------------------------------------------------------------------------
79
| Test Case
@@ -15,7 +17,7 @@
1517

1618
uses(
1719
Tests\TestCase::class,
18-
// Illuminate\Foundation\Testing\RefreshDatabase::class,
20+
LazilyRefreshDatabase::class
1921
)->in('Feature');
2022

2123
/*
@@ -29,9 +31,9 @@
2931
|
3032
*/
3133

32-
expect()->extend('toBeOne', function () {
33-
return $this->toBe(1);
34-
});
34+
// expect()->extend('toBeOne', function () {
35+
// return $this->toBe(1);
36+
// });
3537

3638
/*
3739
|--------------------------------------------------------------------------

tests/TestCase.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44

55
namespace Tests;
66

7-
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
7+
use App\Models\User;
88
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
99
use Illuminate\Support\Facades\Http;
1010

1111
abstract class TestCase extends BaseTestCase
1212
{
1313
use CreatesApplication;
14-
use LazilyRefreshDatabase;
1514

1615
protected function setUp(): void
1716
{
@@ -21,4 +20,13 @@ protected function setUp(): void
2120

2221
Http::preventStrayRequests();
2322
}
23+
24+
protected function login(User $user = null)
25+
{
26+
$user ??= User::factory()->create()->first();
27+
28+
$this->actingAs($user);
29+
30+
return $this;
31+
}
2432
}

0 commit comments

Comments
 (0)