@sneat/api 0.1.2 → 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 +6 -0
- package/esm2022/index.js.map +1 -0
- package/esm2022/lib/sneat-api-service-factory.js +60 -0
- package/esm2022/lib/sneat-api-service-factory.js.map +1 -0
- package/esm2022/lib/sneat-api-service.interface.js +2 -0
- package/esm2022/lib/sneat-api-service.interface.js.map +1 -0
- package/esm2022/lib/sneat-api-service.js +107 -0
- package/esm2022/lib/sneat-api-service.js.map +1 -0
- package/esm2022/lib/sneat-api.module.js +18 -0
- package/esm2022/lib/sneat-api.module.js.map +1 -0
- package/esm2022/lib/sneat-firestore.service.js +75 -0
- package/esm2022/lib/sneat-firestore.service.js.map +1 -0
- package/esm2022/sneat-api.js +5 -0
- package/esm2022/sneat-api.js.map +1 -0
- package/lib/sneat-api-service-factory.d.ts +9 -0
- package/lib/sneat-api-service.d.ts +29 -0
- package/lib/sneat-api-service.interface.d.ts +20 -0
- package/lib/sneat-api.module.d.ts +7 -0
- package/lib/sneat-firestore.service.d.ts +30 -0
- package/package.json +14 -2
- package/sneat-api.d.ts +5 -0
- package/eslint.config.js +0 -7
- package/ng-package.json +0 -7
- package/project.json +0 -38
- package/src/lib/sneat-api-service-factory.spec.ts +0 -132
- package/src/lib/sneat-api-service-factory.ts +0 -59
- package/src/lib/sneat-api-service.interface.ts +0 -50
- package/src/lib/sneat-api-service.spec.ts +0 -298
- package/src/lib/sneat-api-service.ts +0 -138
- package/src/lib/sneat-api.module.spec.ts +0 -18
- package/src/lib/sneat-api.module.ts +0 -10
- package/src/lib/sneat-firestore.service.spec.ts +0 -465
- package/src/lib/sneat-firestore.service.ts +0 -157
- 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
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { TestBed } from '@angular/core/testing';
|
|
3
|
-
import {
|
|
4
|
-
SneatApiServiceFactory,
|
|
5
|
-
getStoreUrl,
|
|
6
|
-
} from './sneat-api-service-factory';
|
|
7
|
-
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
|
8
|
-
import { Auth } from '@angular/fire/auth';
|
|
9
|
-
import { SneatApiBaseUrl } from './sneat-api-service';
|
|
10
|
-
import * as coreModule from '@sneat/core';
|
|
11
|
-
|
|
12
|
-
const onIdTokenChangedMock = vi.fn();
|
|
13
|
-
|
|
14
|
-
vi.mock('@angular/fire/auth', () => ({
|
|
15
|
-
onIdTokenChanged: (...args: unknown[]) => onIdTokenChangedMock(...args),
|
|
16
|
-
Auth: vi.fn(),
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
describe('SneatApiServiceFactory', () => {
|
|
20
|
-
let factory: SneatApiServiceFactory;
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
TestBed.configureTestingModule({
|
|
24
|
-
providers: [
|
|
25
|
-
SneatApiServiceFactory,
|
|
26
|
-
provideHttpClientTesting(),
|
|
27
|
-
{ provide: SneatApiBaseUrl, useValue: undefined },
|
|
28
|
-
{ provide: Auth, useValue: {} },
|
|
29
|
-
],
|
|
30
|
-
});
|
|
31
|
-
factory = TestBed.inject(SneatApiServiceFactory);
|
|
32
|
-
onIdTokenChangedMock.mockClear();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should be created', () => {
|
|
36
|
-
expect(factory).toBeTruthy();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe('getSneatApiService', () => {
|
|
40
|
-
it('should throw error if storeId is empty', () => {
|
|
41
|
-
expect(() => factory.getSneatApiService('')).toThrow(
|
|
42
|
-
'storeRef is a required parameter, got empty: string',
|
|
43
|
-
);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should throw error if storeRef.type is empty', () => {
|
|
47
|
-
// Mock parseStoreRef to return an object with empty type
|
|
48
|
-
const parseStoreRefSpy = vi.spyOn(coreModule, 'parseStoreRef');
|
|
49
|
-
parseStoreRefSpy.mockReturnValue({ type: '' } as any);
|
|
50
|
-
|
|
51
|
-
expect(() => factory.getSneatApiService('test')).toThrow(
|
|
52
|
-
'storeRef.type is a required parameter, got empty: string',
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
parseStoreRefSpy.mockRestore();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should throw error for unknown store type', () => {
|
|
59
|
-
TestBed.runInInjectionContext(() => {
|
|
60
|
-
// 'github' is a valid storeId for parseStoreRef but not handled by factory
|
|
61
|
-
expect(() => factory.getSneatApiService('github')).toThrow(
|
|
62
|
-
'unknown store type: github',
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should return service for firestore type', () => {
|
|
68
|
-
TestBed.runInInjectionContext(() => {
|
|
69
|
-
const service = factory.getSneatApiService('firestore');
|
|
70
|
-
expect(service).toBeTruthy();
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should cache and return same service instance for same storeId', () => {
|
|
75
|
-
TestBed.runInInjectionContext(() => {
|
|
76
|
-
const service1 = factory.getSneatApiService('firestore');
|
|
77
|
-
const service2 = factory.getSneatApiService('firestore');
|
|
78
|
-
expect(service1).toBe(service2);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('getStoreUrl', () => {
|
|
84
|
-
it('should return localhost URL for firestore without trailing slash', () => {
|
|
85
|
-
const url = getStoreUrl('firestore');
|
|
86
|
-
expect(url).toBe('http://localhost:4300/v0');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should remove trailing slash from firestore URL', () => {
|
|
90
|
-
// The function returns without trailing slash already
|
|
91
|
-
const url = getStoreUrl('firestore');
|
|
92
|
-
expect(url).not.toMatch(/\/$/);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should return storeId if it is empty', () => {
|
|
96
|
-
const url = getStoreUrl('');
|
|
97
|
-
expect(url).toBe('');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should return storeId if it matches http URL pattern', () => {
|
|
101
|
-
const httpUrl = 'http://example.com/api';
|
|
102
|
-
const url = getStoreUrl(httpUrl);
|
|
103
|
-
expect(url).toBe(httpUrl);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should return storeId if it matches https URL pattern', () => {
|
|
107
|
-
const httpsUrl = 'https://example.com/api';
|
|
108
|
-
const url = getStoreUrl(httpsUrl);
|
|
109
|
-
expect(url).toBe(httpsUrl);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should convert http- prefix to http://', () => {
|
|
113
|
-
const url = getStoreUrl('http-example.com');
|
|
114
|
-
expect(url).toBe('http://example.com');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should convert https- prefix to https://', () => {
|
|
118
|
-
const url = getStoreUrl('https-example.com');
|
|
119
|
-
expect(url).toBe('https://example.com');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should handle host:port format', () => {
|
|
123
|
-
const url = getStoreUrl('localhost:8080');
|
|
124
|
-
expect(url).toBe('//localhost:8080');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('should handle host:port:path format', () => {
|
|
128
|
-
const url = getStoreUrl('localhost:8080:api');
|
|
129
|
-
expect(url).toBe('//localhost:8080:api');
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { Injectable, inject } from '@angular/core';
|
|
2
|
-
import { ISneatApiService } from './sneat-api-service.interface';
|
|
3
|
-
import { parseStoreRef } from '@sneat/core';
|
|
4
|
-
import { SneatApiService } from './sneat-api-service';
|
|
5
|
-
|
|
6
|
-
export const getStoreUrl = (storeId: string): string => {
|
|
7
|
-
if (storeId === 'firestore') {
|
|
8
|
-
const v = 'http://localhost:4300/v0'; //environment.agents.firestoreStoreAgent;
|
|
9
|
-
return v.endsWith('/') ? v.substring(0, v.length - 1) : v;
|
|
10
|
-
}
|
|
11
|
-
if (!storeId || storeId.match(/https?:\/\//)) {
|
|
12
|
-
return storeId;
|
|
13
|
-
}
|
|
14
|
-
if (storeId.startsWith('http-')) {
|
|
15
|
-
return storeId.replace('http-', 'http' + '://');
|
|
16
|
-
}
|
|
17
|
-
if (storeId.startsWith('https-')) {
|
|
18
|
-
return storeId.replace('https-', 'https://');
|
|
19
|
-
}
|
|
20
|
-
const a = storeId.split(':');
|
|
21
|
-
storeId = `//${a[0]}:${a[1]}`;
|
|
22
|
-
if (a[2]) {
|
|
23
|
-
storeId += ':' + a[2];
|
|
24
|
-
}
|
|
25
|
-
return storeId;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
@Injectable({ providedIn: 'root' })
|
|
29
|
-
export class SneatApiServiceFactory {
|
|
30
|
-
private services: Record<string, ISneatApiService> = {};
|
|
31
|
-
|
|
32
|
-
public getSneatApiService(storeId: string): ISneatApiService {
|
|
33
|
-
if (!storeId) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
'storeRef is a required parameter, got empty: ' + typeof storeId,
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
const storeRef = parseStoreRef(storeId);
|
|
39
|
-
if (!storeRef.type) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
'storeRef.type is a required parameter, got empty: ' +
|
|
42
|
-
typeof storeRef.type,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
const id = `${storeRef.type}:${storeRef.url}`;
|
|
46
|
-
let service = this.services[id];
|
|
47
|
-
if (service) {
|
|
48
|
-
return service;
|
|
49
|
-
}
|
|
50
|
-
// const baseUrl = getStoreUrl(storeRefToId(storeRef));
|
|
51
|
-
switch (storeRef.type) {
|
|
52
|
-
case 'firestore':
|
|
53
|
-
this.services[id] = service = inject(SneatApiService);
|
|
54
|
-
return service;
|
|
55
|
-
default:
|
|
56
|
-
throw new Error('unknown store type: ' + storeRef.type);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Observable } from 'rxjs';
|
|
2
|
-
import { HttpHeaders, HttpParams } from '@angular/common/http';
|
|
3
|
-
|
|
4
|
-
export interface IHttpRequestOptions {
|
|
5
|
-
headers?: HttpHeaders | Record<string, string | string[]>;
|
|
6
|
-
observe?: 'body';
|
|
7
|
-
params?: HttpParams | Record<string, string | string[]>;
|
|
8
|
-
reportProgress?: boolean;
|
|
9
|
-
responseType?: 'json';
|
|
10
|
-
withCredentials?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// const SneatApiService = new InjectionToken('ISneatApiService');
|
|
14
|
-
|
|
15
|
-
export interface ISneatApiResponse<T> {
|
|
16
|
-
// TODO: Either use or delete
|
|
17
|
-
data: T;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ISneatApiService {
|
|
21
|
-
post<I, O>(
|
|
22
|
-
endpoint: string,
|
|
23
|
-
body: I,
|
|
24
|
-
options?: IHttpRequestOptions,
|
|
25
|
-
): Observable<O>;
|
|
26
|
-
|
|
27
|
-
put<I, O>(
|
|
28
|
-
endpoint: string,
|
|
29
|
-
body: I,
|
|
30
|
-
options?: IHttpRequestOptions,
|
|
31
|
-
): Observable<O>;
|
|
32
|
-
|
|
33
|
-
get<T>(
|
|
34
|
-
endpoint: string,
|
|
35
|
-
params?: HttpParams,
|
|
36
|
-
options?: IHttpRequestOptions,
|
|
37
|
-
): Observable<T>;
|
|
38
|
-
|
|
39
|
-
getAsAnonymous<T>(
|
|
40
|
-
endpoint: string,
|
|
41
|
-
params?: HttpParams,
|
|
42
|
-
options?: IHttpRequestOptions,
|
|
43
|
-
): Observable<T>;
|
|
44
|
-
|
|
45
|
-
delete<T>(
|
|
46
|
-
endpoint: string,
|
|
47
|
-
params?: HttpParams,
|
|
48
|
-
body?: unknown,
|
|
49
|
-
): Observable<T>;
|
|
50
|
-
}
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import { TestBed } from '@angular/core/testing';
|
|
2
|
-
import {
|
|
3
|
-
HttpTestingController,
|
|
4
|
-
provideHttpClientTesting,
|
|
5
|
-
} from '@angular/common/http/testing';
|
|
6
|
-
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
7
|
-
import { firstValueFrom } from 'rxjs';
|
|
8
|
-
import {
|
|
9
|
-
DefaultSneatAppApiBaseUrl,
|
|
10
|
-
SneatApiBaseUrl,
|
|
11
|
-
SneatApiService,
|
|
12
|
-
} from './sneat-api-service';
|
|
13
|
-
import { Auth, User } from '@angular/fire/auth';
|
|
14
|
-
|
|
15
|
-
const onIdTokenChangedMock = vi.fn();
|
|
16
|
-
|
|
17
|
-
vi.mock('@angular/fire/auth', () => ({
|
|
18
|
-
onIdTokenChanged: (...args: unknown[]) => onIdTokenChangedMock(...args),
|
|
19
|
-
Auth: vi.fn(),
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
const NOT_AUTHENTICATED_ERROR =
|
|
23
|
-
'User is not authenticated yet - no Firebase ID token';
|
|
24
|
-
|
|
25
|
-
describe('SneatApiService', () => {
|
|
26
|
-
let httpMock: HttpTestingController;
|
|
27
|
-
let service: SneatApiService;
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
TestBed.configureTestingModule({
|
|
31
|
-
providers: [
|
|
32
|
-
provideHttpClientTesting(),
|
|
33
|
-
{ provide: SneatApiBaseUrl, useValue: undefined },
|
|
34
|
-
{ provide: Auth, useValue: {} },
|
|
35
|
-
],
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
httpMock = TestBed.inject(HttpTestingController);
|
|
39
|
-
service = TestBed.inject(SneatApiService);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
httpMock.verify();
|
|
44
|
-
vi.clearAllMocks();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('registers for ID token changes on construction', () => {
|
|
48
|
-
expect(onIdTokenChangedMock).toHaveBeenCalledTimes(1);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('uses custom base URL when provided', async () => {
|
|
52
|
-
const customService = Object.create(SneatApiService.prototype);
|
|
53
|
-
customService.baseUrl = 'https://custom-api.com/';
|
|
54
|
-
customService.httpClient = TestBed.inject(HttpClient);
|
|
55
|
-
// Manually implement setApiAuthToken as it's an arrow function property in the original class
|
|
56
|
-
customService.setApiAuthToken = (token?: string) => {
|
|
57
|
-
(customService as unknown as { authToken?: string }).authToken = token;
|
|
58
|
-
};
|
|
59
|
-
customService.setApiAuthToken('token-123');
|
|
60
|
-
|
|
61
|
-
// Manually implement headers and errorIfNotAuthenticated as they are used by get/post etc
|
|
62
|
-
customService.headers = (
|
|
63
|
-
service as unknown as { headers: () => void }
|
|
64
|
-
).headers.bind(customService);
|
|
65
|
-
customService.errorIfNotAuthenticated = (
|
|
66
|
-
service as unknown as { errorIfNotAuthenticated: () => void }
|
|
67
|
-
).errorIfNotAuthenticated.bind(customService);
|
|
68
|
-
|
|
69
|
-
const responsePromise = firstValueFrom(customService.get('health'));
|
|
70
|
-
const req = httpMock.expectOne('https://custom-api.com/health');
|
|
71
|
-
|
|
72
|
-
expect(req.request.method).toBe('GET');
|
|
73
|
-
req.flush({ ok: true });
|
|
74
|
-
|
|
75
|
-
await expect(responsePromise).resolves.toEqual({ ok: true });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('handles onIdTokenChanged callback for authenticated user', async () => {
|
|
79
|
-
const callback = onIdTokenChangedMock.mock.calls[0][1];
|
|
80
|
-
const mockUser = {
|
|
81
|
-
getIdToken: vi.fn().mockResolvedValue('new-token'),
|
|
82
|
-
} as unknown as User;
|
|
83
|
-
|
|
84
|
-
callback.next(mockUser);
|
|
85
|
-
|
|
86
|
-
// Wait for promise resolution
|
|
87
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
88
|
-
|
|
89
|
-
service.setApiAuthToken('new-token'); // Manual set because we can't easily wait for the internal async call
|
|
90
|
-
const responsePromise = firstValueFrom(service.get('auth-test'));
|
|
91
|
-
const req = httpMock.expectOne(`${DefaultSneatAppApiBaseUrl}auth-test`);
|
|
92
|
-
expect(req.request.headers.get('Authorization')).toBe('Bearer new-token');
|
|
93
|
-
req.flush({ ok: true });
|
|
94
|
-
await responsePromise;
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('handles onIdTokenChanged callback for unauthenticated user', async () => {
|
|
98
|
-
const callback = onIdTokenChangedMock.mock.calls[0][1];
|
|
99
|
-
service.setApiAuthToken('old-token');
|
|
100
|
-
|
|
101
|
-
callback.next(null);
|
|
102
|
-
service.setApiAuthToken(undefined);
|
|
103
|
-
|
|
104
|
-
await expect(firstValueFrom(service.get('auth-test'))).rejects.toBe(
|
|
105
|
-
NOT_AUTHENTICATED_ERROR,
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('handles getIdToken error', async () => {
|
|
110
|
-
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {
|
|
111
|
-
/* ignore */
|
|
112
|
-
});
|
|
113
|
-
const callback = onIdTokenChangedMock.mock.calls[0][1];
|
|
114
|
-
const mockUser = {
|
|
115
|
-
getIdToken: vi.fn().mockRejectedValue(new Error('token error')),
|
|
116
|
-
} as unknown as User;
|
|
117
|
-
|
|
118
|
-
callback.next(mockUser);
|
|
119
|
-
|
|
120
|
-
// Wait for promise resolution
|
|
121
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
122
|
-
|
|
123
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
124
|
-
'getIdToken() error:',
|
|
125
|
-
expect.any(Error),
|
|
126
|
-
);
|
|
127
|
-
consoleSpy.mockRestore();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('handles onIdTokenChanged error', () => {
|
|
131
|
-
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {
|
|
132
|
-
/* ignore */
|
|
133
|
-
});
|
|
134
|
-
const callback = onIdTokenChangedMock.mock.calls[0][1];
|
|
135
|
-
|
|
136
|
-
callback.error(new Error('auth error'));
|
|
137
|
-
|
|
138
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
139
|
-
'onIdTokenChanged() error:',
|
|
140
|
-
expect.any(Error),
|
|
141
|
-
);
|
|
142
|
-
consoleSpy.mockRestore();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('handles onIdTokenChanged complete', () => {
|
|
146
|
-
const callback = onIdTokenChangedMock.mock.calls[0][1];
|
|
147
|
-
expect(callback.complete()).toBeUndefined();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('emits on destroyed subject when ngOnDestroy is called', () => {
|
|
151
|
-
const nextSpy = vi.spyOn(
|
|
152
|
-
(service as unknown as { destroyed: { next: () => void } }).destroyed,
|
|
153
|
-
'next',
|
|
154
|
-
);
|
|
155
|
-
const completeSpy = vi.spyOn(
|
|
156
|
-
(service as unknown as { destroyed: { complete: () => void } }).destroyed,
|
|
157
|
-
'complete',
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
service.ngOnDestroy();
|
|
161
|
-
|
|
162
|
-
expect(nextSpy).toHaveBeenCalled();
|
|
163
|
-
expect(completeSpy).toHaveBeenCalled();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('performs PUT requests', async () => {
|
|
167
|
-
service.setApiAuthToken('token');
|
|
168
|
-
const responsePromise = firstValueFrom(service.put('update', { x: 1 }));
|
|
169
|
-
const req = httpMock.expectOne(`${DefaultSneatAppApiBaseUrl}update`);
|
|
170
|
-
expect(req.request.method).toBe('PUT');
|
|
171
|
-
expect(req.request.body).toEqual({ x: 1 });
|
|
172
|
-
req.flush({ ok: true });
|
|
173
|
-
await expect(responsePromise).resolves.toEqual({ ok: true });
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('performs GET requests with params', async () => {
|
|
177
|
-
service.setApiAuthToken('token');
|
|
178
|
-
const params = new HttpParams().set('id', '123');
|
|
179
|
-
const responsePromise = firstValueFrom(service.get('search', params));
|
|
180
|
-
const req = httpMock.expectOne(
|
|
181
|
-
(r) =>
|
|
182
|
-
r.url === `${DefaultSneatAppApiBaseUrl}search` && r.params.has('id'),
|
|
183
|
-
);
|
|
184
|
-
expect(req.request.method).toBe('GET');
|
|
185
|
-
expect(req.request.params.get('id')).toBe('123');
|
|
186
|
-
req.flush({ ok: true });
|
|
187
|
-
await expect(responsePromise).resolves.toEqual({ ok: true });
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('performs GET as anonymous with params', async () => {
|
|
191
|
-
const params = new HttpParams().set('q', 'test');
|
|
192
|
-
const responsePromise = firstValueFrom(
|
|
193
|
-
service.getAsAnonymous('search', params),
|
|
194
|
-
);
|
|
195
|
-
const req = httpMock.expectOne(
|
|
196
|
-
(r) =>
|
|
197
|
-
r.url === `${DefaultSneatAppApiBaseUrl}search` && r.params.has('q'),
|
|
198
|
-
);
|
|
199
|
-
expect(req.request.params.get('q')).toBe('test');
|
|
200
|
-
req.flush({ ok: true });
|
|
201
|
-
await expect(responsePromise).resolves.toEqual({ ok: true });
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('handles headers when not authenticated', () => {
|
|
205
|
-
(service as unknown as { authToken?: string }).authToken = undefined;
|
|
206
|
-
const headers = (
|
|
207
|
-
service as unknown as {
|
|
208
|
-
headers: () => {
|
|
209
|
-
has: (name: string) => boolean;
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
).headers();
|
|
213
|
-
expect(headers.has('Authorization')).toBe(false);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('uses default base URL when none provided', async () => {
|
|
217
|
-
service.setApiAuthToken('token-123');
|
|
218
|
-
|
|
219
|
-
const responsePromise = firstValueFrom(service.get('health'));
|
|
220
|
-
const req = httpMock.expectOne(`${DefaultSneatAppApiBaseUrl}health`);
|
|
221
|
-
|
|
222
|
-
expect(req.request.method).toBe('GET');
|
|
223
|
-
req.flush({ ok: true });
|
|
224
|
-
|
|
225
|
-
await expect(responsePromise).resolves.toEqual({ ok: true });
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('throws when not authenticated', async () => {
|
|
229
|
-
await expect(firstValueFrom(service.get('spaces'))).rejects.toBe(
|
|
230
|
-
NOT_AUTHENTICATED_ERROR,
|
|
231
|
-
);
|
|
232
|
-
await expect(firstValueFrom(service.post('spaces', {}))).rejects.toBe(
|
|
233
|
-
NOT_AUTHENTICATED_ERROR,
|
|
234
|
-
);
|
|
235
|
-
await expect(firstValueFrom(service.put('spaces', {}))).rejects.toBe(
|
|
236
|
-
NOT_AUTHENTICATED_ERROR,
|
|
237
|
-
);
|
|
238
|
-
await expect(firstValueFrom(service.delete('spaces'))).rejects.toBe(
|
|
239
|
-
NOT_AUTHENTICATED_ERROR,
|
|
240
|
-
);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('adds Authorization header when authenticated', async () => {
|
|
244
|
-
service.setApiAuthToken('abc123');
|
|
245
|
-
|
|
246
|
-
const responsePromise = firstValueFrom(
|
|
247
|
-
service.post('spaces', { name: 'x' }),
|
|
248
|
-
);
|
|
249
|
-
const req = httpMock.expectOne(`${DefaultSneatAppApiBaseUrl}spaces`);
|
|
250
|
-
|
|
251
|
-
expect(req.request.method).toBe('POST');
|
|
252
|
-
expect(req.request.headers.get('Authorization')).toBe('Bearer abc123');
|
|
253
|
-
expect(req.request.body).toEqual({ name: 'x' });
|
|
254
|
-
|
|
255
|
-
req.flush({ ok: true });
|
|
256
|
-
|
|
257
|
-
await expect(responsePromise).resolves.toEqual({ ok: true });
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('performs anonymous requests without auth header', async () => {
|
|
261
|
-
const getPromise = firstValueFrom(service.getAsAnonymous('public'));
|
|
262
|
-
const getReq = httpMock.expectOne(`${DefaultSneatAppApiBaseUrl}public`);
|
|
263
|
-
|
|
264
|
-
expect(getReq.request.method).toBe('GET');
|
|
265
|
-
expect(getReq.request.headers.get('Authorization')).toBeNull();
|
|
266
|
-
getReq.flush({ ok: true });
|
|
267
|
-
await expect(getPromise).resolves.toEqual({ ok: true });
|
|
268
|
-
|
|
269
|
-
const postPromise = firstValueFrom(
|
|
270
|
-
service.postAsAnonymous('public', { hello: 'world' }),
|
|
271
|
-
);
|
|
272
|
-
const postReq = httpMock.expectOne(`${DefaultSneatAppApiBaseUrl}public`);
|
|
273
|
-
|
|
274
|
-
expect(postReq.request.method).toBe('POST');
|
|
275
|
-
expect(postReq.request.headers.get('Authorization')).toBeNull();
|
|
276
|
-
expect(postReq.request.body).toEqual({ hello: 'world' });
|
|
277
|
-
postReq.flush({ ok: true });
|
|
278
|
-
await expect(postPromise).resolves.toEqual({ ok: true });
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('sends body on delete when provided', async () => {
|
|
282
|
-
service.setApiAuthToken('delete-token');
|
|
283
|
-
|
|
284
|
-
const responsePromise = firstValueFrom(
|
|
285
|
-
service.delete('spaces', undefined, { id: 'space-1' }),
|
|
286
|
-
);
|
|
287
|
-
const req = httpMock.expectOne(`${DefaultSneatAppApiBaseUrl}spaces`);
|
|
288
|
-
|
|
289
|
-
expect(req.request.method).toBe('DELETE');
|
|
290
|
-
expect(req.request.headers.get('Authorization')).toBe(
|
|
291
|
-
'Bearer delete-token',
|
|
292
|
-
);
|
|
293
|
-
expect(req.request.body).toEqual({ id: 'space-1' });
|
|
294
|
-
req.flush({ ok: true });
|
|
295
|
-
|
|
296
|
-
await expect(responsePromise).resolves.toEqual({ ok: true });
|
|
297
|
-
});
|
|
298
|
-
});
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
2
|
-
import {
|
|
3
|
-
Inject,
|
|
4
|
-
Injectable,
|
|
5
|
-
InjectionToken,
|
|
6
|
-
OnDestroy,
|
|
7
|
-
Optional,
|
|
8
|
-
} from '@angular/core';
|
|
9
|
-
import { Auth, onIdTokenChanged } from '@angular/fire/auth';
|
|
10
|
-
import { Observable, Subject, throwError } from 'rxjs';
|
|
11
|
-
import { ISneatApiService } from './sneat-api-service.interface';
|
|
12
|
-
|
|
13
|
-
const userIsNotAuthenticatedNoFirebaseToken =
|
|
14
|
-
'User is not authenticated yet - no Firebase ID token';
|
|
15
|
-
|
|
16
|
-
export const SneatApiAuthTokenProvider = new InjectionToken(
|
|
17
|
-
'SneatApiAuthTokenProvider',
|
|
18
|
-
);
|
|
19
|
-
export const SneatApiBaseUrl = new InjectionToken('SneatApiBaseUrl');
|
|
20
|
-
export const DefaultSneatAppApiBaseUrl = 'https://api.sneat.ws/v0/';
|
|
21
|
-
|
|
22
|
-
@Injectable({ providedIn: 'root' }) // Should it be in root? Probably it is OK.
|
|
23
|
-
export class SneatApiService implements ISneatApiService, OnDestroy {
|
|
24
|
-
private readonly baseUrl: string;
|
|
25
|
-
|
|
26
|
-
private readonly destroyed = new Subject<void>();
|
|
27
|
-
private authToken?: string;
|
|
28
|
-
|
|
29
|
-
constructor(
|
|
30
|
-
private readonly httpClient: HttpClient,
|
|
31
|
-
@Inject(SneatApiBaseUrl) @Optional() baseUrl: string | null,
|
|
32
|
-
private readonly afAuth: Auth,
|
|
33
|
-
) {
|
|
34
|
-
this.baseUrl = baseUrl ?? DefaultSneatAppApiBaseUrl;
|
|
35
|
-
// console.log('SneatApiService.constructor()', this.baseUrl);
|
|
36
|
-
onIdTokenChanged(this.afAuth, {
|
|
37
|
-
next: (user) => {
|
|
38
|
-
user
|
|
39
|
-
?.getIdToken()
|
|
40
|
-
.then(this.setApiAuthToken)
|
|
41
|
-
.catch((err) => console.error('getIdToken() error:', err));
|
|
42
|
-
},
|
|
43
|
-
error: (error) => {
|
|
44
|
-
console.error('onIdTokenChanged() error:', error);
|
|
45
|
-
},
|
|
46
|
-
complete: () => void 0,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
ngOnDestroy() {
|
|
51
|
-
this.destroyed.next();
|
|
52
|
-
this.destroyed.complete();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// TODO: It's made public because we use it in Login page, might be a bad idea consider making private and rely on afAuth.idToken event
|
|
56
|
-
setApiAuthToken = (token?: string) => {
|
|
57
|
-
// console.log('setApiAuthToken()', token);
|
|
58
|
-
this.authToken = token;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
public post<T>(endpoint: string, body: unknown): Observable<T> {
|
|
62
|
-
const url = this.baseUrl + endpoint;
|
|
63
|
-
// console.log('post()', endpoint, url, body);
|
|
64
|
-
return (
|
|
65
|
-
this.errorIfNotAuthenticated() ||
|
|
66
|
-
this.httpClient.post<T>(url, body, {
|
|
67
|
-
headers: this.headers(),
|
|
68
|
-
})
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
public put<T>(endpoint: string, body: unknown): Observable<T> {
|
|
73
|
-
return (
|
|
74
|
-
this.errorIfNotAuthenticated() ||
|
|
75
|
-
this.httpClient.put<T>(this.baseUrl + endpoint, body, {
|
|
76
|
-
headers: this.headers(),
|
|
77
|
-
})
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
public get<T>(endpoint: string, params?: HttpParams): Observable<T> {
|
|
82
|
-
return (
|
|
83
|
-
this.errorIfNotAuthenticated() ||
|
|
84
|
-
this.httpClient.get<T>(this.baseUrl + endpoint, {
|
|
85
|
-
headers: this.headers(),
|
|
86
|
-
params,
|
|
87
|
-
})
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
public getAsAnonymous<T>(
|
|
92
|
-
endpoint: string,
|
|
93
|
-
params?: HttpParams,
|
|
94
|
-
): Observable<T> {
|
|
95
|
-
return this.httpClient.get<T>(this.baseUrl + endpoint, {
|
|
96
|
-
params,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
public postAsAnonymous<T>(endpoint: string, body: unknown): Observable<T> {
|
|
101
|
-
const url = this.baseUrl + endpoint;
|
|
102
|
-
// alert('postAsAnonymous(), url=' + url);
|
|
103
|
-
return this.httpClient.post<T>(url, body);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
public delete<T>(
|
|
107
|
-
endpoint: string,
|
|
108
|
-
params?: HttpParams,
|
|
109
|
-
body?: unknown,
|
|
110
|
-
): Observable<T> {
|
|
111
|
-
// console.log('delete()', endpoint, params);
|
|
112
|
-
const url = this.baseUrl + endpoint;
|
|
113
|
-
return (
|
|
114
|
-
this.errorIfNotAuthenticated() ||
|
|
115
|
-
this.httpClient.delete<T>(url, {
|
|
116
|
-
params,
|
|
117
|
-
headers: this.headers(),
|
|
118
|
-
body,
|
|
119
|
-
})
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private errorIfNotAuthenticated(): Observable<never> | undefined {
|
|
124
|
-
const result: Observable<never> | undefined =
|
|
125
|
-
(!this.authToken &&
|
|
126
|
-
throwError(() => userIsNotAuthenticatedNoFirebaseToken)) ||
|
|
127
|
-
undefined;
|
|
128
|
-
return result;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private headers(): HttpHeaders {
|
|
132
|
-
let headers = new HttpHeaders();
|
|
133
|
-
if (this.authToken) {
|
|
134
|
-
headers = headers.append('Authorization', 'Bearer ' + this.authToken);
|
|
135
|
-
}
|
|
136
|
-
return headers;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { TestBed } from '@angular/core/testing';
|
|
2
|
-
import { SneatApiModule } from './sneat-api.module';
|
|
3
|
-
|
|
4
|
-
describe('SneatApiModule', () => {
|
|
5
|
-
it('should create module', () => {
|
|
6
|
-
const module = new SneatApiModule();
|
|
7
|
-
expect(module).toBeTruthy();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('should be importable in TestBed', () => {
|
|
11
|
-
TestBed.configureTestingModule({
|
|
12
|
-
imports: [SneatApiModule],
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const module = TestBed.inject(SneatApiModule);
|
|
16
|
-
expect(module).toBeTruthy();
|
|
17
|
-
});
|
|
18
|
-
});
|