Skip to content

Commit 1946df0

Browse files
committed
PER-10096-archive-invitations
PER-10096-archive-invitations -Create new route for archives dialog -Redirect that new route based on the url to the pending archives dialog
1 parent 78aac96 commit 1946df0

File tree

7 files changed

+330
-12
lines changed

7 files changed

+330
-12
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import {
3+
Router,
4+
UrlTree,
5+
ActivatedRouteSnapshot,
6+
RouterStateSnapshot,
7+
} from '@angular/router';
8+
import { AccountService } from '@shared/services/account/account.service';
9+
import { AccountVO } from '@models/account-vo';
10+
import { AuthGuard } from './auth.guard';
11+
12+
describe('AuthGuard', () => {
13+
let guard: AuthGuard;
14+
let accountServiceSpy: jasmine.SpyObj<AccountService>;
15+
let routerSpy: jasmine.SpyObj<Router>;
16+
17+
beforeEach(() => {
18+
accountServiceSpy = jasmine.createSpyObj('AccountService', [
19+
'getAccount',
20+
'hasOwnArchives',
21+
]);
22+
23+
routerSpy = jasmine.createSpyObj('Router', ['createUrlTree', 'parseUrl']);
24+
25+
TestBed.configureTestingModule({
26+
providers: [
27+
AuthGuard,
28+
{ provide: AccountService, useValue: accountServiceSpy },
29+
{ provide: Router, useValue: routerSpy },
30+
],
31+
});
32+
33+
guard = TestBed.inject(AuthGuard);
34+
});
35+
36+
function createMockRouteSnapshot(
37+
query: Record<string, string> = {},
38+
): ActivatedRouteSnapshot {
39+
return {
40+
queryParamMap: {
41+
get: (key: string) => query[key] ?? null,
42+
},
43+
} as any;
44+
}
45+
46+
it('should allow access if no account is found', () => {
47+
accountServiceSpy.getAccount.and.returnValue(null);
48+
const result = guard.canActivate(createMockRouteSnapshot(), {
49+
url: '/signup',
50+
} as RouterStateSnapshot);
51+
52+
expect(result).toBeTrue();
53+
});
54+
55+
it('should redirect to /app/dialog:archives/pending if on /signup with invite params', () => {
56+
accountServiceSpy.getAccount.and.returnValue(
57+
new AccountVO({ accountId: 1 }),
58+
);
59+
60+
const query = {
61+
inviteCode: 'xyz',
62+
fullName: 'Test User',
63+
primaryEmail: '[email protected]',
64+
};
65+
66+
const expectedTree = {} as UrlTree;
67+
routerSpy.createUrlTree.and.returnValue(expectedTree);
68+
69+
const result = guard.canActivate(createMockRouteSnapshot(query), {
70+
url: '/signup',
71+
} as RouterStateSnapshot);
72+
73+
expect(routerSpy.createUrlTree).toHaveBeenCalledWith([
74+
'/app',
75+
{ outlets: { primary: 'private', dialog: 'archives/pending' } },
76+
]);
77+
78+
expect(result).toBe(expectedTree);
79+
});
80+
81+
it('should redirect to /app/private if account has own archives', async () => {
82+
accountServiceSpy.getAccount.and.returnValue(
83+
new AccountVO({ accountId: 1 }),
84+
);
85+
accountServiceSpy.hasOwnArchives.and.returnValue(Promise.resolve(true));
86+
87+
const expectedUrl = {} as UrlTree;
88+
routerSpy.parseUrl.and.returnValue(expectedUrl);
89+
90+
const result = await guard.canActivate(createMockRouteSnapshot(), {
91+
url: '/app/anything',
92+
} as RouterStateSnapshot);
93+
94+
expect(accountServiceSpy.hasOwnArchives).toHaveBeenCalled();
95+
expect(result).toBe(expectedUrl);
96+
expect(routerSpy.parseUrl).toHaveBeenCalledWith('/app/private');
97+
});
98+
99+
it('should redirect to /app/onboarding if account has no own archives', async () => {
100+
accountServiceSpy.getAccount.and.returnValue(
101+
new AccountVO({ accountId: 1 }),
102+
);
103+
accountServiceSpy.hasOwnArchives.and.returnValue(Promise.resolve(false));
104+
105+
const expectedUrl = {} as UrlTree;
106+
routerSpy.parseUrl.and.returnValue(expectedUrl);
107+
108+
const result = await guard.canActivate(createMockRouteSnapshot(), {
109+
url: '/app/anything',
110+
} as RouterStateSnapshot);
111+
112+
expect(accountServiceSpy.hasOwnArchives).toHaveBeenCalled();
113+
expect(result).toBe(expectedUrl);
114+
expect(routerSpy.parseUrl).toHaveBeenCalledWith('/app/onboarding');
115+
});
116+
});

