@dk/jolly 0.1.5 → 0.1.7
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.
- package/bun.lock +59 -1
- package/dist/bootstrap.js +42 -19
- package/dist/index.js +91 -68
- package/package.json +6 -6
- package/src/api/auth.ts +1 -1
- package/src/api/client.ts +93 -20
- package/src/commands/app.ts +26 -26
- package/src/commands/store.ts +23 -23
- package/src/test/command-handlers.test.ts +34 -29
- package/src/test/e2e-flows.test.ts +50 -31
- package/src/test/entry-points.test.ts +4 -1
- package/src/test/error-handling.test.ts +8 -8
- package/src/test/mocks.ts +55 -15
package/src/api/client.ts
CHANGED
|
@@ -10,63 +10,136 @@ export class SaleorCloudClient {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
async request<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
|
13
|
-
const
|
|
13
|
+
const mergedOptions: RequestInit = {
|
|
14
|
+
method: options?.method || 'GET',
|
|
14
15
|
...options,
|
|
15
16
|
headers: {
|
|
16
|
-
'Authorization': `
|
|
17
|
+
'Authorization': `Token ${this.token}`,
|
|
17
18
|
'Content-Type': 'application/json',
|
|
18
19
|
...options?.headers,
|
|
19
20
|
},
|
|
20
|
-
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`, mergedOptions);
|
|
21
24
|
|
|
22
25
|
if (!response.ok) {
|
|
23
|
-
|
|
26
|
+
const body = await response.text();
|
|
27
|
+
throw new Error(`API error: ${response.status} ${response.statusText} - ${body}`);
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
return response.json();
|
|
30
|
+
return response.json() as T;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
// Organizations
|
|
34
|
+
async getOrganizations() {
|
|
35
|
+
return this.request<{ organizations: Organization[] }>('/organizations');
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
// Projects
|
|
39
|
+
async getProjects(organizationSlug: string) {
|
|
40
|
+
return this.request<{ projects: Project[] }>(`/organizations/${organizationSlug}/projects`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async createProject(organizationSlug: string, name: string, region: string) {
|
|
44
|
+
return this.request<{ project: Project }>(`/organizations/${organizationSlug}/projects`, {
|
|
35
45
|
method: 'POST',
|
|
36
46
|
body: JSON.stringify({ name, region }),
|
|
37
47
|
});
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
// Environments
|
|
51
|
+
async getEnvironments(organizationSlug: string, projectSlug: string) {
|
|
52
|
+
return this.request<{ environments: Environment[] }>(
|
|
53
|
+
`/organizations/${organizationSlug}/projects/${projectSlug}/environments`
|
|
54
|
+
);
|
|
42
55
|
}
|
|
43
56
|
|
|
44
|
-
async createEnvironment(
|
|
45
|
-
return this.request<{ environment: Environment }>(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
async createEnvironment(organizationSlug: string, projectSlug: string, name: string, region: string) {
|
|
58
|
+
return this.request<{ environment: Environment; task: Task }>(
|
|
59
|
+
`/organizations/${organizationSlug}/projects/${projectSlug}/environments`,
|
|
60
|
+
{
|
|
61
|
+
method: 'POST',
|
|
62
|
+
body: JSON.stringify({ name, region }),
|
|
63
|
+
}
|
|
64
|
+
);
|
|
49
65
|
}
|
|
50
66
|
|
|
67
|
+
async getEnvironment(organizationSlug: string, projectSlug: string, environmentSlug: string) {
|
|
68
|
+
return this.request<Environment>(
|
|
69
|
+
`/organizations/${organizationSlug}/projects/${projectSlug}/environments/${environmentSlug}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Apps
|
|
51
74
|
async registerApp(environmentId: string, appType: string, name: string) {
|
|
52
75
|
return this.request<{ app: App }>(`/environments/${environmentId}/apps`, {
|
|
53
76
|
method: 'POST',
|
|
54
77
|
body: JSON.stringify({ type: appType, name }),
|
|
55
78
|
});
|
|
56
79
|
}
|
|
80
|
+
|
|
81
|
+
// Backward compatibility methods
|
|
82
|
+
async getStores() {
|
|
83
|
+
return this.getOrganizations();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async createStore(name: string, region: string = 'us-east-1') {
|
|
87
|
+
// Create organization as "store" - requires org slug
|
|
88
|
+
// For now, create in first available org
|
|
89
|
+
const { organizations } = await this.getOrganizations();
|
|
90
|
+
if (organizations.length === 0) {
|
|
91
|
+
throw new Error('No organizations found. Create one at https://cloud.saleor.io');
|
|
92
|
+
}
|
|
93
|
+
return this.createProject(organizations[0].slug, name, region);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async createEnvironmentFromStore(storeId: string, name: string) {
|
|
97
|
+
// storeId is used as organization slug for backward compatibility
|
|
98
|
+
const { environments } = await this.getEnvironments(storeId, 'default');
|
|
99
|
+
// Create sandbox environment
|
|
100
|
+
return this.createEnvironment(storeId, environments.length > 0 ? environments[0].project?.slug || 'default' : 'default', name, 'default');
|
|
101
|
+
}
|
|
57
102
|
}
|
|
58
103
|
|
|
59
|
-
export interface
|
|
60
|
-
|
|
104
|
+
export interface Organization {
|
|
105
|
+
slug: string;
|
|
106
|
+
name: string;
|
|
107
|
+
created: string;
|
|
108
|
+
company_name?: string;
|
|
109
|
+
owner_email: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface Project {
|
|
113
|
+
slug: string;
|
|
61
114
|
name: string;
|
|
62
115
|
region: string;
|
|
63
|
-
|
|
116
|
+
created: string;
|
|
117
|
+
billing_period?: { start: string };
|
|
118
|
+
sandboxes: { count: number };
|
|
64
119
|
}
|
|
65
120
|
|
|
66
121
|
export interface Environment {
|
|
122
|
+
key: string;
|
|
123
|
+
name: string;
|
|
124
|
+
domain: string;
|
|
125
|
+
service: {
|
|
126
|
+
version: string;
|
|
127
|
+
type: string;
|
|
128
|
+
region: string;
|
|
129
|
+
};
|
|
130
|
+
created: string;
|
|
131
|
+
project: { name: string; slug: string };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface Task {
|
|
135
|
+
id: string;
|
|
136
|
+
status: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface Store {
|
|
67
140
|
id: string;
|
|
68
141
|
name: string;
|
|
69
|
-
|
|
142
|
+
region: string;
|
|
70
143
|
created_at: string;
|
|
71
144
|
}
|
|
72
145
|
|
package/src/commands/app.ts
CHANGED
|
@@ -31,27 +31,27 @@ export async function createApp(
|
|
|
31
31
|
environmentId?: string,
|
|
32
32
|
provider?: PaymentProvider
|
|
33
33
|
): Promise<void> {
|
|
34
|
-
info(`Creating ${type} app: ${name}`);
|
|
34
|
+
console.log(info(`Creating ${type} app: ${name}`));
|
|
35
35
|
|
|
36
36
|
if (type === 'payment') {
|
|
37
37
|
const paymentProvider = provider || 'dummy';
|
|
38
|
-
info(`Payment provider: ${paymentProvider}`);
|
|
39
|
-
info(`Using hosted payment app: ${PAYMENT_APP_URLS[paymentProvider]}`);
|
|
38
|
+
console.log(info(`Payment provider: ${paymentProvider}`));
|
|
39
|
+
console.log(info(`Using hosted payment app: ${PAYMENT_APP_URLS[paymentProvider]}`));
|
|
40
40
|
|
|
41
41
|
if (environmentId) {
|
|
42
42
|
await registerHostedApp(environmentId, name, paymentProvider);
|
|
43
43
|
} else {
|
|
44
|
-
success(`\nPayment app "${name}" configured to use ${PAYMENT_APP_URLS[paymentProvider]}`);
|
|
45
|
-
info(`\nTo complete setup:`);
|
|
46
|
-
info(`1. Go to your dashboard at https://cloud.saleor.io`);
|
|
47
|
-
info(`2. Navigate to Apps > Third party apps`);
|
|
48
|
-
info(`3. Add the ${paymentProvider} payment app from ${PAYMENT_APP_URLS[paymentProvider]}`);
|
|
44
|
+
console.log(success(`\nPayment app "${name}" configured to use ${PAYMENT_APP_URLS[paymentProvider]}`));
|
|
45
|
+
console.log(info(`\nTo complete setup:`));
|
|
46
|
+
console.log(info(`1. Go to your dashboard at https://cloud.saleor.io`));
|
|
47
|
+
console.log(info(`2. Navigate to Apps > Third party apps`));
|
|
48
|
+
console.log(info(`3. Add the ${paymentProvider} payment app from ${PAYMENT_APP_URLS[paymentProvider]}`));
|
|
49
49
|
}
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const template = APP_TEMPLATES[type];
|
|
54
|
-
info(`Cloning template from: ${template.repo}`);
|
|
54
|
+
console.log(info(`Cloning template from: ${template.repo}`));
|
|
55
55
|
|
|
56
56
|
try {
|
|
57
57
|
const { spawn } = await import('child_process');
|
|
@@ -61,23 +61,23 @@ export async function createApp(
|
|
|
61
61
|
|
|
62
62
|
child.on('close', (code) => {
|
|
63
63
|
if (code === 0) {
|
|
64
|
-
success(`\n${type} app "${name}" created successfully!`);
|
|
65
|
-
info(`\nTo get started:`);
|
|
66
|
-
info(`cd ${name}`);
|
|
67
|
-
info(`npm install`);
|
|
68
|
-
info(`npm run dev`);
|
|
64
|
+
console.log(success(`\n${type} app "${name}" created successfully!`));
|
|
65
|
+
console.log(info(`\nTo get started:`));
|
|
66
|
+
console.log(info(`cd ${name}`));
|
|
67
|
+
console.log(info(`npm install`));
|
|
68
|
+
console.log(info(`npm run dev`));
|
|
69
69
|
|
|
70
70
|
if (environmentId) {
|
|
71
|
-
info(`\nRegistering app with environment ${environmentId}...`);
|
|
71
|
+
console.log(info(`\nRegistering app with environment ${environmentId}...`));
|
|
72
72
|
registerLocalApp(environmentId, name, type);
|
|
73
73
|
}
|
|
74
74
|
} else {
|
|
75
|
-
error(`Failed to clone template (exit code: ${code})`);
|
|
75
|
+
console.log(error(`Failed to clone template (exit code: ${code})`));
|
|
76
76
|
process.exit(1);
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
79
|
} catch (err) {
|
|
80
|
-
error(`Failed to create app: ${err}`);
|
|
80
|
+
console.log(error(`Failed to create app: ${err}`));
|
|
81
81
|
process.exit(1);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -90,15 +90,15 @@ async function registerHostedApp(
|
|
|
90
90
|
const token = requireToken();
|
|
91
91
|
const client = new SaleorCloudClient(token);
|
|
92
92
|
|
|
93
|
-
info(`Registering hosted ${provider} payment app with environment...`);
|
|
93
|
+
console.log(info(`Registering hosted ${provider} payment app with environment...`));
|
|
94
94
|
|
|
95
95
|
try {
|
|
96
96
|
const result = await client.registerApp(environmentId, 'payment', name);
|
|
97
|
-
success(`Payment app registered successfully!`);
|
|
98
|
-
info(`App ID: ${result.app.id}`);
|
|
99
|
-
info(`Payment URL: ${PAYMENT_APP_URLS[provider]}`);
|
|
97
|
+
console.log(success(`Payment app registered successfully!`));
|
|
98
|
+
console.log(info(`App ID: ${result.app.id}`));
|
|
99
|
+
console.log(info(`Payment URL: ${PAYMENT_APP_URLS[provider]}`));
|
|
100
100
|
} catch (err) {
|
|
101
|
-
error(`Failed to register app: ${err}`);
|
|
101
|
+
console.log(error(`Failed to register app: ${err}`));
|
|
102
102
|
process.exit(1);
|
|
103
103
|
}
|
|
104
104
|
}
|
|
@@ -113,11 +113,11 @@ async function registerLocalApp(
|
|
|
113
113
|
|
|
114
114
|
try {
|
|
115
115
|
const result = await client.registerApp(environmentId, type, name);
|
|
116
|
-
success(`App registered with environment!`);
|
|
117
|
-
info(`App ID: ${result.app.id}`);
|
|
116
|
+
console.log(success(`App registered with environment!`));
|
|
117
|
+
console.log(info(`App ID: ${result.app.id}`));
|
|
118
118
|
} catch (err) {
|
|
119
|
-
warning(`Could not register app automatically: ${err}`);
|
|
120
|
-
info(`You can register manually in the dashboard.`);
|
|
119
|
+
console.log(warning(`Could not register app automatically: ${err}`));
|
|
120
|
+
console.log(info(`You can register manually in the dashboard.`));
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
package/src/commands/store.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { SaleorCloudClient } from '../api/client.js';
|
|
2
2
|
import { requireToken } from '../api/auth.js';
|
|
3
|
-
import {
|
|
3
|
+
import { success, error, info } from '../tui/components.js';
|
|
4
4
|
|
|
5
5
|
export async function createStore(name: string, region: string): Promise<void> {
|
|
6
6
|
const token = requireToken();
|
|
7
7
|
const client = new SaleorCloudClient(token);
|
|
8
8
|
|
|
9
|
-
info(`Creating store: ${name} in ${region}...`);
|
|
9
|
+
console.log(info(`Creating store: ${name} in ${region}...`));
|
|
10
10
|
|
|
11
11
|
try {
|
|
12
12
|
const result = await client.createStore(name, region);
|
|
13
|
-
success(`Store created successfully!`);
|
|
14
|
-
info(`
|
|
15
|
-
info(`Dashboard: https://cloud.saleor.io/
|
|
13
|
+
console.log(success(`Store created successfully!`));
|
|
14
|
+
console.log(info(`Project slug: ${result.project.slug}`));
|
|
15
|
+
console.log(info(`Dashboard: https://cloud.saleor.io/organizations/default/projects/${result.project.slug}`));
|
|
16
16
|
} catch (err) {
|
|
17
|
-
error(`Failed to create store: ${err}`);
|
|
17
|
+
console.log(error(`Failed to create store: ${err}`));
|
|
18
18
|
process.exit(1);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -23,42 +23,42 @@ export async function listStores(): Promise<void> {
|
|
|
23
23
|
const token = requireToken();
|
|
24
24
|
const client = new SaleorCloudClient(token);
|
|
25
25
|
|
|
26
|
-
info('Fetching
|
|
26
|
+
console.log(info('Fetching organizations...'));
|
|
27
27
|
|
|
28
28
|
try {
|
|
29
|
-
const
|
|
29
|
+
const { organizations } = await client.getOrganizations();
|
|
30
30
|
|
|
31
|
-
if (
|
|
32
|
-
info('No
|
|
31
|
+
if (organizations.length === 0) {
|
|
32
|
+
console.log(info('No organizations found. Create one at https://cloud.saleor.io'));
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
success(`Found ${
|
|
37
|
-
for (const
|
|
38
|
-
console.log(` ${
|
|
39
|
-
console.log(`
|
|
40
|
-
console.log(` Created: ${new Date(
|
|
36
|
+
console.log(success(`Found ${organizations.length} organization(s):\n`));
|
|
37
|
+
for (const org of organizations) {
|
|
38
|
+
console.log(` ${org.name} (${org.slug})`);
|
|
39
|
+
console.log(` Email: ${org.owner_email}`);
|
|
40
|
+
console.log(` Created: ${new Date(org.created).toLocaleDateString()}`);
|
|
41
41
|
console.log();
|
|
42
42
|
}
|
|
43
43
|
} catch (err) {
|
|
44
|
-
error(`Failed to list stores: ${err}`);
|
|
44
|
+
console.log(error(`Failed to list stores: ${err}`));
|
|
45
45
|
process.exit(1);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export async function createEnvironment(
|
|
49
|
+
export async function createEnvironment(organizationSlug: string, name: string): Promise<void> {
|
|
50
50
|
const token = requireToken();
|
|
51
51
|
const client = new SaleorCloudClient(token);
|
|
52
52
|
|
|
53
|
-
info(`Creating environment: ${name} for
|
|
53
|
+
console.log(info(`Creating environment: ${name} for organization ${organizationSlug}...`));
|
|
54
54
|
|
|
55
55
|
try {
|
|
56
|
-
const
|
|
57
|
-
success(`Environment created successfully!`);
|
|
58
|
-
info(`Environment
|
|
59
|
-
info(`
|
|
56
|
+
const { environment } = await client.createEnvironment(organizationSlug, 'default', name, 'us-east-1');
|
|
57
|
+
console.log(success(`Environment created successfully!`));
|
|
58
|
+
console.log(info(`Environment key: ${environment.key}`));
|
|
59
|
+
console.log(info(`Domain: ${environment.domain}`));
|
|
60
60
|
} catch (err) {
|
|
61
|
-
error(`Failed to create environment: ${err}`);
|
|
61
|
+
console.log(error(`Failed to create environment: ${err}`));
|
|
62
62
|
process.exit(1);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -36,67 +36,72 @@ describe('Command Handlers', () => {
|
|
|
36
36
|
describe('Store Handlers', () => {
|
|
37
37
|
Given('a valid token and successful API', () => {
|
|
38
38
|
When('calling createStore', () => {
|
|
39
|
-
Then('it should call POST /
|
|
39
|
+
Then('it should call POST /organizations/:slug/projects with name and region', async () => {
|
|
40
40
|
const fetchMock = mockFetch({
|
|
41
|
-
'/
|
|
41
|
+
'/organizations': { organizations: [fixtures.organization] },
|
|
42
|
+
'/organizations/my-org/projects': { project: fixtures.project },
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
const { createStore } = await import('../commands/store');
|
|
45
46
|
await createStore('my-store', 'us-east-1');
|
|
46
47
|
|
|
47
|
-
expect(fetchMock).
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
expect(
|
|
48
|
+
expect(fetchMock).toHaveBeenCalled();
|
|
49
|
+
const createCall = fetchMock.mock.calls.find(([url]: [string]) =>
|
|
50
|
+
(url as string).includes('/organizations/my-org/projects')
|
|
51
|
+
);
|
|
52
|
+
expect(createCall).toBeDefined();
|
|
53
|
+
expect(createCall?.[1].method).toBe('POST');
|
|
54
|
+
expect(JSON.parse(createCall?.[1].body)).toEqual({ name: 'my-store', region: 'us-east-1' });
|
|
52
55
|
});
|
|
53
56
|
});
|
|
54
57
|
|
|
55
58
|
When('calling listStores with results', () => {
|
|
56
|
-
Then('it should display
|
|
59
|
+
Then('it should display organization names and slugs', async () => {
|
|
57
60
|
mockFetch({
|
|
58
|
-
'/
|
|
61
|
+
'/organizations': { organizations: [fixtures.organization, fixtures.organization2] },
|
|
59
62
|
});
|
|
60
63
|
|
|
61
64
|
const { listStores } = await import('../commands/store');
|
|
62
65
|
await listStores();
|
|
63
66
|
|
|
64
67
|
const allOutput = console_.logs.join('\n');
|
|
65
|
-
expect(allOutput).toContain('
|
|
66
|
-
expect(allOutput).toContain('
|
|
67
|
-
expect(allOutput).toContain('
|
|
68
|
-
expect(allOutput).toContain('
|
|
68
|
+
expect(allOutput).toContain('My Organization');
|
|
69
|
+
expect(allOutput).toContain('my-org');
|
|
70
|
+
expect(allOutput).toContain('Other Organization');
|
|
71
|
+
expect(allOutput).toContain('other-org');
|
|
69
72
|
});
|
|
70
73
|
});
|
|
71
74
|
|
|
72
75
|
When('calling listStores with empty results', () => {
|
|
73
76
|
Then('it should display a helpful message', async () => {
|
|
74
77
|
mockFetch({
|
|
75
|
-
'/
|
|
78
|
+
'/organizations': { organizations: [] },
|
|
76
79
|
});
|
|
77
80
|
|
|
78
81
|
const { listStores } = await import('../commands/store');
|
|
79
82
|
await listStores();
|
|
80
83
|
|
|
81
84
|
const infoMessages = tuiCalls.filter(c => c.fn === 'info').map(c => c.msg).join('\n');
|
|
82
|
-
expect(infoMessages).toContain('No
|
|
85
|
+
expect(infoMessages).toContain('No organizations found');
|
|
83
86
|
});
|
|
84
87
|
});
|
|
85
88
|
|
|
86
89
|
When('calling createEnvironment', () => {
|
|
87
|
-
Then('it should call POST /
|
|
90
|
+
Then('it should call POST /organizations/:slug/projects/:slug/environments', async () => {
|
|
88
91
|
const fetchMock = mockFetch({
|
|
89
|
-
'/
|
|
92
|
+
'/organizations/my-org/projects/default/environments': { environment: fixtures.environment },
|
|
90
93
|
});
|
|
91
94
|
|
|
92
95
|
const { createEnvironment } = await import('../commands/store');
|
|
93
|
-
await createEnvironment('
|
|
94
|
-
|
|
95
|
-
expect(fetchMock).
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
expect(
|
|
96
|
+
await createEnvironment('my-org', 'staging');
|
|
97
|
+
|
|
98
|
+
expect(fetchMock).toHaveBeenCalled();
|
|
99
|
+
const createCall = fetchMock.mock.calls.find(([url]: [string]) =>
|
|
100
|
+
(url as string).includes('/organizations/my-org/projects/default/environments')
|
|
101
|
+
);
|
|
102
|
+
expect(createCall).toBeDefined();
|
|
103
|
+
expect(createCall?.[1].method).toBe('POST');
|
|
104
|
+
expect(JSON.parse(createCall?.[1].body)).toEqual({ name: 'staging', region: 'us-east-1' });
|
|
100
105
|
});
|
|
101
106
|
});
|
|
102
107
|
});
|
|
@@ -137,7 +142,7 @@ describe('Command Handlers', () => {
|
|
|
137
142
|
const { createEnvironment } = await import('../commands/store');
|
|
138
143
|
|
|
139
144
|
try {
|
|
140
|
-
await createEnvironment('bad-
|
|
145
|
+
await createEnvironment('bad-org', 'staging');
|
|
141
146
|
} catch {}
|
|
142
147
|
|
|
143
148
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
@@ -194,15 +199,15 @@ describe('Command Handlers', () => {
|
|
|
194
199
|
When('calling createApp', () => {
|
|
195
200
|
Then('it should register the hosted app via API', async () => {
|
|
196
201
|
const fetchMock = mockFetch({
|
|
197
|
-
'/environments/
|
|
202
|
+
'/environments/staging/apps': { app: fixtures.app },
|
|
198
203
|
});
|
|
199
204
|
|
|
200
205
|
const { createApp } = await import('../commands/app');
|
|
201
|
-
await createApp('pay-app', 'payment', '
|
|
206
|
+
await createApp('pay-app', 'payment', 'staging', 'dummy');
|
|
202
207
|
|
|
203
208
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
204
209
|
const [url, opts] = fetchMock.mock.calls[0];
|
|
205
|
-
expect(url).toContain('/environments/
|
|
210
|
+
expect(url).toContain('/environments/staging/apps');
|
|
206
211
|
expect(opts.method).toBe('POST');
|
|
207
212
|
});
|
|
208
213
|
});
|
|
@@ -216,7 +221,7 @@ describe('Command Handlers', () => {
|
|
|
216
221
|
const { createApp } = await import('../commands/app');
|
|
217
222
|
|
|
218
223
|
try {
|
|
219
|
-
await createApp('pay-app', 'payment', '
|
|
224
|
+
await createApp('pay-app', 'payment', 'staging', 'dummy');
|
|
220
225
|
} catch {}
|
|
221
226
|
|
|
222
227
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
@@ -36,30 +36,31 @@ describe('End-to-End Flows', () => {
|
|
|
36
36
|
describe('Store Creation Flow', () => {
|
|
37
37
|
Given('a valid token and working API', () => {
|
|
38
38
|
When('creating a store end-to-end', () => {
|
|
39
|
-
Then('it should call API and display
|
|
39
|
+
Then('it should call API and display project slug and dashboard URL', async () => {
|
|
40
40
|
const fetchMock = mockFetch({
|
|
41
|
-
'/
|
|
41
|
+
'/organizations': { organizations: [fixtures.organization] },
|
|
42
|
+
'/organizations/my-org/projects': { project: fixtures.project },
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
const { createStore } = await import('../commands/store');
|
|
45
46
|
await createStore('my-store', 'us-east-1');
|
|
46
47
|
|
|
47
|
-
expect(fetchMock).
|
|
48
|
+
expect(fetchMock).toHaveBeenCalled();
|
|
48
49
|
|
|
49
50
|
const allTui = tuiCalls.map(c => c.msg).join('\n');
|
|
50
|
-
expect(allTui).toContain('store
|
|
51
|
-
expect(allTui).toContain('cloud.saleor.io
|
|
51
|
+
expect(allTui).toContain('my-store');
|
|
52
|
+
expect(allTui).toContain('cloud.saleor.io');
|
|
52
53
|
});
|
|
53
54
|
});
|
|
54
55
|
});
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
describe('Store List Flow', () => {
|
|
58
|
-
Given('a valid token and multiple
|
|
59
|
+
Given('a valid token and multiple organizations', () => {
|
|
59
60
|
When('listing stores end-to-end', () => {
|
|
60
|
-
Then('it should display all
|
|
61
|
+
Then('it should display all organizations with emails and dates', async () => {
|
|
61
62
|
mockFetch({
|
|
62
|
-
'/
|
|
63
|
+
'/organizations': { organizations: [fixtures.organization, fixtures.organization2] },
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
const { listStores } = await import('../commands/store');
|
|
@@ -67,31 +68,31 @@ describe('End-to-End Flows', () => {
|
|
|
67
68
|
|
|
68
69
|
const allTui = tuiCalls.map(c => c.msg).join('\n');
|
|
69
70
|
const allConsole = console_.logs.join('\n');
|
|
70
|
-
expect(allTui).toContain('2
|
|
71
|
-
expect(allConsole).toContain('
|
|
72
|
-
expect(allConsole).toContain('
|
|
73
|
-
expect(allConsole).toContain('
|
|
74
|
-
expect(allConsole).toContain('
|
|
71
|
+
expect(allTui).toContain('2 organization(s)');
|
|
72
|
+
expect(allConsole).toContain('My Organization');
|
|
73
|
+
expect(allConsole).toContain('owner@example.com');
|
|
74
|
+
expect(allConsole).toContain('Other Organization');
|
|
75
|
+
expect(allConsole).toContain('other@example.com');
|
|
75
76
|
});
|
|
76
77
|
});
|
|
77
78
|
});
|
|
78
79
|
});
|
|
79
80
|
|
|
80
81
|
describe('Environment Creation Flow', () => {
|
|
81
|
-
Given('a valid token and existing
|
|
82
|
+
Given('a valid token and existing organization', () => {
|
|
82
83
|
When('creating an environment end-to-end', () => {
|
|
83
|
-
Then('it should call API and display environment
|
|
84
|
+
Then('it should call API and display environment key and domain', async () => {
|
|
84
85
|
const fetchMock = mockFetch({
|
|
85
|
-
'/
|
|
86
|
+
'/organizations/my-org/projects/default/environments': { environment: fixtures.environment },
|
|
86
87
|
});
|
|
87
88
|
|
|
88
89
|
const { createEnvironment } = await import('../commands/store');
|
|
89
|
-
await createEnvironment('
|
|
90
|
+
await createEnvironment('my-org', 'staging');
|
|
90
91
|
|
|
91
|
-
expect(fetchMock).
|
|
92
|
+
expect(fetchMock).toHaveBeenCalled();
|
|
92
93
|
|
|
93
94
|
const allTui = tuiCalls.map(c => c.msg).join('\n');
|
|
94
|
-
expect(allTui).toContain('
|
|
95
|
+
expect(allTui).toContain('staging');
|
|
95
96
|
expect(allTui).toContain('saleor.cloud');
|
|
96
97
|
});
|
|
97
98
|
});
|
|
@@ -103,11 +104,11 @@ describe('End-to-End Flows', () => {
|
|
|
103
104
|
When('creating a payment app with environment', () => {
|
|
104
105
|
Then('it should register via API and display app ID', async () => {
|
|
105
106
|
const fetchMock = mockFetch({
|
|
106
|
-
'/environments/
|
|
107
|
+
'/environments/staging/apps': { app: fixtures.app },
|
|
107
108
|
});
|
|
108
109
|
|
|
109
110
|
const { createApp } = await import('../commands/app');
|
|
110
|
-
await createApp('my-payment', 'payment', '
|
|
111
|
+
await createApp('my-payment', 'payment', 'staging', 'stripe');
|
|
111
112
|
|
|
112
113
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
113
114
|
|
|
@@ -138,7 +139,7 @@ describe('End-to-End Flows', () => {
|
|
|
138
139
|
When('attempting any store operation', () => {
|
|
139
140
|
Then('it should fail fast with auth error before API call', async () => {
|
|
140
141
|
withoutToken();
|
|
141
|
-
const fetchMock = mockFetch({ '/
|
|
142
|
+
const fetchMock = mockFetch({ '/organizations': { organizations: [] } });
|
|
142
143
|
|
|
143
144
|
const { listStores } = await import('../commands/store');
|
|
144
145
|
|
|
@@ -173,37 +174,55 @@ describe('End-to-End Flows', () => {
|
|
|
173
174
|
|
|
174
175
|
describe('API Client Request Construction', () => {
|
|
175
176
|
Given('a SaleorCloudClient', () => {
|
|
176
|
-
When('
|
|
177
|
+
When('listing organizations', () => {
|
|
178
|
+
Then('it should send correct URL, method, and headers', async () => {
|
|
179
|
+
const fetchMock = mockFetch({
|
|
180
|
+
'/organizations': { organizations: [fixtures.organization] },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const { SaleorCloudClient } = await import('../api/client');
|
|
184
|
+
const client = new SaleorCloudClient(fixtures.token);
|
|
185
|
+
await client.getOrganizations();
|
|
186
|
+
|
|
187
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
188
|
+
const [url, opts] = fetchMock.mock.calls[0];
|
|
189
|
+
expect(url).toBe('https://cloud.saleor.io/api/organizations');
|
|
190
|
+
expect(opts.method).toBe('GET');
|
|
191
|
+
expect(opts.headers.Authorization).toBe(`Token ${fixtures.token}`);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
When('creating a project', () => {
|
|
177
196
|
Then('it should send correct URL, method, headers, and body', async () => {
|
|
178
197
|
const fetchMock = mockFetch({
|
|
179
|
-
'/
|
|
198
|
+
'/organizations/default/projects': { project: fixtures.project },
|
|
180
199
|
});
|
|
181
200
|
|
|
182
201
|
const { SaleorCloudClient } = await import('../api/client');
|
|
183
202
|
const client = new SaleorCloudClient(fixtures.token);
|
|
184
|
-
await client.
|
|
203
|
+
await client.createProject('default', 'test-store', 'eu-west-1');
|
|
185
204
|
|
|
186
205
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
187
206
|
const [url, opts] = fetchMock.mock.calls[0];
|
|
188
|
-
expect(url).toBe('https://cloud.saleor.io/api/
|
|
207
|
+
expect(url).toBe('https://cloud.saleor.io/api/organizations/default/projects');
|
|
189
208
|
expect(opts.method).toBe('POST');
|
|
190
|
-
expect(opts.headers.Authorization).toBe(`
|
|
209
|
+
expect(opts.headers.Authorization).toBe(`Token ${fixtures.token}`);
|
|
191
210
|
expect(JSON.parse(opts.body)).toEqual({ name: 'test-store', region: 'eu-west-1' });
|
|
192
211
|
});
|
|
193
212
|
});
|
|
194
213
|
|
|
195
214
|
When('registering an app', () => {
|
|
196
|
-
Then('it should send correct URL with environment
|
|
215
|
+
Then('it should send correct URL with environment key', async () => {
|
|
197
216
|
const fetchMock = mockFetch({
|
|
198
|
-
'/environments/
|
|
217
|
+
'/environments/staging/apps': { app: fixtures.app },
|
|
199
218
|
});
|
|
200
219
|
|
|
201
220
|
const { SaleorCloudClient } = await import('../api/client');
|
|
202
221
|
const client = new SaleorCloudClient(fixtures.token);
|
|
203
|
-
await client.registerApp('
|
|
222
|
+
await client.registerApp('staging', 'payment', 'my-app');
|
|
204
223
|
|
|
205
224
|
const [url, opts] = fetchMock.mock.calls[0];
|
|
206
|
-
expect(url).toBe('https://cloud.saleor.io/api/environments/
|
|
225
|
+
expect(url).toBe('https://cloud.saleor.io/api/environments/staging/apps');
|
|
207
226
|
expect(JSON.parse(opts.body)).toEqual({ type: 'payment', name: 'my-app' });
|
|
208
227
|
});
|
|
209
228
|
});
|