Skip to content

Commit 3d601d9

Browse files
committed
fix: resolve canonical repository coordinates
1 parent 3cfab35 commit 3d601d9

12 files changed

Lines changed: 551 additions & 92 deletions

‎server/src/models/Repository.ts‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ export class RepositoryModel {
6060
return result || null;
6161
}
6262

63+
findByPlatformFullName(
64+
platform: Platform,
65+
fullName: string
66+
): Repository | null {
67+
const result = this.db.get<Repository>(
68+
`SELECT * FROM repository
69+
WHERE platform = ? AND full_name = ?`,
70+
[platform, fullName]
71+
);
72+
return result || null;
73+
}
74+
6375
/**
6476
* 获取指定安装的所有仓库
6577
*/
@@ -201,6 +213,9 @@ export class RepositoryModel {
201213
| 'webhook_url'
202214
| 'is_active'
203215
| 'remote_id'
216+
| 'owner'
217+
| 'name'
218+
| 'full_name'
204219
| 'description'
205220
| 'is_private'
206221
| 'language'
@@ -237,6 +252,18 @@ export class RepositoryModel {
237252
fields.push('remote_id = ?');
238253
params.push(toNullableText(updates.remote_id));
239254
}
255+
if (updates.owner !== undefined) {
256+
fields.push('owner = ?');
257+
params.push(updates.owner);
258+
}
259+
if (updates.name !== undefined) {
260+
fields.push('name = ?');
261+
params.push(updates.name);
262+
}
263+
if (updates.full_name !== undefined) {
264+
fields.push('full_name = ?');
265+
params.push(updates.full_name);
266+
}
240267
if (updates.description !== undefined) {
241268
fields.push('description = ?');
242269
params.push(toNullableText(updates.description));
@@ -321,14 +348,18 @@ export class RepositoryModel {
321348
}
322349

323350
upsert(dto: CreateRepositoryDTO): Repository {
324-
const existing = this.findByPlatformOwnerName(dto.platform, dto.owner, dto.name);
351+
const existing = this.findByPlatformOwnerName(dto.platform, dto.owner, dto.name)
352+
|| this.findByPlatformFullName(dto.platform, dto.full_name);
325353

326354
if (!existing) {
327355
return this.create(dto);
328356
}
329357

330358
const updated = this.update(existing.id, {
331359
remote_id: dto.remote_id,
360+
owner: dto.owner,
361+
name: dto.name,
362+
full_name: dto.full_name,
332363
description: dto.description,
333364
is_private: dto.is_private ?? false,
334365
language: dto.language,

‎server/src/review/pullRequestSummaries.ts‎

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Analysis, Job, ReviewReportSummary, JobPayload, Platform } from '../models/types';
2+
import { parseRepositoryFullName } from '../utils/repositoryCoordinates';
23
import { normalizeApiTimestamp } from '../utils/time';
34

45
export type ReviewRiskLevel = 'low' | 'medium' | 'high' | 'critical' | 'unknown';
@@ -80,23 +81,7 @@ export function buildPullRequestKey(target: PullRequestTarget): string {
8081
return `${buildRepositoryKey(target.platform, target.owner, target.repoName)}#${target.prNumber}`;
8182
}
8283

83-
export function parseRepositoryFullName(
84-
value: string | null | undefined
85-
): { owner: string; repoName: string } | null {
86-
if (!value) {
87-
return null;
88-
}
89-
90-
const separatorIndex = value.indexOf('/');
91-
if (separatorIndex <= 0 || separatorIndex >= value.length - 1) {
92-
return null;
93-
}
94-
95-
return {
96-
owner: value.slice(0, separatorIndex),
97-
repoName: value.slice(separatorIndex + 1),
98-
};
99-
}
84+
export { parseRepositoryFullName } from '../utils/repositoryCoordinates';
10085

10186
export function parseJsonObject(value: string | null | undefined): Record<string, unknown> | null {
10287
if (!value) {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import express from 'express';
2+
import request from 'supertest';
3+
4+
const repositoryModelMock = {
5+
findById: jest.fn(),
6+
update: jest.fn(),
7+
};
8+
9+
const oauthInstallationModelMock = {
10+
findById: jest.fn(),
11+
};
12+
13+
const oauthInstallationServiceMock = {
14+
ensureValidAccessToken: jest.fn(),
15+
};
16+
17+
const analysisModelMock = {
18+
findByRepository: jest.fn(),
19+
};
20+
21+
const analysisJobModelMock = {
22+
findByAnalysisId: jest.fn(),
23+
};
24+
25+
const jobModelMock = {
26+
findByType: jest.fn(),
27+
};
28+
29+
const queueServiceMock = {
30+
getJobModel: jest.fn(() => jobModelMock),
31+
};
32+
33+
const reviewTriggerServiceMock = {
34+
triggerByRepositoryId: jest.fn(),
35+
};
36+
37+
const platformClientMock = {
38+
listPullRequests: jest.fn(),
39+
};
40+
41+
const createPlatformClientMock = jest.fn(() => platformClientMock);
42+
43+
jest.mock('../models/Repository', () => ({
44+
getRepositoryModel: () => repositoryModelMock,
45+
}));
46+
47+
jest.mock('../models/OAuthInstallation', () => ({
48+
getOAuthInstallationModel: () => oauthInstallationModelMock,
49+
}));
50+
51+
jest.mock('../models/Analysis', () => ({
52+
getAnalysisModel: () => analysisModelMock,
53+
}));
54+
55+
jest.mock('../models/AnalysisJob', () => ({
56+
getAnalysisJobModel: () => analysisJobModelMock,
57+
}));
58+
59+
jest.mock('../jobs/QueueService', () => ({
60+
getQueueService: () => queueServiceMock,
61+
}));
62+
63+
jest.mock('../platform/client', () => ({
64+
createPlatformClient: createPlatformClientMock,
65+
}));
66+
67+
jest.mock('../services/OAuthInstallationService', () => ({
68+
getOAuthInstallationService: () => oauthInstallationServiceMock,
69+
}));
70+
71+
jest.mock('../services/ReviewTriggerService', () => ({
72+
getReviewTriggerService: () => reviewTriggerServiceMock,
73+
}));
74+
75+
import repositoryRoutes from './repositoryRoutes';
76+
77+
describe('repositoryRoutes', () => {
78+
let app: express.Application;
79+
80+
beforeEach(() => {
81+
jest.clearAllMocks();
82+
app = express();
83+
app.use(express.json());
84+
app.use('/', repositoryRoutes);
85+
86+
oauthInstallationModelMock.findById.mockReturnValue({
87+
id: 3,
88+
platform: 'gitee',
89+
is_active: true,
90+
access_token: 'token',
91+
auth_type: 'oauth',
92+
});
93+
oauthInstallationServiceMock.ensureValidAccessToken.mockResolvedValue({
94+
id: 3,
95+
platform: 'gitee',
96+
is_active: true,
97+
access_token: 'token',
98+
auth_type: 'oauth',
99+
});
100+
platformClientMock.listPullRequests.mockResolvedValue([]);
101+
analysisModelMock.findByRepository.mockReturnValue([]);
102+
analysisJobModelMock.findByAnalysisId.mockReturnValue([]);
103+
jobModelMock.findByType.mockReturnValue([]);
104+
});
105+
106+
it('uses canonical coordinates from full_name for pull request listing', async () => {
107+
repositoryModelMock.findById.mockReturnValue({
108+
id: 9163,
109+
platform: 'gitee',
110+
owner: 'mars167',
111+
name: 'API REIVEW PRO1',
112+
full_name: 'api-review-test-group/api-reivew-pro1',
113+
installation_id: 3,
114+
is_active: true,
115+
});
116+
repositoryModelMock.update.mockReturnValue({
117+
id: 9163,
118+
platform: 'gitee',
119+
owner: 'api-review-test-group',
120+
name: 'api-reivew-pro1',
121+
full_name: 'api-review-test-group/api-reivew-pro1',
122+
installation_id: 3,
123+
is_active: true,
124+
});
125+
126+
const response = await request(app)
127+
.get('/9163/pull-requests')
128+
.query({ state: 'open', page: '1', limit: '20' });
129+
130+
expect(response.status).toBe(200);
131+
expect(repositoryModelMock.update).toHaveBeenCalledWith(9163, {
132+
owner: 'api-review-test-group',
133+
name: 'api-reivew-pro1',
134+
full_name: 'api-review-test-group/api-reivew-pro1',
135+
});
136+
expect(platformClientMock.listPullRequests).toHaveBeenCalledWith(
137+
'api-review-test-group',
138+
'api-reivew-pro1',
139+
expect.objectContaining({
140+
state: 'open',
141+
page: 1,
142+
per_page: 20,
143+
})
144+
);
145+
expect(response.body.repository).toEqual(expect.objectContaining({
146+
owner: 'api-review-test-group',
147+
name: 'api-reivew-pro1',
148+
full_name: 'api-review-test-group/api-reivew-pro1',
149+
}));
150+
});
151+
});

0 commit comments

Comments
 (0)