Skip to content

Commit

Permalink
Adds AuthController tests
Browse files Browse the repository at this point in the history
  • Loading branch information
syropian committed Mar 9, 2024
1 parent 3eaf696 commit cb04222
Show file tree
Hide file tree
Showing 16 changed files with 227 additions and 37 deletions.
8 changes: 3 additions & 5 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ name: run-tests

on:
push:
branches:
- main
branches: [main, next]
pull_request:
branches:
- main
branches: [main, next]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down
7 changes: 7 additions & 0 deletions app/Exceptions/InvalidAccessTokenException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace App\Exceptions;

class InvalidAccessTokenException extends \RuntimeException
{
}
8 changes: 6 additions & 2 deletions app/Http/Controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Socialite;

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

public function redirectToProvider(Request $request)
{
$request->validate([
'scope' => ['nullable', 'string', Rule::in(['read:user', 'public_repo'])],
]);

$scope = $request->input('scope', 'read:user');
$request->session()->put(['auth_scope' => $scope]);

Expand Down Expand Up @@ -61,9 +66,8 @@ public function logout(Request $request)
auth()->logout();

$request->session()->invalidate();

$request->session()->regenerateToken();

// return redirect(route('auth.show'));
return hybridly()->external(route('auth.show'));
}
}
7 changes: 0 additions & 7 deletions app/Http/Controllers/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@

