Skip to content

Commit dd46039

Browse files
authored
Merge pull request #356 from jimmykane/refactor/claimsandroles
Clains and roles refactoring
2 parents 3ba45b9 + 6eb41ab commit dd46039

File tree

81 files changed

+2794
-1318
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+2794
-1318
lines changed

functions/src/OAuth2.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ vi.mock('./utils', () => ({
109109
isCorsAllowed: vi.fn().mockReturnValue(true),
110110
setAccessControlHeadersOnResponse: vi.fn(),
111111
getUserIDFromFirebaseToken: vi.fn().mockResolvedValue('testUserID'),
112-
isProUser: vi.fn().mockResolvedValue(true),
112+
hasProAccess: vi.fn().mockResolvedValue(true),
113113
PRO_REQUIRED_MESSAGE: 'Service sync is a Pro feature.'
114114
}));
115115

functions/src/config.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
3+
// Hoisted admin mock & dotenv noop
4+
const adminMock = vi.hoisted(() => ({
5+
instanceId: vi.fn(() => ({
6+
app: { options: { projectId: 'mock-project' } }
7+
}))
8+
}));
9+
10+
vi.mock('dotenv', () => ({ config: vi.fn() }));
11+
12+
vi.mock('firebase-admin', () => ({
13+
default: {
14+
instanceId: adminMock.instanceId
15+
},
16+
instanceId: adminMock.instanceId
17+
}));
18+
19+
const envBackup: NodeJS.ProcessEnv = { ...process.env };
20+
21+
describe('config.ts', () => {
22+
beforeEach(() => {
23+
vi.resetModules();
24+
Object.assign(process.env, {
25+
SUUNTOAPP_CLIENT_ID: 'suunto-id',
26+
SUUNTOAPP_CLIENT_SECRET: 'suunto-secret',
27+
SUUNTOAPP_SUBSCRIPTION_KEY: 'suunto-sub',
28+
COROSAPI_CLIENT_ID: 'coros-id',
29+
COROSAPI_CLIENT_SECRET: 'coros-secret',
30+
GARMINAPI_CLIENT_ID: 'garmin-id',
31+
GARMINAPI_CLIENT_SECRET: 'garmin-secret',
32+
});
33+
delete process.env.GCLOUD_PROJECT; // force fallback to admin.instanceId
34+
});
35+
36+
afterEach(() => {
37+
process.env = { ...envBackup };
38+
vi.clearAllMocks();
39+
});
40+
41+
it('returns configured values and derives cloudtasks defaults from admin project', async () => {
42+
const { config } = await import('./config');
43+
44+
expect(config.suuntoapp.client_id).toBe('suunto-id');
45+
expect(config.suuntoapp.subscription_key).toBe('suunto-sub');
46+
expect(config.corosapi.client_secret).toBe('coros-secret');
47+
expect(config.garminapi.client_id).toBe('garmin-id');
48+
49+
expect(config.cloudtasks.projectId).toBe('mock-project');
50+
expect(config.cloudtasks.serviceAccountEmail).toBe('[email protected]');
51+
expect(config.debug.bucketName).toBe('quantified-self-io-debug-files');
52+
});
53+
54+
it('throws when a required env var is missing', async () => {
55+
delete process.env.SUUNTOAPP_CLIENT_ID;
56+
const { config } = await import('./config');
57+
58+
expect(() => config.suuntoapp.client_id).toThrow(/Missing required environment variable: SUUNTOAPP_CLIENT_ID/);
59+
});
60+
});

functions/src/coros/auth/wrapper.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ vi.mock('firebase-functions/v1', () => ({
2828
}));
2929

3030
vi.mock('../../utils', () => ({
31-
isProUser: vi.fn().mockResolvedValue(true),
31+
hasProAccess: vi.fn().mockResolvedValue(true),
3232
PRO_REQUIRED_MESSAGE: 'Service sync is a Pro feature.'
3333
}));
3434

