@flink-app/github-app-plugin 0.12.1-alpha.38
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/CHANGELOG.md +209 -0
- package/LICENSE +21 -0
- package/README.md +667 -0
- package/SECURITY.md +498 -0
- package/dist/GitHubAppInternalContext.d.ts +44 -0
- package/dist/GitHubAppInternalContext.js +2 -0
- package/dist/GitHubAppPlugin.d.ts +45 -0
- package/dist/GitHubAppPlugin.js +367 -0
- package/dist/GitHubAppPluginContext.d.ts +242 -0
- package/dist/GitHubAppPluginContext.js +2 -0
- package/dist/GitHubAppPluginOptions.d.ts +369 -0
- package/dist/GitHubAppPluginOptions.js +2 -0
- package/dist/handlers/InitiateInstallation.d.ts +32 -0
- package/dist/handlers/InitiateInstallation.js +66 -0
- package/dist/handlers/InstallationCallback.d.ts +42 -0
- package/dist/handlers/InstallationCallback.js +248 -0
- package/dist/handlers/UninstallHandler.d.ts +37 -0
- package/dist/handlers/UninstallHandler.js +153 -0
- package/dist/handlers/WebhookHandler.d.ts +54 -0
- package/dist/handlers/WebhookHandler.js +157 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +23 -0
- package/dist/repos/GitHubAppSessionRepo.d.ts +24 -0
- package/dist/repos/GitHubAppSessionRepo.js +32 -0
- package/dist/repos/GitHubInstallationRepo.d.ts +53 -0
- package/dist/repos/GitHubInstallationRepo.js +83 -0
- package/dist/repos/GitHubWebhookEventRepo.d.ts +29 -0
- package/dist/repos/GitHubWebhookEventRepo.js +42 -0
- package/dist/schemas/GitHubAppSession.d.ts +13 -0
- package/dist/schemas/GitHubAppSession.js +2 -0
- package/dist/schemas/GitHubInstallation.d.ts +28 -0
- package/dist/schemas/GitHubInstallation.js +2 -0
- package/dist/schemas/InstallationCallbackRequest.d.ts +10 -0
- package/dist/schemas/InstallationCallbackRequest.js +2 -0
- package/dist/schemas/WebhookEvent.d.ts +16 -0
- package/dist/schemas/WebhookEvent.js +2 -0
- package/dist/schemas/WebhookPayload.d.ts +35 -0
- package/dist/schemas/WebhookPayload.js +2 -0
- package/dist/services/GitHubAPIClient.d.ts +143 -0
- package/dist/services/GitHubAPIClient.js +167 -0
- package/dist/services/GitHubAuthService.d.ts +85 -0
- package/dist/services/GitHubAuthService.js +160 -0
- package/dist/services/WebhookValidator.d.ts +93 -0
- package/dist/services/WebhookValidator.js +123 -0
- package/dist/utils/error-utils.d.ts +67 -0
- package/dist/utils/error-utils.js +121 -0
- package/dist/utils/jwt-utils.d.ts +35 -0
- package/dist/utils/jwt-utils.js +67 -0
- package/dist/utils/state-utils.d.ts +38 -0
- package/dist/utils/state-utils.js +74 -0
- package/dist/utils/token-cache-utils.d.ts +47 -0
- package/dist/utils/token-cache-utils.js +74 -0
- package/dist/utils/webhook-signature-utils.d.ts +22 -0
- package/dist/utils/webhook-signature-utils.js +57 -0
- package/examples/basic-installation.ts +246 -0
- package/examples/create-issue.ts +392 -0
- package/examples/error-handling.ts +396 -0
- package/examples/multi-event-webhook.ts +367 -0
- package/examples/organization-installation.ts +316 -0
- package/examples/repository-access.ts +480 -0
- package/examples/webhook-handling.ts +343 -0
- package/examples/with-jwt-auth.ts +319 -0
- package/package.json +41 -0
- package/spec/core-utilities.spec.ts +243 -0
- package/spec/handlers.spec.ts +216 -0
- package/spec/helpers/reporter.ts +41 -0
- package/spec/integration-and-security.spec.ts +483 -0
- package/spec/plugin-core.spec.ts +258 -0
- package/spec/project-setup.spec.ts +56 -0
- package/spec/repos-and-schemas.spec.ts +288 -0
- package/spec/services.spec.ts +108 -0
- package/spec/support/jasmine.json +7 -0
- package/src/GitHubAppPlugin.ts +411 -0
- package/src/GitHubAppPluginContext.ts +254 -0
- package/src/GitHubAppPluginOptions.ts +412 -0
- package/src/handlers/InstallationCallback.ts +292 -0
- package/src/handlers/WebhookHandler.ts +179 -0
- package/src/index.ts +29 -0
- package/src/repos/GitHubAppSessionRepo.ts +36 -0
- package/src/repos/GitHubInstallationRepo.ts +95 -0
- package/src/repos/GitHubWebhookEventRepo.ts +48 -0
- package/src/schemas/GitHubAppSession.ts +13 -0
- package/src/schemas/GitHubInstallation.ts +28 -0
- package/src/schemas/InstallationCallbackRequest.ts +10 -0
- package/src/schemas/WebhookEvent.ts +16 -0
- package/src/schemas/WebhookPayload.ts +35 -0
- package/src/services/GitHubAPIClient.ts +244 -0
- package/src/services/GitHubAuthService.ts +188 -0
- package/src/services/WebhookValidator.ts +159 -0
- package/src/utils/error-utils.ts +148 -0
- package/src/utils/jwt-utils.ts +64 -0
- package/src/utils/state-utils.ts +72 -0
- package/src/utils/token-cache-utils.ts +89 -0
- package/src/utils/webhook-signature-utils.ts +57 -0
- package/tsconfig.dist.json +4 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Utilities Test Suite
|
|
3
|
+
*
|
|
4
|
+
* Tests for JWT utilities, token cache, webhook signature validation,
|
|
5
|
+
* state utilities, and error utilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
import { generateJWT, detectPEMFormat } from "../src/utils/jwt-utils";
|
|
10
|
+
import { validateWebhookSignature } from "../src/utils/webhook-signature-utils";
|
|
11
|
+
import { TokenCache } from "../src/utils/token-cache-utils";
|
|
12
|
+
import { generateState, generateSessionId, validateState } from "../src/utils/state-utils";
|
|
13
|
+
import { GitHubAppErrorCodes, createGitHubAppError } from "../src/utils/error-utils";
|
|
14
|
+
|
|
15
|
+
describe("Core Utilities", () => {
|
|
16
|
+
// Test private keys (PKCS#1 and PKCS#8 formats)
|
|
17
|
+
// These are real test keys generated for testing purposes only
|
|
18
|
+
const pkcs1PrivateKey = `-----BEGIN RSA PRIVATE KEY-----
|
|
19
|
+
MIIEogIBAAKCAQEAq6ubHwwPDOQM2+GhrYRSw8K6S+Jqp3znXDaHMMIg89Y6u1G2
|
|
20
|
+
VbnoS608F606uRXkuYLv1HzhtKs2Z9OLhhtamZWNaeUDVVOchsCmLl5Mh+z11Kwh
|
|
21
|
+
Ho0qMM9LqLsN/tXd2Y014NEaChDf7D251/1DXXxZCILi64tNbm7IxwSrwl9sxPID
|
|
22
|
+
OC6keNtPA2la2tAf6W49Bv1Yqp0KbxNnpdxotJFl0FAF4XyBRyaz2/UY3tKPrn72
|
|
23
|
+
qzN/P03xG8d2VOSxigx+9btrwEk8QHNjPlbGK5w275Oo0qv4XcU2/S3l1/I/5qxo
|
|
24
|
+
AmahBI7FhY8qAdVwHaxcw4ddGUG38fIytFXJFQIDAQABAoIBAAkBx3FBEjcUbgJJ
|
|
25
|
+
W9C9TRRdTqX1mq/v8zmY2M37mXwBpPI4Ds9/ogr6a1k4qwiT9/ytvISTCsqOYxve
|
|
26
|
+
cwcVv1KokJNaQysCaId/axiqtOw6yAkhANnYATsvXSJcshbJRMsJyCZkAjA+A2mj
|
|
27
|
+
MXF+Jb8ta4RxTZObKvRc1qbufU6E2iAv5h3Gphc/dkI2aFBrCoovwan23RZ4iunB
|
|
28
|
+
JdjqAvZxQkzik7/NxFTf2LaI8/ejhhF6U8ipnaO8UCaKal0u0HoHVguxTzQ4EJ8f
|
|
29
|
+
htAGJb/0G+0glM+vGkxqtxCQsa2pJviGi209aRKsHbyYTm9jagFORGhHdDscNCYJ
|
|
30
|
+
0WP+m+8CgYEA4wUSGGcFierhdGSMZ/LQz99AYjZIW3GOXEJDpmobhO36WTIn3XDK
|
|
31
|
+
OT7itpVBZmIlGAtNu2yrVDxTj3pkAPHX4arL/QLucxCrFetTN88nlBy7iHRHuR04
|
|
32
|
+
JkSDB/A0UoRMyNS8jLoInQ+Q/UeEpcY8z3zqGEiXW1BKfUEF5WfeDmcCgYEAwZW7
|
|
33
|
+
yTB5tPlLFGGwbNU1jasVQoJYXj60twM54JKcdIMVaXKTK9+zpg2v79i5sLsH8ntL
|
|
34
|
+
WCHNH9rjwbhtG2fNOApykgcZcLx6UDwGTX3tu4/M2WwbqM/G+90//SXI+mC5oa4Y
|
|
35
|
+
zucSYxlrylk2Zzg4S7M9W0JA3C61PxDRisWjByMCgYAlyQZGAX+ugOWdlc64znVq
|
|
36
|
+
4+G3dwl8Dt5/BJh17ls+OM3eYra36Ln/5TOe6CDGhbde1SLO+ztY/eF6lAhpD9e6
|
|
37
|
+
u87QAdjmVfPj5hMnytbvlAiyoYf+i5p45BZbD+PliBevpZjsY1pjqd+cCHdPkDs2
|
|
38
|
+
3beo6wwmKqr7RgNRN4SCKQKBgB8i40JX3quCEVZk5AiNPoDbzJ6W8nmuIkjxZuS9
|
|
39
|
+
EBcZYl9Eg3FiGLYTq4GrXSqU2pFgzVyOizyda1akQEBRMMvbulPMeoYMeqvfC7B5
|
|
40
|
+
Gby6Q1uRLN25Fas7CejApBPJbPIZW3oj5mw0EYdJVBvECiH64VqFTINdq99J6Dom
|
|
41
|
+
0bL7AoGAEoaROmQZU0rzQHW5I1ZtH3HJd95a0OHSeMWMbYZxOROVL5gPrHPHTsSf
|
|
42
|
+
m3iZcKhX/utexGcZms7C8Ss95jxO4bWzAp61UMZSwUa+X54T4y05LVEtT8nSuTrK
|
|
43
|
+
qWWL8eC5rF2KHb2wPUwVmfPG8IiLKQdPtvoVsDc6uOy5DRvJsWI=
|
|
44
|
+
-----END RSA PRIVATE KEY-----`;
|
|
45
|
+
|
|
46
|
+
const pkcs8PrivateKey = `-----BEGIN PRIVATE KEY-----
|
|
47
|
+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9BfzBJ094CsjQ
|
|
48
|
+
P9Qo/yNKgS7QjzFyk9jXm/VUVoELcsLfCg4dwbxOd3ljpVqWknIpgnBgqH0RiPWB
|
|
49
|
+
tC5C1tpPHy24YEXUY0pBrDXUZUsuLzAh1y96RblqrgQxD3QkHmH3mtFYdTPzYwR4
|
|
50
|
+
akiH7loaA2DMN+2iC5BOFARQTM+V8VrcPN9rd1+Mtrou6ExvK9T36VK8uEJJb4WV
|
|
51
|
+
VWuSyc+4immPID15Ou0QhZr6clVur9qg0pyrukbJxcXsIe1TqDYTVOunLxbVOiEs
|
|
52
|
+
kUIh2Lq5RKaCFoxgIBbizSK5PEGLMcMI0PwVAlWsxchpna0LY5f2GvHGvnKX8SHl
|
|
53
|
+
JeN58cTFAgMBAAECggEAJYeOy3rWmGrrvA0wPoOJqj1D4jzMAIfCQezBJOGX9YHv
|
|
54
|
+
lwEUFGxmyt2FyHcIKWUiLYOsdER/sH+U3w+7L6Ig7hyuozDaLHUaRTe/6E/EQYM0
|
|
55
|
+
90MWNhyp17h9NJBw6srtgI/IiNucWPKL7KyNgg+c7BVHnsRr9gR9vkLTKG5XuNk/
|
|
56
|
+
DL1mxG9lenlrynhkohUg7aQS2PcEGsHAKXFkMvL8Vxgi1PwY8ySE1HFxnohKvIbZ
|
|
57
|
+
xTa1JIqrj8Cx2PjmIGz+MoVbKE1A0hA7rcLmPXhQdEAgyjSF/5t52clX/Qro91IE
|
|
58
|
+
qRLoTrPZo4a55syODEQEJAji9Xn7Wai1pSmxhGKnAQKBgQDxHh9EUW4rajfyCGl6
|
|
59
|
+
GA625OGRzDzqg+527b8VDX1ysc1XLJdrkUY2/dyGijMLm7bp1Kb/przN7zEURHYJ
|
|
60
|
+
iMdah7d+Dg49HHn08e3Go7ibBAXPpbO8iAC6W+yZxOaad2MUgkJVoOUDREnRLTPM
|
|
61
|
+
yT2tW1q331omQmPmJRoo/7azFQKBgQDIsLp1NmchNfR3pVnF76v4o5OCWuvvx0cU
|
|
62
|
+
QgnA6WqYy3h9O5nHxOgy8gkMkbv/XGyUU+meh40rhq9rWJslKc3C7ENdv13lnkmz
|
|
63
|
+
O7CLxo4Gb24dkC6NqGdC7PZgsYr4K2+yAZGI6SIdnXCa+TywAFExCXTu8C/y3PzC
|
|
64
|
+
mWs2nqn28QKBgCMwz0VsURT7CrFDcwmDy1n8K8PYuCdOHBa1ekb7UgzUUHDhrDPh
|
|
65
|
+
3wqVoILuVqbiEh8sjzcOwc2YlGQt3cBkexwGZMx8Bq36ov4R9S8hpAbT3nlA6Oui
|
|
66
|
+
OeD5G54Rs8pllEtg+4d91Q7V/6QM4duIn3zWsXXWnlSpKeVkEt5a+/JFAoGAVvxy
|
|
67
|
+
9RcNgFGYkrtyu950VaLg7uFl3lorrtYo0Brb/zpCEVXiA7qPQnWyAmawa7Ctx2TP
|
|
68
|
+
n8z1HWaVZhvTszn5W4F4eYvWsQ34t90pWoxHRvbJbbru0quphlKbP7H0oDiDg042
|
|
69
|
+
vHcAOIHjKujYqxiYGH8W1fH5dnTegaJp3BTNaqECgYEA4zGjcKVTVAkmnIXb0VZk
|
|
70
|
+
SOmv9R3tKh+wUwYDKfw+kdUM6b+Tr7E589FgzIQuld68+FwNVupw7Eas56BTIVzW
|
|
71
|
+
y8Ym74NTGSMnlTW8R/X2p5y5O1KrYjAAF621eRBJZ1nUBQLvL5XKNPt4ZmwURh6g
|
|
72
|
+
tr8aCB8V88C1ffc7AfMLzzE=
|
|
73
|
+
-----END PRIVATE KEY-----`;
|
|
74
|
+
|
|
75
|
+
describe("JWT Utilities", () => {
|
|
76
|
+
it("should detect PKCS#1 format correctly", () => {
|
|
77
|
+
const format = detectPEMFormat(pkcs1PrivateKey);
|
|
78
|
+
expect(format).toBe("pkcs1");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should detect PKCS#8 format correctly", () => {
|
|
82
|
+
const format = detectPEMFormat(pkcs8PrivateKey);
|
|
83
|
+
expect(format).toBe("pkcs8");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should generate valid JWT with PKCS#1 key", () => {
|
|
87
|
+
const appId = "123456";
|
|
88
|
+
const jwt = generateJWT(appId, pkcs1PrivateKey);
|
|
89
|
+
expect(jwt).toBeTruthy();
|
|
90
|
+
expect(typeof jwt).toBe("string");
|
|
91
|
+
expect(jwt.split(".").length).toBe(3); // JWT has 3 parts
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should generate valid JWT with PKCS#8 key", () => {
|
|
95
|
+
const appId = "123456";
|
|
96
|
+
const jwt = generateJWT(appId, pkcs8PrivateKey);
|
|
97
|
+
expect(jwt).toBeTruthy();
|
|
98
|
+
expect(typeof jwt).toBe("string");
|
|
99
|
+
expect(jwt.split(".").length).toBe(3);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should throw error for invalid private key format", () => {
|
|
103
|
+
const invalidKey = "not a valid key";
|
|
104
|
+
expect(() => detectPEMFormat(invalidKey)).toThrow();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("Webhook Signature Utilities", () => {
|
|
109
|
+
const secret = "test-webhook-secret";
|
|
110
|
+
const payload = JSON.stringify({ test: "data" });
|
|
111
|
+
|
|
112
|
+
it("should validate correct webhook signature", () => {
|
|
113
|
+
// Generate valid signature
|
|
114
|
+
const signature = "sha256=" + crypto.createHmac("sha256", secret).update(payload).digest("hex");
|
|
115
|
+
|
|
116
|
+
const isValid = validateWebhookSignature(payload, signature, secret);
|
|
117
|
+
expect(isValid).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should reject invalid webhook signature", () => {
|
|
121
|
+
const invalidSignature = "sha256=invalid_signature";
|
|
122
|
+
|
|
123
|
+
const isValid = validateWebhookSignature(payload, invalidSignature, secret);
|
|
124
|
+
expect(isValid).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should reject signature with wrong secret", () => {
|
|
128
|
+
const wrongSecret = "wrong-secret";
|
|
129
|
+
const signature = "sha256=" + crypto.createHmac("sha256", wrongSecret).update(payload).digest("hex");
|
|
130
|
+
|
|
131
|
+
const isValid = validateWebhookSignature(payload, signature, secret);
|
|
132
|
+
expect(isValid).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("Token Cache Utilities", () => {
|
|
137
|
+
let cache: TokenCache;
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
cache = new TokenCache();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should store and retrieve token", () => {
|
|
144
|
+
const installationId = 12345;
|
|
145
|
+
const token = "test-token";
|
|
146
|
+
const ttl = 3600;
|
|
147
|
+
|
|
148
|
+
cache.setToken(installationId, token, ttl);
|
|
149
|
+
const retrieved = cache.getToken(installationId);
|
|
150
|
+
|
|
151
|
+
expect(retrieved).toBe(token);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should return null for expired token", (done) => {
|
|
155
|
+
const installationId = 12345;
|
|
156
|
+
const token = "test-token";
|
|
157
|
+
const ttl = 1; // 1 second
|
|
158
|
+
|
|
159
|
+
cache.setToken(installationId, token, ttl);
|
|
160
|
+
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
const retrieved = cache.getToken(installationId);
|
|
163
|
+
expect(retrieved).toBeNull();
|
|
164
|
+
done();
|
|
165
|
+
}, 1100); // Wait for expiration
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should clear cache", () => {
|
|
169
|
+
cache.setToken(12345, "token1", 3600);
|
|
170
|
+
cache.setToken(67890, "token2", 3600);
|
|
171
|
+
|
|
172
|
+
cache.clearCache();
|
|
173
|
+
|
|
174
|
+
expect(cache.getToken(12345)).toBeNull();
|
|
175
|
+
expect(cache.getToken(67890)).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("State Utilities", () => {
|
|
180
|
+
it("should generate unique state parameter", () => {
|
|
181
|
+
const state1 = generateState();
|
|
182
|
+
const state2 = generateState();
|
|
183
|
+
|
|
184
|
+
expect(state1).toBeTruthy();
|
|
185
|
+
expect(state2).toBeTruthy();
|
|
186
|
+
expect(state1).not.toBe(state2);
|
|
187
|
+
expect(state1.length).toBe(64); // 32 bytes as hex = 64 chars
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should generate unique session ID", () => {
|
|
191
|
+
const sessionId1 = generateSessionId();
|
|
192
|
+
const sessionId2 = generateSessionId();
|
|
193
|
+
|
|
194
|
+
expect(sessionId1).toBeTruthy();
|
|
195
|
+
expect(sessionId2).toBeTruthy();
|
|
196
|
+
expect(sessionId1).not.toBe(sessionId2);
|
|
197
|
+
expect(sessionId1.length).toBe(32); // 16 bytes as hex = 32 chars
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should validate matching state using constant-time comparison", () => {
|
|
201
|
+
const state = generateState();
|
|
202
|
+
const isValid = validateState(state, state);
|
|
203
|
+
|
|
204
|
+
expect(isValid).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should reject non-matching state", () => {
|
|
208
|
+
const state1 = generateState();
|
|
209
|
+
const state2 = generateState();
|
|
210
|
+
|
|
211
|
+
const isValid = validateState(state1, state2);
|
|
212
|
+
expect(isValid).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("Error Utilities", () => {
|
|
217
|
+
it("should have kebab-case error codes", () => {
|
|
218
|
+
expect(GitHubAppErrorCodes.INVALID_STATE).toBe("invalid-state");
|
|
219
|
+
expect(GitHubAppErrorCodes.SESSION_EXPIRED).toBe("session-expired");
|
|
220
|
+
expect(GitHubAppErrorCodes.INSTALLATION_NOT_FOUND).toBe("installation-not-found");
|
|
221
|
+
expect(GitHubAppErrorCodes.INVALID_PRIVATE_KEY).toBe("invalid-private-key");
|
|
222
|
+
expect(GitHubAppErrorCodes.JWT_SIGNING_FAILED).toBe("jwt-signing-failed");
|
|
223
|
+
expect(GitHubAppErrorCodes.TOKEN_EXCHANGE_FAILED).toBe("token-exchange-failed");
|
|
224
|
+
expect(GitHubAppErrorCodes.WEBHOOK_SIGNATURE_INVALID).toBe("webhook-signature-invalid");
|
|
225
|
+
expect(GitHubAppErrorCodes.WEBHOOK_PAYLOAD_INVALID).toBe("webhook-payload-invalid");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should create GitHub App error with code and message", () => {
|
|
229
|
+
const error = createGitHubAppError("invalid-state", "State parameter is invalid");
|
|
230
|
+
|
|
231
|
+
expect(error.code).toBe("invalid-state");
|
|
232
|
+
expect(error.message).toBe("State parameter is invalid");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should create error with optional details", () => {
|
|
236
|
+
const error = createGitHubAppError("network-error", "Network failure", { statusCode: 500 });
|
|
237
|
+
|
|
238
|
+
expect(error.code).toBe("network-error");
|
|
239
|
+
expect(error.message).toBe("Network failure");
|
|
240
|
+
expect(error.details).toEqual({ statusCode: 500 });
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler Tests for GitHub App Plugin
|
|
3
|
+
*
|
|
4
|
+
* Tests for installation callback and webhook handlers.
|
|
5
|
+
* Focused tests covering critical paths only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import InstallationCallback from "../src/handlers/InstallationCallback";
|
|
9
|
+
import WebhookHandler from "../src/handlers/WebhookHandler";
|
|
10
|
+
import { generateSessionId, generateState } from "../src/utils/state-utils";
|
|
11
|
+
|
|
12
|
+
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
|
+
describe("WebhookHandler", () => {
|
|
141
|
+
it("should validate signature and call webhook callback", async () => {
|
|
142
|
+
const secret = "test-secret";
|
|
143
|
+
const payload = JSON.stringify({
|
|
144
|
+
action: "created",
|
|
145
|
+
installation: {
|
|
146
|
+
id: 12345,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const mockCtx: any = {
|
|
151
|
+
repos: {
|
|
152
|
+
githubWebhookEventRepo: {
|
|
153
|
+
create: jasmine.createSpy("create").and.returnValue(Promise.resolve({})),
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
plugins: {
|
|
157
|
+
githubApp: {
|
|
158
|
+
webhookValidator: {
|
|
159
|
+
validateSignature: jasmine.createSpy("validateSignature").and.returnValue(true),
|
|
160
|
+
parsePayload: jasmine.createSpy("parsePayload").and.returnValue({
|
|
161
|
+
action: "created",
|
|
162
|
+
installation: { id: 12345 },
|
|
163
|
+
}),
|
|
164
|
+
extractInstallationId: jasmine.createSpy("extractInstallationId").and.returnValue(12345),
|
|
165
|
+
},
|
|
166
|
+
options: {
|
|
167
|
+
logWebhookEvents: true,
|
|
168
|
+
onWebhookEvent: jasmine.createSpy("onWebhookEvent").and.returnValue(Promise.resolve()),
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const mockReq: any = {
|
|
175
|
+
headers: {
|
|
176
|
+
"x-github-event": "installation",
|
|
177
|
+
"x-github-delivery": "delivery-123",
|
|
178
|
+
"x-hub-signature-256": "sha256=fake-signature",
|
|
179
|
+
},
|
|
180
|
+
body: payload,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const result = await WebhookHandler({ ctx: mockCtx, req: mockReq } as any);
|
|
184
|
+
|
|
185
|
+
expect(result.status).toBe(200);
|
|
186
|
+
expect(mockCtx.plugins.githubApp.webhookValidator.validateSignature).toHaveBeenCalled();
|
|
187
|
+
expect(mockCtx.plugins.githubApp.options.onWebhookEvent).toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should reject invalid signature", async () => {
|
|
191
|
+
const mockCtx: any = {
|
|
192
|
+
plugins: {
|
|
193
|
+
githubApp: {
|
|
194
|
+
webhookValidator: {
|
|
195
|
+
validateSignature: jasmine.createSpy("validateSignature").and.returnValue(false),
|
|
196
|
+
},
|
|
197
|
+
options: {},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const mockReq: any = {
|
|
203
|
+
headers: {
|
|
204
|
+
"x-github-event": "installation",
|
|
205
|
+
"x-github-delivery": "delivery-123",
|
|
206
|
+
"x-hub-signature-256": "invalid-signature",
|
|
207
|
+
},
|
|
208
|
+
body: "{}",
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const result = await WebhookHandler({ ctx: mockCtx, req: mockReq } as any);
|
|
212
|
+
|
|
213
|
+
expect(result.status).toBe(401);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jasmine Spec Reporter Configuration
|
|
3
|
+
* Provides cleaner, more readable test output
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SpecReporter, StacktraceOption } from 'jasmine-spec-reporter';
|
|
7
|
+
|
|
8
|
+
// Remove default reporter
|
|
9
|
+
jasmine.getEnv().clearReporters();
|
|
10
|
+
|
|
11
|
+
// Add spec reporter with custom configuration
|
|
12
|
+
jasmine.getEnv().addReporter(new SpecReporter({
|
|
13
|
+
spec: {
|
|
14
|
+
displayPending: true,
|
|
15
|
+
displayDuration: true,
|
|
16
|
+
displayErrorMessages: true,
|
|
17
|
+
displayStacktrace: StacktraceOption.PRETTY,
|
|
18
|
+
displaySuccessful: true,
|
|
19
|
+
displayFailed: true,
|
|
20
|
+
},
|
|
21
|
+
summary: {
|
|
22
|
+
displayPending: true,
|
|
23
|
+
displayDuration: true,
|
|
24
|
+
displayErrorMessages: true,
|
|
25
|
+
displayStacktrace: StacktraceOption.PRETTY,
|
|
26
|
+
displaySuccessful: false,
|
|
27
|
+
displayFailed: true,
|
|
28
|
+
},
|
|
29
|
+
colors: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
successful: 'green',
|
|
32
|
+
failed: 'red',
|
|
33
|
+
pending: 'yellow',
|
|
34
|
+
},
|
|
35
|
+
prefixes: {
|
|
36
|
+
successful: '✓ ',
|
|
37
|
+
failed: '✗ ',
|
|
38
|
+
pending: '○ ',
|
|
39
|
+
},
|
|
40
|
+
customProcessors: [],
|
|
41
|
+
}));
|