@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.
Files changed (119) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +96 -0
  5. package/LICENSE +21 -0
  6. package/README.md +61 -0
  7. package/dist/api/client.d.ts +58 -0
  8. package/dist/api/client.d.ts.map +1 -0
  9. package/dist/api/client.js +225 -0
  10. package/dist/api/client.js.map +1 -0
  11. package/dist/config.d.ts +11 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +38 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/core/connect.d.ts +18 -0
  16. package/dist/core/connect.d.ts.map +1 -0
  17. package/dist/core/connect.js +32 -0
  18. package/dist/core/connect.js.map +1 -0
  19. package/dist/core/connection-url.d.ts +2 -0
  20. package/dist/core/connection-url.d.ts.map +1 -0
  21. package/dist/core/connection-url.js +8 -0
  22. package/dist/core/connection-url.js.map +1 -0
  23. package/dist/core/errors.d.ts +10 -0
  24. package/dist/core/errors.d.ts.map +1 -0
  25. package/dist/core/errors.js +17 -0
  26. package/dist/core/errors.js.map +1 -0
  27. package/dist/core/source-control.d.ts +14 -0
  28. package/dist/core/source-control.d.ts.map +1 -0
  29. package/dist/core/source-control.js +177 -0
  30. package/dist/core/source-control.js.map +1 -0
  31. package/dist/core/webhook.d.ts +38 -0
  32. package/dist/core/webhook.d.ts.map +1 -0
  33. package/dist/core/webhook.js +162 -0
  34. package/dist/core/webhook.js.map +1 -0
  35. package/dist/db/connections.d.ts +19 -0
  36. package/dist/db/connections.d.ts.map +1 -0
  37. package/dist/db/connections.js +42 -0
  38. package/dist/db/connections.js.map +1 -0
  39. package/dist/db/db.d.ts +191 -0
  40. package/dist/db/db.d.ts.map +1 -0
  41. package/dist/db/db.js +18 -0
  42. package/dist/db/db.js.map +1 -0
  43. package/dist/db/migrations.d.ts +2 -0
  44. package/dist/db/migrations.d.ts.map +1 -0
  45. package/dist/db/migrations.js +5 -0
  46. package/dist/db/migrations.js.map +1 -0
  47. package/dist/db/schema/common.d.ts +2 -0
  48. package/dist/db/schema/common.d.ts.map +1 -0
  49. package/dist/db/schema/common.js +4 -0
  50. package/dist/db/schema/common.js.map +1 -0
  51. package/dist/db/schema/connections.d.ts +97 -0
  52. package/dist/db/schema/connections.d.ts.map +1 -0
  53. package/dist/db/schema/connections.js +28 -0
  54. package/dist/db/schema/connections.js.map +1 -0
  55. package/dist/index.d.ts +39 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +42 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/presentation/dto/integrations.d.ts +17 -0
  60. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  61. package/dist/presentation/dto/integrations.js +11 -0
  62. package/dist/presentation/dto/integrations.js.map +1 -0
  63. package/dist/presentation/routes/connections.d.ts +13 -0
  64. package/dist/presentation/routes/connections.d.ts.map +1 -0
  65. package/dist/presentation/routes/connections.js +47 -0
  66. package/dist/presentation/routes/connections.js.map +1 -0
  67. package/dist/presentation/routes/errors.d.ts +2 -0
  68. package/dist/presentation/routes/errors.d.ts.map +1 -0
  69. package/dist/presentation/routes/errors.js +36 -0
  70. package/dist/presentation/routes/errors.js.map +1 -0
  71. package/dist/presentation/routes/webhooks.d.ts +11 -0
  72. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  73. package/dist/presentation/routes/webhooks.js +109 -0
  74. package/dist/presentation/routes/webhooks.js.map +1 -0
  75. package/dist/tsconfig.test.tsbuildinfo +1 -0
  76. package/drizzle/0000_initial.sql +10 -0
  77. package/drizzle/meta/0000_snapshot.json +96 -0
  78. package/drizzle/meta/_journal.json +13 -0
  79. package/drizzle.config.ts +7 -0
  80. package/package.json +66 -0
  81. package/src/api/client.test.ts +367 -0
  82. package/src/api/client.ts +364 -0
  83. package/src/config.ts +42 -0
  84. package/src/connection-external-url.test.ts +36 -0
  85. package/src/core/connect.test.ts +149 -0
  86. package/src/core/connect.ts +52 -0
  87. package/src/core/connection-url.ts +8 -0
  88. package/src/core/errors.ts +17 -0
  89. package/src/core/source-control-clone-url.test.ts +83 -0
  90. package/src/core/source-control.test.ts +349 -0
  91. package/src/core/source-control.ts +231 -0
  92. package/src/core/webhook.test.ts +396 -0
  93. package/src/core/webhook.ts +203 -0
  94. package/src/db/connections.test.ts +69 -0
  95. package/src/db/connections.ts +80 -0
  96. package/src/db/db.ts +18 -0
  97. package/src/db/migrations.ts +4 -0
  98. package/src/db/schema/common.ts +3 -0
  99. package/src/db/schema/connections.ts +32 -0
  100. package/src/index.test.ts +23 -0
  101. package/src/index.ts +83 -0
  102. package/src/presentation/dto/integrations.ts +14 -0
  103. package/src/presentation/routes/connections.test.ts +200 -0
  104. package/src/presentation/routes/connections.ts +63 -0
  105. package/src/presentation/routes/errors.ts +35 -0
  106. package/src/presentation/routes/webhooks.test.ts +188 -0
  107. package/src/presentation/routes/webhooks.ts +111 -0
  108. package/test/env.ts +11 -0
  109. package/test/factories/gitea-connection.ts +19 -0
  110. package/test/factories/index.ts +1 -0
  111. package/test/fixtures/gitea-webhook.ts +237 -0
  112. package/test/globalSetup.ts +14 -0
  113. package/test/index.ts +6 -0
  114. package/test/setup.ts +17 -0
  115. package/tsconfig.build.json +9 -0
  116. package/tsconfig.build.tsbuildinfo +1 -0
  117. package/tsconfig.json +3 -0
  118. package/tsconfig.test.json +9 -0
  119. package/vitest.config.ts +12 -0
