@myapihq/cli 1.2.6 → 1.2.8

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,139 @@
1
+ // SDK-level unit tests for the container module. Verifies request
2
+ // URL/body shape, response parsing, and error envelopes against
3
+ // myapi-hq/internal/routes/container/. Mocks global fetch — no network.
4
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
+ import { container } from '@myapihq/sdk';
6
+ const API_KEY = 'myapi_test_abc';
7
+ const ORG_ID = '11111111-1111-4111-8111-111111111111';
8
+ const C_ID = '55555555-5555-4555-8555-555555555555';
9
+ let fetchMock;
10
+ function ok(data, status = 200) {
11
+ return new Response(JSON.stringify({ success: true, data, meta: {} }), {
12
+ status,
13
+ headers: { 'content-type': 'application/json' },
14
+ });
15
+ }
16
+ function fail(code, message, status = 422) {
17
+ return new Response(JSON.stringify({ success: false, error: { code, message }, meta: {} }), {
18
+ status,
19
+ headers: { 'content-type': 'application/json' },
20
+ });
21
+ }
22
+ const SAMPLE = {
23
+ id: C_ID, org_id: ORG_ID, name: 'api', type: 'service', env: {},
24
+ cpu: '1', memory: '512Mi', min_instances: 0, max_instances: 3, port: 8080,
25
+ status: 'created', created_at: 't', updated_at: 't',
26
+ };
27
+ beforeEach(() => {
28
+ fetchMock = vi.fn();
29
+ globalThis.fetch = fetchMock;
30
+ });
31
+ afterEach(() => {
32
+ vi.restoreAllMocks();
33
+ });
34
+ describe('container.createContainer', () => {
35
+ it('POSTs the payload with bearer auth + JSON body', async () => {
36
+ fetchMock.mockResolvedValueOnce(ok({ container: SAMPLE, scoped_api_key: 'hq_live_x', scoped_api_key_id: 'key_x' }, 201));
37
+ const res = await container.createContainer(API_KEY, ORG_ID, { name: 'api', type: 'service', port: 8080 });
38
+ const [url, init] = fetchMock.mock.calls[0];
39
+ expect(url).toContain(`/container/orgs/${ORG_ID}/containers`);
40
+ expect(init.method).toBe('POST');
41
+ expect(init.headers.Authorization).toBe(`Bearer ${API_KEY}`);
42
+ expect(JSON.parse(init.body)).toEqual({ name: 'api', type: 'service', port: 8080 });
43
+ expect(res.container.id).toBe(C_ID);
44
+ expect(res.scoped_api_key).toBe('hq_live_x');
45
+ });
46
+ it('surfaces INVALID_TYPE (422) and NAME_TAKEN (409)', async () => {
47
+ fetchMock.mockResolvedValueOnce(fail('INVALID_TYPE', 'cron_schedule is only valid for type=job', 422));
48
+ await expect(container.createContainer(API_KEY, ORG_ID, { name: 'x', type: 'service', cron_schedule: '* * * * *' }))
49
+ .rejects.toMatchObject({ code: 'INVALID_TYPE', status: 422 });
50
+ fetchMock.mockResolvedValueOnce(fail('NAME_TAKEN', 'already used', 409));
51
+ await expect(container.createContainer(API_KEY, ORG_ID, { name: 'x' }))
52
+ .rejects.toMatchObject({ code: 'NAME_TAKEN', status: 409 });
53
+ });
54
+ });
55
+ describe('container.listContainers / getContainer', () => {
56
+ it('GETs the container list', async () => {
57
+ fetchMock.mockResolvedValueOnce(ok([SAMPLE]));
58
+ const list = await container.listContainers(API_KEY, ORG_ID);
59
+ expect(list).toHaveLength(1);
60
+ expect(fetchMock.mock.calls[0][1].method).toBe('GET');
61
+ });
62
+ it('GETs a single container', async () => {
63
+ fetchMock.mockResolvedValueOnce(ok(SAMPLE));
64
+ const c = await container.getContainer(API_KEY, ORG_ID, C_ID);
65
+ expect(c.name).toBe('api');
66
+ expect(fetchMock.mock.calls[0][0]).toContain(`/container/orgs/${ORG_ID}/containers/${C_ID}`);
67
+ });
68
+ it('surfaces container_not_found (404)', async () => {
69
+ fetchMock.mockResolvedValueOnce(fail('container_not_found', 'not found', 404));
70
+ await expect(container.getContainer(API_KEY, ORG_ID, C_ID))
71
+ .rejects.toMatchObject({ code: 'container_not_found', status: 404 });
72
+ });
73
+ });
74
+ describe('container.deleteContainer', () => {
75
+ it('DELETEs and resolves on 204', async () => {
76
+ fetchMock.mockResolvedValueOnce(new Response(null, { status: 204 }));
77
+ await container.deleteContainer(API_KEY, ORG_ID, C_ID);
78
+ const [url, init] = fetchMock.mock.calls[0];
79
+ expect(url).toContain(`/container/orgs/${ORG_ID}/containers/${C_ID}`);
80
+ expect(init.method).toBe('DELETE');
81
+ });
82
+ });
83
+ describe('container.deployContainer', () => {
84
+ it('POSTs {image} and returns the rotated key + url', async () => {
85
+ fetchMock.mockResolvedValueOnce(ok({
86
+ container_id: C_ID, revision_id: 'rev_1', url: 'https://c.run.app', status: 'active', scoped_api_key: 'hq_live_rotated',
87
+ }));
88
+ const res = await container.deployContainer(API_KEY, ORG_ID, C_ID, 'registry/app:v2');
89
+ const [url, init] = fetchMock.mock.calls[0];
90
+ expect(url).toContain(`/container/orgs/${ORG_ID}/containers/${C_ID}/deploy`);
91
+ expect(init.method).toBe('POST');
92
+ expect(JSON.parse(init.body)).toEqual({ image: 'registry/app:v2' });
93
+ expect(res.scoped_api_key).toBe('hq_live_rotated');
94
+ expect(res.url).toBe('https://c.run.app');
95
+ });
96
+ it('surfaces RUNTIME_UNAVAILABLE (503) and IMAGE_REQUIRED (422)', async () => {
97
+ fetchMock.mockResolvedValueOnce(fail('RUNTIME_UNAVAILABLE', 'Cloud Run not configured', 503));
98
+ await expect(container.deployContainer(API_KEY, ORG_ID, C_ID, 'img'))
99
+ .rejects.toMatchObject({ code: 'RUNTIME_UNAVAILABLE', status: 503 });
100
+ fetchMock.mockResolvedValueOnce(fail('IMAGE_REQUIRED', 'image ref required', 422));
101
+ await expect(container.deployContainer(API_KEY, ORG_ID, C_ID, ''))
102
+ .rejects.toMatchObject({ code: 'IMAGE_REQUIRED', status: 422 });
103
+ });
104
+ });
105
+ describe('container.getContainerLogs', () => {
106
+ it('GETs /logs and returns the entries', async () => {
107
+ fetchMock.mockResolvedValueOnce(ok([
108
+ { timestamp: 't2', severity: 'INFO', text: 'started' },
109
+ { timestamp: 't1', severity: 'ERROR', text: 'boom' },
110
+ ]));
111
+ const entries = await container.getContainerLogs(API_KEY, ORG_ID, C_ID);
112
+ expect(entries).toHaveLength(2);
113
+ expect(entries[1].severity).toBe('ERROR');
114
+ const [url, init] = fetchMock.mock.calls[0];
115
+ expect(url).toContain(`/container/orgs/${ORG_ID}/containers/${C_ID}/logs`);
116
+ expect(init.method).toBe('GET');
117
+ });
118
+ it('appends ?tail=N when a tail count is given', async () => {
119
+ fetchMock.mockResolvedValueOnce(ok([]));
120
+ await container.getContainerLogs(API_KEY, ORG_ID, C_ID, 250);
121
+ expect(fetchMock.mock.calls[0][0]).toMatch(/\/logs\?tail=250$/);
122
+ });
123
+ it('returns an empty array for an undeployed container', async () => {
124
+ fetchMock.mockResolvedValueOnce(ok([]));
125
+ expect(await container.getContainerLogs(API_KEY, ORG_ID, C_ID)).toEqual([]);
126
+ });
127
+ });
128
+ describe('container.EXPOSES', () => {
129
+ it('covers the 6 container endpoints', () => {
130
+ expect(container.EXPOSES).toEqual([
131
+ 'POST /container/orgs/{org_id}/containers',
132
+ 'GET /container/orgs/{org_id}/containers',
133
+ 'GET /container/orgs/{org_id}/containers/{id}',
134
+ 'DELETE /container/orgs/{org_id}/containers/{id}',
135
+ 'POST /container/orgs/{org_id}/containers/{id}/deploy',
136
+ 'GET /container/orgs/{org_id}/containers/{id}/logs',
137
+ ]);
138
+ });
139
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "1.2.6",
4
+ "version": "1.2.8",
5
5
  "description": "MyAPI command-line interface",
6
6
  "type": "module",
7
7
  "files": [
@@ -29,12 +29,10 @@
29
29
  "lint:changelog": "node ../../scripts/lint-changelog.js"
30
30
  },
31
31
  "dependencies": {
32
- "@myapihq/sdk": "^1.2.6",
33
- "omelette": "^0.4.17"
32
+ "@myapihq/sdk": "^1.2.8"
34
33
  },
35
34
  "devDependencies": {
36
35
  "@types/node": "^25.6.0",
37
- "@types/omelette": "^0.4.5",
38
36
  "typescript": "^5.4.0",
39
37
  "vitest": "^4.1.5"
40
38
  }