class UserController extends Controller
{
public function revokeGrant()
{
auth()->user()->revokeGrant();

return hybridly()->external(route('auth.destroy'));
}

public function destroy()
{
$user = auth()->user();
Expand Down
4 changes: 2 additions & 2 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ public function revokeGrant(): self
->withHeaders(['Accept' => 'application/vnd.github.v3+json'])
->delete("https://api.github.com/applications/{$clientId}/grant", ['access_token' => $this->access_token]);

$this->update(['access_token' => null]);

if ($response->getStatusCode() == 404) {
throw new InvalidAccessTokenException();
}

$this->update(['access_token' => null]);

return $this;
}

Expand Down
2 changes: 1 addition & 1 deletion app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class RouteServiceProvider extends ServiceProvider
*
* @var string
*/
public const HOME = '/home';
public const HOME = '/';

/**
* Define your route model bindings, pattern filters, and other route configuration.
Expand Down
3 changes: 2 additions & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="DB_DATABASE" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
Expand Down
11 changes: 7 additions & 4 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@
|
*/

Route::get('auth', [AuthController::class, 'show'])->name('auth.show');
Route::get('auth/github', [AuthController::class, 'redirectToProvider'])->name('github.auth');
Route::get('auth/github/callback', [AuthController::class, 'handleProviderCallback'])->name('github.callback');
Route::group(['middleware' => 'guest'], function () {
Route::get('auth', [AuthController::class, 'show'])->name('auth.show');
Route::get('auth/github', [AuthController::class, 'redirectToProvider'])->name('github.auth');
Route::get('auth/github/callback', [AuthController::class, 'handleProviderCallback'])->name('github.callback');
});

Route::get('logout', [AuthController::class, 'logout'])
->middleware('auth')
->name('auth.destroy');
Expand Down Expand Up @@ -66,6 +69,6 @@
Route::put('openai-token', OpenAiTokenController::class)->name('openai-token.update');
Route::post('openai-summary', OpenAiReadmeSummaryController::class)->name('openai-summary.fetch');

Route::post('/revoke-grant', [UserController::class, 'revokeGrant'])->name('revoke-grant');
Route::post('/revoke-grant', [AuthController::class, 'revokeGrant'])->name('revoke-grant');
Route::delete('/user', [UserController::class, 'destroy'])->name('user.destroy');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

use App\Models\User;
use App\Providers\RouteServiceProvider;
use Laravel\Socialite\Facades\Socialite;

it('creates a new user if the user doesn\'t exist and logs them in', function () {
mockSocialiteFacade();

$this->assertDatabaseMissing(User::class, [
'github_id' => 1234567890,
]);

session()->put('auth_scope', 'read:user');

$this->get('/auth/github/callback')->assertRedirect(RouteServiceProvider::HOME);

$this->assertAuthenticated();

$this->assertDatabaseHas(User::class, [
'github_id' => 1234567890,
'username' => 'JaneDoe',
'name' => 'Jane Doe',
'avatar' => 'https://en.gravatar.com/userimage',
'scope' => 'read:user',
]);
});

it('updates the user\'s info and logins them in if they already exist', function () {
mockSocialiteFacade();

$user = User::factory()->create([
'github_id' => 1234567890,
'username' => 'OldUsername',
'name' => 'Old Name',
'avatar' => 'https://old.gravatar.com/userimage',
'scope' => 'read:user',
]);

session()->put('auth_scope', 'read:user');

$this->get('/auth/github/callback')->assertRedirect(RouteServiceProvider::HOME);

$this->assertAuthenticated();

$this->assertDatabaseHas(User::class, [
'github_id' => 1234567890,
'username' => 'JaneDoe',
'name' => 'Jane Doe',
'avatar' => 'https://en.gravatar.com/userimage',
'scope' => 'read:user',
]);

expect(User::count())->toBe(1);
});

it('redirects authenticated users back to the dashboard')
->login()
->get('/auth/github/callback')
->assertRedirect(RouteServiceProvider::HOME);

// Helpers
function mockSocialiteFacade()
{
$abstractUser = Mockery::mock(Laravel\Socialite\Two\User::class);
$abstractUser->shouldReceive('getId')
->andReturn(1234567890)
->shouldReceive('getNickname')
->andReturn('JaneDoe')
->shouldReceive('getName')
->andReturn('Jane Doe')
->shouldReceive('getAvatar')
->andReturn('https://en.gravatar.com/userimage');
$abstractUser->token = 'abcde12345';

$provider = Mockery::mock(Laravel\Socialite\Contracts\Provider::class);
$provider->shouldReceive('user')->andReturn($abstractUser);

Socialite::shouldReceive('driver')->with('github')->andReturn($provider);
}
13 changes: 13 additions & 0 deletions tests/Feature/Controllers/AuthController/LogoutTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

it('logs out an authenticated user', function() {
$this->login()
->get('/logout')
->assertRedirect(route('auth.show'));

$this->assertGuest();
});

it('redirects guest users back to the login page')
->get('/logout')
->assertRedirect('/login');
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

use App\Providers\RouteServiceProvider;

it('validates the scope if present', function (array $badData, array|string $errors) {
$this
->get(route('github.auth', $badData))
->assertInvalid($errors);
})->with([
[['scope' => 'admin:org'], 'scope'],
[['scope' => 'repo'], 'scope'],
[['scope' => 'user'], 'scope'],
]);

it('stores a valid scope in the current session', function (string $scope) {
$this
->get(route('github.auth', ['scope' => $scope]))
->assertSessionHas('auth_scope', $scope);
})->with(['read:user', 'public_repo']);

it('defaults to the `read:user` scope if no scope is provided', function () {
$this
->get(route('github.auth', ['scope' => null]))
->assertSessionHas('auth_scope', 'read:user');
});

it('redirects to the auth provider when a valid scope is present', function (?string $scope) {
// TODO: Can we perform some assertions on what we passed to Socialite?
$this->get(route('github.auth', ['scope' => $scope]))->assertRedirect();
})->with(['read:user', 'public_repo', null]);

it('redirects authenticated users back to the dashboard')
->login()
->get('/auth/github')
->assertRedirect(RouteServiceProvider::HOME);
36 changes: 36 additions & 0 deletions tests/Feature/Controllers/AuthController/RevokeGrantTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

use Illuminate\Support\Facades\Http;
use App\Exceptions\InvalidAccessTokenException;

it('sends an API reqest to GitHub to revoke the user\'s access token', function () {
Http::fake([
'api.github.com/*' => Http::response('ok', 200),
]);

$this
->login()
->post('/revoke-grant')->assertRedirect(route('auth.destroy'));

expect(auth()->user()->access_token)->toBeNull();
});

it('throws an InvalidAccessTokenException if the api request comes back with a 404', function () {
$this->withoutExceptionHandling();

Http::fake([
'api.github.com/*' => Http::response('not-found', 404),
]);

$this
->login()
->post('/revoke-grant');

expect(auth()->user()->access_token)->not->toBeNull();
})->throws(InvalidAccessTokenException::class);

it('redirects guest users back to the login page')
->post('/revoke-grant')
->assertRedirect('/login');
15 changes: 15 additions & 0 deletions tests/Feature/Controllers/AuthController/ShowTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

use App\Providers\RouteServiceProvider;

it('renders the login page for unauthenticated users')
->get('/auth')
->assertStatus(200)
->assertHybridView('auth');

it('redirects authenticated users back to the dashboard')
->login()
->get('/auth')
->assertRedirect(RouteServiceProvider::HOME);
9 changes: 0 additions & 9 deletions tests/Feature/ExampleTest.php

This file was deleted.

10 changes: 6 additions & 4 deletions tests/Pest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

declare(strict_types=1);

use Illuminate\Foundation\Testing\LazilyRefreshDatabase;

/*
|--------------------------------------------------------------------------
| Test Case
Expand All @@ -15,7 +17,7 @@

uses(
Tests\TestCase::class,
// Illuminate\Foundation\Testing\RefreshDatabase::class,
LazilyRefreshDatabase::class
)->in('Feature');

/*
Expand All @@ -29,9 +31,9 @@
|
*/

expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
// expect()->extend('toBeOne', function () {
// return $this->toBe(1);
// });

/*
|--------------------------------------------------------------------------
Expand Down
12 changes: 10 additions & 2 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@

namespace Tests;

use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use App\Models\User;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Http;

abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
use LazilyRefreshDatabase;

protected function setUp(): void
{
Expand All @@ -21,4 +20,13 @@ protected function setUp(): void

Http::preventStrayRequests();
}

protected function login(User $user = null)
{
$user ??= User::factory()->create()->first();

$this->actingAs($user);

return $this;
}
}

0 comments on commit cb04222

Please sign in to comment.