From 8909cef3e202fb99733ecee4970f73a827937e0d Mon Sep 17 00:00:00 2001 From: DYNOSuprovo Date: Fri, 2 Oct 2026 22:40:07 +0530 Subject: [PATCH] fix(metadata): match restricted words using word boundaries (#4501) --- CHANGELOG.md | 2 + .../__tests__/infoRestrictedWords.test.ts | 121 ++++++++++++++++++ .../apple/rules/infoRestrictedWords.ts | 11 +- 3 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 packages/eas-cli/src/metadata/apple/rules/__tests__/infoRestrictedWords.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f44ba9589..4fcc3418df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ This is the log of notable changes to EAS CLI and related packages. ### 🐛 Bug fixes +- [eas-cli] Match restricted words in Apple metadata using word boundaries to avoid false positives on words containing "beta" (e.g. Danish "Betal", "Alphabet"). ([#4501](https://github.com/expo/eas-cli/issues/4501) by [@DYNOSuprovo](https://github.com/DYNOSuprovo)) + - [build-tools] Pass the step cancellation signal to `eas/download_build`. ([#4511](https://github.com/expo/eas-cli/pull/4511) by [@sjchmiela](https://github.com/sjchmiela)) - [build-tools] Accept `null` for the TestFlight group `hasAccessToAllBuilds` attribute from App Store Connect. ([#4528](https://github.com/expo/eas-cli/pull/4528) by [@sswrk](https://github.com/sswrk)) - [eas-cli] Support `queued` and `starting` status filters in `eas simulator:list` and keep simulator commands compatible with the current GraphQL schema. ([#4527](https://github.com/expo/eas-cli/pull/4527) by [@sjchmiela](https://github.com/sjchmiela)) diff --git a/packages/eas-cli/src/metadata/apple/rules/__tests__/infoRestrictedWords.test.ts b/packages/eas-cli/src/metadata/apple/rules/__tests__/infoRestrictedWords.test.ts new file mode 100644 index 0000000000..a1134d3449 --- /dev/null +++ b/packages/eas-cli/src/metadata/apple/rules/__tests__/infoRestrictedWords.test.ts @@ -0,0 +1,121 @@ +import { infoRestrictedWords } from '../infoRestrictedWords'; + +describe('infoRestrictedWords', () => { + it('returns null when apple.info is missing or empty', () => { + expect(infoRestrictedWords.validate({} as any)).toBeNull(); + expect(infoRestrictedWords.validate({ apple: { info: {} } } as any)).toBeNull(); + }); + + it('allows text without restricted words', () => { + const config = { + apple: { + info: { + 'en-US': { + title: 'My Wonderful App', + subtitle: 'Productivity made simple', + description: 'The best app to organize your daily life.', + keywords: 'productivity, notes, organize', + }, + }, + }, + }; + + expect(infoRestrictedWords.validate(config as any)).toEqual([]); + }); + + it('does not flag words that contain "beta" as a substring (e.g. Danish "Betal", "Alphabet")', () => { + const config = { + apple: { + info: { + da: { + title: 'Betal med appen', + subtitle: 'Hurtig og sikker betaling', + description: 'Betal nemt og hurtigt for dine varer i butikken.', + keywords: 'betal, butik, nemt', + }, + de: { + title: 'Alphabet Trainer', + subtitle: 'Lerne das Alphabet', + description: 'Buchstaben von A bis Z lernen.', + keywords: 'alphabet, lernen, kinder', + }, + nl: { + title: 'Veilig betalen', + subtitle: 'Eenvoudig online betalen', + description: 'Snel en veilig betalen met iDEAL.', + keywords: ['veilig', 'betalen', 'bank'], + }, + 'en-US': { + title: 'Diabetes Health Tracker', + subtitle: 'Track blood sugar and meals', + description: 'Manage diabetes with daily logs and insights.', + keywords: ['diabetes', 'health', 'tracker'], + }, + }, + }, + }; + + expect(infoRestrictedWords.validate(config as any)).toEqual([]); + }); + + it('flags restricted word "beta" in title, subtitle, description, and keywords', () => { + const config = { + apple: { + info: { + 'en-US': { + title: 'CoolApp Beta', + subtitle: 'Now in beta test', + description: 'Welcome to the public beta release.', + keywords: ['utility', 'tools', 'beta'], + }, + }, + }, + }; + + const issues = infoRestrictedWords.validate(config as any) as any[]; + expect(issues).toHaveLength(4); + + expect(issues[0]).toEqual({ + id: 'apple.info.restrictedWords', + severity: 1, + path: ['apple', 'info', 'en-US', 'title'], + message: 'Apple restricts the word "beta" and synonyms implying incomplete functionality.', + }); + expect(issues[1]).toEqual({ + id: 'apple.info.restrictedWords', + severity: 1, + path: ['apple', 'info', 'en-US', 'subtitle'], + message: 'Apple restricts the word "beta" and synonyms implying incomplete functionality.', + }); + expect(issues[2]).toEqual({ + id: 'apple.info.restrictedWords', + severity: 1, + path: ['apple', 'info', 'en-US', 'description'], + message: 'Apple restricts the word "beta" and synonyms implying incomplete functionality.', + }); + expect(issues[3]).toEqual({ + id: 'apple.info.restrictedWords', + severity: 1, + path: ['apple', 'info', 'en-US', 'keywords'], + message: 'Apple restricts the word "beta" and synonyms implying incomplete functionality.', + }); + }); + + it('flags case-insensitive and punctuation-delimited instances of "beta"', () => { + const config = { + apple: { + info: { + 'en-US': { + title: 'App (BETA)', + subtitle: 'Version 2.0-beta', + description: 'beta: explore new experimental features!', + keywords: 'v2, beta, test', + }, + }, + }, + }; + + const issues = infoRestrictedWords.validate(config as any) as any[]; + expect(issues).toHaveLength(4); + }); +}); diff --git a/packages/eas-cli/src/metadata/apple/rules/infoRestrictedWords.ts b/packages/eas-cli/src/metadata/apple/rules/infoRestrictedWords.ts index 86c653f0ea..530cd2f760 100644 --- a/packages/eas-cli/src/metadata/apple/rules/infoRestrictedWords.ts +++ b/packages/eas-cli/src/metadata/apple/rules/infoRestrictedWords.ts @@ -13,6 +13,11 @@ const RESTRICTED_WORDS = { beta: 'Apple restricts the word "beta" and synonyms implying incomplete functionality.', }; +const RESTRICTED_WORD_PATTERNS = Object.entries(RESTRICTED_WORDS).map(([word, description]) => ({ + pattern: new RegExp(`\\b${word}\\b`, 'i'), + description, +})); + /** * Apple restricts certain words from being used in name, description, or keywords. * Using these words likely result in a rejection. @@ -49,10 +54,8 @@ export const infoRestrictedWords: IssueRule = { }; function getDescriptionForFirstMatch(value: string): string | null { - const sanitized = value.toLowerCase(); - - for (const [word, description] of Object.entries(RESTRICTED_WORDS)) { - if (sanitized.includes(word)) { + for (const { pattern, description } of RESTRICTED_WORD_PATTERNS) { + if (pattern.test(value)) { return description; } }