|
4 | 4 | * SPDX-License-Identifier: Apache-2.0 |
5 | 5 | */ |
6 | 6 |
|
| 7 | +import { execSync } from 'node:child_process'; |
| 8 | +import { readFileSync } from 'node:fs'; |
| 9 | +import { join } from 'node:path'; |
| 10 | + |
7 | 11 | import { describe, it, expect } from '@jest/globals'; |
8 | | -import { FEATURE_GROUPS, featureGroupKey } from '../../features/feature-config'; |
| 12 | +import { |
| 13 | + FEATURE_GROUPS, |
| 14 | + featureGroupKey, |
| 15 | + getAllPossibleScopes, |
| 16 | +} from '../../features/feature-config'; |
9 | 17 |
|
10 | 18 | describe('feature-config', () => { |
11 | 19 | it('should have unique feature group keys', () => { |
@@ -57,3 +65,64 @@ describe('feature-config', () => { |
57 | 65 | expect(timeRead!.scopes).toEqual([]); |
58 | 66 | }); |
59 | 67 | }); |
| 68 | + |
| 69 | +describe('getAllPossibleScopes (issue #323)', () => { |
| 70 | + it('should include both write and readonly scopes for paired groups', () => { |
| 71 | + const scopes = getAllPossibleScopes(); |
| 72 | + // Both must be registered on the consent screen — users may flip |
| 73 | + // <service>.write off, which causes <service>.readonly to be requested. |
| 74 | + expect(scopes).toContain('https://www.googleapis.com/auth/drive'); |
| 75 | + expect(scopes).toContain('https://www.googleapis.com/auth/drive.readonly'); |
| 76 | + expect(scopes).toContain('https://www.googleapis.com/auth/gmail.modify'); |
| 77 | + expect(scopes).toContain('https://www.googleapis.com/auth/gmail.readonly'); |
| 78 | + expect(scopes).toContain('https://www.googleapis.com/auth/calendar'); |
| 79 | + expect(scopes).toContain( |
| 80 | + 'https://www.googleapis.com/auth/calendar.readonly', |
| 81 | + ); |
| 82 | + }); |
| 83 | + |
| 84 | + it('should exclude default-OFF group scopes that are not in any default-ON group', () => { |
| 85 | + const scopes = getAllPossibleScopes(); |
| 86 | + // tasks.* are default-OFF and their scopes are not shared with any |
| 87 | + // default-ON group, so they shouldn't be in the registration list. |
| 88 | + expect(scopes).not.toContain('https://www.googleapis.com/auth/tasks'); |
| 89 | + expect(scopes).not.toContain( |
| 90 | + 'https://www.googleapis.com/auth/tasks.readonly', |
| 91 | + ); |
| 92 | + }); |
| 93 | + |
| 94 | + it('should be sorted and deduplicated', () => { |
| 95 | + const scopes = getAllPossibleScopes(); |
| 96 | + const sortedUnique = [...new Set(scopes)].sort(); |
| 97 | + expect(scopes).toEqual(sortedUnique); |
| 98 | + }); |
| 99 | + |
| 100 | + it('print-scopes.ts should emit the same list (drift guard for setup-gcp.sh)', () => { |
| 101 | + // setup-gcp.sh shells out to scripts/print-scopes.ts; if this test |
| 102 | + // fails, the consent screen registration list will drift from |
| 103 | + // FEATURE_GROUPS — which is the bug in issue #323. |
| 104 | + const repoRoot = join(__dirname, '..', '..', '..', '..'); |
| 105 | + // execSync (not execFileSync) so Windows can resolve npx.cmd via the |
| 106 | + // shell. Tests run on ubuntu/macos/windows. |
| 107 | + const output = execSync( |
| 108 | + 'npx --no-install ts-node --transpile-only scripts/print-scopes.ts', |
| 109 | + { cwd: repoRoot, encoding: 'utf8' }, |
| 110 | + ); |
| 111 | + const printed = output.trim().split(/\r?\n/); |
| 112 | + expect(printed).toEqual(getAllPossibleScopes()); |
| 113 | + }); |
| 114 | + |
| 115 | + it('setup-gcp.sh should not contain a hardcoded SCOPES list (drift guard)', () => { |
| 116 | + // If someone re-inlines the list, this test catches it. |
| 117 | + const repoRoot = join(__dirname, '..', '..', '..', '..'); |
| 118 | + const setupScript = readFileSync( |
| 119 | + join(repoRoot, 'scripts', 'setup-gcp.sh'), |
| 120 | + 'utf8', |
| 121 | + ); |
| 122 | + // A hardcoded list would have a literal scope URL inside SCOPES=( ... ). |
| 123 | + const hardcodedMatch = setupScript.match( |
| 124 | + /SCOPES=\(\s*"https:\/\/www\.googleapis\.com\//, |
| 125 | + ); |
| 126 | + expect(hardcodedMatch).toBeNull(); |
| 127 | + }); |
| 128 | +}); |
0 commit comments