@flink-app/github-app-plugin 0.12.1-alpha.38 → 0.12.1-alpha.39
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/README.md +106 -163
- package/dist/GitHubAppInternalContext.d.ts +4 -33
- package/dist/GitHubAppPlugin.js +16 -7
- package/dist/GitHubAppPluginContext.d.ts +57 -0
- package/dist/GitHubAppPluginOptions.d.ts +10 -53
- package/dist/handlers/InstallationCallback.d.ts +3 -9
- package/dist/handlers/WebhookHandler.d.ts +3 -10
- package/dist/index.d.ts +11 -10
- package/dist/index.js +3 -1
- package/dist/services/InstallationService.d.ts +109 -0
- package/dist/services/InstallationService.js +224 -0
- package/package.json +2 -2
- package/spec/handlers.spec.ts +1 -130
- package/spec/integration-and-security.spec.ts +45 -47
- package/spec/plugin-core.spec.ts +6 -10
- package/spec/project-setup.spec.ts +0 -2
- package/src/GitHubAppInternalContext.ts +16 -0
- package/src/GitHubAppPlugin.ts +23 -7
- package/src/GitHubAppPluginContext.ts +58 -0
- package/src/GitHubAppPluginOptions.ts +11 -55
- package/src/handlers/WebhookHandler.ts +3 -11
- package/src/index.ts +14 -10
- package/src/services/InstallationService.ts +302 -0
- package/src/handlers/InstallationCallback.ts +0 -292
- package/src/schemas/InstallationCallbackRequest.ts +0 -10
package/dist/index.d.ts
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
* - Webhook handling with signature validation
|
|
8
8
|
* - API client wrapper
|
|
9
9
|
*/
|
|
10
|
-
export { githubAppPlugin } from
|
|
11
|
-
export type { GitHubAppPluginOptions,
|
|
12
|
-
export type { GitHubAppPluginContext } from
|
|
13
|
-
export type { default as GitHubInstallation } from
|
|
14
|
-
export type { default as GitHubAppSession } from
|
|
15
|
-
export type { default as WebhookEvent } from
|
|
16
|
-
export type { default as WebhookPayload } from
|
|
17
|
-
export { GitHubAPIClient, type Repository, type Content, type Issue, type CreateIssueParams } from
|
|
18
|
-
export { GitHubAuthService } from
|
|
19
|
-
export {
|
|
10
|
+
export { githubAppPlugin } from "./GitHubAppPlugin";
|
|
11
|
+
export type { GitHubAppPluginOptions, WebhookEventParams, } from "./GitHubAppPluginOptions";
|
|
12
|
+
export type { GitHubAppPluginContext } from "./GitHubAppPluginContext";
|
|
13
|
+
export type { default as GitHubInstallation } from "./schemas/GitHubInstallation";
|
|
14
|
+
export type { default as GitHubAppSession } from "./schemas/GitHubAppSession";
|
|
15
|
+
export type { default as WebhookEvent } from "./schemas/WebhookEvent";
|
|
16
|
+
export type { default as WebhookPayload } from "./schemas/WebhookPayload";
|
|
17
|
+
export { GitHubAPIClient, type Repository, type Content, type Issue, type CreateIssueParams } from "./services/GitHubAPIClient";
|
|
18
|
+
export { GitHubAuthService } from "./services/GitHubAuthService";
|
|
19
|
+
export { InstallationService, type CompleteInstallationResult } from "./services/InstallationService";
|
|
20
|
+
export { GitHubAppErrorCodes } from "./utils/error-utils";
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - API client wrapper
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.GitHubAppErrorCodes = exports.GitHubAuthService = exports.GitHubAPIClient = exports.githubAppPlugin = void 0;
|
|
12
|
+
exports.GitHubAppErrorCodes = exports.InstallationService = exports.GitHubAuthService = exports.GitHubAPIClient = exports.githubAppPlugin = void 0;
|
|
13
13
|
// Plugin factory
|
|
14
14
|
var GitHubAppPlugin_1 = require("./GitHubAppPlugin");
|
|
15
15
|
Object.defineProperty(exports, "githubAppPlugin", { enumerable: true, get: function () { return GitHubAppPlugin_1.githubAppPlugin; } });
|
|
@@ -18,6 +18,8 @@ var GitHubAPIClient_1 = require("./services/GitHubAPIClient");
|
|
|
18
18
|
Object.defineProperty(exports, "GitHubAPIClient", { enumerable: true, get: function () { return GitHubAPIClient_1.GitHubAPIClient; } });
|
|
19
19
|
var GitHubAuthService_1 = require("./services/GitHubAuthService");
|
|
20
20
|
Object.defineProperty(exports, "GitHubAuthService", { enumerable: true, get: function () { return GitHubAuthService_1.GitHubAuthService; } });
|
|
21
|
+
var InstallationService_1 = require("./services/InstallationService");
|
|
22
|
+
Object.defineProperty(exports, "InstallationService", { enumerable: true, get: function () { return InstallationService_1.InstallationService; } });
|
|
21
23
|
// Error utilities
|
|
22
24
|
var error_utils_1 = require("./utils/error-utils");
|
|
23
25
|
Object.defineProperty(exports, "GitHubAppErrorCodes", { enumerable: true, get: function () { return error_utils_1.GitHubAppErrorCodes; } });
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installation Service
|
|
3
|
+
*
|
|
4
|
+
* Handles GitHub App installation completion flow including:
|
|
5
|
+
* - State validation (CSRF protection)
|
|
6
|
+
* - Fetching installation details from GitHub API
|
|
7
|
+
* - Storing installation in database
|
|
8
|
+
*/
|
|
9
|
+
import { GitHubAuthService } from "./GitHubAuthService";
|
|
10
|
+
import GitHubAppSessionRepo from "../repos/GitHubAppSessionRepo";
|
|
11
|
+
import GitHubInstallationRepo from "../repos/GitHubInstallationRepo";
|
|
12
|
+
import GitHubInstallation from "../schemas/GitHubInstallation";
|
|
13
|
+
/**
|
|
14
|
+
* Result of completing installation
|
|
15
|
+
*/
|
|
16
|
+
export interface CompleteInstallationResult {
|
|
17
|
+
success: boolean;
|
|
18
|
+
installation?: GitHubInstallation;
|
|
19
|
+
error?: {
|
|
20
|
+
code: string;
|
|
21
|
+
message: string;
|
|
22
|
+
details?: any;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Installation Service
|
|
27
|
+
*
|
|
28
|
+
* Provides methods for completing GitHub App installation flow.
|
|
29
|
+
* Host applications call these methods from their own handlers with
|
|
30
|
+
* their own authentication and authorization logic.
|
|
31
|
+
*/
|
|
32
|
+
export declare class InstallationService {
|
|
33
|
+
private authService;
|
|
34
|
+
private sessionRepo;
|
|
35
|
+
private installationRepo;
|
|
36
|
+
private baseUrl;
|
|
37
|
+
constructor(authService: GitHubAuthService, sessionRepo: GitHubAppSessionRepo, installationRepo: GitHubInstallationRepo, baseUrl?: string);
|
|
38
|
+
/**
|
|
39
|
+
* Complete GitHub App installation
|
|
40
|
+
*
|
|
41
|
+
* This method handles all the boilerplate of:
|
|
42
|
+
* 1. Validating the state parameter (CSRF protection)
|
|
43
|
+
* 2. Fetching installation details from GitHub API
|
|
44
|
+
* 3. Storing the installation in database
|
|
45
|
+
*
|
|
46
|
+
* The host application should:
|
|
47
|
+
* - Check if user is authenticated
|
|
48
|
+
* - Parse query parameters from the callback URL
|
|
49
|
+
* - Call this method with userId
|
|
50
|
+
* - Handle the response and redirect
|
|
51
|
+
*
|
|
52
|
+
* @param params - Installation completion parameters
|
|
53
|
+
* @param params.installationId - GitHub installation ID from query params
|
|
54
|
+
* @param params.state - State parameter from query params (CSRF protection)
|
|
55
|
+
* @param params.userId - Application user ID (from auth system)
|
|
56
|
+
* @returns Result with installation details or error
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* // In your custom handler
|
|
61
|
+
* export default async function GitHubCallback({ ctx, req }: GetHandlerParams) {
|
|
62
|
+
* // 1. Check authentication (your way)
|
|
63
|
+
* if (!ctx.auth?.tokenData?.userId) {
|
|
64
|
+
* return unauthorized('Please log in first');
|
|
65
|
+
* }
|
|
66
|
+
*
|
|
67
|
+
* // 2. Parse query params
|
|
68
|
+
* const { installation_id, state } = req.query;
|
|
69
|
+
*
|
|
70
|
+
* // 3. Complete installation
|
|
71
|
+
* const result = await ctx.plugins.githubApp.completeInstallation({
|
|
72
|
+
* installationId: parseInt(installation_id),
|
|
73
|
+
* state,
|
|
74
|
+
* userId: ctx.auth.tokenData.userId
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* // 4. Handle response (your way)
|
|
78
|
+
* if (!result.success) {
|
|
79
|
+
* return redirect(`/error?code=${result.error.code}`);
|
|
80
|
+
* }
|
|
81
|
+
*
|
|
82
|
+
* return redirect('/dashboard/github');
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
completeInstallation(params: {
|
|
87
|
+
installationId: number;
|
|
88
|
+
state: string;
|
|
89
|
+
userId: string;
|
|
90
|
+
}): Promise<CompleteInstallationResult>;
|
|
91
|
+
/**
|
|
92
|
+
* Fetch installation details from GitHub API
|
|
93
|
+
*
|
|
94
|
+
* @param installationId - GitHub installation ID
|
|
95
|
+
* @param jwt - GitHub App JWT
|
|
96
|
+
* @returns Installation details
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
private fetchInstallationDetails;
|
|
100
|
+
/**
|
|
101
|
+
* Fetch repositories accessible by installation
|
|
102
|
+
*
|
|
103
|
+
* @param installationId - GitHub installation ID
|
|
104
|
+
* @param jwt - GitHub App JWT
|
|
105
|
+
* @returns Array of repositories
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
private fetchInstallationRepositories;
|
|
109
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Installation Service
|
|
4
|
+
*
|
|
5
|
+
* Handles GitHub App installation completion flow including:
|
|
6
|
+
* - State validation (CSRF protection)
|
|
7
|
+
* - Fetching installation details from GitHub API
|
|
8
|
+
* - Storing installation in database
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.InstallationService = void 0;
|
|
12
|
+
const flink_1 = require("@flink-app/flink");
|
|
13
|
+
const error_utils_1 = require("../utils/error-utils");
|
|
14
|
+
const state_utils_1 = require("../utils/state-utils");
|
|
15
|
+
/**
|
|
16
|
+
* Installation Service
|
|
17
|
+
*
|
|
18
|
+
* Provides methods for completing GitHub App installation flow.
|
|
19
|
+
* Host applications call these methods from their own handlers with
|
|
20
|
+
* their own authentication and authorization logic.
|
|
21
|
+
*/
|
|
22
|
+
class InstallationService {
|
|
23
|
+
constructor(authService, sessionRepo, installationRepo, baseUrl = "https://api.github.com") {
|
|
24
|
+
this.authService = authService;
|
|
25
|
+
this.sessionRepo = sessionRepo;
|
|
26
|
+
this.installationRepo = installationRepo;
|
|
27
|
+
this.baseUrl = baseUrl;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Complete GitHub App installation
|
|
31
|
+
*
|
|
32
|
+
* This method handles all the boilerplate of:
|
|
33
|
+
* 1. Validating the state parameter (CSRF protection)
|
|
34
|
+
* 2. Fetching installation details from GitHub API
|
|
35
|
+
* 3. Storing the installation in database
|
|
36
|
+
*
|
|
37
|
+
* The host application should:
|
|
38
|
+
* - Check if user is authenticated
|
|
39
|
+
* - Parse query parameters from the callback URL
|
|
40
|
+
* - Call this method with userId
|
|
41
|
+
* - Handle the response and redirect
|
|
42
|
+
*
|
|
43
|
+
* @param params - Installation completion parameters
|
|
44
|
+
* @param params.installationId - GitHub installation ID from query params
|
|
45
|
+
* @param params.state - State parameter from query params (CSRF protection)
|
|
46
|
+
* @param params.userId - Application user ID (from auth system)
|
|
47
|
+
* @returns Result with installation details or error
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* // In your custom handler
|
|
52
|
+
* export default async function GitHubCallback({ ctx, req }: GetHandlerParams) {
|
|
53
|
+
* // 1. Check authentication (your way)
|
|
54
|
+
* if (!ctx.auth?.tokenData?.userId) {
|
|
55
|
+
* return unauthorized('Please log in first');
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* // 2. Parse query params
|
|
59
|
+
* const { installation_id, state } = req.query;
|
|
60
|
+
*
|
|
61
|
+
* // 3. Complete installation
|
|
62
|
+
* const result = await ctx.plugins.githubApp.completeInstallation({
|
|
63
|
+
* installationId: parseInt(installation_id),
|
|
64
|
+
* state,
|
|
65
|
+
* userId: ctx.auth.tokenData.userId
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* // 4. Handle response (your way)
|
|
69
|
+
* if (!result.success) {
|
|
70
|
+
* return redirect(`/error?code=${result.error.code}`);
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* return redirect('/dashboard/github');
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
async completeInstallation(params) {
|
|
78
|
+
const { installationId, state, userId } = params;
|
|
79
|
+
try {
|
|
80
|
+
// 1. Find session by state
|
|
81
|
+
const session = await this.sessionRepo.getOne({ state });
|
|
82
|
+
if (!session) {
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
error: {
|
|
86
|
+
code: error_utils_1.GitHubAppErrorCodes.SESSION_EXPIRED,
|
|
87
|
+
message: "Installation session not found or expired. Please try again.",
|
|
88
|
+
details: { state: state.substring(0, 10) + "..." },
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// 2. Validate state parameter (CSRF protection)
|
|
93
|
+
if (!(0, state_utils_1.validateState)(state, session.state)) {
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
error: {
|
|
97
|
+
code: error_utils_1.GitHubAppErrorCodes.INVALID_STATE,
|
|
98
|
+
message: "Invalid state parameter. Possible CSRF attack detected.",
|
|
99
|
+
details: { providedState: state.substring(0, 10) + "..." },
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// 3. Delete session immediately after validation (one-time use)
|
|
104
|
+
await this.sessionRepo.deleteBySessionId(session.sessionId);
|
|
105
|
+
// 4. Generate GitHub App JWT
|
|
106
|
+
const jwt = this.authService.generateAppJWT();
|
|
107
|
+
// 5. Fetch installation details from GitHub API
|
|
108
|
+
const installationDetails = await this.fetchInstallationDetails(installationId, jwt);
|
|
109
|
+
// 6. Fetch repositories accessible by this installation
|
|
110
|
+
const repositories = await this.fetchInstallationRepositories(installationId, jwt);
|
|
111
|
+
// 7. Store installation in database
|
|
112
|
+
const installation = await this.installationRepo.create({
|
|
113
|
+
userId,
|
|
114
|
+
installationId,
|
|
115
|
+
accountId: installationDetails.account.id,
|
|
116
|
+
accountLogin: installationDetails.account.login,
|
|
117
|
+
accountType: installationDetails.account.type,
|
|
118
|
+
avatarUrl: installationDetails.account.avatar_url,
|
|
119
|
+
repositories: repositories.map((repo) => ({
|
|
120
|
+
id: repo.id,
|
|
121
|
+
name: repo.name,
|
|
122
|
+
fullName: repo.full_name,
|
|
123
|
+
private: repo.private,
|
|
124
|
+
})),
|
|
125
|
+
permissions: installationDetails.permissions || {},
|
|
126
|
+
events: installationDetails.events || [],
|
|
127
|
+
createdAt: new Date(),
|
|
128
|
+
updatedAt: new Date(),
|
|
129
|
+
});
|
|
130
|
+
flink_1.log.info("GitHub App installation completed", {
|
|
131
|
+
userId,
|
|
132
|
+
installationId,
|
|
133
|
+
accountLogin: installationDetails.account.login,
|
|
134
|
+
repositoryCount: repositories.length,
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
success: true,
|
|
138
|
+
installation,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
flink_1.log.error("GitHub App installation completion failed", {
|
|
143
|
+
userId,
|
|
144
|
+
installationId,
|
|
145
|
+
error: error.message,
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
error: {
|
|
150
|
+
code: error.code || error_utils_1.GitHubAppErrorCodes.SERVER_ERROR,
|
|
151
|
+
message: error.message || "Installation completion failed. Please try again.",
|
|
152
|
+
details: error.details,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Fetch installation details from GitHub API
|
|
159
|
+
*
|
|
160
|
+
* @param installationId - GitHub installation ID
|
|
161
|
+
* @param jwt - GitHub App JWT
|
|
162
|
+
* @returns Installation details
|
|
163
|
+
* @private
|
|
164
|
+
*/
|
|
165
|
+
async fetchInstallationDetails(installationId, jwt) {
|
|
166
|
+
const url = `${this.baseUrl}/app/installations/${installationId}`;
|
|
167
|
+
const response = await fetch(url, {
|
|
168
|
+
method: "GET",
|
|
169
|
+
headers: {
|
|
170
|
+
Authorization: `Bearer ${jwt}`,
|
|
171
|
+
Accept: "application/vnd.github+json",
|
|
172
|
+
"User-Agent": "Flink-GitHub-App-Plugin",
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
const errorBody = await response.text();
|
|
177
|
+
throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.SERVER_ERROR, `Failed to fetch installation details: ${response.statusText}`, { status: response.status, body: errorBody });
|
|
178
|
+
}
|
|
179
|
+
return (await response.json());
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Fetch repositories accessible by installation
|
|
183
|
+
*
|
|
184
|
+
* @param installationId - GitHub installation ID
|
|
185
|
+
* @param jwt - GitHub App JWT
|
|
186
|
+
* @returns Array of repositories
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
async fetchInstallationRepositories(installationId, jwt) {
|
|
190
|
+
const url = `${this.baseUrl}/installation/repositories`;
|
|
191
|
+
// First get an installation token
|
|
192
|
+
const tokenUrl = `${this.baseUrl}/app/installations/${installationId}/access_tokens`;
|
|
193
|
+
const tokenResponse = await fetch(tokenUrl, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `Bearer ${jwt}`,
|
|
197
|
+
Accept: "application/vnd.github+json",
|
|
198
|
+
"User-Agent": "Flink-GitHub-App-Plugin",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
if (!tokenResponse.ok) {
|
|
202
|
+
const errorBody = await tokenResponse.text();
|
|
203
|
+
throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.TOKEN_EXCHANGE_FAILED, `Failed to get installation token: ${tokenResponse.statusText}`, { status: tokenResponse.status, body: errorBody });
|
|
204
|
+
}
|
|
205
|
+
const tokenData = await tokenResponse.json();
|
|
206
|
+
const token = tokenData.token;
|
|
207
|
+
// Now fetch repositories with the installation token
|
|
208
|
+
const reposResponse = await fetch(url, {
|
|
209
|
+
method: "GET",
|
|
210
|
+
headers: {
|
|
211
|
+
Authorization: `Bearer ${token}`,
|
|
212
|
+
Accept: "application/vnd.github+json",
|
|
213
|
+
"User-Agent": "Flink-GitHub-App-Plugin",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
if (!reposResponse.ok) {
|
|
217
|
+
const errorBody = await reposResponse.text();
|
|
218
|
+
throw (0, error_utils_1.createGitHubAppError)(error_utils_1.GitHubAppErrorCodes.SERVER_ERROR, `Failed to fetch repositories: ${reposResponse.statusText}`, { status: reposResponse.status, body: errorBody });
|
|
219
|
+
}
|
|
220
|
+
const data = await reposResponse.json();
|
|
221
|
+
return data.repositories || [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
exports.InstallationService = InstallationService;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/github-app-plugin",
|
|
3
|
-
"version": "0.12.1-alpha.
|
|
3
|
+
"version": "0.12.1-alpha.39",
|
|
4
4
|
"description": "Flink plugin for GitHub App integration with installation management and webhook handling",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --preserve-symlinks -r ts-node/register -- node_modules/jasmine/bin/jasmine --config=./spec/support/jasmine.json",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"tsc-watch": "^4.2.9",
|
|
38
38
|
"typescript": "5.4.5"
|
|
39
39
|
},
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "5be6cf2e80d665d08d380bc5e495bf9e20ff7b6e"
|
|
41
41
|
}
|
package/spec/handlers.spec.ts
CHANGED
|
@@ -1,142 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Handler Tests for GitHub App Plugin
|
|
3
3
|
*
|
|
4
|
-
* Tests for
|
|
4
|
+
* Tests for webhook handler.
|
|
5
5
|
* Focused tests covering critical paths only.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import InstallationCallback from "../src/handlers/InstallationCallback";
|
|
9
8
|
import WebhookHandler from "../src/handlers/WebhookHandler";
|
|
10
|
-
import { generateSessionId, generateState } from "../src/utils/state-utils";
|
|
11
9
|
|
|
12
10
|
describe("GitHub App Handlers", () => {
|
|
13
|
-
describe("InstallationCallback", () => {
|
|
14
|
-
it("should validate state and store installation", async () => {
|
|
15
|
-
const state = generateState();
|
|
16
|
-
const mockSession = {
|
|
17
|
-
sessionId: generateSessionId(),
|
|
18
|
-
state,
|
|
19
|
-
userId: "user123",
|
|
20
|
-
createdAt: new Date(),
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const mockInstallation = {
|
|
24
|
-
id: 12345,
|
|
25
|
-
account: {
|
|
26
|
-
id: 67890,
|
|
27
|
-
login: "testuser",
|
|
28
|
-
type: "User" as const,
|
|
29
|
-
avatar_url: "https://example.com/avatar.png",
|
|
30
|
-
},
|
|
31
|
-
repository_selection: "selected",
|
|
32
|
-
permissions: {
|
|
33
|
-
contents: "read",
|
|
34
|
-
issues: "write",
|
|
35
|
-
},
|
|
36
|
-
events: ["push", "pull_request"],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const mockRepositories = [
|
|
40
|
-
{
|
|
41
|
-
id: 1,
|
|
42
|
-
name: "repo1",
|
|
43
|
-
full_name: "testuser/repo1",
|
|
44
|
-
private: false,
|
|
45
|
-
},
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
const mockCtx: any = {
|
|
49
|
-
repos: {
|
|
50
|
-
githubAppSessionRepo: {
|
|
51
|
-
getOne: jasmine.createSpy("getOne").and.returnValue(Promise.resolve(mockSession)),
|
|
52
|
-
deleteBySessionId: jasmine.createSpy("deleteBySessionId").and.returnValue(Promise.resolve(1)),
|
|
53
|
-
},
|
|
54
|
-
githubInstallationRepo: {
|
|
55
|
-
create: jasmine.createSpy("create").and.returnValue(Promise.resolve({})),
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
plugins: {
|
|
59
|
-
githubApp: {
|
|
60
|
-
authService: {
|
|
61
|
-
generateAppJWT: jasmine.createSpy("generateAppJWT").and.returnValue("mock-jwt"),
|
|
62
|
-
},
|
|
63
|
-
options: {
|
|
64
|
-
baseUrl: "https://api.github.com",
|
|
65
|
-
onInstallationSuccess: jasmine.createSpy("onInstallationSuccess").and.returnValue(
|
|
66
|
-
Promise.resolve({
|
|
67
|
-
userId: "user123",
|
|
68
|
-
redirectUrl: "/dashboard",
|
|
69
|
-
})
|
|
70
|
-
),
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Mock fetch for installation details and repositories
|
|
77
|
-
global.fetch = jasmine.createSpy("fetch").and.returnValues(
|
|
78
|
-
// First call: get installation details
|
|
79
|
-
Promise.resolve({
|
|
80
|
-
ok: true,
|
|
81
|
-
json: () => Promise.resolve(mockInstallation),
|
|
82
|
-
} as any),
|
|
83
|
-
// Second call: get installation token
|
|
84
|
-
Promise.resolve({
|
|
85
|
-
ok: true,
|
|
86
|
-
json: () => Promise.resolve({ token: "mock-token" }),
|
|
87
|
-
} as any),
|
|
88
|
-
// Third call: get repositories
|
|
89
|
-
Promise.resolve({
|
|
90
|
-
ok: true,
|
|
91
|
-
json: () => Promise.resolve({ repositories: mockRepositories }),
|
|
92
|
-
} as any)
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const mockReq: any = {
|
|
96
|
-
query: {
|
|
97
|
-
installation_id: "12345",
|
|
98
|
-
setup_action: "install",
|
|
99
|
-
state,
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const result = await InstallationCallback({ ctx: mockCtx, req: mockReq } as any);
|
|
104
|
-
|
|
105
|
-
expect(result.status).toBe(302);
|
|
106
|
-
expect(mockCtx.repos.githubAppSessionRepo.getOne).toHaveBeenCalledWith({ state });
|
|
107
|
-
expect(mockCtx.repos.githubAppSessionRepo.deleteBySessionId).toHaveBeenCalledWith(mockSession.sessionId);
|
|
108
|
-
expect(mockCtx.plugins.githubApp.options.onInstallationSuccess).toHaveBeenCalled();
|
|
109
|
-
expect(mockCtx.repos.githubInstallationRepo.create).toHaveBeenCalled();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("should reject invalid state", async () => {
|
|
113
|
-
const mockCtx: any = {
|
|
114
|
-
repos: {
|
|
115
|
-
githubAppSessionRepo: {
|
|
116
|
-
getOne: jasmine.createSpy("getOne").and.returnValue(Promise.resolve(null)),
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
plugins: {
|
|
120
|
-
githubApp: {
|
|
121
|
-
options: {},
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const mockReq: any = {
|
|
127
|
-
query: {
|
|
128
|
-
installation_id: "12345",
|
|
129
|
-
setup_action: "install",
|
|
130
|
-
state: "invalid-state",
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const result = await InstallationCallback({ ctx: mockCtx, req: mockReq } as any);
|
|
135
|
-
|
|
136
|
-
expect(result.status).toBe(400);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
11
|
describe("WebhookHandler", () => {
|
|
141
12
|
it("should validate signature and call webhook callback", async () => {
|
|
142
13
|
const secret = "test-secret";
|