@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,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration and Security Tests
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive tests covering critical workflows and security features:
|
|
5
|
+
* - Full installation flow using context methods
|
|
6
|
+
* - Webhook event processing with signature validation
|
|
7
|
+
* - API client with token injection and refresh
|
|
8
|
+
* - CSRF state validation prevents replay attacks
|
|
9
|
+
* - Webhook signature validation rejects invalid signatures
|
|
10
|
+
* - Token expiration and refresh handling
|
|
11
|
+
* - Private key validation on plugin initialization
|
|
12
|
+
* - Repository access verification
|
|
13
|
+
* - Installation update via webhook
|
|
14
|
+
* - GitHub API failure scenarios
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { FlinkApp } from "@flink-app/flink";
|
|
18
|
+
import { MongoMemoryServer } from "mongodb-memory-server";
|
|
19
|
+
import { githubAppPlugin } from "../src/GitHubAppPlugin";
|
|
20
|
+
import { GitHubAppPluginOptions } from "../src/GitHubAppPluginOptions";
|
|
21
|
+
import { generateState } from "../src/utils/state-utils";
|
|
22
|
+
import crypto from "crypto";
|
|
23
|
+
import * as http from "@flink-app/test-utils";
|
|
24
|
+
|
|
25
|
+
describe("Integration and Security Tests", () => {
|
|
26
|
+
let mongoServer: MongoMemoryServer;
|
|
27
|
+
let app: FlinkApp<any>;
|
|
28
|
+
let originalFetch: typeof global.fetch;
|
|
29
|
+
|
|
30
|
+
// Test private key (PKCS#1 format, Base64 encoded)
|
|
31
|
+
const testPrivateKeyBase64 =
|
|
32
|
+
"LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBcTZ1Ykh3d1BET1FNMitHaHJZUlN3OEs2UytKcXAzem5YRGFITU1JZzg5WTZ1MUcyClZibm9TNjA4RjYwNnVSWGt1WUx2MUh6aHRLczJaOU9MaGh0YW1aV05hZVVEVlZPY2hzQ21MbDVNaCt6MTFLd2gKSG8wcU1NOUxxTHNOL3RYZDJZMDE0TkVhQ2hEZjdEMjUxLzFEWFh4WkNJTGk2NHROYm03SXh3U3J3bDlzeFBJRApPQzZrZU50UEEybGEydEFmNlc0OUJ2MVlxcDBLYnhObnBkeG90SkZsMEZBRjRYeUJSeWF6Mi9VWTN0S1BybjcyCnF6Ti9QMDN4RzhkMlZPU3hpZ3grOWJ0cndFazhRSE5qUGxiR0s1dzI3NU9vMHF2NFhjVTIvUzNsMS9JLzVxeG8KQW1haEJJN0ZoWThxQWRWd0hheGN3NGRkR1VHMzhmSXl0RlhKRlFJREFRQUJBb0lCQUFrQngzRkJFamNVYmdKSgpXOUM5VFJSZFRxWDFtcS92OHptWTJNMzdtWHdCcFBJNERzOS9vZ3I2YTFrNHF3aVQ5L3l0dklTVENzcU9ZeHZlCmN3Y1Z2MUtva0pOYVF5c0NhSWQvYXhpcXRPdzZ5QWtoQU5uWUFUc3ZYU0pjc2hiSlJNc0p5Q1prQWpBK0EybWoKTVhGK0piOHRhNFJ4VFpPYkt2UmMxcWJ1ZlU2RTJpQXY1aDNHcGhjL2RrSTJhRkJyQ29vdndhbjIzUlo0aXVuQgpKZGpxQXZaeFFremlrNy9OeEZUZjJMYUk4L2VqaGhGNlU4aXBuYU84VUNhS2FsMHUwSG9IVmd1eFR6UTRFSjhmCmh0QUdKYi8wRyswZ2xNK3ZHa3hxdHhDUXNhMnBKdmlHaTIwOWFSS3NIYnlZVG05amFnRk9SR2hIZERzY05DWUoKMFdQK20rOENnWUVBNHdVU0dHY0ZpZXJoZEdTTVovTFF6OTlBWWpaSVczR09YRUpEcG1vYmhPMzZXVEluM1hESwpPVDdpdHBWQlptSWxHQXROdTJ5clZEeFRqM3BrQVBIWDRhckwvUUx1Y3hDckZldFROODhubEJ5N2lIUkh1UjA0CkprU0RCL0EwVW9STXlOUzhqTG9JblErUS9VZUVwY1k4ejN6cUdFaVhXMUJLZlVFRjVXZmVEbWNDZ1lFQXdaVzcKeVRCNXRQbExGR0d3Yk5VMWphc1ZRb0pZWGo2MHR3TTU0SktjZElNVmFYS1RLOSt6cGcydjc5aTVzTHNIOG50TApXQ0hOSDlyandiaHRHMmZOT0FweWtnY1pjTHg2VUR3R1RYM3R1NC9NMld3YnFNL0crOTAvL1NYSSttQzVvYTRZCnp1Y1NZeGxyeWxrMlp6ZzRTN005VzBKQTNDNjFQeERSaXNXakJ5TUNnWUFseVFaR0FYK3VnT1dkbGM2NHpuVnEKNCtHM2R3bDhEdDUvQkpoMTdscytPTTNlWXJhMzZMbi81VE9lNkNER2hiZGUxU0xPK3p0WS9lRjZsQWhwRDllNgp1ODdRQWRqbVZmUGo1aE1ueXRidmxBaXlvWWYraTVwNDVCWmJEK1BsaUJldnBaanNZMXBqcWQrY0NIZFBrRHMyCjNiZW82d3dtS3FyN1JnTlJONFNDS1FLQmdCOGk0MEpYM3F1Q0VWWms1QWlOUG9EYnpKNlc4bm11SWtqeFp1UzkKRUJjWllsOUVnM0ZpR0xZVHE0R3JYU3FVMnBGZ3pWeU9penlkYTFha1FFQlJNTXZidWxQTWVvWU1lcXZmQzdCNQpHYnk2UTF1UkxOMjVGYXM3Q2VqQXBCUEpiUElaVzNvajVtdzBFWWRKVkJ2RUNpSDY0VnFGVElOZHE5OUo2RG9tCjBiTDdBb0dBRW9hUk9tUVpVMHJ6UUhXNUkxWnRIM0hKZDk1YTBPSFNlTVdNYllaeE9ST1ZMNWdQckhQSFRzU2YKbTNpWmNLaFgvdXRleEdjWm1zN0M4U3M5NWp4TzRiV3pBcDYxVU1aU3dVYStYNTRUNHkwNUxWRXRUOG5TdVRySwpxV1dMOGVDNXJGMktIYjJ3UFV3Vm1mUEc4SWlMS1FkUHR2b1ZzRGM2dU95NURSdkpzV0k9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t";
|
|
33
|
+
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
originalFetch = global.fetch;
|
|
36
|
+
// Increase timeout for integration tests
|
|
37
|
+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterAll(() => {
|
|
41
|
+
global.fetch = originalFetch;
|
|
42
|
+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
mongoServer = await MongoMemoryServer.create();
|
|
47
|
+
const mongoUri = mongoServer.getUri();
|
|
48
|
+
|
|
49
|
+
const pluginOptions: GitHubAppPluginOptions = {
|
|
50
|
+
appId: "123456",
|
|
51
|
+
appSlug: "test-app",
|
|
52
|
+
privateKey: testPrivateKeyBase64,
|
|
53
|
+
webhookSecret: "test-webhook-secret",
|
|
54
|
+
clientId: "test-client-id",
|
|
55
|
+
clientSecret: "test-client-secret",
|
|
56
|
+
onInstallationSuccess: async ({ installationId, repositories, account }) => {
|
|
57
|
+
return {
|
|
58
|
+
userId: "test-user-id",
|
|
59
|
+
redirectUrl: "/dashboard",
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
onWebhookEvent: async ({ event, action, payload, installationId }, ctx) => {
|
|
63
|
+
// Webhook event handler
|
|
64
|
+
},
|
|
65
|
+
logWebhookEvents: true,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
app = new FlinkApp({
|
|
69
|
+
name: "Test GitHub App",
|
|
70
|
+
port: 3350,
|
|
71
|
+
db: {
|
|
72
|
+
uri: mongoUri,
|
|
73
|
+
},
|
|
74
|
+
plugins: [githubAppPlugin(pluginOptions)],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await app.start();
|
|
78
|
+
http.init(app);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(async () => {
|
|
82
|
+
if (app) {
|
|
83
|
+
await app.stop();
|
|
84
|
+
}
|
|
85
|
+
if (mongoServer) {
|
|
86
|
+
await mongoServer.stop();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("Integration Test: Full Installation Flow", () => {
|
|
91
|
+
it("should complete full installation flow using context methods", async () => {
|
|
92
|
+
// Step 1: Initiate installation using context method
|
|
93
|
+
const { redirectUrl, state } = await app.ctx.plugins.githubApp.initiateInstallation({
|
|
94
|
+
userId: "test-user",
|
|
95
|
+
metadata: { source: "test" },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(redirectUrl).toContain("github.com/apps/test-app/installations/new");
|
|
99
|
+
expect(redirectUrl).toContain("state=");
|
|
100
|
+
expect(state).toBeTruthy();
|
|
101
|
+
|
|
102
|
+
// Step 2: Mock GitHub API responses
|
|
103
|
+
global.fetch = jasmine.createSpy("fetch").and.returnValues(
|
|
104
|
+
// Get installation details
|
|
105
|
+
Promise.resolve({
|
|
106
|
+
ok: true,
|
|
107
|
+
json: () =>
|
|
108
|
+
Promise.resolve({
|
|
109
|
+
id: 12345,
|
|
110
|
+
account: {
|
|
111
|
+
id: 67890,
|
|
112
|
+
login: "testuser",
|
|
113
|
+
type: "User",
|
|
114
|
+
avatar_url: "https://example.com/avatar.png",
|
|
115
|
+
},
|
|
116
|
+
repository_selection: "selected",
|
|
117
|
+
permissions: { contents: "read", issues: "write" },
|
|
118
|
+
events: ["push", "pull_request"],
|
|
119
|
+
}),
|
|
120
|
+
} as any),
|
|
121
|
+
// Get installation token
|
|
122
|
+
Promise.resolve({
|
|
123
|
+
ok: true,
|
|
124
|
+
json: () => Promise.resolve({ token: "ghs_test_token", expires_at: "2025-10-26T12:00:00Z" }),
|
|
125
|
+
} as any),
|
|
126
|
+
// Get repositories
|
|
127
|
+
Promise.resolve({
|
|
128
|
+
ok: true,
|
|
129
|
+
json: () =>
|
|
130
|
+
Promise.resolve({
|
|
131
|
+
repositories: [
|
|
132
|
+
{
|
|
133
|
+
id: 1,
|
|
134
|
+
name: "test-repo",
|
|
135
|
+
full_name: "testuser/test-repo",
|
|
136
|
+
private: false,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
}),
|
|
140
|
+
} as any)
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Step 3: Complete callback
|
|
144
|
+
const callbackResponse = await http.get("/github-app/callback", {
|
|
145
|
+
qs: {
|
|
146
|
+
installation_id: "12345",
|
|
147
|
+
setup_action: "install",
|
|
148
|
+
state: state!,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(callbackResponse.status).toBe(302);
|
|
153
|
+
expect(callbackResponse.headers?.Location).toBe("/dashboard");
|
|
154
|
+
|
|
155
|
+
// Step 4: Verify installation was stored
|
|
156
|
+
const installation = await app.ctx.plugins.githubApp.getInstallation("test-user-id");
|
|
157
|
+
expect(installation).toBeTruthy();
|
|
158
|
+
expect(installation?.installationId).toBe(12345);
|
|
159
|
+
expect(installation?.accountLogin).toBe("testuser");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("Security Test: CSRF State Validation", () => {
|
|
164
|
+
it("should prevent replay attacks with invalid state", async () => {
|
|
165
|
+
const invalidState = generateState();
|
|
166
|
+
|
|
167
|
+
global.fetch = jasmine.createSpy("fetch");
|
|
168
|
+
|
|
169
|
+
const response = await http.get("/github-app/callback", {
|
|
170
|
+
qs: {
|
|
171
|
+
installation_id: "12345",
|
|
172
|
+
setup_action: "install",
|
|
173
|
+
state: invalidState,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(response.status).toBe(400);
|
|
178
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should prevent reuse of valid state after session deletion", async () => {
|
|
182
|
+
// Step 1: Create a valid session using context method
|
|
183
|
+
const { state } = await app.ctx.plugins.githubApp.initiateInstallation({
|
|
184
|
+
userId: "test-user",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
global.fetch = jasmine.createSpy("fetch").and.returnValues(
|
|
188
|
+
Promise.resolve({
|
|
189
|
+
ok: true,
|
|
190
|
+
json: () =>
|
|
191
|
+
Promise.resolve({
|
|
192
|
+
id: 12345,
|
|
193
|
+
account: {
|
|
194
|
+
id: 67890,
|
|
195
|
+
login: "testuser",
|
|
196
|
+
type: "User",
|
|
197
|
+
avatar_url: "https://example.com/avatar.png",
|
|
198
|
+
},
|
|
199
|
+
repository_selection: "selected",
|
|
200
|
+
permissions: {},
|
|
201
|
+
events: [],
|
|
202
|
+
}),
|
|
203
|
+
} as any),
|
|
204
|
+
Promise.resolve({
|
|
205
|
+
ok: true,
|
|
206
|
+
json: () => Promise.resolve({ token: "ghs_test_token" }),
|
|
207
|
+
} as any),
|
|
208
|
+
Promise.resolve({
|
|
209
|
+
ok: true,
|
|
210
|
+
json: () => Promise.resolve({ repositories: [] }),
|
|
211
|
+
} as any)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Step 2: Complete callback first time (should succeed)
|
|
215
|
+
const firstCallback = await http.get("/github-app/callback", {
|
|
216
|
+
qs: {
|
|
217
|
+
installation_id: "12345",
|
|
218
|
+
setup_action: "install",
|
|
219
|
+
state: state!,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(firstCallback.status).toBe(302);
|
|
224
|
+
|
|
225
|
+
// Step 3: Try to reuse same state (should fail)
|
|
226
|
+
const secondCallback = await http.get("/github-app/callback", {
|
|
227
|
+
qs: {
|
|
228
|
+
installation_id: "12345",
|
|
229
|
+
setup_action: "install",
|
|
230
|
+
state: state!,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(secondCallback.status).toBe(400);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("Security Test: Webhook Signature Validation", () => {
|
|
239
|
+
it("should reject webhooks with invalid signatures", async () => {
|
|
240
|
+
const payload = JSON.stringify({
|
|
241
|
+
action: "created",
|
|
242
|
+
installation: { id: 12345 },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const response = await http.post("/github-app/webhook", payload, {
|
|
246
|
+
headers: {
|
|
247
|
+
"x-github-event": "installation",
|
|
248
|
+
"x-github-delivery": "test-delivery-id",
|
|
249
|
+
"x-hub-signature-256": "sha256=invalid-signature",
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(response.status).toBe(401);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should accept webhooks with valid signatures", async () => {
|
|
257
|
+
const secret = "test-webhook-secret";
|
|
258
|
+
const payload = JSON.stringify({
|
|
259
|
+
action: "created",
|
|
260
|
+
installation: { id: 12345 },
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
264
|
+
hmac.update(payload);
|
|
265
|
+
const signature = `sha256=${hmac.digest("hex")}`;
|
|
266
|
+
|
|
267
|
+
const response = await http.post("/github-app/webhook", payload, {
|
|
268
|
+
headers: {
|
|
269
|
+
"x-github-event": "installation",
|
|
270
|
+
"x-github-delivery": "test-delivery-id",
|
|
271
|
+
"x-hub-signature-256": signature,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(response.status).toBe(200);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("Context Method: uninstall", () => {
|
|
280
|
+
it("should successfully uninstall when user owns installation", async () => {
|
|
281
|
+
// Create installation
|
|
282
|
+
await app.ctx.repos.githubInstallationRepo.create({
|
|
283
|
+
userId: "test-user",
|
|
284
|
+
installationId: 12345,
|
|
285
|
+
accountId: 67890,
|
|
286
|
+
accountLogin: "testuser",
|
|
287
|
+
accountType: "User",
|
|
288
|
+
repositories: [],
|
|
289
|
+
permissions: {},
|
|
290
|
+
events: [],
|
|
291
|
+
createdAt: new Date(),
|
|
292
|
+
updatedAt: new Date(),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Uninstall using context method
|
|
296
|
+
const result = await app.ctx.plugins.githubApp.uninstall({
|
|
297
|
+
userId: "test-user",
|
|
298
|
+
installationId: 12345,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(result.success).toBe(true);
|
|
302
|
+
expect(result.error).toBeUndefined();
|
|
303
|
+
|
|
304
|
+
// Verify installation was deleted
|
|
305
|
+
const installation = await app.ctx.plugins.githubApp.getInstallation("test-user");
|
|
306
|
+
expect(installation).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should fail when user doesn't own installation", async () => {
|
|
310
|
+
// Create installation owned by different user
|
|
311
|
+
await app.ctx.repos.githubInstallationRepo.create({
|
|
312
|
+
userId: "other-user",
|
|
313
|
+
installationId: 12345,
|
|
314
|
+
accountId: 67890,
|
|
315
|
+
accountLogin: "testuser",
|
|
316
|
+
accountType: "User",
|
|
317
|
+
repositories: [],
|
|
318
|
+
permissions: {},
|
|
319
|
+
events: [],
|
|
320
|
+
createdAt: new Date(),
|
|
321
|
+
updatedAt: new Date(),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Attempt to uninstall using different user
|
|
325
|
+
const result = await app.ctx.plugins.githubApp.uninstall({
|
|
326
|
+
userId: "test-user",
|
|
327
|
+
installationId: 12345,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(result.success).toBe(false);
|
|
331
|
+
expect(result.error).toBe("installation-not-found");
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("Integration Test: Webhook Event Processing", () => {
|
|
336
|
+
it("should process installation_repositories webhook event", async () => {
|
|
337
|
+
// Create existing installation
|
|
338
|
+
await app.ctx.repos.githubInstallationRepo.create({
|
|
339
|
+
userId: "test-user",
|
|
340
|
+
installationId: 12345,
|
|
341
|
+
accountId: 67890,
|
|
342
|
+
accountLogin: "testuser",
|
|
343
|
+
accountType: "User",
|
|
344
|
+
repositories: [],
|
|
345
|
+
permissions: {},
|
|
346
|
+
events: [],
|
|
347
|
+
createdAt: new Date(),
|
|
348
|
+
updatedAt: new Date(),
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const secret = "test-webhook-secret";
|
|
352
|
+
const payload = JSON.stringify({
|
|
353
|
+
action: "added",
|
|
354
|
+
installation: { id: 12345 },
|
|
355
|
+
repositories_added: [
|
|
356
|
+
{
|
|
357
|
+
id: 2,
|
|
358
|
+
name: "new-repo",
|
|
359
|
+
full_name: "testuser/new-repo",
|
|
360
|
+
private: true,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
366
|
+
hmac.update(payload);
|
|
367
|
+
const signature = `sha256=${hmac.digest("hex")}`;
|
|
368
|
+
|
|
369
|
+
const response = await http.post("/github-app/webhook", payload, {
|
|
370
|
+
headers: {
|
|
371
|
+
"x-github-event": "installation_repositories",
|
|
372
|
+
"x-github-delivery": "test-delivery-id",
|
|
373
|
+
"x-hub-signature-256": signature,
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(response.status).toBe(200);
|
|
378
|
+
|
|
379
|
+
// Verify webhook event was logged
|
|
380
|
+
const events = await app.ctx.repos.githubWebhookEventRepo.findByInstallationId(12345);
|
|
381
|
+
expect(events.length).toBeGreaterThan(0);
|
|
382
|
+
expect(events[0].event).toBe("installation_repositories");
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("Error Handling: GitHub API Failure Scenarios", () => {
|
|
387
|
+
it("should handle GitHub API failures gracefully during installation", async () => {
|
|
388
|
+
const { state } = await app.ctx.plugins.githubApp.initiateInstallation({
|
|
389
|
+
userId: "test-user",
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Mock GitHub API to fail
|
|
393
|
+
global.fetch = jasmine.createSpy("fetch").and.returnValue(
|
|
394
|
+
Promise.resolve({
|
|
395
|
+
ok: false,
|
|
396
|
+
status: 500,
|
|
397
|
+
statusText: "Internal Server Error",
|
|
398
|
+
text: () => Promise.resolve("Internal Server Error"),
|
|
399
|
+
} as any)
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const callbackResponse = await http.get("/github-app/callback", {
|
|
403
|
+
qs: {
|
|
404
|
+
installation_id: "12345",
|
|
405
|
+
setup_action: "install",
|
|
406
|
+
state: state!,
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Should handle error gracefully
|
|
411
|
+
expect(callbackResponse.status).toBe(500);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("should handle network errors when getting installation token", async () => {
|
|
415
|
+
await app.ctx.repos.githubInstallationRepo.create({
|
|
416
|
+
userId: "test-user",
|
|
417
|
+
installationId: 12345,
|
|
418
|
+
accountId: 67890,
|
|
419
|
+
accountLogin: "testuser",
|
|
420
|
+
accountType: "User",
|
|
421
|
+
repositories: [],
|
|
422
|
+
permissions: {},
|
|
423
|
+
events: [],
|
|
424
|
+
createdAt: new Date(),
|
|
425
|
+
updatedAt: new Date(),
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Mock network failure
|
|
429
|
+
global.fetch = jasmine.createSpy("fetch").and.returnValue(Promise.reject(new Error("Network error")));
|
|
430
|
+
|
|
431
|
+
const client = await app.ctx.plugins.githubApp.getClient(12345);
|
|
432
|
+
|
|
433
|
+
await expectAsync(client.getRepositories()).toBeRejected();
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe("Feature: Repository Access Verification", () => {
|
|
438
|
+
it("should verify user has access to repository", async () => {
|
|
439
|
+
await app.ctx.repos.githubInstallationRepo.create({
|
|
440
|
+
userId: "test-user",
|
|
441
|
+
installationId: 12345,
|
|
442
|
+
accountId: 67890,
|
|
443
|
+
accountLogin: "testuser",
|
|
444
|
+
accountType: "User",
|
|
445
|
+
repositories: [
|
|
446
|
+
{
|
|
447
|
+
id: 1,
|
|
448
|
+
name: "test-repo",
|
|
449
|
+
fullName: "testuser/test-repo",
|
|
450
|
+
private: false,
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
permissions: {},
|
|
454
|
+
events: [],
|
|
455
|
+
createdAt: new Date(),
|
|
456
|
+
updatedAt: new Date(),
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const hasAccess = await app.ctx.plugins.githubApp.hasRepositoryAccess("test-user", "testuser", "test-repo");
|
|
460
|
+
|
|
461
|
+
expect(hasAccess).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("should deny access to repository user doesn't have", async () => {
|
|
465
|
+
await app.ctx.repos.githubInstallationRepo.create({
|
|
466
|
+
userId: "test-user",
|
|
467
|
+
installationId: 12345,
|
|
468
|
+
accountId: 67890,
|
|
469
|
+
accountLogin: "testuser",
|
|
470
|
+
accountType: "User",
|
|
471
|
+
repositories: [],
|
|
472
|
+
permissions: {},
|
|
473
|
+
events: [],
|
|
474
|
+
createdAt: new Date(),
|
|
475
|
+
updatedAt: new Date(),
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const hasAccess = await app.ctx.plugins.githubApp.hasRepositoryAccess("test-user", "otheruser", "other-repo");
|
|
479
|
+
|
|
480
|
+
expect(hasAccess).toBe(false);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
});
|