xuanwu-cli 2.2.0 → 2.3.2
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/.env.test.example +14 -0
- package/__tests__/E2E_TEST_REPORT.md +206 -0
- package/__tests__/README.md +322 -0
- package/__tests__/TEST_SUMMARY.md +215 -0
- package/__tests__/global-setup.ts +13 -0
- package/__tests__/global-teardown.ts +3 -0
- package/__tests__/helpers/test-utils.ts +70 -0
- package/__tests__/integration/app.integration.test.ts +363 -0
- package/__tests__/integration/auth.integration.test.ts +243 -0
- package/__tests__/integration/build.integration.test.ts +215 -0
- package/__tests__/integration/e2e.test.ts +267 -0
- package/__tests__/integration/service.integration.test.ts +267 -0
- package/__tests__/integration/webhook.integration.test.ts +246 -0
- package/__tests__/run-e2e.js +360 -0
- package/__tests__/setup.ts +9 -0
- package/bin/xuanwu +0 -0
- package/dist/api/client.d.ts +23 -4
- package/dist/api/client.js +104 -29
- package/dist/commands/auth/login.js +5 -4
- package/dist/commands/deploy.js +25 -49
- package/dist/commands/env.js +31 -48
- package/dist/commands/project.d.ts +5 -0
- package/dist/commands/project.js +134 -0
- package/dist/config/types.d.ts +1 -0
- package/dist/index.js +2 -0
- package/jest.config.js +18 -0
- package/package.json +10 -2
- package/src/api/client.ts +128 -33
- package/src/commands/auth/login.ts +6 -4
- package/src/commands/deploy.ts +32 -49
- package/src/commands/env.ts +35 -52
- package/src/commands/project.ts +153 -0
- package/src/config/types.ts +1 -0
- package/src/index.ts +2 -0
- package/test/cli-integration.sh +245 -0
- package/test/integration.js +3 -3
- package/test/integration.sh +252 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 认证集成测试
|
|
3
|
+
* 测试 CLI 授权登录流程
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { APIClient } from '../../src/api/client'
|
|
7
|
+
import { SessionManager } from '../../src/lib/session'
|
|
8
|
+
import { Connection } from '../../src/config/types'
|
|
9
|
+
|
|
10
|
+
const API_URL = process.env.TEST_API_URL || 'https://i.xuanwu.dev.aimstek.cn'
|
|
11
|
+
const TEST_EMAIL = process.env.TEST_USER_EMAIL || ''
|
|
12
|
+
const TEST_PASSWORD = process.env.TEST_USER_PASSWORD || ''
|
|
13
|
+
|
|
14
|
+
describe('Authentication Integration Tests', () => {
|
|
15
|
+
let sessionManager: SessionManager
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
sessionManager = new SessionManager()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await sessionManager.clearSession()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('Device Authorization Flow', () => {
|
|
26
|
+
test('should generate device code successfully', async () => {
|
|
27
|
+
const response = await fetch(`${API_URL}/api/cli/auth/device-code`, {
|
|
28
|
+
method: 'POST'
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
expect(response.ok).toBeTruthy()
|
|
32
|
+
|
|
33
|
+
const data = await response.json() as { sessionId: string; loginUrl: string; code: string }
|
|
34
|
+
expect(data.sessionId).toBeDefined()
|
|
35
|
+
expect(data.loginUrl).toBeDefined()
|
|
36
|
+
expect(data.code).toBeDefined()
|
|
37
|
+
expect(data.loginUrl).toContain('session_id')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('should poll device auth status', async () => {
|
|
41
|
+
const deviceAuthRes = await fetch(`${API_URL}/api/cli/auth/device-code`, {
|
|
42
|
+
method: 'POST'
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const { sessionId } = await deviceAuthRes.json() as { sessionId: string }
|
|
46
|
+
|
|
47
|
+
const pollRes = await fetch(`${API_URL}/api/cli/auth/poll?session_id=${sessionId}`)
|
|
48
|
+
expect(pollRes.ok).toBeTruthy()
|
|
49
|
+
|
|
50
|
+
const pollData = await pollRes.json() as { status: string }
|
|
51
|
+
expect(['pending', 'expired']).toContain(pollData.status)
|
|
52
|
+
}, 10000)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('Email/Password Login', () => {
|
|
56
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should login with credentials successfully', async () => {
|
|
57
|
+
const response = await fetch(`${API_URL}/api/cli/auth/login`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
email: TEST_EMAIL,
|
|
62
|
+
password: TEST_PASSWORD,
|
|
63
|
+
deviceName: 'Test CLI',
|
|
64
|
+
expiresIn: '30d'
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(response.ok).toBeTruthy()
|
|
69
|
+
|
|
70
|
+
const data = await response.json() as { token: string; user: { id: string; name: string; email: string; role: string } }
|
|
71
|
+
expect(data.token).toBeDefined()
|
|
72
|
+
expect(data.user).toBeDefined()
|
|
73
|
+
expect(data.user.email).toBe(TEST_EMAIL)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('should fail with invalid credentials', async () => {
|
|
77
|
+
const response = await fetch(`${API_URL}/api/cli/auth/login`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
email: 'invalid@example.com',
|
|
82
|
+
password: 'wrongpassword',
|
|
83
|
+
deviceName: 'Test CLI'
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
expect(response.ok).toBeFalsy()
|
|
88
|
+
expect(response.status).toBe(401)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('Token Management', () => {
|
|
93
|
+
let testToken: string
|
|
94
|
+
let apiClient: APIClient
|
|
95
|
+
|
|
96
|
+
beforeAll(async () => {
|
|
97
|
+
if (TEST_EMAIL && TEST_PASSWORD) {
|
|
98
|
+
const response = await fetch(`${API_URL}/api/cli/auth/login`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
email: TEST_EMAIL,
|
|
103
|
+
password: TEST_PASSWORD,
|
|
104
|
+
deviceName: 'Test CLI',
|
|
105
|
+
expiresIn: '30d'
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
if (response.ok) {
|
|
110
|
+
const data = await response.json() as { token: string; user: { id: string } }
|
|
111
|
+
testToken = data.token
|
|
112
|
+
|
|
113
|
+
const conn: Connection = {
|
|
114
|
+
name: 'test',
|
|
115
|
+
endpoint: API_URL,
|
|
116
|
+
token: testToken,
|
|
117
|
+
isDefault: true
|
|
118
|
+
}
|
|
119
|
+
apiClient = new APIClient(conn)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should verify token successfully', async () => {
|
|
125
|
+
const response = await fetch(`${API_URL}/api/cli/auth/verify`, {
|
|
126
|
+
headers: {
|
|
127
|
+
'Authorization': `Bearer ${testToken}`
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
expect(response.ok).toBeTruthy()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should list tokens', async () => {
|
|
135
|
+
const response = await fetch(`${API_URL}/api/cli/auth/tokens`, {
|
|
136
|
+
headers: {
|
|
137
|
+
'Authorization': `Bearer ${testToken}`
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
expect(response.ok).toBeTruthy()
|
|
142
|
+
|
|
143
|
+
const data = await response.json() as { tokens: any[] }
|
|
144
|
+
expect(Array.isArray(data.tokens)).toBeTruthy()
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('Session Management', () => {
|
|
149
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should save and load session', async () => {
|
|
150
|
+
const response = await fetch(`${API_URL}/api/cli/auth/login`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { 'Content-Type': 'application/json' },
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
email: TEST_EMAIL,
|
|
155
|
+
password: TEST_PASSWORD,
|
|
156
|
+
deviceName: 'Test CLI',
|
|
157
|
+
expiresIn: '30d'
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const data = await response.json() as { token: string; user: { id: string; name: string; email: string; role: string } }
|
|
162
|
+
|
|
163
|
+
await sessionManager.saveSession({
|
|
164
|
+
token: data.token,
|
|
165
|
+
userId: data.user.id,
|
|
166
|
+
userName: data.user.name,
|
|
167
|
+
userEmail: data.user.email,
|
|
168
|
+
userRole: data.user.role,
|
|
169
|
+
deviceId: 'Test CLI',
|
|
170
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
171
|
+
apiUrl: API_URL
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const session = await sessionManager.loadSession()
|
|
175
|
+
expect(session).toBeDefined()
|
|
176
|
+
expect(session?.token).toBe(data.token)
|
|
177
|
+
expect(session?.userEmail).toBe(TEST_EMAIL)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('should clear session', async () => {
|
|
181
|
+
await sessionManager.saveSession({
|
|
182
|
+
token: 'test-token',
|
|
183
|
+
userId: 'test-user',
|
|
184
|
+
userName: 'Test User',
|
|
185
|
+
userEmail: 'test@example.com',
|
|
186
|
+
userRole: 'USER',
|
|
187
|
+
deviceId: 'Test Device',
|
|
188
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
189
|
+
apiUrl: API_URL
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
await sessionManager.clearSession()
|
|
193
|
+
|
|
194
|
+
const session = await sessionManager.loadSession()
|
|
195
|
+
expect(session).toBeNull()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('should detect expired session', () => {
|
|
199
|
+
const expiredSession = {
|
|
200
|
+
token: 'test-token',
|
|
201
|
+
userId: 'test-user',
|
|
202
|
+
userName: 'Test User',
|
|
203
|
+
userEmail: 'test@example.com',
|
|
204
|
+
userRole: 'USER',
|
|
205
|
+
deviceId: 'Test Device',
|
|
206
|
+
expiresAt: new Date(Date.now() - 1000).toISOString(),
|
|
207
|
+
apiUrl: API_URL
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
expect(sessionManager.isExpired(expiredSession)).toBeTruthy()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('should detect non-expired session', () => {
|
|
214
|
+
const validSession = {
|
|
215
|
+
token: 'test-token',
|
|
216
|
+
userId: 'test-user',
|
|
217
|
+
userName: 'Test User',
|
|
218
|
+
userEmail: 'test@example.com',
|
|
219
|
+
userRole: 'USER',
|
|
220
|
+
deviceId: 'Test Device',
|
|
221
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
222
|
+
apiUrl: API_URL
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
expect(sessionManager.isExpired(validSession)).toBeFalsy()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('should handle session without expiration', () => {
|
|
229
|
+
const permanentSession = {
|
|
230
|
+
token: 'test-token',
|
|
231
|
+
userId: 'test-user',
|
|
232
|
+
userName: 'Test User',
|
|
233
|
+
userEmail: 'test@example.com',
|
|
234
|
+
userRole: 'USER',
|
|
235
|
+
deviceId: 'Test Device',
|
|
236
|
+
expiresAt: null,
|
|
237
|
+
apiUrl: API_URL
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
expect(sessionManager.isExpired(permanentSession)).toBeFalsy()
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
})
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build 命令集成测试
|
|
3
|
+
* 测试 Build 相关的 CLI API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { APIClient } from '../../src/api/client'
|
|
7
|
+
import { Connection } from '../../src/config/types'
|
|
8
|
+
import { generateRandomCode } from '../helpers/test-utils'
|
|
9
|
+
|
|
10
|
+
const API_URL = process.env.TEST_API_URL || 'https://i.xuanwu.dev.aimstek.cn'
|
|
11
|
+
const TEST_EMAIL = process.env.TEST_USER_EMAIL || ''
|
|
12
|
+
const TEST_PASSWORD = process.env.TEST_USER_PASSWORD || ''
|
|
13
|
+
|
|
14
|
+
describe('Build Integration Tests', () => {
|
|
15
|
+
let apiClient: APIClient
|
|
16
|
+
let testToken: string
|
|
17
|
+
let testAppCode: string
|
|
18
|
+
let createdApps: string[] = []
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
if (!TEST_EMAIL || !TEST_PASSWORD) {
|
|
22
|
+
console.warn('Skipping tests: TEST_USER_EMAIL and TEST_USER_PASSWORD not set')
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const response = await fetch(`${API_URL}/api/cli/auth/login`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
email: TEST_EMAIL,
|
|
31
|
+
password: TEST_PASSWORD,
|
|
32
|
+
deviceName: 'Test CLI',
|
|
33
|
+
expiresIn: '30d'
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error('Failed to login')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const data = await response.json() as { token: string }
|
|
42
|
+
testToken = data.token
|
|
43
|
+
|
|
44
|
+
const conn: Connection = {
|
|
45
|
+
name: 'test',
|
|
46
|
+
endpoint: API_URL,
|
|
47
|
+
token: testToken,
|
|
48
|
+
isDefault: true
|
|
49
|
+
}
|
|
50
|
+
apiClient = new APIClient(conn)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
afterAll(async () => {
|
|
54
|
+
for (const code of createdApps) {
|
|
55
|
+
try {
|
|
56
|
+
await apiClient.deleteApplication(code)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`Failed to cleanup app ${code}:`, error)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('List Builds', () => {
|
|
64
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should list all builds', async () => {
|
|
65
|
+
const result = await apiClient.listBuilds()
|
|
66
|
+
|
|
67
|
+
expect(result.success).toBeTruthy()
|
|
68
|
+
expect(Array.isArray(result.data)).toBeTruthy()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should list builds with filter by app code', async () => {
|
|
72
|
+
testAppCode = generateRandomCode('build-test')
|
|
73
|
+
await apiClient.createApplication({
|
|
74
|
+
name: '构建测试应用',
|
|
75
|
+
code: testAppCode,
|
|
76
|
+
gitRepo: 'https://github.com/example/test.git'
|
|
77
|
+
})
|
|
78
|
+
createdApps.push(testAppCode)
|
|
79
|
+
|
|
80
|
+
await apiClient.triggerApplicationBuild(testAppCode)
|
|
81
|
+
|
|
82
|
+
const result = await apiClient.listBuilds({ appCode: testAppCode })
|
|
83
|
+
|
|
84
|
+
expect(result.success).toBeTruthy()
|
|
85
|
+
expect(Array.isArray(result.data)).toBeTruthy()
|
|
86
|
+
expect(result.data!.length).toBeGreaterThan(0)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should list builds with filter by status', async () => {
|
|
90
|
+
const result = await apiClient.listBuilds({ status: 'SUCCESS' })
|
|
91
|
+
|
|
92
|
+
expect(result.success).toBeTruthy()
|
|
93
|
+
expect(Array.isArray(result.data)).toBeTruthy()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should return empty array for non-existent app', async () => {
|
|
97
|
+
const result = await apiClient.listBuilds({ appCode: 'non-existent-app' })
|
|
98
|
+
|
|
99
|
+
expect(result.success).toBeTruthy()
|
|
100
|
+
expect(Array.isArray(result.data)).toBeTruthy()
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('Get Build', () => {
|
|
105
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should get build by id', async () => {
|
|
106
|
+
testAppCode = generateRandomCode('build-test')
|
|
107
|
+
await apiClient.createApplication({
|
|
108
|
+
name: '构建测试应用',
|
|
109
|
+
code: testAppCode,
|
|
110
|
+
gitRepo: 'https://github.com/example/test.git'
|
|
111
|
+
})
|
|
112
|
+
createdApps.push(testAppCode)
|
|
113
|
+
|
|
114
|
+
const buildResult = await apiClient.triggerApplicationBuild(testAppCode)
|
|
115
|
+
const buildId = buildResult.data!.id
|
|
116
|
+
|
|
117
|
+
const result = await apiClient.getBuild(buildId)
|
|
118
|
+
|
|
119
|
+
expect(result.success).toBeTruthy()
|
|
120
|
+
expect(result.data).toBeDefined()
|
|
121
|
+
expect(result.data?.id).toBe(buildId)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should return build with correct fields', async () => {
|
|
125
|
+
testAppCode = generateRandomCode('build-test')
|
|
126
|
+
await apiClient.createApplication({
|
|
127
|
+
name: '构建测试应用',
|
|
128
|
+
code: testAppCode,
|
|
129
|
+
gitRepo: 'https://github.com/example/test.git'
|
|
130
|
+
})
|
|
131
|
+
createdApps.push(testAppCode)
|
|
132
|
+
|
|
133
|
+
const buildResult = await apiClient.triggerApplicationBuild(testAppCode)
|
|
134
|
+
const buildId = buildResult.data!.id
|
|
135
|
+
|
|
136
|
+
const result = await apiClient.getBuild(buildId)
|
|
137
|
+
|
|
138
|
+
expect(result.success).toBeTruthy()
|
|
139
|
+
expect(result.data).toHaveProperty('id')
|
|
140
|
+
expect(result.data).toHaveProperty('buildNumber')
|
|
141
|
+
expect(result.data).toHaveProperty('status')
|
|
142
|
+
expect(result.data).toHaveProperty('createdAt')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should fail to get non-existent build', async () => {
|
|
146
|
+
const result = await apiClient.getBuild('non-existent-build-id')
|
|
147
|
+
|
|
148
|
+
expect(result.success).toBeFalsy()
|
|
149
|
+
expect(result.error).toBeDefined()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('Cancel Build', () => {
|
|
154
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should cancel running build', async () => {
|
|
155
|
+
testAppCode = generateRandomCode('build-test')
|
|
156
|
+
await apiClient.createApplication({
|
|
157
|
+
name: '构建测试应用',
|
|
158
|
+
code: testAppCode,
|
|
159
|
+
gitRepo: 'https://github.com/example/test.git'
|
|
160
|
+
})
|
|
161
|
+
createdApps.push(testAppCode)
|
|
162
|
+
|
|
163
|
+
const buildResult = await apiClient.triggerApplicationBuild(testAppCode)
|
|
164
|
+
const buildId = buildResult.data!.id
|
|
165
|
+
|
|
166
|
+
const result = await apiClient.cancelBuild(buildId)
|
|
167
|
+
|
|
168
|
+
expect(result.success).toBeTruthy()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should fail to cancel non-existent build', async () => {
|
|
172
|
+
const result = await apiClient.cancelBuild('non-existent-build-id')
|
|
173
|
+
|
|
174
|
+
expect(result.success).toBeFalsy()
|
|
175
|
+
expect(result.error).toBeDefined()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should fail to cancel completed build', async () => {
|
|
179
|
+
const buildsResult = await apiClient.listBuilds({ status: 'SUCCESS' })
|
|
180
|
+
|
|
181
|
+
if (!buildsResult.data || buildsResult.data.length === 0) {
|
|
182
|
+
console.warn('No completed builds found, skipping test')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const completedBuildId = buildsResult.data[0].id
|
|
187
|
+
const result = await apiClient.cancelBuild(completedBuildId)
|
|
188
|
+
|
|
189
|
+
expect(result.success).toBeFalsy()
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('Build Lifecycle', () => {
|
|
194
|
+
test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should track build status changes', async () => {
|
|
195
|
+
testAppCode = generateRandomCode('build-test')
|
|
196
|
+
await apiClient.createApplication({
|
|
197
|
+
name: '构建测试应用',
|
|
198
|
+
code: testAppCode,
|
|
199
|
+
gitRepo: 'https://github.com/example/test.git'
|
|
200
|
+
})
|
|
201
|
+
createdApps.push(testAppCode)
|
|
202
|
+
|
|
203
|
+
const buildResult = await apiClient.triggerApplicationBuild(testAppCode)
|
|
204
|
+
expect(buildResult.data?.status).toMatch(/PENDING|RUNNING/)
|
|
205
|
+
|
|
206
|
+
const buildId = buildResult.data!.id
|
|
207
|
+
|
|
208
|
+
let build = await apiClient.getBuild(buildId)
|
|
209
|
+
expect(['PENDING', 'RUNNING', 'SUCCESS', 'FAILED']).toContain(build.data?.status)
|
|
210
|
+
|
|
211
|
+
const builds = await apiClient.listApplicationBuilds(testAppCode)
|
|
212
|
+
expect(builds.data?.some(b => b.id === buildId)).toBeTruthy()
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
})
|