@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.
Files changed (96) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/LICENSE +21 -0
  3. package/README.md +667 -0
  4. package/SECURITY.md +498 -0
  5. package/dist/GitHubAppInternalContext.d.ts +44 -0
  6. package/dist/GitHubAppInternalContext.js +2 -0
  7. package/dist/GitHubAppPlugin.d.ts +45 -0
  8. package/dist/GitHubAppPlugin.js +367 -0
  9. package/dist/GitHubAppPluginContext.d.ts +242 -0
  10. package/dist/GitHubAppPluginContext.js +2 -0
  11. package/dist/GitHubAppPluginOptions.d.ts +369 -0
  12. package/dist/GitHubAppPluginOptions.js +2 -0
  13. package/dist/handlers/InitiateInstallation.d.ts +32 -0
  14. package/dist/handlers/InitiateInstallation.js +66 -0
  15. package/dist/handlers/InstallationCallback.d.ts +42 -0
  16. package/dist/handlers/InstallationCallback.js +248 -0
  17. package/dist/handlers/UninstallHandler.d.ts +37 -0
  18. package/dist/handlers/UninstallHandler.js +153 -0
  19. package/dist/handlers/WebhookHandler.d.ts +54 -0
  20. package/dist/handlers/WebhookHandler.js +157 -0
  21. package/dist/index.d.ts +19 -0
  22. package/dist/index.js +23 -0
  23. package/dist/repos/GitHubAppSessionRepo.d.ts +24 -0
  24. package/dist/repos/GitHubAppSessionRepo.js +32 -0
  25. package/dist/repos/GitHubInstallationRepo.d.ts +53 -0
  26. package/dist/repos/GitHubInstallationRepo.js +83 -0
  27. package/dist/repos/GitHubWebhookEventRepo.d.ts +29 -0
  28. package/dist/repos/GitHubWebhookEventRepo.js +42 -0
  29. package/dist/schemas/GitHubAppSession.d.ts +13 -0
  30. package/dist/schemas/GitHubAppSession.js +2 -0
  31. package/dist/schemas/GitHubInstallation.d.ts +28 -0
  32. package/dist/schemas/GitHubInstallation.js +2 -0
  33. package/dist/schemas/InstallationCallbackRequest.d.ts +10 -0
  34. package/dist/schemas/InstallationCallbackRequest.js +2 -0
  35. package/dist/schemas/WebhookEvent.d.ts +16 -0
  36. package/dist/schemas/WebhookEvent.js +2 -0
  37. package/dist/schemas/WebhookPayload.d.ts +35 -0
  38. package/dist/schemas/WebhookPayload.js +2 -0
  39. package/dist/services/GitHubAPIClient.d.ts +143 -0
  40. package/dist/services/GitHubAPIClient.js +167 -0
  41. package/dist/services/GitHubAuthService.d.ts +85 -0
  42. package/dist/services/GitHubAuthService.js +160 -0
  43. package/dist/services/WebhookValidator.d.ts +93 -0
  44. package/dist/services/WebhookValidator.js +123 -0
  45. package/dist/utils/error-utils.d.ts +67 -0
  46. package/dist/utils/error-utils.js +121 -0
  47. package/dist/utils/jwt-utils.d.ts +35 -0
  48. package/dist/utils/jwt-utils.js +67 -0
  49. package/dist/utils/state-utils.d.ts +38 -0
  50. package/dist/utils/state-utils.js +74 -0
  51. package/dist/utils/token-cache-utils.d.ts +47 -0
  52. package/dist/utils/token-cache-utils.js +74 -0
  53. package/dist/utils/webhook-signature-utils.d.ts +22 -0
  54. package/dist/utils/webhook-signature-utils.js +57 -0
  55. package/examples/basic-installation.ts +246 -0
  56. package/examples/create-issue.ts +392 -0
  57. package/examples/error-handling.ts +396 -0
  58. package/examples/multi-event-webhook.ts +367 -0
  59. package/examples/organization-installation.ts +316 -0
  60. package/examples/repository-access.ts +480 -0
  61. package/examples/webhook-handling.ts +343 -0
  62. package/examples/with-jwt-auth.ts +319 -0
  63. package/package.json +41 -0
  64. package/spec/core-utilities.spec.ts +243 -0
  65. package/spec/handlers.spec.ts +216 -0
  66. package/spec/helpers/reporter.ts +41 -0
  67. package/spec/integration-and-security.spec.ts +483 -0
  68. package/spec/plugin-core.spec.ts +258 -0
  69. package/spec/project-setup.spec.ts +56 -0
  70. package/spec/repos-and-schemas.spec.ts +288 -0
  71. package/spec/services.spec.ts +108 -0
  72. package/spec/support/jasmine.json +7 -0
  73. package/src/GitHubAppPlugin.ts +411 -0
  74. package/src/GitHubAppPluginContext.ts +254 -0
  75. package/src/GitHubAppPluginOptions.ts +412 -0
  76. package/src/handlers/InstallationCallback.ts +292 -0
  77. package/src/handlers/WebhookHandler.ts +179 -0
  78. package/src/index.ts +29 -0
  79. package/src/repos/GitHubAppSessionRepo.ts +36 -0
  80. package/src/repos/GitHubInstallationRepo.ts +95 -0
  81. package/src/repos/GitHubWebhookEventRepo.ts +48 -0
  82. package/src/schemas/GitHubAppSession.ts +13 -0
  83. package/src/schemas/GitHubInstallation.ts +28 -0
  84. package/src/schemas/InstallationCallbackRequest.ts +10 -0
  85. package/src/schemas/WebhookEvent.ts +16 -0
  86. package/src/schemas/WebhookPayload.ts +35 -0
  87. package/src/services/GitHubAPIClient.ts +244 -0
  88. package/src/services/GitHubAuthService.ts +188 -0
  89. package/src/services/WebhookValidator.ts +159 -0
  90. package/src/utils/error-utils.ts +148 -0
  91. package/src/utils/jwt-utils.ts +64 -0
  92. package/src/utils/state-utils.ts +72 -0
  93. package/src/utils/token-cache-utils.ts +89 -0
  94. package/src/utils/webhook-signature-utils.ts +57 -0
  95. package/tsconfig.dist.json +4 -0
  96. 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
+ }));