ios-app-review-plugin 1.0.0
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/.claude/settings.local.json +42 -0
- package/.github/actions/ios-review/action.yml +106 -0
- package/.github/workflows/ci.yml +103 -0
- package/.github/workflows/publish.yml +57 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +175 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/bitrise/step.sh +128 -0
- package/bitrise/step.yml +101 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzers/asc-iap.d.ts.map +1 -0
- package/dist/analyzers/asc-metadata.d.ts.map +1 -0
- package/dist/analyzers/asc-screenshots.d.ts.map +1 -0
- package/dist/analyzers/asc-version.d.ts.map +1 -0
- package/dist/analyzers/code-scanner.d.ts.map +1 -0
- package/dist/analyzers/deprecated-api.d.ts.map +1 -0
- package/dist/analyzers/entitlements.d.ts.map +1 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/info-plist.d.ts.map +1 -0
- package/dist/analyzers/privacy.d.ts.map +1 -0
- package/dist/analyzers/private-api.d.ts.map +1 -0
- package/dist/analyzers/security.d.ts.map +1 -0
- package/dist/analyzers/ui-ux.d.ts.map +1 -0
- package/dist/asc/auth.d.ts.map +1 -0
- package/dist/asc/client.d.ts.map +1 -0
- package/dist/asc/endpoints/apps.d.ts.map +1 -0
- package/dist/asc/endpoints/iap.d.ts.map +1 -0
- package/dist/asc/endpoints/screenshots.d.ts.map +1 -0
- package/dist/asc/endpoints/versions.d.ts.map +1 -0
- package/dist/asc/errors.d.ts.map +1 -0
- package/dist/asc/index.d.ts.map +1 -0
- package/dist/asc/types.d.ts.map +1 -0
- package/dist/badge/generator.d.ts.map +1 -0
- package/dist/badge/index.d.ts.map +1 -0
- package/dist/badge/types.d.ts.map +1 -0
- package/dist/cache/file-cache.d.ts.map +1 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/version.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/git/diff.d.ts.map +1 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/types.d.ts.map +1 -0
- package/dist/guidelines/database.d.ts.map +1 -0
- package/dist/guidelines/index.d.ts.map +1 -0
- package/dist/guidelines/matcher.d.ts.map +1 -0
- package/dist/guidelines/types.d.ts.map +1 -0
- package/dist/history/comparator.d.ts.map +1 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/store.d.ts.map +1 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +994 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/plist.d.ts.map +1 -0
- package/dist/parsers/xcodeproj.d.ts.map +1 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/reporter.d.ts.map +1 -0
- package/dist/progress/types.d.ts.map +1 -0
- package/dist/reports/html.d.ts.map +1 -0
- package/dist/reports/index.d.ts.map +1 -0
- package/dist/reports/json.d.ts.map +1 -0
- package/dist/reports/markdown.d.ts.map +1 -0
- package/dist/reports/types.d.ts.map +1 -0
- package/dist/rules/engine.d.ts.map +1 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/loader.d.ts.map +1 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/docs/ANALYZERS.md +237 -0
- package/docs/API.md +308 -0
- package/docs/BADGES.md +130 -0
- package/docs/CI_CD.md +283 -0
- package/docs/CLI.md +140 -0
- package/docs/REPORTS.md +212 -0
- package/docs/ROADMAP.md +267 -0
- package/docs/RULES.md +182 -0
- package/docs/SECURITY.md +89 -0
- package/docs/TROUBLESHOOTING.md +227 -0
- package/docs/tutorials/ASC_SETUP.md +188 -0
- package/docs/tutorials/CI_INTEGRATION.md +292 -0
- package/docs/tutorials/CUSTOM_RULES.md +291 -0
- package/docs/tutorials/GETTING_STARTED.md +226 -0
- package/docs/video-scripts/01-introduction.md +106 -0
- package/docs/video-scripts/02-cli-usage.md +120 -0
- package/docs/video-scripts/03-ci-integration.md +198 -0
- package/eslint.config.js +33 -0
- package/examples/.ios-review-rules.json +82 -0
- package/examples/bitrise-workflow.yml +129 -0
- package/examples/fastlane-lane.rb +71 -0
- package/examples/github-action.yml +147 -0
- package/fastlane/Fastfile.example +114 -0
- package/fastlane/README.md +99 -0
- package/jest.config.js +36 -0
- package/package.json +65 -0
- package/scripts/benchmark.ts +112 -0
- package/scripts/debug-parser.ts +37 -0
- package/scripts/debug-pbxproj.ts +36 -0
- package/scripts/debug-specific.ts +47 -0
- package/scripts/test-analyze.ts +67 -0
- package/scripts/xcode-cloud-review.sh +167 -0
- package/src/analyzer.ts +227 -0
- package/src/analyzers/asc-iap.ts +300 -0
- package/src/analyzers/asc-metadata.ts +326 -0
- package/src/analyzers/asc-screenshots.ts +310 -0
- package/src/analyzers/asc-version.ts +368 -0
- package/src/analyzers/code-scanner.ts +408 -0
- package/src/analyzers/deprecated-api.ts +390 -0
- package/src/analyzers/entitlements.ts +345 -0
- package/src/analyzers/index.ts +12 -0
- package/src/analyzers/info-plist.ts +409 -0
- package/src/analyzers/privacy.ts +376 -0
- package/src/analyzers/private-api.ts +377 -0
- package/src/analyzers/security.ts +327 -0
- package/src/analyzers/ui-ux.ts +509 -0
- package/src/asc/auth.ts +204 -0
- package/src/asc/client.ts +258 -0
- package/src/asc/endpoints/apps.ts +115 -0
- package/src/asc/endpoints/iap.ts +171 -0
- package/src/asc/endpoints/screenshots.ts +164 -0
- package/src/asc/endpoints/versions.ts +174 -0
- package/src/asc/errors.ts +109 -0
- package/src/asc/index.ts +108 -0
- package/src/asc/types.ts +369 -0
- package/src/badge/generator.ts +48 -0
- package/src/badge/index.ts +2 -0
- package/src/badge/types.ts +5 -0
- package/src/cache/file-cache.ts +75 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/types.ts +10 -0
- package/src/cli/commands/help.ts +41 -0
- package/src/cli/commands/scan.ts +44 -0
- package/src/cli/commands/version.ts +12 -0
- package/src/cli/index.ts +92 -0
- package/src/cli/types.ts +17 -0
- package/src/git/diff.ts +21 -0
- package/src/git/index.ts +2 -0
- package/src/git/types.ts +5 -0
- package/src/guidelines/database.ts +344 -0
- package/src/guidelines/index.ts +4 -0
- package/src/guidelines/matcher.ts +84 -0
- package/src/guidelines/types.ts +28 -0
- package/src/history/comparator.ts +114 -0
- package/src/history/index.ts +3 -0
- package/src/history/store.ts +135 -0
- package/src/history/types.ts +40 -0
- package/src/index.ts +1113 -0
- package/src/parsers/index.ts +3 -0
- package/src/parsers/plist.ts +253 -0
- package/src/parsers/xcodeproj.ts +265 -0
- package/src/progress/index.ts +2 -0
- package/src/progress/reporter.ts +65 -0
- package/src/progress/types.ts +9 -0
- package/src/reports/html.ts +322 -0
- package/src/reports/index.ts +20 -0
- package/src/reports/json.ts +92 -0
- package/src/reports/markdown.ts +187 -0
- package/src/reports/types.ts +26 -0
- package/src/rules/engine.ts +121 -0
- package/src/rules/index.ts +3 -0
- package/src/rules/loader.ts +83 -0
- package/src/rules/types.ts +25 -0
- package/src/types/index.ts +247 -0
- package/tests/analyzer.test.ts +142 -0
- package/tests/analyzers/asc-iap.test.ts +228 -0
- package/tests/analyzers/asc-metadata.test.ts +210 -0
- package/tests/analyzers/asc-screenshots.test.ts +135 -0
- package/tests/analyzers/asc-version.test.ts +259 -0
- package/tests/analyzers/code-scanner.test.ts +745 -0
- package/tests/analyzers/deprecated-api.test.ts +286 -0
- package/tests/analyzers/entitlements.test.ts +411 -0
- package/tests/analyzers/info-plist.test.ts +148 -0
- package/tests/analyzers/privacy.test.ts +623 -0
- package/tests/analyzers/private-api.test.ts +255 -0
- package/tests/analyzers/security.test.ts +300 -0
- package/tests/analyzers/ui-ux.test.ts +357 -0
- package/tests/asc/auth.test.ts +189 -0
- package/tests/asc/client.test.ts +207 -0
- package/tests/asc/endpoints.test.ts +1359 -0
- package/tests/badge/generator.test.ts +73 -0
- package/tests/cache/file-cache.test.ts +124 -0
- package/tests/cli/cli-index.test.ts +510 -0
- package/tests/cli/commands.test.ts +67 -0
- package/tests/cli/scan.test.ts +152 -0
- package/tests/git/diff.test.ts +69 -0
- package/tests/guidelines/matcher.test.ts +209 -0
- package/tests/history/comparator.test.ts +272 -0
- package/tests/history/store.test.ts +200 -0
- package/tests/integration/cli.test.ts +95 -0
- package/tests/integration/e2e.test.ts +130 -0
- package/tests/parsers/plist.test.ts +240 -0
- package/tests/parsers/xcodeproj.test.ts +289 -0
- package/tests/progress/reporter.test.ts +117 -0
- package/tests/reports/html.test.ts +176 -0
- package/tests/reports/json.test.ts +235 -0
- package/tests/reports/markdown.test.ts +196 -0
- package/tests/rules/engine.test.ts +229 -0
- package/tests/rules/loader.test.ts +187 -0
- package/tests/setup.ts +15 -0
- package/tsconfig.json +27 -0
- package/tsconfig.test.json +9 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { printHelp } from '../../src/cli/commands/help.js';
|
|
2
|
+
import { printVersion } from '../../src/cli/commands/version.js';
|
|
3
|
+
|
|
4
|
+
describe('CLI Commands', () => {
|
|
5
|
+
let consoleLogSpy: jest.SpyInstance;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
consoleLogSpy.mockRestore();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('help', () => {
|
|
16
|
+
it('should print help text', () => {
|
|
17
|
+
printHelp();
|
|
18
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
|
19
|
+
const output = consoleLogSpy.mock.calls[0]![0] as string;
|
|
20
|
+
expect(output).toContain('ios-app-review');
|
|
21
|
+
expect(output).toContain('USAGE');
|
|
22
|
+
expect(output).toContain('COMMANDS');
|
|
23
|
+
expect(output).toContain('scan');
|
|
24
|
+
expect(output).toContain('help');
|
|
25
|
+
expect(output).toContain('version');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should list scan options', () => {
|
|
29
|
+
printHelp();
|
|
30
|
+
const output = consoleLogSpy.mock.calls[0]![0] as string;
|
|
31
|
+
expect(output).toContain('--format');
|
|
32
|
+
expect(output).toContain('--output');
|
|
33
|
+
expect(output).toContain('--analyzers');
|
|
34
|
+
expect(output).toContain('--include-asc');
|
|
35
|
+
expect(output).toContain('--changed-since');
|
|
36
|
+
expect(output).toContain('--badge');
|
|
37
|
+
expect(output).toContain('--save-history');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should list available analyzers', () => {
|
|
41
|
+
printHelp();
|
|
42
|
+
const output = consoleLogSpy.mock.calls[0]![0] as string;
|
|
43
|
+
expect(output).toContain('info-plist');
|
|
44
|
+
expect(output).toContain('privacy');
|
|
45
|
+
expect(output).toContain('security');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should list exit codes', () => {
|
|
49
|
+
printHelp();
|
|
50
|
+
const output = consoleLogSpy.mock.calls[0]![0] as string;
|
|
51
|
+
expect(output).toContain('EXIT CODES');
|
|
52
|
+
expect(output).toContain('0');
|
|
53
|
+
expect(output).toContain('1');
|
|
54
|
+
expect(output).toContain('2');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('version', () => {
|
|
59
|
+
it('should print a version string', () => {
|
|
60
|
+
printVersion();
|
|
61
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
|
62
|
+
const output = consoleLogSpy.mock.calls[0]![0] as string;
|
|
63
|
+
// Should be semver-like
|
|
64
|
+
expect(output).toMatch(/^\d+\.\d+\.\d+$/);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { runScan } from '../../src/cli/commands/scan.js';
|
|
5
|
+
import type { ScanOptions } from '../../src/cli/types.js';
|
|
6
|
+
|
|
7
|
+
// Mock the analyzer to avoid needing a real Xcode project
|
|
8
|
+
jest.mock('../../src/analyzer.js', () => ({
|
|
9
|
+
runAnalysis: jest.fn().mockResolvedValue({
|
|
10
|
+
projectPath: '/mock/project.xcodeproj',
|
|
11
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
12
|
+
results: [
|
|
13
|
+
{
|
|
14
|
+
analyzer: 'Code Scanner',
|
|
15
|
+
passed: true,
|
|
16
|
+
issues: [],
|
|
17
|
+
duration: 100,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
summary: {
|
|
21
|
+
totalIssues: 0,
|
|
22
|
+
errors: 0,
|
|
23
|
+
warnings: 0,
|
|
24
|
+
info: 0,
|
|
25
|
+
passed: true,
|
|
26
|
+
duration: 100,
|
|
27
|
+
},
|
|
28
|
+
score: 95,
|
|
29
|
+
enrichedIssues: [],
|
|
30
|
+
guidelinesCited: [],
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
describe('runScan', () => {
|
|
35
|
+
let consoleLogSpy: jest.SpyInstance;
|
|
36
|
+
let consoleErrorSpy: jest.SpyInstance;
|
|
37
|
+
let tempDir: string;
|
|
38
|
+
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'scan-test-'));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterAll(async () => {
|
|
44
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
49
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
consoleLogSpy.mockRestore();
|
|
54
|
+
consoleErrorSpy.mockRestore();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const baseOptions: ScanOptions = {
|
|
58
|
+
projectPath: '/mock/project.xcodeproj',
|
|
59
|
+
format: 'markdown',
|
|
60
|
+
output: undefined,
|
|
61
|
+
analyzers: undefined,
|
|
62
|
+
includeAsc: false,
|
|
63
|
+
changedSince: undefined,
|
|
64
|
+
config: undefined,
|
|
65
|
+
badge: false,
|
|
66
|
+
saveHistory: false,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
it('should return 0 when no errors found', async () => {
|
|
70
|
+
const exitCode = await runScan(baseOptions);
|
|
71
|
+
expect(exitCode).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should output to stdout by default', async () => {
|
|
75
|
+
await runScan(baseOptions);
|
|
76
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should write to file when output is specified', async () => {
|
|
80
|
+
const outputPath = path.join(tempDir, 'report.md');
|
|
81
|
+
await runScan({ ...baseOptions, output: outputPath });
|
|
82
|
+
|
|
83
|
+
const content = await fs.readFile(outputPath, 'utf-8');
|
|
84
|
+
expect(content.length).toBeGreaterThan(0);
|
|
85
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Report written to'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should generate badge when badge option is true', async () => {
|
|
89
|
+
const outputPath = path.join(tempDir, 'report2.md');
|
|
90
|
+
await runScan({ ...baseOptions, output: outputPath, badge: true });
|
|
91
|
+
|
|
92
|
+
const badgePath = path.join(tempDir, 'badge.svg');
|
|
93
|
+
const svg = await fs.readFile(badgePath, 'utf-8');
|
|
94
|
+
expect(svg).toContain('<svg');
|
|
95
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Badge written to'));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return 1 when errors found', async () => {
|
|
99
|
+
const { runAnalysis } = require('../../src/analyzer.js');
|
|
100
|
+
(runAnalysis as jest.Mock).mockResolvedValueOnce({
|
|
101
|
+
projectPath: '/mock/project.xcodeproj',
|
|
102
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
103
|
+
results: [
|
|
104
|
+
{
|
|
105
|
+
analyzer: 'Code Scanner',
|
|
106
|
+
passed: false,
|
|
107
|
+
issues: [
|
|
108
|
+
{
|
|
109
|
+
id: 'test-error',
|
|
110
|
+
title: 'Test Error',
|
|
111
|
+
description: 'Test error',
|
|
112
|
+
severity: 'error',
|
|
113
|
+
category: 'code',
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
duration: 100,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
summary: {
|
|
120
|
+
totalIssues: 1,
|
|
121
|
+
errors: 1,
|
|
122
|
+
warnings: 0,
|
|
123
|
+
info: 0,
|
|
124
|
+
passed: false,
|
|
125
|
+
duration: 100,
|
|
126
|
+
},
|
|
127
|
+
score: 50,
|
|
128
|
+
enrichedIssues: [],
|
|
129
|
+
guidelinesCited: [],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const exitCode = await runScan(baseOptions);
|
|
133
|
+
expect(exitCode).toBe(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should support json format', async () => {
|
|
137
|
+
const outputPath = path.join(tempDir, 'report.json');
|
|
138
|
+
await runScan({ ...baseOptions, format: 'json', output: outputPath });
|
|
139
|
+
|
|
140
|
+
const content = await fs.readFile(outputPath, 'utf-8');
|
|
141
|
+
// JSON format should be parseable
|
|
142
|
+
expect(() => JSON.parse(content)).not.toThrow();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should support html format', async () => {
|
|
146
|
+
const outputPath = path.join(tempDir, 'report.html');
|
|
147
|
+
await runScan({ ...baseOptions, format: 'html', output: outputPath });
|
|
148
|
+
|
|
149
|
+
const content = await fs.readFile(outputPath, 'utf-8');
|
|
150
|
+
expect(content).toContain('<');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { getChangedFiles } from '../../src/git/diff.js';
|
|
3
|
+
|
|
4
|
+
jest.mock('child_process', () => ({
|
|
5
|
+
execSync: jest.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
const mockExecSync = execSync as jest.MockedFunction<typeof execSync>;
|
|
9
|
+
|
|
10
|
+
describe('getChangedFiles', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return changed files from git diff', () => {
|
|
16
|
+
mockExecSync.mockReturnValue('src/foo.ts\nsrc/bar.ts\n');
|
|
17
|
+
|
|
18
|
+
const files = getChangedFiles('/project', 'main');
|
|
19
|
+
|
|
20
|
+
expect(mockExecSync).toHaveBeenCalledWith('git diff --name-only main', {
|
|
21
|
+
cwd: '/project',
|
|
22
|
+
encoding: 'utf-8',
|
|
23
|
+
timeout: 10000,
|
|
24
|
+
});
|
|
25
|
+
expect(files).toHaveLength(2);
|
|
26
|
+
expect(files[0]).toMatch(/src\/foo\.ts$/);
|
|
27
|
+
expect(files[1]).toMatch(/src\/bar\.ts$/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should resolve paths relative to basePath', () => {
|
|
31
|
+
mockExecSync.mockReturnValue('src/file.swift\n');
|
|
32
|
+
|
|
33
|
+
const files = getChangedFiles('/my/project', 'HEAD~1');
|
|
34
|
+
|
|
35
|
+
expect(files[0]).toContain('/my/project/');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should filter empty lines', () => {
|
|
39
|
+
mockExecSync.mockReturnValue('file.ts\n\n\n');
|
|
40
|
+
|
|
41
|
+
const files = getChangedFiles('/project', 'main');
|
|
42
|
+
expect(files).toHaveLength(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return empty array on git error', () => {
|
|
46
|
+
mockExecSync.mockImplementation(() => {
|
|
47
|
+
throw new Error('not a git repository');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const files = getChangedFiles('/not-a-repo', 'main');
|
|
51
|
+
expect(files).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return empty array on invalid ref', () => {
|
|
55
|
+
mockExecSync.mockImplementation(() => {
|
|
56
|
+
throw new Error('unknown revision');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const files = getChangedFiles('/project', 'nonexistent-branch');
|
|
60
|
+
expect(files).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle empty diff output', () => {
|
|
64
|
+
mockExecSync.mockReturnValue('');
|
|
65
|
+
|
|
66
|
+
const files = getChangedFiles('/project', 'main');
|
|
67
|
+
expect(files).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { GuidelineMatcher } from '../../src/guidelines/matcher.js';
|
|
2
|
+
import type { AnalysisReport, Issue } from '../../src/types/index.js';
|
|
3
|
+
|
|
4
|
+
describe('GuidelineMatcher', () => {
|
|
5
|
+
let matcher: GuidelineMatcher;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
matcher = new GuidelineMatcher();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const mockReport: AnalysisReport = {
|
|
13
|
+
projectPath: '/test/project',
|
|
14
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
15
|
+
results: [
|
|
16
|
+
{
|
|
17
|
+
analyzer: 'security',
|
|
18
|
+
passed: false,
|
|
19
|
+
issues: [
|
|
20
|
+
{
|
|
21
|
+
id: 'security-md5',
|
|
22
|
+
title: 'MD5 usage detected',
|
|
23
|
+
description: 'MD5 is insecure',
|
|
24
|
+
severity: 'warning',
|
|
25
|
+
category: 'security',
|
|
26
|
+
guideline: 'Guideline 2.5.4 - Security',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
duration: 100,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
summary: {
|
|
33
|
+
totalIssues: 1,
|
|
34
|
+
errors: 0,
|
|
35
|
+
warnings: 1,
|
|
36
|
+
info: 0,
|
|
37
|
+
passed: true,
|
|
38
|
+
duration: 100,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe('matchIssue', () => {
|
|
43
|
+
it('should match by ISSUE_GUIDELINE_MAP (e.g., security-md5 -> guideline 2.5.4)', () => {
|
|
44
|
+
const issue: Issue = {
|
|
45
|
+
id: 'security-md5',
|
|
46
|
+
title: 'MD5 usage detected',
|
|
47
|
+
description: 'MD5 is insecure',
|
|
48
|
+
severity: 'warning',
|
|
49
|
+
category: 'security',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const matches = matcher.matchIssue(issue);
|
|
53
|
+
|
|
54
|
+
expect(matches).toHaveLength(1);
|
|
55
|
+
expect(matches[0].section).toBe('2.5.4');
|
|
56
|
+
expect(matches[0].title).toBe('Security');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should parse section from guideline string as fallback', () => {
|
|
60
|
+
const issue: Issue = {
|
|
61
|
+
id: 'some-unmapped-issue',
|
|
62
|
+
title: 'Some issue',
|
|
63
|
+
description: 'Description',
|
|
64
|
+
severity: 'warning',
|
|
65
|
+
category: 'security',
|
|
66
|
+
guideline: 'Guideline 3.1.1 - In-App Purchase',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const matches = matcher.matchIssue(issue);
|
|
70
|
+
|
|
71
|
+
expect(matches).toHaveLength(1);
|
|
72
|
+
expect(matches[0].section).toBe('3.1.1');
|
|
73
|
+
expect(matches[0].title).toBe('In-App Purchase');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return empty array for unmapped issues', () => {
|
|
77
|
+
const issue: Issue = {
|
|
78
|
+
id: 'totally-unknown-issue',
|
|
79
|
+
title: 'Unknown issue',
|
|
80
|
+
description: 'Description',
|
|
81
|
+
severity: 'info',
|
|
82
|
+
category: 'code',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const matches = matcher.matchIssue(issue);
|
|
86
|
+
|
|
87
|
+
expect(matches).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('calculateScore', () => {
|
|
92
|
+
it('should return 100 for report with no issues', () => {
|
|
93
|
+
const emptyReport: AnalysisReport = {
|
|
94
|
+
projectPath: '/test/project',
|
|
95
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
96
|
+
results: [
|
|
97
|
+
{
|
|
98
|
+
analyzer: 'security',
|
|
99
|
+
passed: true,
|
|
100
|
+
issues: [],
|
|
101
|
+
duration: 50,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
summary: {
|
|
105
|
+
totalIssues: 0,
|
|
106
|
+
errors: 0,
|
|
107
|
+
warnings: 0,
|
|
108
|
+
info: 0,
|
|
109
|
+
passed: true,
|
|
110
|
+
duration: 50,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const score = matcher.calculateScore(emptyReport);
|
|
115
|
+
|
|
116
|
+
expect(score).toBe(100);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should deduct based on severity weights', () => {
|
|
120
|
+
const score = matcher.calculateScore(mockReport);
|
|
121
|
+
|
|
122
|
+
// security-md5 maps to guideline 2.5.4 with severityWeight 8
|
|
123
|
+
// severity 'warning' has multiplier 0.5
|
|
124
|
+
// deduction = 8 * 0.5 = 4
|
|
125
|
+
// score = 100 - 4 = 96
|
|
126
|
+
expect(score).toBe(96);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should deduct more for error severity', () => {
|
|
130
|
+
const errorReport: AnalysisReport = {
|
|
131
|
+
projectPath: '/test/project',
|
|
132
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
133
|
+
results: [
|
|
134
|
+
{
|
|
135
|
+
analyzer: 'security',
|
|
136
|
+
passed: false,
|
|
137
|
+
issues: [
|
|
138
|
+
{
|
|
139
|
+
id: 'security-md5',
|
|
140
|
+
title: 'MD5 usage detected',
|
|
141
|
+
description: 'MD5 is insecure',
|
|
142
|
+
severity: 'error',
|
|
143
|
+
category: 'security',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
duration: 100,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
summary: {
|
|
150
|
+
totalIssues: 1,
|
|
151
|
+
errors: 1,
|
|
152
|
+
warnings: 0,
|
|
153
|
+
info: 0,
|
|
154
|
+
passed: false,
|
|
155
|
+
duration: 100,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const score = matcher.calculateScore(errorReport);
|
|
160
|
+
|
|
161
|
+
// security-md5 maps to guideline 2.5.4 with severityWeight 8
|
|
162
|
+
// severity 'error' has multiplier 1.0
|
|
163
|
+
// deduction = 8 * 1.0 = 8
|
|
164
|
+
// score = 100 - 8 = 92
|
|
165
|
+
expect(score).toBe(92);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('enrichReport', () => {
|
|
170
|
+
it('should add guidelineUrl and guidelineExcerpt to enriched issues', () => {
|
|
171
|
+
const enriched = matcher.enrichReport(mockReport);
|
|
172
|
+
|
|
173
|
+
expect(enriched.enrichedIssues).toHaveLength(1);
|
|
174
|
+
const issue = enriched.enrichedIssues[0];
|
|
175
|
+
expect(issue.guidelineUrl).toBe(
|
|
176
|
+
'https://developer.apple.com/app-store/review/guidelines/#software-requirements',
|
|
177
|
+
);
|
|
178
|
+
expect(issue.guidelineExcerpt).toContain('appropriate security measures');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should include score in enriched report', () => {
|
|
182
|
+
const enriched = matcher.enrichReport(mockReport);
|
|
183
|
+
|
|
184
|
+
expect(enriched.score).toBeDefined();
|
|
185
|
+
expect(typeof enriched.score).toBe('number');
|
|
186
|
+
expect(enriched.score).toBeLessThanOrEqual(100);
|
|
187
|
+
expect(enriched.score).toBeGreaterThanOrEqual(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should include severityScore on enriched issues', () => {
|
|
191
|
+
const enriched = matcher.enrichReport(mockReport);
|
|
192
|
+
|
|
193
|
+
const issue = enriched.enrichedIssues[0];
|
|
194
|
+
// severityWeight 8 * SEVERITY_MULTIPLIER warning 0.5 = 4
|
|
195
|
+
expect(issue.severityScore).toBe(4);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should preserve original issue properties', () => {
|
|
199
|
+
const enriched = matcher.enrichReport(mockReport);
|
|
200
|
+
|
|
201
|
+
const issue = enriched.enrichedIssues[0];
|
|
202
|
+
expect(issue.id).toBe('security-md5');
|
|
203
|
+
expect(issue.title).toBe('MD5 usage detected');
|
|
204
|
+
expect(issue.description).toBe('MD5 is insecure');
|
|
205
|
+
expect(issue.severity).toBe('warning');
|
|
206
|
+
expect(issue.category).toBe('security');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|