@sneat/auth-core 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2022/index.js +8 -0
- package/esm2022/index.js.map +1 -0
- package/esm2022/lib/login-required-service.service.js +20 -0
- package/esm2022/lib/login-required-service.service.js.map +1 -0
- package/esm2022/lib/private-token-store.service.js +36 -0
- package/esm2022/lib/private-token-store.service.js.map +1 -0
- package/esm2022/lib/sneat-auth-guard.js +79 -0
- package/esm2022/lib/sneat-auth-guard.js.map +1 -0
- package/esm2022/lib/sneat-auth-state-service.js +267 -0
- package/esm2022/lib/sneat-auth-state-service.js.map +1 -0
- package/{src/lib/sneat-auth.interface.ts → esm2022/lib/sneat-auth.interface.js} +1 -5
- package/esm2022/lib/sneat-auth.interface.js.map +1 -0
- package/esm2022/lib/telegram-auth.service.js +39 -0
- package/esm2022/lib/telegram-auth.service.js.map +1 -0
- package/esm2022/lib/user/index.js +3 -0
- package/esm2022/lib/user/index.js.map +1 -0
- package/esm2022/lib/user/sneat-user.service.js +171 -0
- package/esm2022/lib/user/sneat-user.service.js.map +1 -0
- package/esm2022/lib/user/user-record.service.js +30 -0
- package/esm2022/lib/user/user-record.service.js.map +1 -0
- package/esm2022/sneat-auth-core.js +5 -0
- package/esm2022/sneat-auth-core.js.map +1 -0
- package/lib/login-required-service.service.d.ts +6 -0
- package/lib/private-token-store.service.d.ts +8 -0
- package/lib/sneat-auth-guard.d.ts +26 -0
- package/lib/sneat-auth-state-service.d.ts +51 -0
- package/lib/sneat-auth.interface.d.ts +5 -0
- package/lib/telegram-auth.service.d.ts +9 -0
- package/lib/user/sneat-user.service.d.ts +34 -0
- package/lib/user/user-record.service.d.ts +25 -0
- package/package.json +14 -2
- package/sneat-auth-core.d.ts +5 -0
- package/eslint.config.js +0 -7
- package/ng-package.json +0 -7
- package/project.json +0 -38
- package/src/lib/login-required-service.service.spec.ts +0 -39
- package/src/lib/login-required-service.service.ts +0 -14
- package/src/lib/private-token-store.service.spec.ts +0 -75
- package/src/lib/private-token-store.service.ts +0 -36
- package/src/lib/sneat-auth-guard.spec.ts +0 -124
- package/src/lib/sneat-auth-guard.ts +0 -107
- package/src/lib/sneat-auth-state-service.spec.ts +0 -332
- package/src/lib/sneat-auth-state-service.ts +0 -387
- package/src/lib/sneat-auth.interface.spec.ts +0 -39
- package/src/lib/telegram-auth.service.spec.ts +0 -186
- package/src/lib/telegram-auth.service.ts +0 -49
- package/src/lib/user/sneat-user.service.spec.ts +0 -151
- package/src/lib/user/sneat-user.service.ts +0 -266
- package/src/lib/user/user-record.service.spec.ts +0 -145
- package/src/lib/user/user-record.service.ts +0 -38
- package/src/test-setup.ts +0 -3
- package/tsconfig.json +0 -13
- package/tsconfig.lib.json +0 -19
- package/tsconfig.lib.prod.json +0 -7
- package/tsconfig.spec.json +0 -31
- package/vite.config.mts +0 -10
- /package/{src/index.ts → index.d.ts} +0 -0
- /package/{src/lib/user/index.ts → lib/user/index.d.ts} +0 -0
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { Injectable } from '@angular/core';
|
|
2
|
-
import { Observable, of, Subject } from 'rxjs';
|
|
3
|
-
|
|
4
|
-
const tokenKey = (domain: string, projectId: string) =>
|
|
5
|
-
`private/tokens/${domain}/${projectId}`;
|
|
6
|
-
|
|
7
|
-
export const canceledByUser = 'canceled by user';
|
|
8
|
-
|
|
9
|
-
@Injectable({
|
|
10
|
-
providedIn: 'root',
|
|
11
|
-
})
|
|
12
|
-
export class PrivateTokenStoreService {
|
|
13
|
-
public getPrivateToken(
|
|
14
|
-
domain: string,
|
|
15
|
-
projectId: string,
|
|
16
|
-
): Observable<string> {
|
|
17
|
-
// Consider storing all tokens in a single item
|
|
18
|
-
const key = tokenKey(domain, projectId);
|
|
19
|
-
let token = localStorage.getItem(key);
|
|
20
|
-
if (token) {
|
|
21
|
-
return of(token);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const subj = new Subject<string>();
|
|
25
|
-
setTimeout(() => {
|
|
26
|
-
token = prompt(`Please provide access token for ${domain}:`);
|
|
27
|
-
if (token) {
|
|
28
|
-
localStorage.setItem(key, token);
|
|
29
|
-
subj.next(token);
|
|
30
|
-
} else {
|
|
31
|
-
subj.error(canceledByUser);
|
|
32
|
-
}
|
|
33
|
-
}, 1);
|
|
34
|
-
return subj.asObservable();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { TestBed } from '@angular/core/testing';
|
|
2
|
-
import {
|
|
3
|
-
Router,
|
|
4
|
-
Route,
|
|
5
|
-
UrlSegment,
|
|
6
|
-
ActivatedRouteSnapshot,
|
|
7
|
-
RouterStateSnapshot,
|
|
8
|
-
} from '@angular/router';
|
|
9
|
-
import { Auth } from '@angular/fire/auth';
|
|
10
|
-
import {
|
|
11
|
-
SneatAuthGuard,
|
|
12
|
-
redirectToLoginIfNotSignedIn,
|
|
13
|
-
} from './sneat-auth-guard';
|
|
14
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
15
|
-
import { of, firstValueFrom } from 'rxjs';
|
|
16
|
-
|
|
17
|
-
describe('SneatAuthGuard', () => {
|
|
18
|
-
let guard: SneatAuthGuard;
|
|
19
|
-
let routerMock: {
|
|
20
|
-
createUrlTree: ReturnType<typeof vi.fn>;
|
|
21
|
-
parseUrl: ReturnType<typeof vi.fn>;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
routerMock = {
|
|
26
|
-
createUrlTree: vi.fn(),
|
|
27
|
-
parseUrl: vi.fn(),
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
TestBed.configureTestingModule({
|
|
31
|
-
providers: [
|
|
32
|
-
SneatAuthGuard,
|
|
33
|
-
{ provide: Router, useValue: routerMock },
|
|
34
|
-
{
|
|
35
|
-
provide: Auth,
|
|
36
|
-
useValue: {},
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
guard = TestBed.inject(SneatAuthGuard);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should be created', () => {
|
|
45
|
-
expect(guard).toBeTruthy();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('canLoad', () => {
|
|
49
|
-
it('should return true', () => {
|
|
50
|
-
const route: Route = { path: 'test' };
|
|
51
|
-
const segments: UrlSegment[] = [];
|
|
52
|
-
|
|
53
|
-
const result = guard.canLoad(route, segments);
|
|
54
|
-
expect(result).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe('canActivate', () => {
|
|
59
|
-
it('should return true', () => {
|
|
60
|
-
const route = {} as ActivatedRouteSnapshot;
|
|
61
|
-
const state = { url: '/test' } as RouterStateSnapshot;
|
|
62
|
-
|
|
63
|
-
const result = guard.canActivate(route, state);
|
|
64
|
-
expect(result).toBe(true);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('canActivateChild', () => {
|
|
69
|
-
it('should return true', () => {
|
|
70
|
-
const childRoute = {} as ActivatedRouteSnapshot;
|
|
71
|
-
const state = { url: '/test/child' } as RouterStateSnapshot;
|
|
72
|
-
|
|
73
|
-
const result = guard.canActivateChild(childRoute, state);
|
|
74
|
-
expect(result).toBe(true);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('redirectToLoginIfNotSignedIn', () => {
|
|
80
|
-
it('should return true for authenticated user', async () => {
|
|
81
|
-
const user = { uid: 'test-uid' };
|
|
82
|
-
|
|
83
|
-
const result = await firstValueFrom(
|
|
84
|
-
of(user).pipe(redirectToLoginIfNotSignedIn),
|
|
85
|
-
);
|
|
86
|
-
expect(result).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should return login URL for unauthenticated user at root', async () => {
|
|
90
|
-
const originalPathname = location.pathname;
|
|
91
|
-
Object.defineProperty(window.location, 'pathname', {
|
|
92
|
-
writable: true,
|
|
93
|
-
value: '/',
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const result = await firstValueFrom(
|
|
97
|
-
of(null).pipe(redirectToLoginIfNotSignedIn),
|
|
98
|
-
);
|
|
99
|
-
expect(result).toBe('/login');
|
|
100
|
-
|
|
101
|
-
Object.defineProperty(window.location, 'pathname', {
|
|
102
|
-
writable: true,
|
|
103
|
-
value: originalPathname,
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should return login URL with hash for unauthenticated user at non-root path', async () => {
|
|
108
|
-
const originalPathname = location.pathname;
|
|
109
|
-
Object.defineProperty(window.location, 'pathname', {
|
|
110
|
-
writable: true,
|
|
111
|
-
value: '/protected',
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const result = await firstValueFrom(
|
|
115
|
-
of(null).pipe(redirectToLoginIfNotSignedIn),
|
|
116
|
-
);
|
|
117
|
-
expect(result).toBe('/login#/protected');
|
|
118
|
-
|
|
119
|
-
Object.defineProperty(window.location, 'pathname', {
|
|
120
|
-
writable: true,
|
|
121
|
-
value: originalPathname,
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
});
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ActivatedRouteSnapshot,
|
|
3
|
-
// CanActivate,
|
|
4
|
-
// CanActivateChild,
|
|
5
|
-
// CanLoad,
|
|
6
|
-
Route,
|
|
7
|
-
Router,
|
|
8
|
-
RouterStateSnapshot,
|
|
9
|
-
UrlSegment,
|
|
10
|
-
UrlTree,
|
|
11
|
-
} from '@angular/router';
|
|
12
|
-
import { Observable } from 'rxjs';
|
|
13
|
-
import { Injectable, inject } from '@angular/core';
|
|
14
|
-
import { Auth } from '@angular/fire/auth';
|
|
15
|
-
import { map } from 'rxjs/operators';
|
|
16
|
-
import { AuthPipe } from '@angular/fire/auth-guard';
|
|
17
|
-
|
|
18
|
-
type AuthCanLoadPipeGenerator = (
|
|
19
|
-
route: Route,
|
|
20
|
-
segments: UrlSegment[],
|
|
21
|
-
) => AuthPipe;
|
|
22
|
-
|
|
23
|
-
export const redirectToLoginIfNotSignedIn: AuthPipe = map((user) => {
|
|
24
|
-
if (user) {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
let url = '/login';
|
|
28
|
-
if (location.pathname != '/') {
|
|
29
|
-
url += '#' + location.pathname;
|
|
30
|
-
}
|
|
31
|
-
return url;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
@Injectable({
|
|
35
|
-
providedIn: 'root',
|
|
36
|
-
})
|
|
37
|
-
export class SneatAuthGuard /*implements CanLoad, CanActivate, CanActivateChild*/ {
|
|
38
|
-
private readonly router = inject(Router);
|
|
39
|
-
private readonly auth = inject(Auth);
|
|
40
|
-
|
|
41
|
-
public canLoad(
|
|
42
|
-
_route: Route,
|
|
43
|
-
_segments: UrlSegment[],
|
|
44
|
-
):
|
|
45
|
-
| Observable<boolean | UrlTree>
|
|
46
|
-
| Promise<boolean | UrlTree>
|
|
47
|
-
| boolean
|
|
48
|
-
| UrlTree {
|
|
49
|
-
{
|
|
50
|
-
// console.log('SneatAuthGuard.canLoad', route, segments);
|
|
51
|
-
// const authPipeFactory =
|
|
52
|
-
// (route.data && route.data['authCanLoadGuardPipe'] as AuthCanLoadPipeGenerator) ||
|
|
53
|
-
// (() => redirectToLoginIfNotSignedIn);
|
|
54
|
-
// const subj = new Subject<boolean>();
|
|
55
|
-
// onAuthStateChanged(this.auth, {
|
|
56
|
-
// next: (user) => {
|
|
57
|
-
// console.log('onAuthStateChanged', user);
|
|
58
|
-
// }
|
|
59
|
-
// })
|
|
60
|
-
// return this.auth.user.pipe(
|
|
61
|
-
// map((user) => {
|
|
62
|
-
// console.log('user', user);
|
|
63
|
-
// return user;
|
|
64
|
-
// }),
|
|
65
|
-
// take(1),
|
|
66
|
-
// authPipeFactory(route, segments),
|
|
67
|
-
// map((can) => {
|
|
68
|
-
// console.log('can', can);
|
|
69
|
-
// if (typeof can === 'boolean') {
|
|
70
|
-
// return can;
|
|
71
|
-
// } else if (Array.isArray(can)) {
|
|
72
|
-
// return this.router.createUrlTree(can);
|
|
73
|
-
// } else {
|
|
74
|
-
// return this.router.parseUrl(can);
|
|
75
|
-
// }
|
|
76
|
-
// }),
|
|
77
|
-
// );
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
public canActivate(
|
|
83
|
-
_route: ActivatedRouteSnapshot,
|
|
84
|
-
_state: RouterStateSnapshot, //: Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree
|
|
85
|
-
) {
|
|
86
|
-
// console.log('SneatAuthGuard.canActivate', route, state);
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
canActivateChild(
|
|
91
|
-
_childRoute: ActivatedRouteSnapshot,
|
|
92
|
-
_state: RouterStateSnapshot, // : Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree
|
|
93
|
-
) {
|
|
94
|
-
// console.log('SneatAuthGuard.canActivateChild', childRoute, state);
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export const canLoad = (pipe?: AuthCanLoadPipeGenerator) => ({
|
|
100
|
-
canLoad: [SneatAuthGuard],
|
|
101
|
-
data: { authCanLoadGuardPipe: pipe },
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
export const SNEAT_AUTH_GUARDS = {
|
|
105
|
-
canActivate: [SneatAuthGuard],
|
|
106
|
-
canLoad: [SneatAuthGuard],
|
|
107
|
-
};
|
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import { TestBed } from '@angular/core/testing';
|
|
2
|
-
import { Auth, UserCredential } from '@angular/fire/auth';
|
|
3
|
-
import { AnalyticsService, ErrorLogger } from '@sneat/core';
|
|
4
|
-
import {
|
|
5
|
-
SneatAuthStateService,
|
|
6
|
-
AuthStatuses,
|
|
7
|
-
} from './sneat-auth-state-service';
|
|
8
|
-
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
|
9
|
-
import { firstValueFrom, Observer } from 'rxjs';
|
|
10
|
-
import { User } from '@angular/fire/auth';
|
|
11
|
-
|
|
12
|
-
// Mock Capacitor
|
|
13
|
-
vi.mock('@capacitor/core', () => ({
|
|
14
|
-
Capacitor: {
|
|
15
|
-
isNativePlatform: vi.fn().mockReturnValue(false),
|
|
16
|
-
},
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
// Mock Firebase Authentication
|
|
20
|
-
vi.mock('@capacitor-firebase/authentication', () => ({
|
|
21
|
-
FirebaseAuthentication: {
|
|
22
|
-
signInWithGoogle: vi.fn(),
|
|
23
|
-
signInWithApple: vi.fn(),
|
|
24
|
-
signInWithFacebook: vi.fn(),
|
|
25
|
-
signInWithMicrosoft: vi.fn(),
|
|
26
|
-
},
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
// Mock firebase/auth with proper constructor mocks
|
|
30
|
-
vi.mock('firebase/auth', () => {
|
|
31
|
-
class MockGoogleAuthProvider {}
|
|
32
|
-
class MockOAuthProvider {
|
|
33
|
-
credential = vi.fn();
|
|
34
|
-
}
|
|
35
|
-
class MockGithubAuthProvider {
|
|
36
|
-
addScope = vi.fn();
|
|
37
|
-
}
|
|
38
|
-
class MockFacebookAuthProvider {
|
|
39
|
-
addScope = vi.fn();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
GoogleAuthProvider: MockGoogleAuthProvider,
|
|
44
|
-
OAuthProvider: MockOAuthProvider,
|
|
45
|
-
GithubAuthProvider: MockGithubAuthProvider,
|
|
46
|
-
FacebookAuthProvider: MockFacebookAuthProvider,
|
|
47
|
-
signInWithEmailLink: vi.fn().mockResolvedValue({ user: { uid: 'test' } }),
|
|
48
|
-
signInWithCustomToken: vi.fn().mockResolvedValue({ user: { uid: 'test' } }),
|
|
49
|
-
signInWithPopup: vi.fn().mockResolvedValue({ user: { uid: 'test' } }),
|
|
50
|
-
linkWithPopup: vi.fn().mockResolvedValue({ user: { uid: 'test' } }),
|
|
51
|
-
unlink: vi.fn().mockResolvedValue({ uid: 'test', providerData: [] }),
|
|
52
|
-
signInWithCredential: vi.fn().mockResolvedValue({ user: { uid: 'test' } }),
|
|
53
|
-
getAuth: vi.fn().mockReturnValue({}),
|
|
54
|
-
};
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('SneatAuthStateService', () => {
|
|
58
|
-
let service: SneatAuthStateService;
|
|
59
|
-
let authMock: {
|
|
60
|
-
onIdTokenChanged: Mock;
|
|
61
|
-
onAuthStateChanged: Mock;
|
|
62
|
-
signOut: Mock;
|
|
63
|
-
currentUser: User | null;
|
|
64
|
-
};
|
|
65
|
-
let onAuthStateChangedCallback: Observer<User | null>;
|
|
66
|
-
let onIdTokenChangedCallback: Observer<User | null>;
|
|
67
|
-
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
authMock = {
|
|
70
|
-
onIdTokenChanged: vi.fn().mockImplementation((obs) => {
|
|
71
|
-
onIdTokenChangedCallback = obs;
|
|
72
|
-
}),
|
|
73
|
-
onAuthStateChanged: vi.fn().mockImplementation((obs) => {
|
|
74
|
-
onAuthStateChangedCallback = obs;
|
|
75
|
-
}),
|
|
76
|
-
signOut: vi.fn().mockResolvedValue(undefined),
|
|
77
|
-
currentUser: null,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
TestBed.configureTestingModule({
|
|
81
|
-
providers: [
|
|
82
|
-
SneatAuthStateService,
|
|
83
|
-
{
|
|
84
|
-
provide: ErrorLogger,
|
|
85
|
-
useValue: { logError: vi.fn(), logErrorHandler: () => vi.fn() },
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
provide: AnalyticsService,
|
|
89
|
-
useValue: {
|
|
90
|
-
identify: vi.fn(),
|
|
91
|
-
logEvent: vi.fn(),
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
provide: Auth,
|
|
96
|
-
useValue: authMock,
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
});
|
|
100
|
-
service = TestBed.inject(SneatAuthStateService);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should be created', () => {
|
|
104
|
-
expect(service).toBeTruthy();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should update auth status when onAuthStateChanged triggers', async () => {
|
|
108
|
-
const fbUser = {
|
|
109
|
-
uid: 'u1',
|
|
110
|
-
isAnonymous: false,
|
|
111
|
-
emailVerified: true,
|
|
112
|
-
email: 'test@example.com',
|
|
113
|
-
providerId: 'password',
|
|
114
|
-
providerData: [],
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
onAuthStateChangedCallback.next(fbUser as unknown as User);
|
|
118
|
-
|
|
119
|
-
const status = await firstValueFrom(service.authStatus);
|
|
120
|
-
expect(status).toBe(AuthStatuses.authenticated);
|
|
121
|
-
|
|
122
|
-
const user = await firstValueFrom(service.authUser);
|
|
123
|
-
expect(user?.uid).toBe('u1');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should call fbAuth.signOut when signOut is called', async () => {
|
|
127
|
-
await service.signOut();
|
|
128
|
-
expect(authMock.signOut).toHaveBeenCalled();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should handle null user in onAuthStateChanged', async () => {
|
|
132
|
-
onAuthStateChangedCallback.next(null);
|
|
133
|
-
|
|
134
|
-
const status = await firstValueFrom(service.authStatus);
|
|
135
|
-
expect(status).toBe(AuthStatuses.notAuthenticated);
|
|
136
|
-
|
|
137
|
-
const user = await firstValueFrom(service.authUser);
|
|
138
|
-
expect(user).toBeNull();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should handle user with multiple providerData', async () => {
|
|
142
|
-
const fbUser = {
|
|
143
|
-
uid: 'u2',
|
|
144
|
-
isAnonymous: false,
|
|
145
|
-
emailVerified: true,
|
|
146
|
-
email: 'multi@example.com',
|
|
147
|
-
providerId: 'firebase',
|
|
148
|
-
providerData: [
|
|
149
|
-
{ providerId: 'google.com', uid: 'g123' },
|
|
150
|
-
{ providerId: 'facebook.com', uid: 'f456' },
|
|
151
|
-
],
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
onAuthStateChangedCallback.next(fbUser as unknown as User);
|
|
155
|
-
|
|
156
|
-
const user = await firstValueFrom(service.authUser);
|
|
157
|
-
expect(user?.uid).toBe('u2');
|
|
158
|
-
expect(user?.providerId).toBe('firebase');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should handle user with single providerData', async () => {
|
|
162
|
-
const fbUser = {
|
|
163
|
-
uid: 'u3',
|
|
164
|
-
isAnonymous: false,
|
|
165
|
-
emailVerified: true,
|
|
166
|
-
email: 'single@example.com',
|
|
167
|
-
providerId: 'firebase',
|
|
168
|
-
providerData: [{ providerId: 'google.com', uid: 'g789' }],
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
onAuthStateChangedCallback.next(fbUser as unknown as User);
|
|
172
|
-
|
|
173
|
-
const user = await firstValueFrom(service.authUser);
|
|
174
|
-
expect(user?.providerId).toBe('google.com');
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('should handle onIdTokenChanged with authenticated user', async () => {
|
|
178
|
-
const fbUser = {
|
|
179
|
-
uid: 'u4',
|
|
180
|
-
isAnonymous: false,
|
|
181
|
-
emailVerified: true,
|
|
182
|
-
email: 'token@example.com',
|
|
183
|
-
providerId: 'google.com',
|
|
184
|
-
providerData: [],
|
|
185
|
-
getIdToken: vi.fn().mockResolvedValue('mock-token-123'),
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// First set auth user
|
|
189
|
-
onAuthStateChangedCallback.next(fbUser as unknown as User);
|
|
190
|
-
|
|
191
|
-
// Then trigger token changed
|
|
192
|
-
onIdTokenChangedCallback.next(fbUser as unknown as User);
|
|
193
|
-
|
|
194
|
-
// Wait for async operations
|
|
195
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
196
|
-
|
|
197
|
-
const state = await firstValueFrom(service.authState);
|
|
198
|
-
expect(state.status).toBe(AuthStatuses.authenticated);
|
|
199
|
-
expect(state.token).toBe('mock-token-123');
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('should handle error in getIdToken', async () => {
|
|
203
|
-
const errorLogger = TestBed.inject(ErrorLogger);
|
|
204
|
-
const fbUser = {
|
|
205
|
-
uid: 'u5',
|
|
206
|
-
isAnonymous: false,
|
|
207
|
-
emailVerified: true,
|
|
208
|
-
email: 'error@example.com',
|
|
209
|
-
providerId: 'google.com',
|
|
210
|
-
providerData: [],
|
|
211
|
-
getIdToken: vi.fn().mockRejectedValue(new Error('Token error')),
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
onAuthStateChangedCallback.next(fbUser as unknown as User);
|
|
215
|
-
onIdTokenChangedCallback.next(fbUser as unknown as User);
|
|
216
|
-
|
|
217
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
218
|
-
|
|
219
|
-
expect(errorLogger.logError).toHaveBeenCalled();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('should handle error in onAuthStateChanged', () => {
|
|
223
|
-
const errorLogger = TestBed.inject(ErrorLogger);
|
|
224
|
-
const error = new Error('Auth state error');
|
|
225
|
-
|
|
226
|
-
onAuthStateChangedCallback.error(error);
|
|
227
|
-
|
|
228
|
-
expect(errorLogger.logError).toHaveBeenCalledWith(
|
|
229
|
-
error,
|
|
230
|
-
'failed to retrieve Firebase auth user information',
|
|
231
|
-
);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('should handle error in onIdTokenChanged', () => {
|
|
235
|
-
const errorLogger = TestBed.inject(ErrorLogger);
|
|
236
|
-
const error = new Error('Token changed error');
|
|
237
|
-
|
|
238
|
-
onIdTokenChangedCallback.error(error);
|
|
239
|
-
|
|
240
|
-
expect(errorLogger.logError).toHaveBeenCalledWith(
|
|
241
|
-
error,
|
|
242
|
-
'failed in fbAuth.onIdTokenChanged',
|
|
243
|
-
);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
describe('signInWithToken', () => {
|
|
247
|
-
it('should call signInWithCustomToken', async () => {
|
|
248
|
-
const { signInWithCustomToken } = await import('firebase/auth');
|
|
249
|
-
const mockCredential = { user: { uid: 'test' } } as UserCredential;
|
|
250
|
-
(signInWithCustomToken as Mock).mockResolvedValue(mockCredential);
|
|
251
|
-
|
|
252
|
-
const result = await service.signInWithToken('custom-token');
|
|
253
|
-
|
|
254
|
-
expect(signInWithCustomToken).toHaveBeenCalledWith(
|
|
255
|
-
authMock,
|
|
256
|
-
'custom-token',
|
|
257
|
-
);
|
|
258
|
-
expect(result).toBe(mockCredential);
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe('signInWithEmailLink', () => {
|
|
263
|
-
it('should return observable that calls signInWithEmailLink', async () => {
|
|
264
|
-
const { signInWithEmailLink } = await import('firebase/auth');
|
|
265
|
-
|
|
266
|
-
const result = await firstValueFrom(
|
|
267
|
-
service.signInWithEmailLink('test@example.com'),
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
expect(signInWithEmailLink).toHaveBeenCalled();
|
|
271
|
-
expect(result.user.uid).toBe('test');
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
describe('linkWith', () => {
|
|
276
|
-
it('should reject if no current user', async () => {
|
|
277
|
-
authMock.currentUser = null;
|
|
278
|
-
|
|
279
|
-
await expect(service.linkWith('google.com')).rejects.toBe(
|
|
280
|
-
'no current user',
|
|
281
|
-
);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should link provider to current user', async () => {
|
|
285
|
-
const { linkWithPopup } = await import('firebase/auth');
|
|
286
|
-
const mockUser = { uid: 'current-user' } as User;
|
|
287
|
-
authMock.currentUser = mockUser;
|
|
288
|
-
|
|
289
|
-
const result = await service.linkWith('google.com');
|
|
290
|
-
|
|
291
|
-
expect(linkWithPopup).toHaveBeenCalled();
|
|
292
|
-
expect(result?.user.uid).toBe('test');
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
describe('unlinkAuthProvider', () => {
|
|
297
|
-
it('should reject if no current user', async () => {
|
|
298
|
-
authMock.currentUser = null;
|
|
299
|
-
|
|
300
|
-
await expect(service.unlinkAuthProvider('google.com')).rejects.toBe(
|
|
301
|
-
'No user is currently signed in.',
|
|
302
|
-
);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('should unlink auth provider from current user', async () => {
|
|
306
|
-
const { unlink } = await import('firebase/auth');
|
|
307
|
-
const mockUser = {
|
|
308
|
-
uid: 'current-user',
|
|
309
|
-
isAnonymous: false,
|
|
310
|
-
emailVerified: true,
|
|
311
|
-
providerData: [],
|
|
312
|
-
} as unknown as User;
|
|
313
|
-
authMock.currentUser = mockUser;
|
|
314
|
-
|
|
315
|
-
await service.unlinkAuthProvider('google.com');
|
|
316
|
-
|
|
317
|
-
expect(unlink).toHaveBeenCalledWith(mockUser, 'google.com');
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('should handle unlink error', async () => {
|
|
321
|
-
const { unlink } = await import('firebase/auth');
|
|
322
|
-
const mockUser = { uid: 'current-user' } as User;
|
|
323
|
-
authMock.currentUser = mockUser;
|
|
324
|
-
const error = new Error('Unlink failed');
|
|
325
|
-
(unlink as Mock).mockRejectedValueOnce(error);
|
|
326
|
-
|
|
327
|
-
await expect(service.unlinkAuthProvider('google.com')).rejects.toMatch(
|
|
328
|
-
/Failed to unlink google.com account/,
|
|
329
|
-
);
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
});
|