@shipfox/api-integration-gitea 2.0.0
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/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/api/client.d.ts +58 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +225 -0
- package/dist/api/client.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/core/connect.d.ts +18 -0
- package/dist/core/connect.d.ts.map +1 -0
- package/dist/core/connect.js +32 -0
- package/dist/core/connect.js.map +1 -0
- package/dist/core/connection-url.d.ts +2 -0
- package/dist/core/connection-url.d.ts.map +1 -0
- package/dist/core/connection-url.js +8 -0
- package/dist/core/connection-url.js.map +1 -0
- package/dist/core/errors.d.ts +10 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +17 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/source-control.d.ts +14 -0
- package/dist/core/source-control.d.ts.map +1 -0
- package/dist/core/source-control.js +177 -0
- package/dist/core/source-control.js.map +1 -0
- package/dist/core/webhook.d.ts +38 -0
- package/dist/core/webhook.d.ts.map +1 -0
- package/dist/core/webhook.js +162 -0
- package/dist/core/webhook.js.map +1 -0
- package/dist/db/connections.d.ts +19 -0
- package/dist/db/connections.d.ts.map +1 -0
- package/dist/db/connections.js +42 -0
- package/dist/db/connections.js.map +1 -0
- package/dist/db/db.d.ts +191 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +18 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +5 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/connections.d.ts +97 -0
- package/dist/db/schema/connections.d.ts.map +1 -0
- package/dist/db/schema/connections.js +28 -0
- package/dist/db/schema/connections.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation/dto/integrations.d.ts +17 -0
- package/dist/presentation/dto/integrations.d.ts.map +1 -0
- package/dist/presentation/dto/integrations.js +11 -0
- package/dist/presentation/dto/integrations.js.map +1 -0
- package/dist/presentation/routes/connections.d.ts +13 -0
- package/dist/presentation/routes/connections.d.ts.map +1 -0
- package/dist/presentation/routes/connections.js +47 -0
- package/dist/presentation/routes/connections.js.map +1 -0
- package/dist/presentation/routes/errors.d.ts +2 -0
- package/dist/presentation/routes/errors.d.ts.map +1 -0
- package/dist/presentation/routes/errors.js +36 -0
- package/dist/presentation/routes/errors.js.map +1 -0
- package/dist/presentation/routes/webhooks.d.ts +11 -0
- package/dist/presentation/routes/webhooks.d.ts.map +1 -0
- package/dist/presentation/routes/webhooks.js +109 -0
- package/dist/presentation/routes/webhooks.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +10 -0
- package/drizzle/meta/0000_snapshot.json +96 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +66 -0
- package/src/api/client.test.ts +367 -0
- package/src/api/client.ts +364 -0
- package/src/config.ts +42 -0
- package/src/connection-external-url.test.ts +36 -0
- package/src/core/connect.test.ts +149 -0
- package/src/core/connect.ts +52 -0
- package/src/core/connection-url.ts +8 -0
- package/src/core/errors.ts +17 -0
- package/src/core/source-control-clone-url.test.ts +83 -0
- package/src/core/source-control.test.ts +349 -0
- package/src/core/source-control.ts +231 -0
- package/src/core/webhook.test.ts +396 -0
- package/src/core/webhook.ts +203 -0
- package/src/db/connections.test.ts +69 -0
- package/src/db/connections.ts +80 -0
- package/src/db/db.ts +18 -0
- package/src/db/migrations.ts +4 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/connections.ts +32 -0
- package/src/index.test.ts +23 -0
- package/src/index.ts +83 -0
- package/src/presentation/dto/integrations.ts +14 -0
- package/src/presentation/routes/connections.test.ts +200 -0
- package/src/presentation/routes/connections.ts +63 -0
- package/src/presentation/routes/errors.ts +35 -0
- package/src/presentation/routes/webhooks.test.ts +188 -0
- package/src/presentation/routes/webhooks.ts +111 -0
- package/test/env.ts +11 -0
- package/test/factories/gitea-connection.ts +19 -0
- package/test/factories/index.ts +1 -0
- package/test/fixtures/gitea-webhook.ts +237 -0
- package/test/globalSetup.ts +14 -0
- package/test/index.ts +6 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +9 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {createHmac, randomUUID} from 'node:crypto';
|
|
2
|
+
import {closeApp, createApp} from '@shipfox/node-fastify';
|
|
3
|
+
import type {FastifyInstance} from 'fastify';
|
|
4
|
+
import {giteaPushPayload} from '#test/index.js';
|
|
5
|
+
import {createGiteaWebhookRoutes} from './webhooks.js';
|
|
6
|
+
|
|
7
|
+
// Must match GITEA_WEBHOOK_SECRET in test/env.ts.
|
|
8
|
+
const WEBHOOK_SECRET = 'test-webhook-secret';
|
|
9
|
+
|
|
10
|
+
interface TestApp {
|
|
11
|
+
app: FastifyInstance;
|
|
12
|
+
publishSourcePush: ReturnType<typeof vi.fn>;
|
|
13
|
+
recordDeliveryOnly: ReturnType<typeof vi.fn>;
|
|
14
|
+
getIntegrationConnectionById: ReturnType<typeof vi.fn>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function createTestApp(): Promise<TestApp> {
|
|
18
|
+
const publishSourcePush = vi.fn(() => Promise.resolve({published: true}));
|
|
19
|
+
const recordDeliveryOnly = vi.fn(() => Promise.resolve());
|
|
20
|
+
const getIntegrationConnectionById = vi.fn();
|
|
21
|
+
const routes = createGiteaWebhookRoutes({
|
|
22
|
+
coreDb: () =>
|
|
23
|
+
({
|
|
24
|
+
transaction: (fn: (tx: unknown) => Promise<unknown>) => fn({}),
|
|
25
|
+
}) as never,
|
|
26
|
+
publishSourcePush,
|
|
27
|
+
recordDeliveryOnly,
|
|
28
|
+
getIntegrationConnectionById,
|
|
29
|
+
});
|
|
30
|
+
const app = await createApp({routes: [routes], swagger: false});
|
|
31
|
+
await app.ready();
|
|
32
|
+
return {app, publishSourcePush, recordDeliveryOnly, getIntegrationConnectionById};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function signedHeaders(rawBody: string, event: string, deliveryId: string) {
|
|
36
|
+
const signature = createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('hex');
|
|
37
|
+
return {
|
|
38
|
+
'content-type': 'application/json',
|
|
39
|
+
'x-gitea-signature': signature,
|
|
40
|
+
'x-gitea-event': event,
|
|
41
|
+
'x-gitea-delivery': deliveryId,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('Gitea webhook route', () => {
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
await closeApp();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(async () => {
|
|
51
|
+
await closeApp();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('accepts a signed non-push event and records the delivery', async () => {
|
|
55
|
+
const {app, publishSourcePush, recordDeliveryOnly} = await createTestApp();
|
|
56
|
+
const deliveryId = randomUUID();
|
|
57
|
+
const body = JSON.stringify({hello: 'world'});
|
|
58
|
+
|
|
59
|
+
const res = await app.inject({
|
|
60
|
+
method: 'POST',
|
|
61
|
+
url: '/webhooks/integrations/gitea',
|
|
62
|
+
headers: signedHeaders(body, 'create', deliveryId),
|
|
63
|
+
payload: body,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(res.statusCode).toBe(204);
|
|
67
|
+
expect(publishSourcePush).not.toHaveBeenCalled();
|
|
68
|
+
expect(recordDeliveryOnly).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(recordDeliveryOnly.mock.calls[0]?.[0]).toMatchObject({provider: 'gitea', deliveryId});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('rejects an invalid signature with 401 and persists nothing', async () => {
|
|
73
|
+
const {app, publishSourcePush, recordDeliveryOnly} = await createTestApp();
|
|
74
|
+
const body = JSON.stringify(
|
|
75
|
+
giteaPushPayload({
|
|
76
|
+
owner: 'shipfox',
|
|
77
|
+
repo: 'api',
|
|
78
|
+
ref: 'refs/heads/main',
|
|
79
|
+
defaultBranch: 'main',
|
|
80
|
+
sha: 'zzz',
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const res = await app.inject({
|
|
85
|
+
method: 'POST',
|
|
86
|
+
url: '/webhooks/integrations/gitea',
|
|
87
|
+
headers: {
|
|
88
|
+
'content-type': 'application/json',
|
|
89
|
+
'x-gitea-signature': 'deadbeef',
|
|
90
|
+
'x-gitea-event': 'push',
|
|
91
|
+
'x-gitea-delivery': randomUUID(),
|
|
92
|
+
},
|
|
93
|
+
payload: body,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(res.statusCode).toBe(401);
|
|
97
|
+
expect(publishSourcePush).not.toHaveBeenCalled();
|
|
98
|
+
expect(recordDeliveryOnly).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('rejects a request missing the delivery header with 400', async () => {
|
|
102
|
+
const {app} = await createTestApp();
|
|
103
|
+
const body = JSON.stringify({hello: 'world'});
|
|
104
|
+
|
|
105
|
+
const res = await app.inject({
|
|
106
|
+
method: 'POST',
|
|
107
|
+
url: '/webhooks/integrations/gitea',
|
|
108
|
+
headers: {
|
|
109
|
+
'content-type': 'application/json',
|
|
110
|
+
'x-gitea-signature': createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex'),
|
|
111
|
+
'x-gitea-event': 'push',
|
|
112
|
+
},
|
|
113
|
+
payload: body,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(res.statusCode).toBe(400);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('rejects a request missing the signature header with 401', async () => {
|
|
120
|
+
const {app} = await createTestApp();
|
|
121
|
+
const body = JSON.stringify({hello: 'world'});
|
|
122
|
+
|
|
123
|
+
const res = await app.inject({
|
|
124
|
+
method: 'POST',
|
|
125
|
+
url: '/webhooks/integrations/gitea',
|
|
126
|
+
headers: {
|
|
127
|
+
'content-type': 'application/json',
|
|
128
|
+
'x-gitea-event': 'push',
|
|
129
|
+
'x-gitea-delivery': randomUUID(),
|
|
130
|
+
},
|
|
131
|
+
payload: body,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(res.statusCode).toBe(401);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('rejects a request missing the event header with 400', async () => {
|
|
138
|
+
const {app} = await createTestApp();
|
|
139
|
+
const body = JSON.stringify({hello: 'world'});
|
|
140
|
+
|
|
141
|
+
const res = await app.inject({
|
|
142
|
+
method: 'POST',
|
|
143
|
+
url: '/webhooks/integrations/gitea',
|
|
144
|
+
headers: {
|
|
145
|
+
'content-type': 'application/json',
|
|
146
|
+
'x-gitea-signature': createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex'),
|
|
147
|
+
'x-gitea-delivery': randomUUID(),
|
|
148
|
+
},
|
|
149
|
+
payload: body,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(res.statusCode).toBe(400);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('rejects malformed JSON after a valid signature with 400', async () => {
|
|
156
|
+
const {app, publishSourcePush, recordDeliveryOnly} = await createTestApp();
|
|
157
|
+
const body = '{not valid json';
|
|
158
|
+
|
|
159
|
+
const res = await app.inject({
|
|
160
|
+
method: 'POST',
|
|
161
|
+
url: '/webhooks/integrations/gitea',
|
|
162
|
+
headers: signedHeaders(body, 'push', randomUUID()),
|
|
163
|
+
payload: body,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(res.statusCode).toBe(400);
|
|
167
|
+
expect(res.json()).toMatchObject({error: 'malformed JSON'});
|
|
168
|
+
expect(publishSourcePush).not.toHaveBeenCalled();
|
|
169
|
+
expect(recordDeliveryOnly).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('rejects push payloads that fail schema validation with 400', async () => {
|
|
173
|
+
const {app, publishSourcePush, recordDeliveryOnly} = await createTestApp();
|
|
174
|
+
const body = JSON.stringify({ref: 'refs/heads/main'});
|
|
175
|
+
|
|
176
|
+
const res = await app.inject({
|
|
177
|
+
method: 'POST',
|
|
178
|
+
url: '/webhooks/integrations/gitea',
|
|
179
|
+
headers: signedHeaders(body, 'push', randomUUID()),
|
|
180
|
+
payload: body,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(res.statusCode).toBe(400);
|
|
184
|
+
expect(res.json()).toMatchObject({error: 'malformed push payload'});
|
|
185
|
+
expect(publishSourcePush).not.toHaveBeenCalled();
|
|
186
|
+
expect(recordDeliveryOnly).not.toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {Buffer} from 'node:buffer';
|
|
2
|
+
import type {
|
|
3
|
+
GetIntegrationConnectionByIdFn,
|
|
4
|
+
PublishSourcePushFn,
|
|
5
|
+
RecordDeliveryOnlyFn,
|
|
6
|
+
} from '@shipfox/api-integration-core-dto';
|
|
7
|
+
import {
|
|
8
|
+
defineRoute,
|
|
9
|
+
type RouteGroup,
|
|
10
|
+
rawBodyPlugin,
|
|
11
|
+
verifyHexHmacSignature,
|
|
12
|
+
WEBHOOK_BODY_LIMIT,
|
|
13
|
+
} from '@shipfox/node-fastify';
|
|
14
|
+
import {logger} from '@shipfox/node-opentelemetry';
|
|
15
|
+
import type {NodePgDatabase} from 'drizzle-orm/node-postgres';
|
|
16
|
+
import {config} from '#config.js';
|
|
17
|
+
import {
|
|
18
|
+
GiteaWebhookMalformedJsonError,
|
|
19
|
+
GiteaWebhookMalformedPushPayloadError,
|
|
20
|
+
handleGiteaWebhook,
|
|
21
|
+
} from '#core/webhook.js';
|
|
22
|
+
|
|
23
|
+
const SIGNATURE_HEADER = 'x-gitea-signature';
|
|
24
|
+
const EVENT_HEADER = 'x-gitea-event';
|
|
25
|
+
const DELIVERY_HEADER = 'x-gitea-delivery';
|
|
26
|
+
|
|
27
|
+
export interface CreateGiteaWebhookRoutesOptions {
|
|
28
|
+
coreDb: () => NodePgDatabase<Record<string, unknown>>;
|
|
29
|
+
publishSourcePush: PublishSourcePushFn;
|
|
30
|
+
recordDeliveryOnly: RecordDeliveryOnlyFn;
|
|
31
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createGiteaWebhookRoutes(options: CreateGiteaWebhookRoutesOptions): RouteGroup {
|
|
35
|
+
const pushRoute = defineRoute({
|
|
36
|
+
method: 'POST',
|
|
37
|
+
path: '/',
|
|
38
|
+
auth: [],
|
|
39
|
+
description: 'Gitea org webhook receiver.',
|
|
40
|
+
options: {bodyLimit: WEBHOOK_BODY_LIMIT},
|
|
41
|
+
handler: async (request, reply) => {
|
|
42
|
+
const deliveryId = request.headers[DELIVERY_HEADER];
|
|
43
|
+
const signature = request.headers[SIGNATURE_HEADER];
|
|
44
|
+
const event = request.headers[EVENT_HEADER];
|
|
45
|
+
|
|
46
|
+
if (typeof deliveryId !== 'string' || !deliveryId) {
|
|
47
|
+
reply.code(400);
|
|
48
|
+
return {error: 'missing X-Gitea-Delivery header'};
|
|
49
|
+
}
|
|
50
|
+
if (typeof signature !== 'string' || !signature) {
|
|
51
|
+
reply.code(401);
|
|
52
|
+
return {error: 'missing X-Gitea-Signature header'};
|
|
53
|
+
}
|
|
54
|
+
if (typeof event !== 'string' || !event) {
|
|
55
|
+
reply.code(400);
|
|
56
|
+
return {error: 'missing X-Gitea-Event header'};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const body = request.body;
|
|
60
|
+
if (!Buffer.isBuffer(body)) {
|
|
61
|
+
reply.code(400);
|
|
62
|
+
return {error: 'expected raw JSON body'};
|
|
63
|
+
}
|
|
64
|
+
const rawBody = body.toString('utf8');
|
|
65
|
+
|
|
66
|
+
if (!verifyHexHmacSignature({rawBody, signature, secret: config.GITEA_WEBHOOK_SECRET})) {
|
|
67
|
+
reply.code(401);
|
|
68
|
+
return {error: 'invalid signature'};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await options.coreDb().transaction(async (tx) => {
|
|
73
|
+
await handleGiteaWebhook({
|
|
74
|
+
tx,
|
|
75
|
+
deliveryId,
|
|
76
|
+
event,
|
|
77
|
+
rawBody,
|
|
78
|
+
publishSourcePush: options.publishSourcePush,
|
|
79
|
+
recordDeliveryOnly: options.recordDeliveryOnly,
|
|
80
|
+
getIntegrationConnectionById: options.getIntegrationConnectionById,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error instanceof GiteaWebhookMalformedJsonError) {
|
|
85
|
+
logger().warn({deliveryId, err: error}, 'gitea webhook payload JSON parse failed');
|
|
86
|
+
reply.code(400);
|
|
87
|
+
return {error: 'malformed JSON'};
|
|
88
|
+
}
|
|
89
|
+
if (error instanceof GiteaWebhookMalformedPushPayloadError) {
|
|
90
|
+
logger().warn(
|
|
91
|
+
{deliveryId, issues: error.issues},
|
|
92
|
+
'gitea webhook push payload failed schema validation',
|
|
93
|
+
);
|
|
94
|
+
reply.code(400);
|
|
95
|
+
return {error: 'malformed push payload'};
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
reply.code(204);
|
|
101
|
+
return null;
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
prefix: '/webhooks/integrations/gitea',
|
|
107
|
+
auth: [],
|
|
108
|
+
plugins: [rawBodyPlugin],
|
|
109
|
+
routes: [pushRoute],
|
|
110
|
+
};
|
|
111
|
+
}
|
package/test/env.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
process.env.POSTGRES_HOST ??= 'localhost';
|
|
2
|
+
process.env.POSTGRES_PORT ??= '5432';
|
|
3
|
+
process.env.POSTGRES_USERNAME ??= 'shipfox';
|
|
4
|
+
process.env.POSTGRES_PASSWORD ??= 'password';
|
|
5
|
+
process.env.POSTGRES_DATABASE = 'api_test';
|
|
6
|
+
process.env.POSTGRES_MAX_CONNECTIONS ??= '5';
|
|
7
|
+
process.env.TZ = 'UTC';
|
|
8
|
+
process.env.GITEA_BASE_URL = 'https://gitea.example.com';
|
|
9
|
+
process.env.GITEA_SERVICE_USERNAME = 'shipfox-bot';
|
|
10
|
+
process.env.GITEA_SERVICE_TOKEN = 'test-service-token';
|
|
11
|
+
process.env.GITEA_WEBHOOK_SECRET = 'test-webhook-secret';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {Factory} from 'fishery';
|
|
2
|
+
import {type GiteaConnection, upsertGiteaConnection} from '#db/connections.js';
|
|
3
|
+
|
|
4
|
+
export const giteaConnectionFactory = Factory.define<GiteaConnection>(({sequence, onCreate}) => {
|
|
5
|
+
onCreate((connection) =>
|
|
6
|
+
upsertGiteaConnection({
|
|
7
|
+
connectionId: connection.connectionId,
|
|
8
|
+
org: connection.org,
|
|
9
|
+
}),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
connectionId: crypto.randomUUID(),
|
|
15
|
+
org: `org-${sequence + 1}`,
|
|
16
|
+
createdAt: new Date(),
|
|
17
|
+
updatedAt: new Date(),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {giteaConnectionFactory} from './gitea-connection.js';
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Real Gitea 1.26.2 push payload fixture; keep extra fields to catch schema drift.
|
|
2
|
+
export const capturedGiteaPushPayload = {
|
|
3
|
+
ref: 'refs/heads/main',
|
|
4
|
+
before: '119a2bb6566c3ec17be9b1aaa8090055aa776034',
|
|
5
|
+
after: 'abd1bff3de1a1f5c375a3e6bd06324469141819e',
|
|
6
|
+
compare_url:
|
|
7
|
+
'http://localhost:3000/shipfox/api/compare/119a2bb6566c3ec17be9b1aaa8090055aa776034...abd1bff3de1a1f5c375a3e6bd06324469141819e',
|
|
8
|
+
commits: [
|
|
9
|
+
{
|
|
10
|
+
id: 'abd1bff3de1a1f5c375a3e6bd06324469141819e',
|
|
11
|
+
message: 'Seed test/handler.test.js\n',
|
|
12
|
+
url: 'http://localhost:3000/shipfox/api/commit/abd1bff3de1a1f5c375a3e6bd06324469141819e',
|
|
13
|
+
author: {
|
|
14
|
+
name: 'gitea-admin',
|
|
15
|
+
email: 'gitea-admin@shipfox.local',
|
|
16
|
+
username: 'gitea-admin',
|
|
17
|
+
},
|
|
18
|
+
committer: {
|
|
19
|
+
name: 'gitea-admin',
|
|
20
|
+
email: 'gitea-admin@shipfox.local',
|
|
21
|
+
username: 'gitea-admin',
|
|
22
|
+
},
|
|
23
|
+
verification: null,
|
|
24
|
+
timestamp: '2026-06-21T03:08:17Z',
|
|
25
|
+
added: ['test/handler.test.js'],
|
|
26
|
+
removed: [],
|
|
27
|
+
modified: [],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
total_commits: 1,
|
|
31
|
+
head_commit: {
|
|
32
|
+
id: 'abd1bff3de1a1f5c375a3e6bd06324469141819e',
|
|
33
|
+
message: 'Seed test/handler.test.js\n',
|
|
34
|
+
url: 'http://localhost:3000/shipfox/api/commit/abd1bff3de1a1f5c375a3e6bd06324469141819e',
|
|
35
|
+
author: {
|
|
36
|
+
name: 'gitea-admin',
|
|
37
|
+
email: 'gitea-admin@shipfox.local',
|
|
38
|
+
username: 'gitea-admin',
|
|
39
|
+
},
|
|
40
|
+
committer: {
|
|
41
|
+
name: 'gitea-admin',
|
|
42
|
+
email: 'gitea-admin@shipfox.local',
|
|
43
|
+
username: 'gitea-admin',
|
|
44
|
+
},
|
|
45
|
+
verification: null,
|
|
46
|
+
timestamp: '2026-06-21T03:08:17Z',
|
|
47
|
+
added: ['test/handler.test.js'],
|
|
48
|
+
removed: [],
|
|
49
|
+
modified: [],
|
|
50
|
+
},
|
|
51
|
+
repository: {
|
|
52
|
+
id: 1,
|
|
53
|
+
owner: {
|
|
54
|
+
id: 3,
|
|
55
|
+
login: 'shipfox',
|
|
56
|
+
login_name: '',
|
|
57
|
+
source_id: 0,
|
|
58
|
+
full_name: '',
|
|
59
|
+
email: '',
|
|
60
|
+
avatar_url: 'http://localhost:3000/avatars/d02dc28a1b62136b6e1d5dbc3700c59d',
|
|
61
|
+
html_url: 'http://localhost:3000/shipfox',
|
|
62
|
+
language: '',
|
|
63
|
+
is_admin: false,
|
|
64
|
+
last_login: '0001-01-01T00:00:00Z',
|
|
65
|
+
created: '2026-06-21T03:08:17Z',
|
|
66
|
+
restricted: false,
|
|
67
|
+
active: false,
|
|
68
|
+
prohibit_login: false,
|
|
69
|
+
location: '',
|
|
70
|
+
website: '',
|
|
71
|
+
description: '',
|
|
72
|
+
visibility: 'public',
|
|
73
|
+
followers_count: 0,
|
|
74
|
+
following_count: 0,
|
|
75
|
+
starred_repos_count: 0,
|
|
76
|
+
username: 'shipfox',
|
|
77
|
+
},
|
|
78
|
+
name: 'api',
|
|
79
|
+
full_name: 'shipfox/api',
|
|
80
|
+
description: '',
|
|
81
|
+
empty: false,
|
|
82
|
+
private: true,
|
|
83
|
+
fork: false,
|
|
84
|
+
template: false,
|
|
85
|
+
mirror: false,
|
|
86
|
+
size: 23,
|
|
87
|
+
language: '',
|
|
88
|
+
languages_url: 'http://localhost:3000/api/v1/repos/shipfox/api/languages',
|
|
89
|
+
html_url: 'http://localhost:3000/shipfox/api',
|
|
90
|
+
url: 'http://localhost:3000/api/v1/repos/shipfox/api',
|
|
91
|
+
link: '',
|
|
92
|
+
ssh_url: 'ssh://git@localhost:2222/shipfox/api.git',
|
|
93
|
+
clone_url: 'http://localhost:3000/shipfox/api.git',
|
|
94
|
+
original_url: '',
|
|
95
|
+
website: '',
|
|
96
|
+
stars_count: 0,
|
|
97
|
+
forks_count: 0,
|
|
98
|
+
watchers_count: 2,
|
|
99
|
+
branch_count: 1,
|
|
100
|
+
open_issues_count: 0,
|
|
101
|
+
open_pr_counter: 0,
|
|
102
|
+
release_counter: 0,
|
|
103
|
+
default_branch: 'main',
|
|
104
|
+
archived: false,
|
|
105
|
+
created_at: '2026-06-21T03:08:17Z',
|
|
106
|
+
updated_at: '2026-06-21T03:08:17Z',
|
|
107
|
+
archived_at: '1970-01-01T00:00:00Z',
|
|
108
|
+
permissions: {
|
|
109
|
+
admin: true,
|
|
110
|
+
push: true,
|
|
111
|
+
pull: true,
|
|
112
|
+
},
|
|
113
|
+
has_code: true,
|
|
114
|
+
has_issues: true,
|
|
115
|
+
internal_tracker: {
|
|
116
|
+
enable_time_tracker: true,
|
|
117
|
+
allow_only_contributors_to_track_time: true,
|
|
118
|
+
enable_issue_dependencies: true,
|
|
119
|
+
},
|
|
120
|
+
has_wiki: true,
|
|
121
|
+
has_pull_requests: true,
|
|
122
|
+
has_projects: true,
|
|
123
|
+
projects_mode: 'all',
|
|
124
|
+
has_releases: true,
|
|
125
|
+
has_packages: true,
|
|
126
|
+
has_actions: true,
|
|
127
|
+
ignore_whitespace_conflicts: false,
|
|
128
|
+
allow_merge_commits: true,
|
|
129
|
+
allow_rebase: true,
|
|
130
|
+
allow_rebase_explicit: true,
|
|
131
|
+
allow_squash_merge: true,
|
|
132
|
+
allow_fast_forward_only_merge: true,
|
|
133
|
+
allow_rebase_update: true,
|
|
134
|
+
allow_manual_merge: false,
|
|
135
|
+
autodetect_manual_merge: false,
|
|
136
|
+
default_delete_branch_after_merge: false,
|
|
137
|
+
default_merge_style: 'merge',
|
|
138
|
+
default_allow_maintainer_edit: true,
|
|
139
|
+
avatar_url: '',
|
|
140
|
+
internal: false,
|
|
141
|
+
mirror_interval: '',
|
|
142
|
+
object_format_name: 'sha1',
|
|
143
|
+
mirror_updated: '0001-01-01T00:00:00Z',
|
|
144
|
+
topics: [],
|
|
145
|
+
licenses: [],
|
|
146
|
+
},
|
|
147
|
+
pusher: {
|
|
148
|
+
id: 1,
|
|
149
|
+
login: 'gitea-admin',
|
|
150
|
+
login_name: '',
|
|
151
|
+
source_id: 0,
|
|
152
|
+
full_name: '',
|
|
153
|
+
email: '1+gitea-admin@noreply.localhost',
|
|
154
|
+
avatar_url: 'http://localhost:3000/avatars/857daf2f94a39d8007c7ac02f123c830',
|
|
155
|
+
html_url: 'http://localhost:3000/gitea-admin',
|
|
156
|
+
language: '',
|
|
157
|
+
is_admin: false,
|
|
158
|
+
last_login: '0001-01-01T00:00:00Z',
|
|
159
|
+
created: '2026-06-21T03:08:17Z',
|
|
160
|
+
restricted: false,
|
|
161
|
+
active: false,
|
|
162
|
+
prohibit_login: false,
|
|
163
|
+
location: '',
|
|
164
|
+
website: '',
|
|
165
|
+
description: '',
|
|
166
|
+
visibility: 'public',
|
|
167
|
+
followers_count: 0,
|
|
168
|
+
following_count: 0,
|
|
169
|
+
starred_repos_count: 0,
|
|
170
|
+
username: 'gitea-admin',
|
|
171
|
+
},
|
|
172
|
+
sender: {
|
|
173
|
+
id: 1,
|
|
174
|
+
login: 'gitea-admin',
|
|
175
|
+
login_name: '',
|
|
176
|
+
source_id: 0,
|
|
177
|
+
full_name: '',
|
|
178
|
+
email: '1+gitea-admin@noreply.localhost',
|
|
179
|
+
avatar_url: 'http://localhost:3000/avatars/857daf2f94a39d8007c7ac02f123c830',
|
|
180
|
+
html_url: 'http://localhost:3000/gitea-admin',
|
|
181
|
+
language: '',
|
|
182
|
+
is_admin: false,
|
|
183
|
+
last_login: '0001-01-01T00:00:00Z',
|
|
184
|
+
created: '2026-06-21T03:08:17Z',
|
|
185
|
+
restricted: false,
|
|
186
|
+
active: false,
|
|
187
|
+
prohibit_login: false,
|
|
188
|
+
location: '',
|
|
189
|
+
website: '',
|
|
190
|
+
description: '',
|
|
191
|
+
visibility: 'public',
|
|
192
|
+
followers_count: 0,
|
|
193
|
+
following_count: 0,
|
|
194
|
+
starred_repos_count: 0,
|
|
195
|
+
username: 'gitea-admin',
|
|
196
|
+
},
|
|
197
|
+
} as const;
|
|
198
|
+
|
|
199
|
+
export interface GiteaPushPayloadOptions {
|
|
200
|
+
owner: string;
|
|
201
|
+
repo: string;
|
|
202
|
+
ref: string;
|
|
203
|
+
defaultBranch: string;
|
|
204
|
+
sha: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function giteaPushPayload(options: GiteaPushPayloadOptions) {
|
|
208
|
+
const fullName = `${options.owner}/${options.repo}`;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
...capturedGiteaPushPayload,
|
|
212
|
+
ref: options.ref,
|
|
213
|
+
after: options.sha,
|
|
214
|
+
commits: capturedGiteaPushPayload.commits.map((commit) => ({
|
|
215
|
+
...commit,
|
|
216
|
+
id: options.sha,
|
|
217
|
+
url: `http://localhost:3000/${fullName}/commit/${options.sha}`,
|
|
218
|
+
})),
|
|
219
|
+
head_commit: {
|
|
220
|
+
...capturedGiteaPushPayload.head_commit,
|
|
221
|
+
id: options.sha,
|
|
222
|
+
url: `http://localhost:3000/${fullName}/commit/${options.sha}`,
|
|
223
|
+
},
|
|
224
|
+
repository: {
|
|
225
|
+
...capturedGiteaPushPayload.repository,
|
|
226
|
+
name: options.repo,
|
|
227
|
+
full_name: fullName,
|
|
228
|
+
default_branch: options.defaultBranch,
|
|
229
|
+
owner: {
|
|
230
|
+
...capturedGiteaPushPayload.repository.owner,
|
|
231
|
+
login: options.owner,
|
|
232
|
+
username: options.owner,
|
|
233
|
+
html_url: `http://localhost:3000/${options.owner}`,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import './env.js';
|
|
2
|
+
import {runMigrations} from '@shipfox/node-drizzle';
|
|
3
|
+
import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
|
|
4
|
+
import {closeDb, db} from '#db/db.js';
|
|
5
|
+
import {migrationsPath} from '#db/migrations.js';
|
|
6
|
+
|
|
7
|
+
export async function setup() {
|
|
8
|
+
createPostgresClient();
|
|
9
|
+
|
|
10
|
+
await runMigrations(db(), migrationsPath, '__drizzle_migrations_integrations_gitea');
|
|
11
|
+
|
|
12
|
+
closeDb();
|
|
13
|
+
await closePostgresClient();
|
|
14
|
+
}
|
package/test/index.ts
ADDED
package/test/setup.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import './env.js';
|
|
2
|
+
import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
|
|
3
|
+
import {afterAll, afterEach, beforeAll, vi} from '@shipfox/vitest/vi';
|
|
4
|
+
import {closeDb} from '#db/db.js';
|
|
5
|
+
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
createPostgresClient();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterAll(async () => {
|
|
15
|
+
closeDb();
|
|
16
|
+
await closePostgresClient();
|
|
17
|
+
});
|