@@ -0,0 +1,10 @@
1
+ CREATE TABLE "integrations_gitea_connections" (
2
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
3
+ "connection_id" uuid NOT NULL,
4
+ "org" text NOT NULL,
5
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
6
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
7
+ );
8
+ --> statement-breakpoint
9
+ CREATE UNIQUE INDEX "integrations_gitea_connections_connection_unique" ON "integrations_gitea_connections" USING btree ("connection_id");--> statement-breakpoint
10
+ CREATE UNIQUE INDEX "integrations_gitea_connections_org_unique" ON "integrations_gitea_connections" USING btree ("org");
@@ -0,0 +1,96 @@
1
+ {
2
+ "id": "a2877d36-8a0b-406b-b82a-efc56a452a30",
3
+ "prevId": "00000000-0000-0000-0000-000000000000",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.integrations_gitea_connections": {
8
+ "name": "integrations_gitea_connections",
9
+ "schema": "",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "uuidv7()"
17
+ },
18
+ "connection_id": {
19
+ "name": "connection_id",
20
+ "type": "uuid",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "org": {
25
+ "name": "org",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": true
29
+ },
30
+ "created_at": {
31
+ "name": "created_at",
32
+ "type": "timestamp with time zone",
33
+ "primaryKey": false,
34
+ "notNull": true,
35
+ "default": "now()"
36
+ },
37
+ "updated_at": {
38
+ "name": "updated_at",
39
+ "type": "timestamp with time zone",
40
+ "primaryKey": false,
41
+ "notNull": true,
42
+ "default": "now()"
43
+ }
44
+ },
45
+ "indexes": {
46
+ "integrations_gitea_connections_connection_unique": {
47
+ "name": "integrations_gitea_connections_connection_unique",
48
+ "columns": [
49
+ {
50
+ "expression": "connection_id",
51
+ "isExpression": false,
52
+ "asc": true,
53
+ "nulls": "last"
54
+ }
55
+ ],
56
+ "isUnique": true,
57
+ "concurrently": false,
58
+ "method": "btree",
59
+ "with": {}
60
+ },
61
+ "integrations_gitea_connections_org_unique": {
62
+ "name": "integrations_gitea_connections_org_unique",
63
+ "columns": [
64
+ {
65
+ "expression": "org",
66
+ "isExpression": false,
67
+ "asc": true,
68
+ "nulls": "last"
69
+ }
70
+ ],
71
+ "isUnique": true,
72
+ "concurrently": false,
73
+ "method": "btree",
74
+ "with": {}
75
+ }
76
+ },
77
+ "foreignKeys": {},
78
+ "compositePrimaryKeys": {},
79
+ "uniqueConstraints": {},
80
+ "policies": {},
81
+ "checkConstraints": {},
82
+ "isRLSEnabled": false
83
+ }
84
+ },
85
+ "enums": {},
86
+ "schemas": {},
87
+ "sequences": {},
88
+ "roles": {},
89
+ "policies": {},
90
+ "views": {},
91
+ "_meta": {
92
+ "columns": {},
93
+ "schemas": {},
94
+ "tables": {}
95
+ }
96
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1781978699352,
9
+ "tag": "0000_initial",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,7 @@
1
+ import {defineConfig} from 'drizzle-kit';
2
+
3
+ export default defineConfig({
4
+ schema: './src/db/schema/*.ts',
5
+ out: './drizzle',
6
+ dialect: 'postgresql',
7
+ });
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@shipfox/api-integration-gitea",
3
+ "license": "MIT",
4
+ "version": "2.0.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
8
+ "directory": "libs/api/integration/gitea"
9
+ },
10
+ "private": false,
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "imports": {
15
+ "#test/*": "./test/*",
16
+ "#*": {
17
+ "workspace-source": "./src/*",
18
+ "development": "./src/*",
19
+ "default": "./dist/*"
20
+ }
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "development": {
25
+ "types": "./src/index.ts",
26
+ "default": "./src/index.ts"
27
+ },
28
+ "default": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "drizzle-orm": "^0.45.2",
36
+ "@shipfox/api-auth-context": "2.0.0",
37
+ "@shipfox/api-integration-gitea-dto": "2.0.0",
38
+ "@shipfox/api-integration-core-dto": "2.0.0",
39
+ "@shipfox/config": "1.2.1",
40
+ "@shipfox/node-drizzle": "0.2.1",
41
+ "@shipfox/node-opentelemetry": "0.5.0",
42
+ "@shipfox/node-fastify": "0.2.1",
43
+ "@shipfox/node-postgres": "0.4.1"
44
+ },
45
+ "devDependencies": {
46
+ "@types/pg": "^8.15.5",
47
+ "drizzle-kit": "^0.31.10",
48
+ "fastify": "^5.3.3",
49
+ "fastify-type-provider-zod": "^6.0.0",
50
+ "fishery": "^2.2.2",
51
+ "@shipfox/biome": "1.8.1",
52
+ "@shipfox/swc": "1.2.5",
53
+ "@shipfox/ts-config": "1.3.8",
54
+ "@shipfox/vitest": "1.2.2",
55
+ "@shipfox/typescript": "1.1.6"
56
+ },
57
+ "scripts": {
58
+ "build": "shipfox-swc",
59
+ "check": "shipfox-biome-check",
60
+ "check:fix": "shipfox-biome-check --write",
61
+ "test": "shipfox-vitest-run",
62
+ "test:watch": "shipfox-vitest-watch",
63
+ "type": "shipfox-tsc-check",
64
+ "type:emit": "shipfox-tsc-emit"
65
+ }
66
+ }
@@ -0,0 +1,367 @@
1
+ import {Buffer} from 'node:buffer';
2
+ import {GiteaIntegrationProviderError} from '#core/errors.js';
3
+ import {createGiteaApiClient} from './client.js';
4
+
5
+ const REPOSITORY = {
6
+ id: 7,
7
+ owner: {login: 'shipfox'},
8
+ name: 'platform',
9
+ full_name: 'shipfox/platform',
10
+ default_branch: 'main',
11
+ private: true,
12
+ clone_url: 'https://gitea.example.com/shipfox/platform.git',
13
+ html_url: 'https://gitea.example.com/shipfox/platform',
14
+ };
15
+
16
+ function jsonResponse(
17
+ body: unknown,
18
+ init: {status?: number; headers?: Record<string, string>} = {},
19
+ ): Response {
20
+ return new Response(JSON.stringify(body), {
21
+ status: init.status ?? 200,
22
+ headers: {'content-type': 'application/json', ...init.headers},
23
+ });
24
+ }
25
+
26
+ describe('HttpGiteaApiClient', () => {
27
+ let fetchMock: ReturnType<typeof vi.fn>;
28
+
29
+ beforeEach(() => {
30
+ fetchMock = vi.fn();
31
+ vi.stubGlobal('fetch', fetchMock);
32
+ });
33
+
34
+ afterEach(() => {
35
+ vi.unstubAllGlobals();
36
+ });
37
+
38
+ function requestedUrl(call = 0): URL {
39
+ return new URL(String(fetchMock.mock.calls[call]?.[0]));
40
+ }
41
+
42
+ function requestInit(call = 0): RequestInit {
43
+ return fetchMock.mock.calls[call]?.[1] as RequestInit;
44
+ }
45
+
46
+ it('lists org repositories with basic auth and a page cursor from the link header', async () => {
47
+ fetchMock.mockResolvedValue(
48
+ jsonResponse([REPOSITORY], {
49
+ headers: {
50
+ link: '<https://gitea.example.com/api/v1/orgs/shipfox/repos?page=3&limit=50>; rel="next"',
51
+ },
52
+ }),
53
+ );
54
+ const client = createGiteaApiClient();
55
+
56
+ const result = await client.listOrgRepositories({org: 'shipfox', limit: 50, cursor: '2'});
57
+
58
+ expect(result.repositories[0]?.fullName).toBe('shipfox/platform');
59
+ expect(result.nextCursor).toBe('3');
60
+ const url = requestedUrl();
61
+ expect(url.pathname).toBe('/api/v1/orgs/shipfox/repos');
62
+ expect(url.searchParams.get('page')).toBe('2');
63
+ expect(url.searchParams.get('limit')).toBe('50');
64
+ expect((requestInit().headers as Record<string, string>).authorization).toBe(
65
+ `Basic ${Buffer.from('shipfox-bot:test-service-token').toString('base64')}`,
66
+ );
67
+ });
68
+
69
+ it('returns no next cursor when the link header has no next relation', async () => {
70
+ fetchMock.mockResolvedValue(jsonResponse([REPOSITORY]));
71
+ const client = createGiteaApiClient();
72
+
73
+ const result = await client.listOrgRepositories({org: 'shipfox', limit: 50});
74
+
75
+ expect(result.nextCursor).toBeNull();
76
+ });
77
+
78
+ it('gets a single repository', async () => {
79
+ fetchMock.mockResolvedValue(jsonResponse(REPOSITORY));
80
+ const client = createGiteaApiClient();
81
+
82
+ const result = await client.getRepository({owner: 'shipfox', repo: 'platform'});
83
+
84
+ expect(result).toEqual({
85
+ ownerLogin: 'shipfox',
86
+ name: 'platform',
87
+ fullName: 'shipfox/platform',
88
+ defaultBranch: 'main',
89
+ private: true,
90
+ cloneUrl: 'https://gitea.example.com/shipfox/platform.git',
91
+ htmlUrl: 'https://gitea.example.com/shipfox/platform',
92
+ });
93
+ expect(requestedUrl().pathname).toBe('/api/v1/repos/shipfox/platform');
94
+ });
95
+
96
+ it('resolves a ref to its head commit sha', async () => {
97
+ fetchMock.mockResolvedValue(jsonResponse([{sha: 'abc123'}, {sha: 'older'}]));
98
+ const client = createGiteaApiClient();
99
+
100
+ const sha = await client.resolveRef({owner: 'shipfox', repo: 'platform', ref: 'main'});
101
+
102
+ expect(sha).toBe('abc123');
103
+ const url = requestedUrl();
104
+ expect(url.pathname).toBe('/api/v1/repos/shipfox/platform/commits');
105
+ expect(url.searchParams.get('sha')).toBe('main');
106
+ expect(url.searchParams.get('limit')).toBe('1');
107
+ });
108
+
109
+ it('rejects a ref that resolves to no commit', async () => {
110
+ fetchMock.mockResolvedValue(jsonResponse([]));
111
+ const client = createGiteaApiClient();
112
+
113
+ const result = client.resolveRef({owner: 'shipfox', repo: 'platform', ref: 'missing'});
114
+
115
+ await expect(result).rejects.toMatchObject({reason: 'repository-not-found'});
116
+ });
117
+
118
+ it('lists the recursive tree, keeping blobs and dropping subtrees', async () => {
119
+ fetchMock.mockResolvedValue(
120
+ jsonResponse({
121
+ tree: [
122
+ {path: 'README.md', type: 'blob', size: 12},
123
+ {path: 'src', type: 'tree'},
124
+ {path: 'src/index.ts', type: 'blob', size: 34},
125
+ ],
126
+ truncated: false,
127
+ }),
128
+ );
129
+ const client = createGiteaApiClient();
130
+
131
+ const result = await client.listTree({owner: 'shipfox', repo: 'platform', sha: 'abc123'});
132
+
133
+ expect(result.blobs).toEqual([
134
+ {path: 'README.md', size: 12},
135
+ {path: 'src/index.ts', size: 34},
136
+ ]);
137
+ expect(result.truncated).toBe(false);
138
+ const url = requestedUrl();
139
+ expect(url.pathname).toBe('/api/v1/repos/shipfox/platform/git/trees/abc123');
140
+ expect(url.searchParams.get('recursive')).toBe('true');
141
+ });
142
+
143
+ it('reports a truncated tree', async () => {
144
+ fetchMock.mockResolvedValue(jsonResponse({tree: [], truncated: true}));
145
+ const client = createGiteaApiClient();
146
+
147
+ const result = await client.listTree({owner: 'shipfox', repo: 'platform', sha: 'abc123'});
148
+
149
+ expect(result.truncated).toBe(true);
150
+ });
151
+
152
+ it('fetches and base64-decodes file content', async () => {
153
+ const content = 'name: CI\n';
154
+ fetchMock.mockResolvedValue(
155
+ jsonResponse({
156
+ type: 'file',
157
+ encoding: 'base64',
158
+ path: '.shipfox/workflows/ci.yml',
159
+ size: content.length,
160
+ content: Buffer.from(content).toString('base64'),
161
+ }),
162
+ );
163
+ const client = createGiteaApiClient();
164
+
165
+ const result = await client.fetchFileContent({
166
+ owner: 'shipfox',
167
+ repo: 'platform',
168
+ path: '.shipfox/workflows/ci.yml',
169
+ ref: 'main',
170
+ });
171
+
172
+ expect(result.content).toBe(content);
173
+ const url = requestedUrl();
174
+ expect(url.pathname).toBe('/api/v1/repos/shipfox/platform/contents/.shipfox/workflows/ci.yml');
175
+ expect(url.searchParams.get('ref')).toBe('main');
176
+ });
177
+
178
+ it('rejects a content response that is a directory listing', async () => {
179
+ fetchMock.mockResolvedValue(jsonResponse([{type: 'file', path: 'a'}]));
180
+ const client = createGiteaApiClient();
181
+
182
+ const result = client.fetchFileContent({
183
+ owner: 'shipfox',
184
+ repo: 'platform',
185
+ path: 'src',
186
+ ref: 'main',
187
+ });
188
+
189
+ await expect(result).rejects.toMatchObject({reason: 'file-not-found'});
190
+ });
191
+
192
+ it('rejects file content larger than the supported limit before decoding', async () => {
193
+ fetchMock.mockResolvedValue(
194
+ jsonResponse({
195
+ type: 'file',
196
+ encoding: 'base64',
197
+ path: 'big.bin',
198
+ size: 1_000_001,
199
+ content: '',
200
+ }),
201
+ );
202
+ const client = createGiteaApiClient();
203
+
204
+ const result = client.fetchFileContent({
205
+ owner: 'shipfox',
206
+ repo: 'platform',
207
+ path: 'big.bin',
208
+ ref: 'main',
209
+ });
210
+
211
+ await expect(result).rejects.toMatchObject({reason: 'content-too-large'});
212
+ });
213
+
214
+ it.each([
215
+ [401, 'access-denied'],
216
+ [403, 'access-denied'],
217
+ [404, 'repository-not-found'],
218
+ [429, 'rate-limited'],
219
+ [500, 'provider-unavailable'],
220
+ [503, 'provider-unavailable'],
221
+ ])('maps HTTP %s to the %s reason', async (status, reason) => {
222
+ fetchMock.mockResolvedValue(jsonResponse({}, {status}));
223
+ const client = createGiteaApiClient();
224
+
225
+ const result = client.getRepository({owner: 'shipfox', repo: 'platform'});
226
+
227
+ await expect(result).rejects.toBeInstanceOf(GiteaIntegrationProviderError);
228
+ await expect(result).rejects.toMatchObject({reason});
229
+ });
230
+
231
+ it('carries retry-after seconds on a rate-limited response', async () => {
232
+ fetchMock.mockResolvedValue(jsonResponse({}, {status: 429, headers: {'retry-after': '42'}}));
233
+ const client = createGiteaApiClient();
234
+
235
+ const result = client.getRepository({owner: 'shipfox', repo: 'platform'});
236
+
237
+ await expect(result).rejects.toMatchObject({reason: 'rate-limited', retryAfterSeconds: 42});
238
+ });
239
+
240
+ it('treats an exhausted rate-limit budget on a 403 as rate-limited', async () => {
241
+ fetchMock.mockResolvedValue(
242
+ jsonResponse({}, {status: 403, headers: {'x-ratelimit-remaining': '0'}}),
243
+ );
244
+ const client = createGiteaApiClient();
245
+
246
+ const result = client.getRepository({owner: 'shipfox', repo: 'platform'});
247
+
248
+ await expect(result).rejects.toMatchObject({reason: 'rate-limited'});
249
+ });
250
+
251
+ it('maps a network failure to provider-unavailable', async () => {
252
+ fetchMock.mockRejectedValue(new TypeError('fetch failed'));
253
+ const client = createGiteaApiClient();
254
+
255
+ const result = client.getRepository({owner: 'shipfox', repo: 'platform'});
256
+
257
+ await expect(result).rejects.toMatchObject({reason: 'provider-unavailable'});
258
+ });
259
+
260
+ it('maps a malformed repository payload to malformed-provider-response', async () => {
261
+ fetchMock.mockResolvedValue(jsonResponse({name: 'platform'}));
262
+ const client = createGiteaApiClient();
263
+
264
+ const result = client.getRepository({owner: 'shipfox', repo: 'platform'});
265
+
266
+ await expect(result).rejects.toMatchObject({reason: 'malformed-provider-response'});
267
+ });
268
+
269
+ it('maps a file response without base64 content to malformed-provider-response', async () => {
270
+ fetchMock.mockResolvedValue(
271
+ jsonResponse({type: 'file', encoding: 'utf-8', path: 'a', size: 1, content: 'x'}),
272
+ );
273
+ const client = createGiteaApiClient();
274
+
275
+ const result = client.fetchFileContent({
276
+ owner: 'shipfox',
277
+ repo: 'platform',
278
+ path: 'a',
279
+ ref: 'main',
280
+ });
281
+
282
+ await expect(result).rejects.toMatchObject({reason: 'malformed-provider-response'});
283
+ });
284
+
285
+ it('maps a file response missing a numeric size to malformed-provider-response', async () => {
286
+ fetchMock.mockResolvedValue(
287
+ jsonResponse({type: 'file', encoding: 'base64', path: 'a', content: ''}),
288
+ );
289
+ const client = createGiteaApiClient();
290
+
291
+ const result = client.fetchFileContent({
292
+ owner: 'shipfox',
293
+ repo: 'platform',
294
+ path: 'a',
295
+ ref: 'main',
296
+ });
297
+
298
+ await expect(result).rejects.toMatchObject({reason: 'malformed-provider-response'});
299
+ });
300
+
301
+ it('maps a 404 on the contents endpoint to file-not-found', async () => {
302
+ fetchMock.mockResolvedValue(jsonResponse({}, {status: 404}));
303
+ const client = createGiteaApiClient();
304
+
305
+ const result = client.fetchFileContent({
306
+ owner: 'shipfox',
307
+ repo: 'platform',
308
+ path: 'missing.txt',
309
+ ref: 'main',
310
+ });
311
+
312
+ await expect(result).rejects.toMatchObject({reason: 'file-not-found'});
313
+ });
314
+
315
+ it('rejects a path with traversal segments before issuing a request', async () => {
316
+ const client = createGiteaApiClient();
317
+
318
+ const result = client.fetchFileContent({
319
+ owner: 'shipfox',
320
+ repo: 'platform',
321
+ path: '../../../../user/keys',
322
+ ref: 'main',
323
+ });
324
+
325
+ await expect(result).rejects.toMatchObject({reason: 'file-not-found'});
326
+ expect(fetchMock).not.toHaveBeenCalled();
327
+ });
328
+
329
+ it('maps a request timeout to the timeout reason', async () => {
330
+ fetchMock.mockRejectedValue(
331
+ Object.assign(new Error('The operation timed out'), {name: 'TimeoutError'}),
332
+ );
333
+ const client = createGiteaApiClient();
334
+
335
+ const result = client.getRepository({owner: 'shipfox', repo: 'platform'});
336
+
337
+ await expect(result).rejects.toMatchObject({reason: 'timeout'});
338
+ });
339
+
340
+ it('reports an existing organization as present', async () => {
341
+ fetchMock.mockResolvedValue(jsonResponse({username: 'shipfox'}));
342
+ const client = createGiteaApiClient();
343
+
344
+ const exists = await client.organizationExists({org: 'shipfox'});
345
+
346
+ expect(exists).toBe(true);
347
+ expect(requestedUrl().pathname).toBe('/api/v1/orgs/shipfox');
348
+ });
349
+
350
+ it('reports a missing organization as absent', async () => {
351
+ fetchMock.mockResolvedValue(jsonResponse({}, {status: 404}));
352
+ const client = createGiteaApiClient();
353
+
354
+ const exists = await client.organizationExists({org: 'ghost'});
355
+
356
+ expect(exists).toBe(false);
357
+ });
358
+
359
+ it('surfaces a non-404 error while checking an organization', async () => {
360
+ fetchMock.mockResolvedValue(jsonResponse({}, {status: 403}));
361
+ const client = createGiteaApiClient();
362
+
363
+ const result = client.organizationExists({org: 'shipfox'});
364
+
365
+ await expect(result).rejects.toMatchObject({reason: 'access-denied'});
366
+ });
367
+ });