@hiyve/cli 1.0.17 → 1.0.19

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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The CLI verifies keys and lists packages against the ONE registry npm
3
+ * installs from, through standard npm endpoints. These tests pin the
4
+ * behaviours that the live registry forced (verified 2026-09-01):
5
+ *
6
+ * - /-/whoami answers 200 `{}` for a bad key, so validity is decided by a
7
+ * gated packument fetch (200 vs 401), never by whoami.
8
+ * - the catalogue comes from /-/v1/search, restricted to the @hiyve scope,
9
+ * de-duplicated, sorted, and split into SDK vs component packages.
10
+ */
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
+ import {
13
+ maskApiKey,
14
+ verifyApiKey,
15
+ catalogueFromSearch,
16
+ fetchPackageCatalogue,
17
+ } from './registryApi.js';
18
+
19
+ const REGISTRY = 'https://registry.muziemedia.com/';
20
+ const KEY = 'pk_live_0123456789abcdef0123456789abcdef01234567';
21
+
22
+ function jsonResponse(status, body) {
23
+ return {
24
+ ok: status >= 200 && status < 300,
25
+ status,
26
+ json: () => Promise.resolve(body),
27
+ };
28
+ }
29
+
30
+ let fetchMock;
31
+ beforeEach(() => {
32
+ fetchMock = vi.fn();
33
+ vi.stubGlobal('fetch', fetchMock);
34
+ });
35
+ afterEach(() => {
36
+ vi.unstubAllGlobals();
37
+ });
38
+
39
+ describe('maskApiKey', () => {
40
+ it('keeps the prefix and last four characters only', () => {
41
+ expect(maskApiKey(KEY)).toBe('pk_live_...4567');
42
+ });
43
+ it('leaves a short value alone and tolerates non-strings', () => {
44
+ expect(maskApiKey('pk_short')).toBe('pk_short');
45
+ expect(maskApiKey(undefined)).toBe('');
46
+ });
47
+ });
48
+
49
+ describe('verifyApiKey', () => {
50
+ it('probes a gated packument on the registry with the key as a bearer token', async () => {
51
+ fetchMock.mockResolvedValue(jsonResponse(200, { name: '@hiyve/cli' }));
52
+ await verifyApiKey(KEY);
53
+ const [url, init] = fetchMock.mock.calls[0];
54
+ expect(url).toBe(`${REGISTRY}@hiyve/cli`);
55
+ expect(init.headers.Authorization).toBe(`Bearer ${KEY}`);
56
+ expect(url).not.toContain('cloud.hiyve.io');
57
+ expect(url).not.toContain('/verify');
58
+ });
59
+
60
+ it('reports a masked key on success, never the raw one', async () => {
61
+ fetchMock.mockResolvedValue(jsonResponse(200, {}));
62
+ const result = await verifyApiKey(KEY);
63
+ expect(result).toEqual({ ok: true, apiKey: 'pk_live_...4567' });
64
+ expect(JSON.stringify(result)).not.toContain(KEY);
65
+ });
66
+
67
+ it('treats 401 as an invalid key', async () => {
68
+ fetchMock.mockResolvedValue(jsonResponse(401, {}));
69
+ const result = await verifyApiKey(KEY);
70
+ expect(result.ok).toBe(false);
71
+ expect(result.status).toBe(401);
72
+ expect(result.error).toBe('Invalid API key');
73
+ });
74
+
75
+ it('surfaces the registry error message when it sends one', async () => {
76
+ fetchMock.mockResolvedValue(jsonResponse(403, { error: 'key revoked' }));
77
+ const result = await verifyApiKey(KEY);
78
+ expect(result).toEqual({ ok: false, status: 403, error: 'key revoked' });
79
+ });
80
+
81
+ it('does not misreport an outage as a bad key', async () => {
82
+ fetchMock.mockResolvedValue({ ok: false, status: 502, json: () => Promise.reject(new Error('html')) });
83
+ const result = await verifyApiKey(KEY);
84
+ expect(result.ok).toBe(false);
85
+ expect(result.error).toBe('Registry returned HTTP 502');
86
+ expect(result.error).not.toMatch(/invalid/i);
87
+ });
88
+
89
+ it('lets network failures propagate for the caller to phrase', async () => {
90
+ const abort = Object.assign(new Error('aborted'), { name: 'AbortError' });
91
+ fetchMock.mockRejectedValue(abort);
92
+ await expect(verifyApiKey(KEY)).rejects.toBe(abort);
93
+ });
94
+ });
95
+
96
+ describe('catalogueFromSearch', () => {
97
+ const search = {
98
+ objects: [
99
+ { package: { name: '@hiyve/react-ui', version: '22.0.0' } },
100
+ { package: { name: '@hiyve/core', version: '7.0.0' } },
101
+ { package: { name: 'react', version: '19.0.0' } }, // not ours
102
+ { package: { name: '@hiyve/core', version: '7.0.0' } }, // duplicate
103
+ { package: { name: '@hiyve/whiteboard', version: '3.1.0' } },
104
+ { package: {} }, // malformed
105
+ {},
106
+ ],
107
+ };
108
+
109
+ it('keeps only @hiyve packages, de-duplicated and sorted', () => {
110
+ const cat = catalogueFromSearch(search);
111
+ const names = [...cat.sdk, ...cat.components].map((p) => p.name).sort();
112
+ expect(names).toEqual(['@hiyve/core', '@hiyve/react-ui', '@hiyve/whiteboard']);
113
+ expect(cat.total).toBe(3);
114
+ });
115
+
116
+ it('splits foundation packages from component packages', () => {
117
+ const cat = catalogueFromSearch(search);
118
+ expect(cat.sdk.map((p) => p.name)).toEqual(['@hiyve/core', '@hiyve/react-ui']);
119
+ expect(cat.components.map((p) => p.name)).toEqual(['@hiyve/whiteboard']);
120
+ });
121
+
122
+ it('carries the live version through', () => {
123
+ const cat = catalogueFromSearch(search);
124
+ expect(cat.sdk.find((p) => p.name === '@hiyve/core').version).toBe('7.0.0');
125
+ });
126
+
127
+ it("reads the version from Verdaccio's dist-tags.latest (its search has no `version`)", () => {
128
+ const cat = catalogueFromSearch({
129
+ objects: [{ package: { name: '@hiyve/admin', 'dist-tags': { latest: '2.3.0' } } }],
130
+ });
131
+ expect(cat.sdk).toEqual([{ name: '@hiyve/admin', version: '2.3.0' }]);
132
+ });
133
+
134
+ it('is empty, not broken, for a missing or empty result', () => {
135
+ expect(catalogueFromSearch(undefined)).toEqual({ total: 0, sdk: [], components: [] });
136
+ expect(catalogueFromSearch({})).toEqual({ total: 0, sdk: [], components: [] });
137
+ });
138
+ });
139
+
140
+ describe('fetchPackageCatalogue', () => {
141
+ it('searches the @hiyve scope on the registry, not the retired catalogue', async () => {
142
+ fetchMock.mockResolvedValue(jsonResponse(200, { objects: [] }));
143
+ await fetchPackageCatalogue(KEY);
144
+ const [url] = fetchMock.mock.calls[0];
145
+ expect(url.startsWith(`${REGISTRY}-/v1/search?`)).toBe(true);
146
+ expect(url).toContain('text=%40hiyve');
147
+ expect(url).not.toContain('cloud.hiyve.io');
148
+ expect(url).not.toContain('/packages');
149
+ });
150
+
151
+ it('returns the catalogue shape the commands print', async () => {
152
+ fetchMock.mockResolvedValue(
153
+ jsonResponse(200, { objects: [{ package: { name: '@hiyve/rtc-client', version: '2.5.0' } }] }),
154
+ );
155
+ const result = await fetchPackageCatalogue(KEY);
156
+ expect(result).toEqual({
157
+ ok: true,
158
+ total: 1,
159
+ sdk: [{ name: '@hiyve/rtc-client', version: '2.5.0' }],
160
+ components: [],
161
+ });
162
+ });
163
+
164
+ it('reports a failed search without throwing', async () => {
165
+ fetchMock.mockResolvedValue(jsonResponse(500, {}));
166
+ const result = await fetchPackageCatalogue(KEY);
167
+ expect(result).toEqual({ ok: false, status: 500, error: 'Registry returned HTTP 500' });
168
+ });
169
+ });