src/app/auth/guards/auth.guard.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,21 @@ export class AuthGuard {
2626
| Promise<boolean | UrlTree>
2727
| boolean
2828
| UrlTree {
29+
const account = this.account.getAccount();
30+
const queryParams = route.queryParamMap;
31+
32+
const inviteCode = queryParams.get('inviteCode');
33+
const fullName = queryParams.get('fullName');
34+
const primaryEmail = queryParams.get('primaryEmail');
35+
const isInviteSignup = inviteCode && fullName && primaryEmail;
36+
2937
if (this.account.getAccount()?.accountId) {
38+
if (state.url.includes('/signup') && isInviteSignup) {
39+
return this.router.createUrlTree([
40+
'/app',
41+
{ outlets: { primary: 'private', dialog: 'archives/pending' } },
42+
]);
43+
}
3044
return this.account.hasOwnArchives().then((hasArchives) => {
3145
if (hasArchives) {
3246
return this.router.parseUrl('/app/private');

src/app/core/components/my-archives-dialog/my-archives-dialog.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<div class="panel" #panel>
3333
<ng-container [ngSwitch]="activeTab">
3434
<ng-container *ngSwitchCase="'switch'">
35-
<div class="panel-title">({{ archives.length }}) Archives</div>
35+
<div class="panel-title">({{ archives?.length }}) Archives</div>
3636
<pr-archive-small
3737
*ngFor="let archive of archives"
3838
(archiveClick)="onArchiveClick(archive)"
@@ -45,7 +45,7 @@
4545
archive.accessRole.includes('owner') &&
4646
currentArchive.archiveId !== archive.archiveId &&
4747
account.defaultArchiveId !== archive.archiveId &&
48-
archives.length > 1
48+
archives?.length > 1
4949
"
5050
removeText="Delete Archive"
5151
(removeClick)="onArchiveDeleteClick(archive)"
@@ -58,9 +58,9 @@
5858
</ng-container>
5959
<ng-container *ngSwitchCase="'pending'">
6060
<div class="panel-title">
61-
({{ pendingArchives.length }}) Pending Archives
61+
({{ pendingArchives?.length }}) Pending Archives
6262
</div>
63-
<div *ngIf="!pendingArchives.length">No pending archives</div>
63+
<div *ngIf="!pendingArchives?.length">No pending archives</div>
6464
<pr-archive-small
6565
*ngFor="let archive of pendingArchives"
6666
[largeOnDesktop]="true"
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import {
2+
ComponentFixture,
3+
TestBed,
4+
fakeAsync,
5+
tick,
6+
} from '@angular/core/testing';
7+
import { ArchiveVO, AccountVO } from '@models';
8+
import { AccountService } from '@shared/services/account/account.service';
9+
import { ApiService } from '@shared/services/api/api.service';
10+
import { MessageService } from '@shared/services/message/message.service';
11+
import { PromptService } from '@shared/services/prompt/prompt.service';
12+
import { UntypedFormBuilder } from '@angular/forms';
13+
import { Router, ActivatedRoute } from '@angular/router';
14+
import { DIALOG_DATA, DialogRef } from '@angular/cdk/dialog';
15+
import { MyArchivesDialogComponent } from './my-archives-dialog.component';
16+
17+
describe('MyArchivesDialogComponent', () => {
18+
let component: MyArchivesDialogComponent;
19+
let fixture: ComponentFixture<MyArchivesDialogComponent>;
20+
let accountServiceSpy: jasmine.SpyObj<AccountService>;
21+
let apiServiceSpy: jasmine.SpyObj<ApiService>;
22+
let promptServiceSpy: jasmine.SpyObj<PromptService>;
23+
let messageServiceSpy: jasmine.SpyObj<MessageService>;
24+
let dialogRefSpy: jasmine.SpyObj<DialogRef>;
25+
let dialogData;
26+
27+
beforeEach(async () => {
28+
accountServiceSpy = jasmine.createSpyObj('AccountService', [
29+
'refreshArchives',
30+
'getAccount',
31+
'getArchive',
32+
'getArchives',
33+
'changeArchive',
34+
'updateAccount',
35+
]);
36+
37+
apiServiceSpy = jasmine.createSpyObj('ApiService', ['archive'], {
38+
archive: jasmine.createSpyObj('archive', ['delete', 'accept', 'decline']),
39+
});
40+
41+
promptServiceSpy = jasmine.createSpyObj('PromptService', ['confirm']);
42+
messageServiceSpy = jasmine.createSpyObj('MessageService', ['showError']);
43+
dialogRefSpy = jasmine.createSpyObj('DialogRef', ['close']);
44+
45+
await TestBed.configureTestingModule({
46+
declarations: [MyArchivesDialogComponent],
47+
providers: [
48+
{ provide: AccountService, useValue: accountServiceSpy },
49+
{ provide: ApiService, useValue: apiServiceSpy },
50+
{ provide: PromptService, useValue: promptServiceSpy },
51+
{ provide: MessageService, useValue: messageServiceSpy },
52+
{ provide: DialogRef, useValue: dialogRefSpy },
53+
{
54+
provide: ActivatedRoute,
55+
useValue: { snapshot: { root: { params: {}, children: [] } } },
56+
},
57+
{
58+
provide: Router,
59+
useValue: { routerState: { snapshot: { root: {} } } },
60+
},
61+
{
62+
provide: DIALOG_DATA,
63+
useValue: {},
64+
},
65+
66+
UntypedFormBuilder,
67+
],
68+
}).compileComponents();
69+
70+
fixture = TestBed.createComponent(MyArchivesDialogComponent);
71+
component = fixture.componentInstance;
72+
73+
const mockAccount: AccountVO = new AccountVO({ defaultArchiveId: '123' });
74+
const mockCurrentArchive: ArchiveVO = new ArchiveVO({
75+
archiveId: '123',
76+
fullName: 'Test Archive',
77+
accessRole: 'access.role.owner',
78+
});
79+
80+
accountServiceSpy.getAccount.and.returnValue(mockAccount);
81+
accountServiceSpy.getArchive.and.returnValue(mockCurrentArchive);
82+
accountServiceSpy.getArchives.and.returnValue([
83+
new ArchiveVO({
84+
archiveId: '123',
85+
fullName: 'Test Archive',
86+
status: 'status.generic.ok',
87+
accessRole: 'access.role.owner',
88+
}),
89+
]);
90+
accountServiceSpy.refreshArchives.and.returnValue(
91+
Promise.resolve([new ArchiveVO({ archiveId: 1 })]),
92+
);
93+
94+
fixture.detectChanges();
95+
});
96+
97+
it('should initialize and categorize archives correctly', async () => {
98+
await component.ngOnInit();
99+
100+
expect(component.archives?.length).toBe(1);
101+
expect(component.pendingArchives?.length).toBe(0);
102+
});
103+
104+
it('should switch tabs when setTab is called', () => {
105+
component.setTab('pending');
106+
107+
expect(component.activeTab).toBe('pending');
108+
});
109+
110+
it('should close dialog onDoneClick', () => {
111+
component.onDoneClick();
112+
113+
expect(dialogRefSpy.close).toHaveBeenCalled();
114+
});
115+
116+
it('should initialize and separate pending/regular archives correctly', async () => {
117+
const mockAccount = new AccountVO({ defaultArchiveId: '111' });
118+
const mockCurrentArchive = new ArchiveVO({ archiveId: '111', fullName: 'Current Archive' });
119+
120+
const pending = new ArchiveVO({
121+
archiveId: '222',
122+
fullName: 'Pending Archive',
123+
status: 'status.generic.pending',
124+
});
125+
126+
const active = new ArchiveVO({
127+
archiveId: '333',
128+
fullName: 'Active Archive',
129+
status: 'status.generic.ok',
130+
});
131+
132+
accountServiceSpy.refreshArchives.and.returnValue(Promise.resolve([new ArchiveVO({})]));
133+
accountServiceSpy.getAccount.and.returnValue(mockAccount);
134+
accountServiceSpy.getArchive.and.returnValue(mockCurrentArchive);
135+
accountServiceSpy.getArchives.and.returnValue([pending, active]);
136+
137+
await component.ngOnInit();
138+
139+
expect(accountServiceSpy.refreshArchives).toHaveBeenCalled();
140+
expect(component.account).toEqual(mockAccount);
141+
expect(component.currentArchive).toEqual(mockCurrentArchive);
142+
expect(component.pendingArchives.length).toBe(1);
143+
expect(component.archives.length).toBe(1);
144+
expect(component.pendingArchives[0].archiveId).toBe('222');
145+
expect(component.archives[0].archiveId).toBe('333');
146+
});
147+
148+
});

src/app/core/components/my-archives-dialog/my-archives-dialog.component.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import {
66
ViewChildren,
77
QueryList,
88
Inject,
9+
inject,
910
} from '@angular/core';
1011
import { ArchiveVO, AccountVO } from '@models';
1112
import { AccountService } from '@shared/services/account/account.service';
12-
import { Router } from '@angular/router';
13+
import { ActivatedRoute, Router } from '@angular/router';
1314
import { partition, remove, find, orderBy } from 'lodash';
1415
import { ApiService } from '@shared/services/api/api.service';
1516
import { ArchiveResponse } from '@shared/services/api/archive.repo';
@@ -63,8 +64,8 @@ const ARCHIVE_TYPES: { text: string; value: ArchiveType }[] = [
6364
export class MyArchivesDialogComponent implements OnInit {
6465
account: AccountVO;
6566
currentArchive: ArchiveVO;
66-
archives: ArchiveVO[];
67-
pendingArchives: ArchiveVO[];
67+
archives: ArchiveVO[] = [];
68+
pendingArchives: ArchiveVO[] = [];
6869
waiting = false;
6970

7071
archiveTypes = ARCHIVE_TYPES;
@@ -77,6 +78,8 @@ export class MyArchivesDialogComponent implements OnInit {
7778
@ViewChildren(ArchiveSmallComponent)
7879
archiveComponents: QueryList<ArchiveSmallComponent>;
7980

81+
private tabs = ['switch', 'pending', 'new'];
82+
8083
constructor(
8184
private dialogRef: DialogRef,
8285
@Inject(DIALOG_DATA) public data: any,
@@ -85,6 +88,8 @@ export class MyArchivesDialogComponent implements OnInit {
8588
private prompt: PromptService,
8689
private message: MessageService,
8790
private fb: UntypedFormBuilder,
91+
private route: ActivatedRoute,
92+
private router: Router,
8893
) {
8994
this.newArchiveForm = this.fb.group({
9095
fullName: ['', [Validators.required]],
@@ -97,7 +102,9 @@ export class MyArchivesDialogComponent implements OnInit {
97102
}
98103
}
99104

100-
ngOnInit(): void {
105+
async ngOnInit(): Promise<void> {
106+
await this.accountService.refreshArchives();
107+
101108
this.account = this.accountService.getAccount();
102109
this.currentArchive = this.accountService.getArchive();
103110
[this.pendingArchives, this.archives] = partition(
@@ -106,6 +113,11 @@ export class MyArchivesDialogComponent implements OnInit {
106113
),
107114
{ status: 'status.generic.pending' },
108115
);
116+
117+
const tab = this.getParams(this.router.routerState.snapshot.root);
118+
if (tab.path) {
119+
this.setTab(tab.path);
120+
}
109121
}
110122

111123
setTab(tab: MyArchivesTab) {
@@ -117,6 +129,14 @@ export class MyArchivesDialogComponent implements OnInit {
117129
this.dialogRef.close();
118130
}
119131

132+
getParams = (route) => ({
133+
...route.params,
134+
...route.children?.reduce(
135+
(acc, child) => ({ ...this.getParams(child), ...acc }),
136+
{},
137+
),
138+
});
139+
120140
scrollToArchive(archive: ArchiveVO) {
121141
setTimeout(() => {
122142
const component = find(

0 commit comments

Comments
 (0)