|
3 | 3 | /** |
4 | 4 | * The listing this package publishes as: `objectstack.manifest.json` parses |
5 | 5 | * against the spec's own schema for that file, names the same package id as |
6 | | - * the stack, points at a README that exists, and that README's credentials |
| 6 | + * the stack, points at a README that exists and at the icon the package owns |
| 7 | + * (a public http(s) `iconUrl`), and that README's credentials |
7 | 8 | * section names every credential the app's connectors reference — the |
8 | 9 | * marketplace handbook's "every credential it needs" rule, checked against |
9 | 10 | * the metadata rather than trusted. |
10 | 11 | */ |
11 | 12 |
|
12 | 13 | import { describe, it, expect } from 'vitest'; |
13 | | -import { readFileSync, existsSync } from 'node:fs'; |
| 14 | +import { readFileSync, existsSync, statSync } from 'node:fs'; |
14 | 15 | import { fileURLToPath } from 'node:url'; |
15 | 16 | import { TemplateManifestSchema } from '@objectstack/spec/marketplace'; |
16 | 17 | import stack from '../objectstack.config.js'; |
17 | 18 |
|
18 | 19 | const manifestPath = fileURLToPath(new URL('../objectstack.manifest.json', import.meta.url)); |
19 | 20 | const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>; |
20 | 21 |
|
| 22 | +/** Where the listing icon is served from once this package's tree is on `main`. */ |
| 23 | +const ICON_URL = 'https://raw.githubusercontent.com/objectstack-ai/objectstack/main/packages/apps/connector-github/assets/icon.svg'; |
| 24 | + |
| 25 | +/** |
| 26 | + * Why a manifest's `iconUrl` cannot be the listing icon, or `null` when it can: |
| 27 | + * `os package publish` sends the key as the package's `icon_url`, and the |
| 28 | + * marketplace handbook asks for a public http(s) URL. `TemplateManifestSchema` |
| 29 | + * accepts any URL scheme, so the scheme is checked here. |
| 30 | + */ |
| 31 | +function iconUrlProblem(m: Record<string, unknown>): string | null { |
| 32 | + const value = m.iconUrl; |
| 33 | + if (typeof value !== 'string') return 'iconUrl is absent'; |
| 34 | + let url: URL; |
| 35 | + try { |
| 36 | + url = new URL(value); |
| 37 | + } catch { |
| 38 | + return `iconUrl is not a URL: ${value}`; |
| 39 | + } |
| 40 | + return url.protocol === 'https:' || url.protocol === 'http:' ? null : `iconUrl is not http(s): ${value}`; |
| 41 | +} |
| 42 | + |
21 | 43 | /** The README's `## Credentials` section, up to the next second-level heading. */ |
22 | 44 | function credentialsSection(readme: string): string { |
23 | 45 | const start = readme.indexOf('\n## Credentials\n'); |
@@ -55,6 +77,27 @@ describe('connector-github listing', () => { |
55 | 77 | expect(section).toContain('https://github.com/settings/personal-access-tokens/new'); |
56 | 78 | }); |
57 | 79 |
|
| 80 | + it('carries a public http(s) iconUrl', () => { |
| 81 | + expect(iconUrlProblem(manifest)).toBeNull(); |
| 82 | + }); |
| 83 | + |
| 84 | + it('refuses an iconUrl that is absent or not http(s)', () => { |
| 85 | + const withoutIcon = { ...manifest }; |
| 86 | + delete withoutIcon.iconUrl; |
| 87 | + expect(iconUrlProblem(withoutIcon)).toBe('iconUrl is absent'); |
| 88 | + expect(iconUrlProblem({ ...manifest, iconUrl: 'ftp://example.com/icon.svg' })).toMatch(/not http\(s\)/); |
| 89 | + expect(iconUrlProblem({ ...manifest, iconUrl: 'assets/icon.svg' })).toMatch(/not a URL/); |
| 90 | + }); |
| 91 | + |
| 92 | + it('points iconUrl at the icon this package owns', () => { |
| 93 | + expect(manifest.iconUrl).toBe(ICON_URL); |
| 94 | + const iconPath = fileURLToPath(new URL('../assets/icon.svg', import.meta.url)); |
| 95 | + expect(existsSync(iconPath)).toBe(true); |
| 96 | + // `--icon-file` takes up to 256 KB; hold the owned asset to the same bound. |
| 97 | + expect(statSync(iconPath).size).toBeLessThan(256 * 1024); |
| 98 | + expect(readFileSync(iconPath, 'utf8')).toMatch(/^<svg [^>]*viewBox="0 0 256 256"/); |
| 99 | + }); |
| 100 | + |
58 | 101 | it('carries no secret in metadata', () => { |
59 | 102 | for (const c of stack.connectors ?? []) { |
60 | 103 | expect(c).not.toHaveProperty('authentication.token'); |
|
0 commit comments