@@ -52,7 +52,7 @@ describe('COROS Auth Wrapper', () => {
5252

5353
beforeEach(() => {
5454
vi.clearAllMocks();
55-
(utils.isProUser as any).mockResolvedValue(true);
55+
(utils.hasProAccess as any).mockResolvedValue(true);
5656

5757
context = {
5858
app: { appId: 'test-app' },
@@ -67,7 +67,7 @@ describe('COROS Auth Wrapper', () => {
6767
it('should return redirect URI for pro user', async () => {
6868
const result = await getCOROSAPIAuthRequestTokenRedirectURI(data, context);
6969

70-
expect(utils.isProUser).toHaveBeenCalledWith('testUserID');
70+
expect(utils.hasProAccess).toHaveBeenCalledWith('testUserID');
7171
expect(oauth2.getServiceOAuth2CodeRedirectAndSaveStateToUser).toHaveBeenCalledWith(
7272
'testUserID',
7373
SERVICE_NAME,
@@ -77,7 +77,7 @@ describe('COROS Auth Wrapper', () => {
7777
});
7878

7979
it('should throw error for non-pro user', async () => {
80-
(utils.isProUser as any).mockResolvedValue(false);
80+
(utils.hasProAccess as any).mockResolvedValue(false);
8181

8282
await expect(getCOROSAPIAuthRequestTokenRedirectURI(data, context))
8383
.rejects.toThrow('Service sync is a Pro feature.');

functions/src/coros/auth/wrapper.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import * as functions from 'firebase-functions/v1';
44
import * as logger from 'firebase-functions/logger';
5-
import { isProUser, PRO_REQUIRED_MESSAGE } from '../../utils';
5+
import { hasProAccess, PRO_REQUIRED_MESSAGE } from '../../utils';
66
import {
77
deauthorizeServiceForUser,
88
getAndSetServiceOAuth2AccessTokenForUser,
@@ -38,7 +38,7 @@ export const getCOROSAPIAuthRequestTokenRedirectURI = functions
3838
const userID = context.auth.uid;
3939

4040
// Enforce Pro Access
41-
if (!(await isProUser(userID))) {
41+
if (!(await hasProAccess(userID))) {
4242
logger.warn(`Blocking COROS Auth for non-pro user ${userID}`);
4343
throw new functions.https.HttpsError('permission-denied', PRO_REQUIRED_MESSAGE);
4444
}
@@ -77,7 +77,7 @@ export const requestAndSetCOROSAPIAccessToken = functions
7777
const userID = context.auth.uid;
7878

7979
// Enforce Pro Access
80-
if (!(await isProUser(userID))) {
80+
if (!(await hasProAccess(userID))) {
8181
logger.warn(`Blocking COROS Token Set for non-pro user ${userID}`);
8282
throw new functions.https.HttpsError('permission-denied', PRO_REQUIRED_MESSAGE);
8383
}

functions/src/coros/history-to-queue.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ vi.mock('firebase-functions/v1', () => ({
2929
}));
3030

3131
vi.mock('../utils', () => ({
32-
isProUser: vi.fn().mockResolvedValue(true),
32+
hasProAccess: vi.fn().mockResolvedValue(true),
3333
PRO_REQUIRED_MESSAGE: 'Service sync is a Pro feature.'
3434
}));
3535

@@ -47,7 +47,7 @@ describe('COROS History to Queue', () => {
4747

4848
beforeEach(() => {
4949
vi.clearAllMocks();
50-
(utils.isProUser as any).mockResolvedValue(true);
50+
(utils.hasProAccess as any).mockResolvedValue(true);
5151
(history.getNextAllowedHistoryImportDate as any).mockResolvedValue(null);
5252

5353
const recentDate = new Date();
@@ -177,7 +177,7 @@ describe('COROS History to Queue', () => {
177177
});
178178

179179
it('should throw error for non-pro user', async () => {
180-
(utils.isProUser as any).mockResolvedValue(false);
180+
(utils.hasProAccess as any).mockResolvedValue(false);
181181

182182
await expect(addCOROSAPIHistoryToQueue(data, context))
183183
.rejects.toThrow('Service sync is a Pro feature.');

functions/src/coros/history-to-queue.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import * as functions from 'firebase-functions/v1';
44
import * as logger from 'firebase-functions/logger';
5-
import { isProUser, PRO_REQUIRED_MESSAGE } from '../utils';
5+
import { hasProAccess, PRO_REQUIRED_MESSAGE } from '../utils';
66
import { SERVICE_NAME } from './constants';
77
import { COROS_HISTORY_IMPORT_LIMIT_MONTHS } from '../shared/history-import.constants';
88
import { HistoryImportResult, addHistoryToQueue, getNextAllowedHistoryImportDate } from '../history';
@@ -38,7 +38,7 @@ export const addCOROSAPIHistoryToQueue = functions
3838
const userID = context.auth.uid;
3939

4040
// Enforce Pro Access
41-
if (!(await isProUser(userID))) {
41+
if (!(await hasProAccess(userID))) {
4242
logger.warn(`Blocking history import for non-pro user ${userID}`);
4343
throw new functions.https.HttpsError('permission-denied', PRO_REQUIRED_MESSAGE);
4444
}

functions/src/garmin/auth/wrapper.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ vi.mock('../../utils', () => ({
6262
isCorsAllowed: vi.fn(),
6363
setAccessControlHeadersOnResponse: vi.fn().mockImplementation((req, res) => res),
6464
getUserIDFromFirebaseToken: vi.fn(),
65-
isProUser: vi.fn(),
65+
hasProAccess: vi.fn(),
6666
determineRedirectURI: vi.fn((req) => req.body?.redirectUri || req.query?.redirect_uri),
6767
PRO_REQUIRED_MESSAGE: 'Service sync is a Pro feature.',
6868
}));
@@ -92,7 +92,7 @@ describe('Garmin Auth Wrapper', () => {
9292

9393
vi.mocked(utils.isCorsAllowed).mockReturnValue(true);
9494
vi.mocked(utils.getUserIDFromFirebaseToken).mockResolvedValue('testUserID');
95-
vi.mocked(utils.isProUser).mockResolvedValue(true);
95+
vi.mocked(utils.hasProAccess).mockResolvedValue(true);
9696
vi.mocked(utils.determineRedirectURI).mockReturnValue('https://callback');
9797

9898
context = {
@@ -113,7 +113,7 @@ describe('Garmin Auth Wrapper', () => {
113113
});
114114

115115
it('should throw permission-denied for non-pro user', async () => {
116-
vi.mocked(utils.isProUser).mockResolvedValue(false);
116+
vi.mocked(utils.hasProAccess).mockResolvedValue(false);
117117
const data = { redirectUri: 'https://callback' };
118118

119119
await expect((getGarminAPIAuthRequestTokenRedirectURI as any)(data, context)).rejects.toThrow('Service sync is a Pro feature.');

functions/src/garmin/auth/wrapper.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as functions from 'firebase-functions/v1';
22
import * as logger from 'firebase-functions/logger';
33
import {
4-
isProUser,
4+
hasProAccess,
55
PRO_REQUIRED_MESSAGE
66
} from '../../utils';
77
import {
@@ -48,7 +48,7 @@ export const getGarminAPIAuthRequestTokenRedirectURI = functions.region(FUNCTION
4848
const userID = context.auth.uid;
4949

5050
// 3. Enforce Pro Access
51-
if (!(await isProUser(userID))) {
51+
if (!(await hasProAccess(userID))) {
5252
logger.warn(`Blocking Garmin Auth for non-pro user ${userID}`);
5353
throw new functions.https.HttpsError(
5454
'permission-denied',
@@ -96,7 +96,7 @@ export const requestAndSetGarminAPIAccessToken = functions.region(FUNCTIONS_MANI
9696
const userID = context.auth.uid;
9797

9898
// 3. Enforce Pro Access
99-
if (!(await isProUser(userID))) {
99+
if (!(await hasProAccess(userID))) {
100100
logger.warn(`Blocking Garmin Token Set for non-pro user ${userID}`);
101101
throw new functions.https.HttpsError('permission-denied', PRO_REQUIRED_MESSAGE);
102102
}

functions/src/garmin/backfill.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ vi.mock('firebase-functions/v1', async () => {
6161

6262
vi.mock('../utils', () => ({
6363
getUserIDFromFirebaseToken: vi.fn(),
64-
isProUser: vi.fn(),
64+
hasProAccess: vi.fn(),
6565
isCorsAllowed: vi.fn().mockReturnValue(true),
6666
setAccessControlHeadersOnResponse: vi.fn(),
6767
PRO_REQUIRED_MESSAGE: 'Service sync is a Pro feature.'
@@ -90,7 +90,7 @@ describe('Garmin Backfill', () => {
9090

9191
// Default util mocks
9292
(utils.getUserIDFromFirebaseToken as any).mockResolvedValue('testUserID');
93-
(utils.isProUser as any).mockResolvedValue(true);
93+
(utils.hasProAccess as any).mockResolvedValue(true);
9494

9595
// Mock getTokenData to return a valid token
9696
(tokens.getTokenData as any).mockResolvedValue({

functions/src/garmin/backfill.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as functions from 'firebase-functions/v1';
22
import * as logger from 'firebase-functions/logger';
3-
import { isProUser, PRO_REQUIRED_MESSAGE } from '../utils';
3+
import { hasProAccess, PRO_REQUIRED_MESSAGE } from '../utils';
44

55
import * as requestPromise from '../request-helper';
66
import * as admin from 'firebase-admin';
@@ -44,7 +44,7 @@ export const backfillGarminAPIActivities = functions.region(FUNCTIONS_MANIFEST.b
4444

4545
const userID = context.auth.uid;
4646

47-
if (!(await isProUser(userID))) {
47+
if (!(await hasProAccess(userID))) {
4848
logger.warn(`Blocking history import for non-pro user ${userID}`);
4949
throw new functions.https.HttpsError('permission-denied', PRO_REQUIRED_MESSAGE);
5050
}

0 commit comments

Comments
 (0)