|
| 1 | +/** |
| 2 | + * MIT No Attribution |
| 3 | + * |
| 4 | + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 5 | + * |
| 6 | + * Permission is hereby granted, free of charge, to any person obtaining a copy of |
| 7 | + * the Software without restriction, including without limitation the rights to |
| 8 | + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
| 9 | + * the Software, and to permit persons to whom the Software is furnished to do so. |
| 10 | + * |
| 11 | + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 12 | + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 13 | + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 14 | + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 15 | + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 16 | + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 17 | + * SOFTWARE. |
| 18 | + */ |
| 19 | + |
| 20 | +import * as fs from 'fs'; |
| 21 | +import * as path from 'path'; |
| 22 | + |
| 23 | +/** |
| 24 | + * Cross-language contract: the JSON schema written into Secrets Manager |
| 25 | + * by the CLI's `bgagent linear setup` MUST match what the Lambda-side |
| 26 | + * resolver expects to read. Two TypeScript interfaces define the shape |
| 27 | + * independently — `StoredLinearOauthToken` (CLI) and `StoredOauthToken` |
| 28 | + * (Lambda). Without a contract test, drift between the two is a silent |
| 29 | + * runtime bug: CLI writes `installer_user_id`, Lambda reads |
| 30 | + * `installed_by_platform_user_id`, refresh works, every Lambda |
| 31 | + * invocation logs a missing-field error. |
| 32 | + * |
| 33 | + * This test parses both interface definitions out of source and |
| 34 | + * asserts the field set is equal. It deliberately avoids importing |
| 35 | + * the CLI (cross-package import would couple build orders); a |
| 36 | + * lightweight regex-extract is enough to keep the schemas honest. |
| 37 | + */ |
| 38 | + |
| 39 | +const REPO_ROOT = path.resolve(__dirname, '..', '..', '..'); |
| 40 | +const LAMBDA_RESOLVER = path.join(REPO_ROOT, 'cdk', 'src', 'handlers', 'shared', 'linear-oauth-resolver.ts'); |
| 41 | +const CLI_OAUTH = path.join(REPO_ROOT, 'cli', 'src', 'linear-oauth.ts'); |
| 42 | + |
| 43 | +function extractInterfaceFields(source: string, interfaceName: string): string[] { |
| 44 | + const reBlock = new RegExp(`export\\s+interface\\s+${interfaceName}\\s*\\{([\\s\\S]*?)\\n\\}`); |
| 45 | + const match = reBlock.exec(source); |
| 46 | + if (!match) { |
| 47 | + throw new Error(`Could not find interface ${interfaceName}`); |
| 48 | + } |
| 49 | + const body = match[1]; |
| 50 | + const fields: string[] = []; |
| 51 | + // Match `readonly <name>:` or `<name>:` field declarations. Skip |
| 52 | + // lines that are inside JSDoc comment blocks (start with `*`) or |
| 53 | + // single-line comments (`//`). |
| 54 | + for (const rawLine of body.split('\n')) { |
| 55 | + const line = rawLine.trim(); |
| 56 | + if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) continue; |
| 57 | + const fieldMatch = /^(?:readonly\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\??\s*:/.exec(line); |
| 58 | + if (fieldMatch) { |
| 59 | + fields.push(fieldMatch[1]); |
| 60 | + } |
| 61 | + } |
| 62 | + return fields; |
| 63 | +} |
| 64 | + |
| 65 | +describe('StoredOauthToken / StoredLinearOauthToken cross-language parity', () => { |
| 66 | + test('Lambda and CLI define the same set of fields', () => { |
| 67 | + const lambdaSource = fs.readFileSync(LAMBDA_RESOLVER, 'utf8'); |
| 68 | + const cliSource = fs.readFileSync(CLI_OAUTH, 'utf8'); |
| 69 | + |
| 70 | + const lambdaFields = extractInterfaceFields(lambdaSource, 'StoredOauthToken').sort(); |
| 71 | + const cliFields = extractInterfaceFields(cliSource, 'StoredLinearOauthToken').sort(); |
| 72 | + |
| 73 | + expect(lambdaFields).toEqual(cliFields); |
| 74 | + // Sanity: at least 11 fields per the documented schema. Catches |
| 75 | + // a regex parse failure that returns empty arrays. |
| 76 | + expect(lambdaFields.length).toBeGreaterThanOrEqual(11); |
| 77 | + }); |
| 78 | + |
| 79 | + test('Lambda STORED_OAUTH_TOKEN_REQUIRED_FIELDS const matches the interface', () => { |
| 80 | + const lambdaSource = fs.readFileSync(LAMBDA_RESOLVER, 'utf8'); |
| 81 | + const interfaceFields = extractInterfaceFields(lambdaSource, 'StoredOauthToken').sort(); |
| 82 | + |
| 83 | + const constMatch = /STORED_OAUTH_TOKEN_REQUIRED_FIELDS:\s*ReadonlyArray<keyof StoredOauthToken>\s*=\s*\[([\s\S]*?)\];/.exec(lambdaSource); |
| 84 | + expect(constMatch).not.toBeNull(); |
| 85 | + const constFields = (constMatch![1].match(/'([a-zA-Z_][a-zA-Z0-9_]*)'/g) ?? []) |
| 86 | + .map((s) => s.replace(/'/g, '')) |
| 87 | + .sort(); |
| 88 | + |
| 89 | + expect(constFields).toEqual(interfaceFields); |
| 90 | + }); |
| 91 | +}); |
0 commit comments