import { describe, it, expect, vi, beforeEach } from 'vitest' import { GitHubForge, findForge, isForgeUrl } from '../plugin-forge' describe('GitHubForge', () => { let forge: GitHubForge beforeEach(() => { forge = new GitHubForge() }) describe('canHandle', () => { it('returns true for github.com URLs', () => { expect(forge.canHandle('https://github.com/owner/repo')).toBe(true) expect(forge.canHandle('https://www.github.com/owner/repo')).toBe(true) expect(forge.canHandle('http://github.com/owner/repo')).toBe(true) }) it('returns false for non-GitHub URLs', () => { expect(forge.canHandle('https://gitlab.com/owner/repo')).toBe(false) expect(forge.canHandle('https://example.com/plugin')).toBe(false) expect(forge.canHandle('not-a-url')).toBe(false) }) }) describe('parseRepoUrl', () => { it('parses simple repo URL', () => { const result = forge.parseRepoUrl('https://github.com/liamcain/obsidian-calendar-plugin') expect(result).toEqual({ owner: 'liamcain', repo: 'obsidian-calendar-plugin' }) }) it('parses URL with trailing slash', () => { const result = forge.parseRepoUrl('https://github.com/owner/repo/') expect(result).toEqual({ owner: 'owner', repo: 'repo' }) }) it('parses URL with subpaths', () => { const result = forge.parseRepoUrl('https://github.com/owner/repo/tree/main') expect(result).toEqual({ owner: 'owner', repo: 'repo' }) }) it('returns null for invalid URLs', () => { expect(forge.parseRepoUrl('https://github.com/')).toBe(null) expect(forge.parseRepoUrl('https://github.com/owner')).toBe(null) expect(forge.parseRepoUrl('not-a-url')).toBe(null) }) it('returns null for non-GitHub URLs', () => { expect(forge.parseRepoUrl('https://gitlab.com/owner/repo')).toBe(null) }) }) describe('fetchLatestRelease', () => { it('fetches latest non-prerelease with required assets', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => [ { tag_name: 'v1.6.0', prerelease: true, draft: false, assets: [ { name: 'manifest.json', browser_download_url: 'https://example.com/manifest.json' }, { name: 'main.js', browser_download_url: 'https://example.com/main.js' }, ], }, { tag_name: '1.5.10', prerelease: false, draft: false, assets: [ { name: 'manifest.json', browser_download_url: 'https://example.com/v1.5.10/manifest.json' }, { name: 'main.js', browser_download_url: 'https://example.com/v1.5.10/main.js' }, ], }, ], })) const release = await forge.fetchLatestRelease('liamcain', 'obsidian-calendar-plugin') expect(release.version).toBe('1.5.10') expect(release.tag).toBe('1.5.10') expect(release.prerelease).toBe(false) expect(release.assets.manifestUrl).toBe('https://example.com/v1.5.10/manifest.json') expect(release.assets.mainJsUrl).toBe('https://example.com/v1.5.10/main.js') }) it('strips leading v from version', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => [ { tag_name: 'v1.5.10', prerelease: false, draft: false, assets: [ { name: 'manifest.json', browser_download_url: 'https://example.com/manifest.json' }, { name: 'main.js', browser_download_url: 'https://example.com/main.js' }, ], }, ], })) const release = await forge.fetchLatestRelease('owner', 'repo') expect(release.version).toBe('1.5.10') expect(release.tag).toBe('v1.5.10') }) it('throws when no suitable release found', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => [ { tag_name: '1.0.0', prerelease: false, draft: false, assets: [ // Missing main.js { name: 'manifest.json', browser_download_url: 'https://example.com/manifest.json' }, ], }, ], })) await expect(forge.fetchLatestRelease('owner', 'repo')).rejects.toThrow( /no suitable release found/i ) }) it('throws when repository not found', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 404, statusText: 'Not Found', })) await expect(forge.fetchLatestRelease('owner', 'nonexistent')).rejects.toThrow( /repository not found/i ) }) it('skips draft releases', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => [ { tag_name: 'v2.0.0', prerelease: false, draft: true, // Draft! assets: [ { name: 'manifest.json', browser_download_url: 'https://example.com/v2/manifest.json' }, { name: 'main.js', browser_download_url: 'https://example.com/v2/main.js' }, ], }, { tag_name: 'v1.0.0', prerelease: false, draft: false, assets: [ { name: 'manifest.json', browser_download_url: 'https://example.com/v1/manifest.json' }, { name: 'main.js', browser_download_url: 'https://example.com/v1/main.js' }, ], }, ], })) const release = await forge.fetchLatestRelease('owner', 'repo') expect(release.version).toBe('1.0.0') }) }) describe('downloadManifest', () => { it('downloads and parses manifest', async () => { const mockManifest = { id: 'test-plugin', name: 'Test Plugin', version: '1.0.0', minAppVersion: '0.0.0', author: 'Test', description: 'A test plugin', } vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => mockManifest, })) const result = await forge.downloadManifest({ version: '1.0.0', tag: 'v1.0.0', prerelease: false, assets: { manifestUrl: 'https://example.com/manifest.json', mainJsUrl: 'https://example.com/main.js', }, }) expect(result).toEqual(mockManifest) }) it('throws on download failure', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 404, })) await expect( forge.downloadManifest({ version: '1.0.0', tag: 'v1.0.0', prerelease: false, assets: { manifestUrl: 'https://example.com/manifest.json', mainJsUrl: 'https://example.com/main.js', }, }) ).rejects.toThrow(/failed to download manifest/i) }) }) describe('downloadMainJs', () => { it('downloads main.js source', async () => { const mockSource = 'export default class MyPlugin extends Plugin {}' vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, text: async () => mockSource, })) const result = await forge.downloadMainJs({ version: '1.0.0', tag: 'v1.0.0', prerelease: false, assets: { manifestUrl: 'https://example.com/manifest.json', mainJsUrl: 'https://example.com/main.js', }, }) expect(result).toBe(mockSource) }) it('throws on download failure', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 404, })) await expect( forge.downloadMainJs({ version: '1.0.0', tag: 'v1.0.0', prerelease: false, assets: { manifestUrl: 'https://example.com/manifest.json', mainJsUrl: 'https://example.com/main.js', }, }) ).rejects.toThrow(/failed to download main\.js/i) }) }) }) describe('findForge', () => { it('returns GitHubForge for GitHub URLs', () => { const forge = findForge('https://github.com/owner/repo') expect(forge).not.toBeNull() expect(forge!.name).toBe('GitHub') }) it('returns null for non-forge URLs', () => { const forge = findForge('https://example.com/plugin') expect(forge).toBeNull() }) }) describe('isForgeUrl', () => { it('returns true for GitHub URLs', () => { expect(isForgeUrl('https://github.com/owner/repo')).toBe(true) }) it('returns false for non-forge URLs', () => { expect(isForgeUrl('https://example.com/plugin')).toBe(false) }) })