feat(ext/sw): GiteaHost.putBlob with Git Data API fallback
Same shape as GitHubHost (commit dc660c4) — Gitea v1 has /api/v1/
prefix, otherwise the endpoint shapes are identical. 2 new tests;
total 5 git-host tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { GitHubHost } from '../github';
|
||||
import { GiteaHost } from '../gitea';
|
||||
import { BLOB_THRESHOLD_BYTES } from '../git-host';
|
||||
|
||||
const REPO = 'alee/test-vault';
|
||||
@@ -70,3 +71,55 @@ describe('GitHubHost.putBlob', () => {
|
||||
await expect(host.putBlob('attachments/big.bin', big, 'add big')).rejects.toThrow(/422|too big/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GiteaHost.putBlob', () => {
|
||||
beforeEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('uses Contents API when content is at threshold', async () => {
|
||||
const fetchMock = setupFetch();
|
||||
// First fetch: GET existing file → 404 (create path)
|
||||
fetchMock.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found' } as Response);
|
||||
// Second fetch: POST contents → 201 created
|
||||
fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({}) } as Response);
|
||||
|
||||
const host = new GiteaHost('https://git.example.com', REPO, TOKEN);
|
||||
const small = new Uint8Array(BLOB_THRESHOLD_BYTES); // exactly threshold
|
||||
await host.putBlob('attachments/abc.bin', small, 'add abc');
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(fetchMock.mock.calls[1][0]).toContain('/contents/');
|
||||
expect(fetchMock.mock.calls[1][1]?.method).toBe('POST');
|
||||
});
|
||||
|
||||
it('uses Git Data API fallback when content exceeds threshold', async () => {
|
||||
const fetchMock = setupFetch();
|
||||
// 1. POST /api/v1/.../git/blobs → blob SHA
|
||||
fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({ sha: 'blobsha111' }) } as Response);
|
||||
// 2. GET /api/v1/.../git/refs/heads/main → ref { object: { sha: 'commitsha' } }
|
||||
fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ object: { sha: 'commitsha111' } }) } as Response);
|
||||
// 3. GET /api/v1/.../git/commits/commitsha111 → commit { tree: { sha: 'treesha' } }
|
||||
fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ tree: { sha: 'treesha111' } }) } as Response);
|
||||
// 4. POST /api/v1/.../git/trees → new tree { sha: 'newtreesha' }
|
||||
fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({ sha: 'newtreesha111' }) } as Response);
|
||||
// 5. POST /api/v1/.../git/commits → new commit { sha: 'newcommitsha' }
|
||||
fetchMock.mockResolvedValueOnce({ ok: true, status: 201, json: async () => ({ sha: 'newcommitsha111' }) } as Response);
|
||||
// 6. PATCH /api/v1/.../git/refs/heads/main → updated
|
||||
fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({}) } as Response);
|
||||
|
||||
const host = new GiteaHost('https://git.example.com', REPO, TOKEN);
|
||||
const big = new Uint8Array(BLOB_THRESHOLD_BYTES + 1); // one byte over
|
||||
await host.putBlob('attachments/big.bin', big, 'add big');
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(6);
|
||||
// Gitea uses /api/v1/ prefix
|
||||
expect(fetchMock.mock.calls[0][0]).toContain('/api/v1/repos/');
|
||||
expect(fetchMock.mock.calls[0][0]).toContain('/git/blobs');
|
||||
expect(fetchMock.mock.calls[1][0]).toContain('/git/refs/heads/');
|
||||
expect(fetchMock.mock.calls[2][0]).toContain('/git/commits/');
|
||||
expect(fetchMock.mock.calls[3][0]).toContain('/git/trees');
|
||||
expect(fetchMock.mock.calls[4][0]).toContain('/git/commits');
|
||||
expect(fetchMock.mock.calls[5][1]?.method).toBe('PATCH');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user