Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK-1931] Add support for canActivateChild and canLoad in AuthGuard #45

Merged
merged 11 commits into from
Sep 14, 2020
36 changes: 22 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 69 additions & 20 deletions projects/auth0-angular/src/lib/auth.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,81 @@ import { of } from 'rxjs';
import { AuthGuard } from './auth.guard';

describe('AuthGuard', () => {
let authServiceMock: any;
let guard: AuthGuard;
const routeMock: any = { snapshot: {} };
const routeStateMock: any = { snapshot: {}, url: '/' };

it('should return true for a logged in user', () => {
authServiceMock = {
isAuthenticated$: of(true),
loginWithRedirect: jasmine.createSpy('loginWithRedirect'),
};
guard = new AuthGuard(authServiceMock);
const listener = jasmine.createSpy();
guard.canActivate(routeMock, routeStateMock).subscribe(listener);
expect(authServiceMock.loginWithRedirect).not.toHaveBeenCalled();
expect(listener).toHaveBeenCalledWith(true);
describe('canActivate', () => {
it('should return true for a logged in user', () => {
const authServiceMock: any = {
isAuthenticated$: of(true),
loginWithRedirect: jasmine.createSpy('loginWithRedirect'),
};
guard = new AuthGuard(authServiceMock);
const listener = jasmine.createSpy();
guard.canActivate(routeMock, routeStateMock).subscribe(listener);
expect(authServiceMock.loginWithRedirect).not.toHaveBeenCalled();
expect(listener).toHaveBeenCalledWith(true);
});

it('should redirect a logged out user', () => {
const authServiceMock: any = {
isAuthenticated$: of(false),
loginWithRedirect: jasmine.createSpy('loginWithRedirect'),
};
guard = new AuthGuard(authServiceMock);
guard.canActivate(routeMock, routeStateMock).subscribe();
expect(authServiceMock.loginWithRedirect).toHaveBeenCalledWith({
appState: { target: '/' },
});
});
});

describe('canActivateChild', () => {
it('should return true for a logged in user', () => {
const authServiceMock: any = {
isAuthenticated$: of(true),
loginWithRedirect: jasmine.createSpy('loginWithRedirect'),
};
guard = new AuthGuard(authServiceMock);
const listener = jasmine.createSpy();
guard.canActivateChild(routeMock, routeStateMock).subscribe(listener);
expect(authServiceMock.loginWithRedirect).not.toHaveBeenCalled();
expect(listener).toHaveBeenCalledWith(true);
});

it('should redirect a logged out user', () => {
const authServiceMock: any = {
isAuthenticated$: of(false),
loginWithRedirect: jasmine.createSpy('loginWithRedirect'),
};
guard = new AuthGuard(authServiceMock);
guard.canActivateChild(routeMock, routeStateMock).subscribe();
expect(authServiceMock.loginWithRedirect).toHaveBeenCalledWith({
appState: { target: '/' },
});
});
});

it('should redirect a logged out user', () => {
authServiceMock = {
isAuthenticated$: of(false),
loginWithRedirect: jasmine.createSpy('loginWithRedirect'),
};
guard = new AuthGuard(authServiceMock);
guard.canActivate(routeMock, routeStateMock).subscribe();
expect(authServiceMock.loginWithRedirect).toHaveBeenCalledWith({
appState: { target: '/' },
describe('canLoad', () => {
it('should return true for an authenticated user', () => {
const authServiceMock: any = {
isAuthenticated$: of(true),
};
guard = new AuthGuard(authServiceMock);
const listener = jasmine.createSpy();
guard.canLoad(routeMock, []).subscribe(listener);
expect(listener).toHaveBeenCalledWith(true);
});

it('should return false for an unauthenticated user', () => {
const authServiceMock: any = {
isAuthenticated$: of(false),
};
guard = new AuthGuard(authServiceMock);
const listener = jasmine.createSpy();
guard.canLoad(routeMock, []).subscribe(listener);
expect(listener).toHaveBeenCalledWith(false);
});
});
});
25 changes: 23 additions & 2 deletions projects/auth0-angular/src/lib/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,41 @@ import {
ActivatedRouteSnapshot,
RouterStateSnapshot,
CanActivate,
CanLoad,
Route,
UrlSegment,
CanActivateChild,
} from '@angular/router';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { tap, take } from 'rxjs/operators';
import { AuthService } from './auth.service';

@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
export class AuthGuard implements CanActivate, CanLoad, CanActivateChild {
constructor(private auth: AuthService) {}

canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> {
return this.auth.isAuthenticated$.pipe(take(1));
stevehobbsdev marked this conversation as resolved.
Show resolved Hide resolved
}

canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.redirectIfUnauthenticated(state);
}

canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.redirectIfUnauthenticated(state);
}

private redirectIfUnauthenticated(
state: RouterStateSnapshot
): Observable<boolean> {
return this.auth.isAuthenticated$.pipe(
tap((loggedIn) => {
Expand Down
34 changes: 34 additions & 0 deletions projects/playground/e2e/integration/playground.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,38 @@ describe('Smoke tests', () => {
cy.get('[data-cy=unprotected]').should('be.visible');
cy.get('[data-cy=protected]').should('not.be.visible');
});

it('should see child route content without logging in', () => {
cy.visit('/child');
cy.get('#login').should('be.visible');

cy.get('[data-cy=child-route]').should('be.visible');
});

it('should protect the nested child route and return to the right place after login', () => {
cy.visit('/');
cy.get('[data-cy=nested-child-route]').should('not.be.visible');
cy.visit('/child/nested');

cy.url().should('include', 'https://brucke.auth0.com/login');
loginToAuth0();

cy.url().should('include', '/child/nested');
cy.get('[data-cy=nested-child-route]').should('be.visible');
cy.get('#logout').click();
});

it('should not navigate to the lazy loaded module when not authenticated', () => {
cy.visit('/lazy');
cy.get('[data-cy=lazy-module]').should('not.be.visible');
});

it('should show lazy module content when authenticated', () => {
cy.visit('/');
cy.get('#login').should('be.visible').click();
loginToAuth0();
cy.get('#logout').should('be.visible');
cy.visit('/lazy');
cy.get('[data-cy=lazy-module]').should('be.visible');
});
});
24 changes: 19 additions & 5 deletions projects/playground/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProtectedComponent } from './protected/protected.component';
import { ProtectedComponent } from './components/protected.component';
import { AuthGuard } from 'projects/auth0-angular/src/lib/auth.guard';
import { UnprotectedComponent } from './unprotected/unprotected.component';
import { UnprotectedComponent } from './components/unprotected.component';
import { ChildRouteComponent } from './components/child-route.component';
import { NestedChildRouteComponent } from './components/nested-child-route.component';

const routes: Routes = [
{
path: '',
component: UnprotectedComponent,
pathMatch: 'full',
},
{
path: 'child',
component: ChildRouteComponent,
canActivateChild: [AuthGuard],
stevehobbsdev marked this conversation as resolved.
Show resolved Hide resolved
children: [{ path: 'nested', component: NestedChildRouteComponent }],
},
{
path: 'protected',
component: ProtectedComponent,
canActivate: [AuthGuard],
},
{
path: '',
component: UnprotectedComponent,
pathMatch: 'full',
path: 'lazy',
canLoad: [AuthGuard],
loadChildren: () =>
import('./lazy-module.module').then((m) => m.LazyModuleModule),
},
];

Expand Down
5 changes: 4 additions & 1 deletion projects/playground/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,11 @@ <h2>Artifacts</h2>

<div class="guard-wrapper">
<h2>Test Auth Guard</h2>
<a routerLink="/">Unprotected Route</a> |
stevehobbsdev marked this conversation as resolved.
Show resolved Hide resolved
<a routerLink="/protected">Protected Route</a> |
<a routerLink="/">Unprotected Route</a>
<a routerLink="/lazy">Lazy-loaded route</a> |
<a routerLink="/child">Child route</a> |
<a routerLink="/child/nested">Nested child route</a>
<router-outlet></router-outlet>
</div>
</div>
14 changes: 11 additions & 3 deletions projects/playground/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@ import { ReactiveFormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ProtectedComponent } from './protected/protected.component';
import { UnprotectedComponent } from './unprotected/unprotected.component';
import { ProtectedComponent } from './components/protected.component';
import { UnprotectedComponent } from './components/unprotected.component';
import { ChildRouteComponent } from './components/child-route.component';
import { NestedChildRouteComponent } from './components/nested-child-route.component';

const AUTH0_CONFIG = {
clientId: 'wLSIP47wM39wKdDmOj6Zb5eSEw3JVhVp',
domain: 'brucke.auth0.com',
};

@NgModule({
declarations: [AppComponent, ProtectedComponent, UnprotectedComponent],
declarations: [
AppComponent,
ProtectedComponent,
UnprotectedComponent,
ChildRouteComponent,
NestedChildRouteComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
Expand Down
18 changes: 18 additions & 0 deletions projects/playground/src/app/components/child-route.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-child-route',
template: `
<p data-cy="child-route">
child-route works!
</p>

<router-outlet></router-outlet>
`,
styles: [],
})
export class ChildRouteComponent implements OnInit {
constructor() {}

ngOnInit(): void {}
}
12 changes: 12 additions & 0 deletions projects/playground/src/app/components/lazy-module.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-lazy-module',
template: '<p data-cy="lazy-module">lazy-module works!</p>',
styles: [],
})
export class LazyModuleComponent implements OnInit {
constructor() {}

ngOnInit(): void {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-nested-child-route',
template: `
<p data-cy="nested-child-route">
Nested child-route works!
</p>
`,
styles: [],
})
export class NestedChildRouteComponent implements OnInit {
constructor() {}

ngOnInit(): void {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-protected',
templateUrl: './protected.component.html',
styleUrls: ['./protected.component.css'],
template: '<p data-cy="protected">This route is protected!</p>',
styles: [],
})
export class ProtectedComponent implements OnInit {
constructor() {}
Expand Down
Loading