opensoma 0.5.0 → 0.6.0
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/dist/package.json +5 -1
- package/dist/src/agent-browser-launcher.d.ts +43 -0
- package/dist/src/agent-browser-launcher.d.ts.map +1 -0
- package/dist/src/agent-browser-launcher.js +97 -0
- package/dist/src/agent-browser-launcher.js.map +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +36 -7
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +231 -63
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/agent-browser.d.ts +3 -0
- package/dist/src/commands/agent-browser.d.ts.map +1 -0
- package/dist/src/commands/agent-browser.js +27 -0
- package/dist/src/commands/agent-browser.js.map +1 -0
- package/dist/src/commands/auth.d.ts +1 -1
- package/dist/src/commands/auth.d.ts.map +1 -1
- package/dist/src/commands/auth.js +4 -2
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/dashboard.d.ts +13 -0
- package/dist/src/commands/dashboard.d.ts.map +1 -1
- package/dist/src/commands/dashboard.js +10 -18
- package/dist/src/commands/dashboard.js.map +1 -1
- package/dist/src/commands/helpers.d.ts +1 -1
- package/dist/src/commands/helpers.d.ts.map +1 -1
- package/dist/src/commands/helpers.js +2 -2
- package/dist/src/commands/helpers.js.map +1 -1
- package/dist/src/commands/index.d.ts +2 -1
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/mentoring.d.ts.map +1 -1
- package/dist/src/commands/mentoring.js +54 -29
- package/dist/src/commands/mentoring.js.map +1 -1
- package/dist/src/commands/notice.d.ts.map +1 -1
- package/dist/src/commands/notice.js +2 -1
- package/dist/src/commands/notice.js.map +1 -1
- package/dist/src/commands/report.d.ts.map +1 -1
- package/dist/src/commands/report.js +4 -2
- package/dist/src/commands/report.js.map +1 -1
- package/dist/src/commands/room.d.ts.map +1 -1
- package/dist/src/commands/room.js +125 -2
- package/dist/src/commands/room.js.map +1 -1
- package/dist/src/commands/schedule.d.ts +3 -0
- package/dist/src/commands/schedule.d.ts.map +1 -0
- package/dist/src/commands/schedule.js +27 -0
- package/dist/src/commands/schedule.js.map +1 -0
- package/dist/src/commands/team.d.ts.map +1 -1
- package/dist/src/commands/team.js +55 -4
- package/dist/src/commands/team.js.map +1 -1
- package/dist/src/constants.d.ts +5 -5
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +20 -8
- package/dist/src/constants.js.map +1 -1
- package/dist/src/credential-manager.d.ts +9 -0
- package/dist/src/credential-manager.d.ts.map +1 -1
- package/dist/src/credential-manager.js +24 -0
- package/dist/src/credential-manager.js.map +1 -1
- package/dist/src/formatters.d.ts +11 -3
- package/dist/src/formatters.d.ts.map +1 -1
- package/dist/src/formatters.js +281 -52
- package/dist/src/formatters.js.map +1 -1
- package/dist/src/http.d.ts +8 -0
- package/dist/src/http.d.ts.map +1 -1
- package/dist/src/http.js +29 -1
- package/dist/src/http.js.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/shared/utils/swmaestro.d.ts +34 -1
- package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
- package/dist/src/shared/utils/swmaestro.js +102 -43
- package/dist/src/shared/utils/swmaestro.js.map +1 -1
- package/dist/src/shared/utils/team-action-params.d.ts +3 -0
- package/dist/src/shared/utils/team-action-params.d.ts.map +1 -0
- package/dist/src/shared/utils/team-action-params.js +10 -0
- package/dist/src/shared/utils/team-action-params.js.map +1 -0
- package/dist/src/shared/utils/team-params.d.ts +12 -0
- package/dist/src/shared/utils/team-params.d.ts.map +1 -0
- package/dist/src/shared/utils/team-params.js +38 -0
- package/dist/src/shared/utils/team-params.js.map +1 -0
- package/dist/src/types.d.ts +147 -10
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +74 -6
- package/dist/src/types.js.map +1 -1
- package/package.json +5 -1
- package/src/agent-browser-launcher.test.ts +263 -0
- package/src/agent-browser-launcher.ts +159 -0
- package/src/cli.ts +4 -2
- package/src/client.test.ts +801 -140
- package/src/client.ts +293 -79
- package/src/commands/agent-browser.ts +33 -0
- package/src/commands/auth.test.ts +83 -32
- package/src/commands/auth.ts +5 -3
- package/src/commands/dashboard.test.ts +57 -0
- package/src/commands/dashboard.ts +22 -19
- package/src/commands/helpers.test.ts +79 -32
- package/src/commands/helpers.ts +3 -3
- package/src/commands/index.ts +2 -1
- package/src/commands/mentoring.ts +60 -29
- package/src/commands/notice.ts +2 -1
- package/src/commands/report.test.ts +7 -7
- package/src/commands/report.ts +4 -2
- package/src/commands/room.ts +160 -1
- package/src/commands/schedule.ts +32 -0
- package/src/commands/team.ts +73 -5
- package/src/constants.ts +20 -8
- package/src/credential-manager.test.ts +49 -5
- package/src/credential-manager.ts +27 -0
- package/src/formatters.test.ts +548 -53
- package/src/formatters.ts +309 -55
- package/src/http.test.ts +108 -39
- package/src/http.ts +41 -2
- package/src/index.ts +10 -1
- package/src/shared/utils/mentoring-params.test.ts +16 -16
- package/src/shared/utils/swmaestro.test.ts +326 -11
- package/src/shared/utils/swmaestro.ts +150 -52
- package/src/shared/utils/team-action-params.test.ts +32 -0
- package/src/shared/utils/team-action-params.ts +10 -0
- package/src/shared/utils/team-params.test.ts +141 -0
- package/src/shared/utils/team-params.ts +53 -0
- package/src/shared/utils/toz.test.ts +12 -7
- package/src/token-extractor.test.ts +12 -12
- package/src/toz-http.test.ts +11 -11
- package/src/types.test.ts +235 -206
- package/src/types.ts +87 -7
- package/dist/src/commands/event.d.ts +0 -3
- package/dist/src/commands/event.d.ts.map +0 -1
- package/dist/src/commands/event.js +0 -58
- package/dist/src/commands/event.js.map +0 -1
- package/src/commands/event.ts +0 -73
package/src/http.test.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { afterEach, describe, expect,
|
|
1
|
+
import { afterEach, describe, expect, it, mock } from 'bun:test'
|
|
2
2
|
|
|
3
3
|
import { MENU_NO } from './constants'
|
|
4
4
|
import { AuthenticationError } from './errors'
|
|
5
|
-
import { SomaHttp } from './http'
|
|
5
|
+
import { SomaHttp, UserGb } from './http'
|
|
6
6
|
|
|
7
7
|
const originalFetch = globalThis.fetch
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ afterEach(() => {
|
|
|
12
12
|
})
|
|
13
13
|
|
|
14
14
|
describe('SomaHttp', () => {
|
|
15
|
-
|
|
15
|
+
it('sends query params on GET and stores returned cookies', async () => {
|
|
16
16
|
const fetchMock = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
17
17
|
expect(String(input)).toBe(`https://www.swmaestro.ai/sw/member/user/forLogin.do?menuNo=${MENU_NO.LOGIN}`)
|
|
18
18
|
expect(init?.headers).toEqual({
|
|
@@ -32,7 +32,7 @@ describe('SomaHttp', () => {
|
|
|
32
32
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
it('encodes the POST body and injects the CSRF token', async () => {
|
|
36
36
|
const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
37
37
|
expect(init?.method).toBe('POST')
|
|
38
38
|
expect(init?.headers).toEqual({
|
|
@@ -53,7 +53,7 @@ describe('SomaHttp', () => {
|
|
|
53
53
|
expect(html).toBe('<html>ok</html>')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
it('surfaces alert() errors from script tags that have attributes', async () => {
|
|
57
57
|
const fetchMock = mock(async () =>
|
|
58
58
|
createResponse(
|
|
59
59
|
`<html><head><title>빈페이지</title></head><body><script type='text/javascript'>alert('아이디 혹은 비밀번호가 일치 하지 않습니다.');location.href='/login';</script></body></html>`,
|
|
@@ -68,7 +68,7 @@ describe('SomaHttp', () => {
|
|
|
68
68
|
)
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
it('surfaces alert() errors that are followed by history.back()', async () => {
|
|
72
72
|
const fetchMock = mock(async () =>
|
|
73
73
|
createResponse(
|
|
74
74
|
`<html><head></head><body><script language='JavaScript'>\nalert('잘못된 접근입니다.');\nhistory.back();\n</script></body></html>`,
|
|
@@ -81,7 +81,47 @@ describe('SomaHttp', () => {
|
|
|
81
81
|
await expect(http.post('/mypage/test.do', {})).rejects.toThrow('잘못된 접근입니다.')
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
it('treats success-indicator alerts (정상적으로 등록하였습니다) as non-errors', async () => {
|
|
85
|
+
const fetchMock = mock(async () =>
|
|
86
|
+
createResponse(
|
|
87
|
+
`<html><body><script type='text/javascript'>alert('정상적으로 등록하였습니다.');location.href='/sw/mypage/itemRent/list.do';</script></body></html>`,
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
globalThis.fetch = fetchMock as typeof fetch
|
|
91
|
+
|
|
92
|
+
const http = new SomaHttp({ sessionCookie: 'session-1', csrfToken: 'csrf-1' })
|
|
93
|
+
|
|
94
|
+
const html = await http.post('/mypage/itemRent/insert.do', {})
|
|
95
|
+
expect(html).toContain('정상적으로 등록하였습니다')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('treats success-indicator alerts (완료되었습니다) as non-errors', async () => {
|
|
99
|
+
const fetchMock = mock(async () =>
|
|
100
|
+
createResponse(
|
|
101
|
+
`<html><body><script>alert('예약이 완료되었습니다.');location.href='/sw/mypage/itemRent/list.do';</script></body></html>`,
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
globalThis.fetch = fetchMock as typeof fetch
|
|
105
|
+
|
|
106
|
+
const http = new SomaHttp({ sessionCookie: 'session-1', csrfToken: 'csrf-1' })
|
|
107
|
+
|
|
108
|
+
await expect(http.post('/mypage/itemRent/insert.do', {})).resolves.toBeDefined()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('still surfaces non-success alerts when followed by location.href', async () => {
|
|
112
|
+
const fetchMock = mock(async () =>
|
|
113
|
+
createResponse(
|
|
114
|
+
`<html><body><script>alert('이미 예약된 시간입니다.');location.href='/sw/mypage/itemRent/list.do';</script></body></html>`,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
globalThis.fetch = fetchMock as typeof fetch
|
|
118
|
+
|
|
119
|
+
const http = new SomaHttp({ sessionCookie: 'session-1', csrfToken: 'csrf-1' })
|
|
120
|
+
|
|
121
|
+
await expect(http.post('/mypage/itemRent/insert.do', {})).rejects.toThrow('이미 예약된 시간입니다.')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('ignores alert() calls nested inside function bodies (form validation scripts)', async () => {
|
|
85
125
|
const pageWithValidationScript = `<html><head><title>AI·SW마에스트로 서울</title></head><body>
|
|
86
126
|
<ul class="bbs-reserve"><li class="item">room data</li></ul>
|
|
87
127
|
<script>
|
|
@@ -106,7 +146,7 @@ describe('SomaHttp', () => {
|
|
|
106
146
|
})
|
|
107
147
|
|
|
108
148
|
describe('postMultipart', () => {
|
|
109
|
-
|
|
149
|
+
it('passes the FormData instance through to fetch', async () => {
|
|
110
150
|
const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
111
151
|
expect(init?.body).toBeInstanceOf(FormData)
|
|
112
152
|
return createResponse('<html>ok</html>')
|
|
@@ -118,7 +158,7 @@ describe('SomaHttp', () => {
|
|
|
118
158
|
await expect(http.postMultipart('/test', new FormData())).resolves.toBe('<html>ok</html>')
|
|
119
159
|
})
|
|
120
160
|
|
|
121
|
-
|
|
161
|
+
it('does not set the Content-Type header manually (lets fetch set the boundary)', async () => {
|
|
122
162
|
const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
123
163
|
expect(init?.headers).toEqual({
|
|
124
164
|
'Accept-Language': 'ko,en-US;q=0.9,en;q=0.8',
|
|
@@ -139,7 +179,7 @@ describe('SomaHttp', () => {
|
|
|
139
179
|
await http.postMultipart('/test', new FormData())
|
|
140
180
|
})
|
|
141
181
|
|
|
142
|
-
|
|
182
|
+
it('appends the CSRF token to the FormData body', async () => {
|
|
143
183
|
const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
144
184
|
const body = init?.body
|
|
145
185
|
expect(body).toBeInstanceOf(FormData)
|
|
@@ -153,7 +193,7 @@ describe('SomaHttp', () => {
|
|
|
153
193
|
await http.postMultipart('/test', new FormData())
|
|
154
194
|
})
|
|
155
195
|
|
|
156
|
-
|
|
196
|
+
it('follows redirects manually after a multipart POST', async () => {
|
|
157
197
|
const fetchMock = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
158
198
|
const url = String(input)
|
|
159
199
|
|
|
@@ -189,7 +229,7 @@ describe('SomaHttp', () => {
|
|
|
189
229
|
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
190
230
|
})
|
|
191
231
|
|
|
192
|
-
|
|
232
|
+
it('keeps the existing post() behavior unchanged', async () => {
|
|
193
233
|
const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
194
234
|
expect(init?.method).toBe('POST')
|
|
195
235
|
expect(init?.headers).toEqual({
|
|
@@ -211,7 +251,7 @@ describe('SomaHttp', () => {
|
|
|
211
251
|
})
|
|
212
252
|
|
|
213
253
|
describe('postForm', () => {
|
|
214
|
-
|
|
254
|
+
it('converts a Record body to FormData and delegates to postMultipart', async () => {
|
|
215
255
|
const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
216
256
|
const body = init?.body
|
|
217
257
|
expect(body).toBeInstanceOf(FormData)
|
|
@@ -230,7 +270,7 @@ describe('SomaHttp', () => {
|
|
|
230
270
|
).resolves.toBe('<html>ok</html>')
|
|
231
271
|
})
|
|
232
272
|
|
|
233
|
-
|
|
273
|
+
it('does not set the Content-Type header so FormData can set the multipart boundary', async () => {
|
|
234
274
|
const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
235
275
|
const headers = init?.headers as Record<string, string> | undefined
|
|
236
276
|
expect(headers?.['Content-Type']).toBeUndefined()
|
|
@@ -245,7 +285,7 @@ describe('SomaHttp', () => {
|
|
|
245
285
|
})
|
|
246
286
|
})
|
|
247
287
|
|
|
248
|
-
|
|
288
|
+
it('returns the parsed JSON body from postJson', async () => {
|
|
249
289
|
const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
250
290
|
expect(init?.headers).toEqual({
|
|
251
291
|
'Accept-Language': 'ko,en-US;q=0.9,en;q=0.8',
|
|
@@ -267,7 +307,7 @@ describe('SomaHttp', () => {
|
|
|
267
307
|
expect(json).toEqual({ resultCode: 'SUCCESS' })
|
|
268
308
|
})
|
|
269
309
|
|
|
270
|
-
|
|
310
|
+
it('fetches the CSRF token before posting credentials during login', async () => {
|
|
271
311
|
const fetchMock = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
272
312
|
const url = String(input)
|
|
273
313
|
|
|
@@ -309,12 +349,17 @@ describe('SomaHttp', () => {
|
|
|
309
349
|
expect(http.getCsrfToken()).toBe('csrf-login')
|
|
310
350
|
})
|
|
311
351
|
|
|
312
|
-
|
|
352
|
+
it('returns the user identity when logged in and null when not', async () => {
|
|
313
353
|
const loggedInMock = mock(async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
|
314
354
|
createResponse(
|
|
315
355
|
JSON.stringify({
|
|
316
356
|
resultCode: 'fail',
|
|
317
|
-
userVO: {
|
|
357
|
+
userVO: {
|
|
358
|
+
userId: 'user@example.com',
|
|
359
|
+
userNm: 'Test',
|
|
360
|
+
userNo: 'a1b2c3',
|
|
361
|
+
userGb: 'T',
|
|
362
|
+
},
|
|
318
363
|
}),
|
|
319
364
|
[],
|
|
320
365
|
'application/json',
|
|
@@ -325,6 +370,8 @@ describe('SomaHttp', () => {
|
|
|
325
370
|
await expect(new SomaHttp().checkLogin()).resolves.toEqual({
|
|
326
371
|
userId: 'user@example.com',
|
|
327
372
|
userNm: 'Test',
|
|
373
|
+
userNo: 'a1b2c3',
|
|
374
|
+
userGb: UserGb.Mentor,
|
|
328
375
|
})
|
|
329
376
|
expect(loggedInMock).toHaveBeenCalledWith('https://www.swmaestro.ai/sw/member/user/checkLogin.json', {
|
|
330
377
|
method: 'GET',
|
|
@@ -345,7 +392,29 @@ describe('SomaHttp', () => {
|
|
|
345
392
|
await expect(new SomaHttp().checkLogin()).resolves.toBeNull()
|
|
346
393
|
})
|
|
347
394
|
|
|
348
|
-
|
|
395
|
+
it('maps trainee userGb responses to the UserGb enum', async () => {
|
|
396
|
+
globalThis.fetch = mock(async () =>
|
|
397
|
+
createResponse(
|
|
398
|
+
JSON.stringify({
|
|
399
|
+
resultCode: 'fail',
|
|
400
|
+
userVO: {
|
|
401
|
+
userId: 'trainee@example.com',
|
|
402
|
+
userNm: 'Trainee',
|
|
403
|
+
userNo: 'u-1',
|
|
404
|
+
userGb: 'C',
|
|
405
|
+
},
|
|
406
|
+
}),
|
|
407
|
+
[],
|
|
408
|
+
'application/json',
|
|
409
|
+
),
|
|
410
|
+
) as typeof fetch
|
|
411
|
+
|
|
412
|
+
await expect(new SomaHttp().checkLogin()).resolves.toMatchObject({
|
|
413
|
+
userGb: UserGb.Trainee,
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('returns null from checkLogin when the server redirects to the login page', async () => {
|
|
349
418
|
const fetchMock = mock(async () =>
|
|
350
419
|
createResponse('', [], 'text/html', {
|
|
351
420
|
status: 302,
|
|
@@ -357,7 +426,7 @@ describe('SomaHttp', () => {
|
|
|
357
426
|
await expect(new SomaHttp({ sessionCookie: 'session-1' }).checkLogin()).resolves.toBeNull()
|
|
358
427
|
})
|
|
359
428
|
|
|
360
|
-
|
|
429
|
+
it('returns null from checkLogin when the server serves login HTML instead of JSON', async () => {
|
|
361
430
|
const fetchMock = mock(async () =>
|
|
362
431
|
createResponse(
|
|
363
432
|
'<html><head><title>AI·SW마에스트로</title></head><body><form><input name="username"><input name="password"></form></body></html>',
|
|
@@ -368,7 +437,7 @@ describe('SomaHttp', () => {
|
|
|
368
437
|
await expect(new SomaHttp({ sessionCookie: 'session-1' }).checkLogin()).resolves.toBeNull()
|
|
369
438
|
})
|
|
370
439
|
|
|
371
|
-
|
|
440
|
+
it('calls the logout endpoint', async () => {
|
|
372
441
|
const fetchMock = mock(async (input: RequestInfo | URL) => {
|
|
373
442
|
expect(String(input)).toBe('https://www.swmaestro.ai/sw/member/user/logout.do')
|
|
374
443
|
return createResponse('<html>bye</html>')
|
|
@@ -380,7 +449,7 @@ describe('SomaHttp', () => {
|
|
|
380
449
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
381
450
|
})
|
|
382
451
|
|
|
383
|
-
|
|
452
|
+
it('reads the CSRF token from a hidden input field', async () => {
|
|
384
453
|
const fetchMock = mock(async () => createResponse('<input type="hidden" name="csrfToken" value="csrf-token">'))
|
|
385
454
|
globalThis.fetch = fetchMock as typeof fetch
|
|
386
455
|
|
|
@@ -397,7 +466,7 @@ describe('SomaHttp', () => {
|
|
|
397
466
|
|
|
398
467
|
const expiredMessage = '잘못된 접근입니다. 해당 세션을 전체 초기화 하였습니다.'
|
|
399
468
|
|
|
400
|
-
|
|
469
|
+
it('throws AuthenticationError from post when alert indicates session expiry', async () => {
|
|
401
470
|
const fetchMock = mock(async () => createResponse(sessionExpiredAlert(expiredMessage)))
|
|
402
471
|
globalThis.fetch = fetchMock as typeof fetch
|
|
403
472
|
|
|
@@ -406,7 +475,7 @@ describe('SomaHttp', () => {
|
|
|
406
475
|
await expect(http.post('/mypage/officeMng/list.do', { menuNo: '200058' })).rejects.toThrow(AuthenticationError)
|
|
407
476
|
})
|
|
408
477
|
|
|
409
|
-
|
|
478
|
+
it('throws AuthenticationError from get when alert indicates session expiry', async () => {
|
|
410
479
|
const fetchMock = mock(async () => createResponse(sessionExpiredAlert(expiredMessage)))
|
|
411
480
|
globalThis.fetch = fetchMock as typeof fetch
|
|
412
481
|
|
|
@@ -415,7 +484,7 @@ describe('SomaHttp', () => {
|
|
|
415
484
|
await expect(http.get('/mypage/officeMng/list.do')).rejects.toThrow(AuthenticationError)
|
|
416
485
|
})
|
|
417
486
|
|
|
418
|
-
|
|
487
|
+
it('throws AuthenticationError from postMultipart when alert indicates session expiry', async () => {
|
|
419
488
|
const fetchMock = mock(async () => createResponse(sessionExpiredAlert(expiredMessage)))
|
|
420
489
|
globalThis.fetch = fetchMock as typeof fetch
|
|
421
490
|
|
|
@@ -424,7 +493,7 @@ describe('SomaHttp', () => {
|
|
|
424
493
|
await expect(http.postMultipart('/mypage/test.do', new FormData())).rejects.toThrow(AuthenticationError)
|
|
425
494
|
})
|
|
426
495
|
|
|
427
|
-
|
|
496
|
+
it('treats alerts containing "세션" plus an invalidation keyword as auth errors', async () => {
|
|
428
497
|
const variants = ['세션이 만료되었습니다.', '해당 세션을 초기화하였습니다.', '세션 정보가 유효하지 않습니다.']
|
|
429
498
|
|
|
430
499
|
for (const message of variants) {
|
|
@@ -436,7 +505,7 @@ describe('SomaHttp', () => {
|
|
|
436
505
|
}
|
|
437
506
|
})
|
|
438
507
|
|
|
439
|
-
|
|
508
|
+
it('does not treat an alert containing "세션" without an invalidation keyword as an auth error', async () => {
|
|
440
509
|
const fetchMock = mock(async () => createResponse(sessionExpiredAlert('멘토링 세션이 이미 마감되었습니다.')))
|
|
441
510
|
globalThis.fetch = fetchMock as typeof fetch
|
|
442
511
|
|
|
@@ -445,7 +514,7 @@ describe('SomaHttp', () => {
|
|
|
445
514
|
await expect(http.post('/mypage/test.do', {})).rejects.toThrow('멘토링 세션이 이미 마감되었습니다.')
|
|
446
515
|
})
|
|
447
516
|
|
|
448
|
-
|
|
517
|
+
it('throws a regular Error from post when the alert is unrelated to the session', async () => {
|
|
449
518
|
const fetchMock = mock(async () => createResponse(sessionExpiredAlert('잘못된 접근입니다.')))
|
|
450
519
|
globalThis.fetch = fetchMock as typeof fetch
|
|
451
520
|
|
|
@@ -454,7 +523,7 @@ describe('SomaHttp', () => {
|
|
|
454
523
|
await expect(http.post('/mypage/test.do', {})).rejects.toThrow('잘못된 접근입니다.')
|
|
455
524
|
})
|
|
456
525
|
|
|
457
|
-
|
|
526
|
+
it('throws a regular Error from get when the alert is unrelated to the session', async () => {
|
|
458
527
|
const fetchMock = mock(async () => createResponse(sessionExpiredAlert('잘못된 접근입니다.')))
|
|
459
528
|
globalThis.fetch = fetchMock as typeof fetch
|
|
460
529
|
|
|
@@ -463,7 +532,7 @@ describe('SomaHttp', () => {
|
|
|
463
532
|
await expect(http.get('/mypage/test.do')).rejects.toThrow('잘못된 접근입니다.')
|
|
464
533
|
})
|
|
465
534
|
|
|
466
|
-
|
|
535
|
+
it('detects session-expired alerts that use double quotes', async () => {
|
|
467
536
|
const doubleQuoteAlert = `<html><body><script language='JavaScript'>\nalert("${expiredMessage}");\nhistory.back();\n</script></body></html>`
|
|
468
537
|
const fetchMock = mock(async () => createResponse(doubleQuoteAlert))
|
|
469
538
|
globalThis.fetch = fetchMock as typeof fetch
|
|
@@ -480,7 +549,7 @@ describe('SomaHttp', () => {
|
|
|
480
549
|
})
|
|
481
550
|
const genericErrorJson = JSON.stringify({ error: '처리 중 오류가 발생했습니다.' })
|
|
482
551
|
|
|
483
|
-
|
|
552
|
+
it('throws AuthenticationError from post when the JSON response indicates session expiry', async () => {
|
|
484
553
|
const fetchMock = mock(async () => createResponse(sessionExpiredJson, [], 'application/json'))
|
|
485
554
|
globalThis.fetch = fetchMock as typeof fetch
|
|
486
555
|
|
|
@@ -489,7 +558,7 @@ describe('SomaHttp', () => {
|
|
|
489
558
|
await expect(http.post('/mypage/officeMng/list.do', { menuNo: '200058' })).rejects.toThrow(AuthenticationError)
|
|
490
559
|
})
|
|
491
560
|
|
|
492
|
-
|
|
561
|
+
it('throws a regular Error from post for non-session JSON errors', async () => {
|
|
493
562
|
const fetchMock = mock(async () => createResponse(genericErrorJson, [], 'application/json'))
|
|
494
563
|
globalThis.fetch = fetchMock as typeof fetch
|
|
495
564
|
|
|
@@ -498,7 +567,7 @@ describe('SomaHttp', () => {
|
|
|
498
567
|
await expect(http.post('/mypage/test.do', {})).rejects.toThrow('처리 중 오류가 발생했습니다.')
|
|
499
568
|
})
|
|
500
569
|
|
|
501
|
-
|
|
570
|
+
it('throws AuthenticationError from postJson when the JSON response indicates session expiry', async () => {
|
|
502
571
|
const fetchMock = mock(async () => createResponse(sessionExpiredJson, [], 'application/json'))
|
|
503
572
|
globalThis.fetch = fetchMock as typeof fetch
|
|
504
573
|
|
|
@@ -509,7 +578,7 @@ describe('SomaHttp', () => {
|
|
|
509
578
|
)
|
|
510
579
|
})
|
|
511
580
|
|
|
512
|
-
|
|
581
|
+
it('throws a regular Error from postJson for non-session JSON errors', async () => {
|
|
513
582
|
const fetchMock = mock(async () => createResponse(genericErrorJson, [], 'application/json'))
|
|
514
583
|
globalThis.fetch = fetchMock as typeof fetch
|
|
515
584
|
|
|
@@ -518,7 +587,7 @@ describe('SomaHttp', () => {
|
|
|
518
587
|
await expect(http.postJson('/mypage/test.do', {})).rejects.toThrow('처리 중 오류가 발생했습니다.')
|
|
519
588
|
})
|
|
520
589
|
|
|
521
|
-
|
|
590
|
+
it('still parses valid JSON from postJson when no error field is present', async () => {
|
|
522
591
|
const fetchMock = mock(async () =>
|
|
523
592
|
createResponse(JSON.stringify({ resultCode: 'SUCCESS' }), [], 'application/json'),
|
|
524
593
|
)
|
|
@@ -529,7 +598,7 @@ describe('SomaHttp', () => {
|
|
|
529
598
|
await expect(http.postJson('/mypage/test.do', {})).resolves.toEqual({ resultCode: 'SUCCESS' })
|
|
530
599
|
})
|
|
531
600
|
|
|
532
|
-
|
|
601
|
+
it('does not false-positive on non-JSON bodies that happen to start with "{"', async () => {
|
|
533
602
|
const fetchMock = mock(async () => createResponse('{malformed json', [], 'text/html'))
|
|
534
603
|
globalThis.fetch = fetchMock as typeof fetch
|
|
535
604
|
|
|
@@ -538,7 +607,7 @@ describe('SomaHttp', () => {
|
|
|
538
607
|
await expect(http.get('/test')).resolves.toBe('{malformed json')
|
|
539
608
|
})
|
|
540
609
|
|
|
541
|
-
|
|
610
|
+
it('still detects JSON error responses that have leading whitespace', async () => {
|
|
542
611
|
const paddedJson = ` \n ${sessionExpiredJson}`
|
|
543
612
|
const fetchMock = mock(async () => createResponse(paddedJson, [], 'application/json'))
|
|
544
613
|
globalThis.fetch = fetchMock as typeof fetch
|
|
@@ -548,7 +617,7 @@ describe('SomaHttp', () => {
|
|
|
548
617
|
await expect(http.post('/mypage/test.do', {})).rejects.toThrow(AuthenticationError)
|
|
549
618
|
})
|
|
550
619
|
|
|
551
|
-
|
|
620
|
+
it('throws a regular Error from get for non-session JSON errors', async () => {
|
|
552
621
|
const fetchMock = mock(async () => createResponse(genericErrorJson, [], 'application/json'))
|
|
553
622
|
globalThis.fetch = fetchMock as typeof fetch
|
|
554
623
|
|
|
@@ -557,7 +626,7 @@ describe('SomaHttp', () => {
|
|
|
557
626
|
await expect(http.get('/mypage/test.do')).rejects.toThrow('처리 중 오류가 발생했습니다.')
|
|
558
627
|
})
|
|
559
628
|
|
|
560
|
-
|
|
629
|
+
it('throws AuthenticationError from postJson when the response is an HTML login page', async () => {
|
|
561
630
|
const loginPageHtml =
|
|
562
631
|
'<html><head><title>AI·SW마에스트로</title></head><body><form><input name="username"><input name="password"></form></body></html>'
|
|
563
632
|
const fetchMock = mock(async () => createResponse(loginPageHtml))
|
package/src/http.ts
CHANGED
|
@@ -15,12 +15,26 @@ interface RequestOptions {
|
|
|
15
15
|
|
|
16
16
|
interface CheckLoginResponse {
|
|
17
17
|
resultCode?: string
|
|
18
|
-
userVO?: {
|
|
18
|
+
userVO?: {
|
|
19
|
+
userId?: string
|
|
20
|
+
userNm?: string
|
|
21
|
+
userSn?: number
|
|
22
|
+
userNo?: string
|
|
23
|
+
userGb?: string
|
|
24
|
+
}
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export interface UserIdentity {
|
|
22
28
|
userId: string
|
|
23
29
|
userNm: string
|
|
30
|
+
userNo: string
|
|
31
|
+
userGb: UserGb
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export enum UserGb {
|
|
35
|
+
Unknown = '',
|
|
36
|
+
Trainee = 'C',
|
|
37
|
+
Mentor = 'T',
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
interface HeadersWithCookieHelpers extends Omit<Headers, 'getSetCookie'> {
|
|
@@ -231,6 +245,17 @@ export class SomaHttp {
|
|
|
231
245
|
return message.includes('세션') && /초기화|만료|유효하지/.test(message)
|
|
232
246
|
}
|
|
233
247
|
|
|
248
|
+
private isSuccessAlert(message: string): boolean {
|
|
249
|
+
// SWMaestro returns alert() scripts for both errors and successes (e.g. after room
|
|
250
|
+
// reservation the server responds with alert('정상적으로 등록하였습니다.');location.href=...).
|
|
251
|
+
// These success alerts must not be surfaced as errors to callers. The server emits
|
|
252
|
+
// variants with or without whitespace between the verb and 되었습니다 (e.g. "수정 되었습니다"
|
|
253
|
+
// vs "수정되었습니다"), so match both spellings.
|
|
254
|
+
return /정상적으로|등록\s?하였습니다|등록\s?되었습니다|수정\s?되었습니다|저장\s?되었습니다|완료\s?되었습니다|삭제\s?되었습니다/.test(
|
|
255
|
+
message,
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
234
259
|
private extractErrorFromResponse(body: string, location: string | null, path?: string): string | null {
|
|
235
260
|
this.log(
|
|
236
261
|
'extractErrorFromResponse',
|
|
@@ -250,6 +275,10 @@ export class SomaHttp {
|
|
|
250
275
|
if (this.isSessionExpiredError(alertMatch[1])) {
|
|
251
276
|
return '__AUTH_ERROR__'
|
|
252
277
|
}
|
|
278
|
+
if (this.isSuccessAlert(alertMatch[1])) {
|
|
279
|
+
this.log('Alert is a success message, ignoring:', alertMatch[1])
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
253
282
|
return alertMatch[1]
|
|
254
283
|
}
|
|
255
284
|
|
|
@@ -408,7 +437,12 @@ export class SomaHttp {
|
|
|
408
437
|
|
|
409
438
|
const userId = json.userVO?.userId
|
|
410
439
|
if (!userId) return null
|
|
411
|
-
return {
|
|
440
|
+
return {
|
|
441
|
+
userId,
|
|
442
|
+
userNm: json.userVO?.userNm ?? '',
|
|
443
|
+
userNo: json.userVO?.userNo ?? '',
|
|
444
|
+
userGb: parseUserGb(json.userVO?.userGb),
|
|
445
|
+
}
|
|
412
446
|
}
|
|
413
447
|
|
|
414
448
|
async logout(): Promise<void> {
|
|
@@ -532,3 +566,8 @@ export class SomaHttp {
|
|
|
532
566
|
}
|
|
533
567
|
}
|
|
534
568
|
}
|
|
569
|
+
|
|
570
|
+
function parseUserGb(value: string | undefined): UserGb {
|
|
571
|
+
if (value === UserGb.Trainee || value === UserGb.Mentor) return value
|
|
572
|
+
return UserGb.Unknown
|
|
573
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
export { AgentBrowserLauncher, assertSwmaestroUrl, buildStorageState } from './agent-browser-launcher'
|
|
2
|
+
export type {
|
|
3
|
+
AgentBrowserLauncherOptions,
|
|
4
|
+
AgentBrowserLaunchInput,
|
|
5
|
+
LaunchResult,
|
|
6
|
+
Spawner,
|
|
7
|
+
SpawnedProcess,
|
|
8
|
+
} from './agent-browser-launcher'
|
|
1
9
|
export { SomaClient } from './client'
|
|
2
10
|
export type { SomaClientOptions } from './client'
|
|
3
11
|
export { AuthenticationError } from './errors'
|
|
4
|
-
export { SomaHttp } from './http'
|
|
12
|
+
export { SomaHttp, UserGb } from './http'
|
|
13
|
+
export type { UserIdentity } from './http'
|
|
5
14
|
export { CredentialManager } from './credential-manager'
|
|
6
15
|
export { TozHttp } from './toz-http'
|
|
7
16
|
export type { TozHttpOptions, TozHttpState } from './toz-http'
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
2
|
|
|
3
3
|
import { MENU_NO, REPORT_CD } from '../../constants'
|
|
4
4
|
import { buildMentoringListParams, parseSearchQuery } from './mentoring-params'
|
|
5
5
|
|
|
6
6
|
describe('parseSearchQuery', () => {
|
|
7
|
-
|
|
7
|
+
it('defaults plain text to a title search', () => {
|
|
8
8
|
expect(parseSearchQuery('OpenCode')).toEqual({ field: 'title', value: 'OpenCode' })
|
|
9
9
|
})
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
it('parses the "title:" prefix as a title search', () => {
|
|
12
12
|
expect(parseSearchQuery('title:OpenCode')).toEqual({ field: 'title', value: 'OpenCode' })
|
|
13
13
|
})
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
it('parses the "author:" prefix as an author search', () => {
|
|
16
16
|
expect(parseSearchQuery('author:전수열')).toEqual({ field: 'author', value: '전수열' })
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
it('sets the "me" flag when parsing "author:@me"', () => {
|
|
20
20
|
expect(parseSearchQuery('author:@me')).toEqual({ field: 'author', value: '@me', me: true })
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
it('parses the "content:" prefix as a content search', () => {
|
|
24
24
|
expect(parseSearchQuery('content:하네스')).toEqual({ field: 'content', value: '하네스' })
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
it('treats an unknown prefix as a plain title search', () => {
|
|
28
28
|
expect(parseSearchQuery('foo:bar')).toEqual({ field: 'title', value: 'foo:bar' })
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
it('preserves everything after the first colon when the value contains colons', () => {
|
|
32
32
|
expect(parseSearchQuery('title:foo:bar')).toEqual({ field: 'title', value: 'foo:bar' })
|
|
33
33
|
})
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
describe('buildMentoringListParams', () => {
|
|
37
|
-
|
|
37
|
+
it('returns only menuNo when no options are passed', () => {
|
|
38
38
|
expect(buildMentoringListParams()).toEqual({ menuNo: MENU_NO.MENTORING })
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
it('maps status open to "A" and closed to "C"', () => {
|
|
42
42
|
expect(buildMentoringListParams({ status: 'open' })).toEqual({
|
|
43
43
|
menuNo: MENU_NO.MENTORING,
|
|
44
44
|
searchStatMentolec: 'A',
|
|
@@ -49,7 +49,7 @@ describe('buildMentoringListParams', () => {
|
|
|
49
49
|
})
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
it('maps type public and lecture to their report codes', () => {
|
|
53
53
|
expect(buildMentoringListParams({ type: 'public' })).toEqual({
|
|
54
54
|
menuNo: MENU_NO.MENTORING,
|
|
55
55
|
searchGubunMentolec: REPORT_CD.PUBLIC_MENTORING,
|
|
@@ -60,7 +60,7 @@ describe('buildMentoringListParams', () => {
|
|
|
60
60
|
})
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
it('sets searchCnd=1 for a title search', () => {
|
|
64
64
|
expect(buildMentoringListParams({ search: { field: 'title', value: 'OpenCode' } })).toEqual({
|
|
65
65
|
menuNo: MENU_NO.MENTORING,
|
|
66
66
|
searchCnd: '1',
|
|
@@ -68,7 +68,7 @@ describe('buildMentoringListParams', () => {
|
|
|
68
68
|
})
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
it('sets searchCnd=2 for an author search', () => {
|
|
72
72
|
expect(buildMentoringListParams({ search: { field: 'author', value: '전수열' } })).toEqual({
|
|
73
73
|
menuNo: MENU_NO.MENTORING,
|
|
74
74
|
searchCnd: '2',
|
|
@@ -76,7 +76,7 @@ describe('buildMentoringListParams', () => {
|
|
|
76
76
|
})
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
it('sets searchId and searchWrd from the current user when "author:@me" is used', () => {
|
|
80
80
|
expect(
|
|
81
81
|
buildMentoringListParams({
|
|
82
82
|
search: { field: 'author', value: '@me', me: true },
|
|
@@ -90,7 +90,7 @@ describe('buildMentoringListParams', () => {
|
|
|
90
90
|
})
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
it('composes search with status and type filters', () => {
|
|
94
94
|
expect(
|
|
95
95
|
buildMentoringListParams({
|
|
96
96
|
status: 'open',
|
|
@@ -108,7 +108,7 @@ describe('buildMentoringListParams', () => {
|
|
|
108
108
|
})
|
|
109
109
|
})
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
it('sets pageIndex from the page option', () => {
|
|
112
112
|
expect(buildMentoringListParams({ page: 3 })).toEqual({
|
|
113
113
|
menuNo: MENU_NO.MENTORING,
|
|
114
114
|
pageIndex: '3',
|