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/client.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, describe, expect,
|
|
1
|
+
import { afterEach, describe, expect, it, mock } from 'bun:test'
|
|
2
2
|
import { mkdtemp } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
@@ -7,31 +7,124 @@ import { SomaClient } from './client'
|
|
|
7
7
|
import { MENU_NO } from './constants'
|
|
8
8
|
import { CredentialManager } from './credential-manager'
|
|
9
9
|
import { AuthenticationError } from './errors'
|
|
10
|
-
import
|
|
10
|
+
import { SomaHttp, UserGb, type UserIdentity } from './http'
|
|
11
11
|
|
|
12
12
|
afterEach(() => {
|
|
13
13
|
mock.restore()
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
+
type HttpCall = { method: string; path: string; data: Record<string, string> | undefined }
|
|
17
|
+
|
|
18
|
+
interface FakeHttpConfig {
|
|
19
|
+
identity?: UserIdentity | null
|
|
20
|
+
getBody?: (path: string, data?: Record<string, string>) => string
|
|
21
|
+
postBody?: (path: string, data: Record<string, string>) => string
|
|
22
|
+
postFormBody?: (path: string, data: Record<string, string>) => string
|
|
23
|
+
postJsonBody?: (path: string, data: Record<string, string>) => unknown
|
|
24
|
+
sessionCookie?: string
|
|
25
|
+
csrfToken?: string | null
|
|
26
|
+
onLogin?: (username: string, password: string) => void
|
|
27
|
+
onLogout?: () => void
|
|
28
|
+
checkLoginSequence?: Array<UserIdentity | null>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildMentoringEditFormFixture(fields: {
|
|
32
|
+
qustnrSn: string
|
|
33
|
+
qustnrSj: string
|
|
34
|
+
reportCd: 'MRC010' | 'MRC020'
|
|
35
|
+
receiptType: 'UNTIL_LECTURE' | 'DIRECT'
|
|
36
|
+
bgndeDate: string
|
|
37
|
+
bgndeTime: string
|
|
38
|
+
enddeDate: string
|
|
39
|
+
enddeTime: string
|
|
40
|
+
eventDt: string
|
|
41
|
+
eventStime: string
|
|
42
|
+
eventEtime: string
|
|
43
|
+
applyCnt: string
|
|
44
|
+
place: string
|
|
45
|
+
}): string {
|
|
46
|
+
const option = (id: string, value: string, selected: string) =>
|
|
47
|
+
`<option value="${value}"${value === selected ? ' selected' : ''}>${id}</option>`
|
|
48
|
+
const radio = (name: string, value: string, checked: string) =>
|
|
49
|
+
`<input type="radio" name="${name}" value="${value}"${value === checked ? ' checked' : ''} />`
|
|
50
|
+
return `<form id="board">
|
|
51
|
+
<input type="hidden" name="qustnrSn" value="${fields.qustnrSn}" />
|
|
52
|
+
<input type="hidden" name="stateCd" value="A" />
|
|
53
|
+
${radio('reportCd', 'MRC010', fields.reportCd)}
|
|
54
|
+
${radio('reportCd', 'MRC020', fields.reportCd)}
|
|
55
|
+
<input type="text" name="qustnrSj" value="${fields.qustnrSj}" />
|
|
56
|
+
${radio('receiptType', 'UNTIL_LECTURE', fields.receiptType)}
|
|
57
|
+
${radio('receiptType', 'DIRECT', fields.receiptType)}
|
|
58
|
+
<input type="text" name="bgndeDate" value="${fields.bgndeDate}" />
|
|
59
|
+
<select name="bgndeTime">${option(fields.bgndeTime, fields.bgndeTime, fields.bgndeTime)}</select>
|
|
60
|
+
<input type="text" name="enddeDate" value="${fields.enddeDate}" />
|
|
61
|
+
<select name="enddeTime">${option(fields.enddeTime, fields.enddeTime, fields.enddeTime)}</select>
|
|
62
|
+
<input type="text" name="eventDt" value="${fields.eventDt}" />
|
|
63
|
+
<select name="eventStime">${option(fields.eventStime, fields.eventStime, fields.eventStime)}</select>
|
|
64
|
+
<select name="eventEtime">${option(fields.eventEtime, fields.eventEtime, fields.eventEtime)}</select>
|
|
65
|
+
<input type="text" name="applyCnt" value="${fields.applyCnt}" />
|
|
66
|
+
<select name="place">${option(fields.place, fields.place, fields.place)}</select>
|
|
67
|
+
</form>`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createFakeHttp(config: FakeHttpConfig = {}): { http: SomaHttp; calls: HttpCall[] } {
|
|
71
|
+
const calls: HttpCall[] = []
|
|
72
|
+
const sequence = config.checkLoginSequence ? [...config.checkLoginSequence] : null
|
|
73
|
+
|
|
74
|
+
const fake = {
|
|
75
|
+
checkLogin: async () => {
|
|
76
|
+
if (sequence) {
|
|
77
|
+
return sequence.shift() ?? config.identity ?? null
|
|
78
|
+
}
|
|
79
|
+
return config.identity ?? null
|
|
80
|
+
},
|
|
81
|
+
get: async (path: string, data?: Record<string, string>) => {
|
|
82
|
+
calls.push({ method: 'get', path, data })
|
|
83
|
+
return config.getBody ? config.getBody(path, data) : ''
|
|
84
|
+
},
|
|
85
|
+
post: async (path: string, data: Record<string, string>) => {
|
|
86
|
+
calls.push({ method: 'post', path, data })
|
|
87
|
+
return config.postBody ? config.postBody(path, data) : ''
|
|
88
|
+
},
|
|
89
|
+
postForm: async (path: string, data: Record<string, string>) => {
|
|
90
|
+
calls.push({ method: 'postForm', path, data })
|
|
91
|
+
return config.postFormBody ? config.postFormBody(path, data) : ''
|
|
92
|
+
},
|
|
93
|
+
postJson: async (path: string, data: Record<string, string>) => {
|
|
94
|
+
calls.push({ method: 'postJson', path, data })
|
|
95
|
+
return config.postJsonBody ? config.postJsonBody(path, data) : {}
|
|
96
|
+
},
|
|
97
|
+
postMultipart: async () => '',
|
|
98
|
+
login: async (username: string, password: string) => {
|
|
99
|
+
config.onLogin?.(username, password)
|
|
100
|
+
},
|
|
101
|
+
logout: async () => {
|
|
102
|
+
config.onLogout?.()
|
|
103
|
+
},
|
|
104
|
+
getSessionCookie: () => config.sessionCookie,
|
|
105
|
+
getCsrfToken: () => config.csrfToken ?? null,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { http: fake as unknown as SomaHttp, calls }
|
|
109
|
+
}
|
|
110
|
+
|
|
16
111
|
describe('SomaClient', () => {
|
|
17
|
-
|
|
112
|
+
it('exposes session state passed through options', () => {
|
|
18
113
|
const client = new SomaClient({ sessionCookie: 'session-1', csrfToken: 'csrf-1' })
|
|
19
|
-
const http = Reflect.get(client, 'http') as SomaHttp
|
|
20
114
|
|
|
21
|
-
expect(
|
|
22
|
-
|
|
115
|
+
expect(client.getSessionData()).toEqual({
|
|
116
|
+
sessionCookie: 'session-1',
|
|
117
|
+
csrfToken: 'csrf-1',
|
|
118
|
+
})
|
|
23
119
|
})
|
|
24
120
|
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
get: async (path: string, data?: Record<string, string>) => {
|
|
31
|
-
calls.push({ method: 'get', path, data })
|
|
32
|
-
return '<table><tbody><tr><td>1</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=123">[자유 멘토링] 제목 [접수중]</a></td><td>2026-04-01 ~ 2026-04-02</td><td>2026-04-03(목) 10:00 ~ 11:00</td><td>1 /4</td><td>OK</td><td>[접수중]</td><td>작성자</td><td>2026-04-01</td></tr></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>'
|
|
33
|
-
},
|
|
121
|
+
it('lists mentoring sessions with parsed items and pagination', async () => {
|
|
122
|
+
const { http, calls } = createFakeHttp({
|
|
123
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
124
|
+
getBody: () =>
|
|
125
|
+
'<table><tbody><tr><td>1</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=123">[자유 멘토링] 제목 [접수중]</a></td><td>2026-04-01 ~ 2026-04-02</td><td>2026-04-03(목) 10:00 ~ 11:00</td><td>1 /4</td><td>OK</td><td>[접수중]</td><td>작성자</td><td>2026-04-01</td></tr></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>',
|
|
34
126
|
})
|
|
127
|
+
const client = new SomaClient({ http })
|
|
35
128
|
|
|
36
129
|
const result = await client.mentoring.list({ status: 'open', type: 'public', page: 2 })
|
|
37
130
|
|
|
@@ -51,41 +144,49 @@ describe('SomaClient', () => {
|
|
|
51
144
|
expect(result.pagination).toEqual({ total: 1, currentPage: 1, totalPages: 1 })
|
|
52
145
|
})
|
|
53
146
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
get: async (path: string, data?: Record<string, string>) => {
|
|
60
|
-
captured = { path, data }
|
|
61
|
-
return '<input type="hidden" name="qustnrSn" value="99"><table><tr><th>모집 명</th><td>상세</td><th>상태</th><td>접수중</td></tr><tr><th>접수 기간</th><td>2026-04-01 ~ 2026-04-02</td><th>강의날짜</th><td>2026-04-03(목) 10:00 ~ 11:00</td></tr><tr><th>장소</th><td>온라인(Webex)</td><th>모집인원</th><td>4명</td></tr><tr><th>작성자</th><td>작성자</td><th>등록일</th><td>2026-04-01</td></tr></table><div data-content><p>본문</p></div>'
|
|
62
|
-
},
|
|
147
|
+
it('fetches a single mentoring session from the detail endpoint', async () => {
|
|
148
|
+
const { http, calls } = createFakeHttp({
|
|
149
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
150
|
+
getBody: () =>
|
|
151
|
+
'<input type="hidden" name="qustnrSn" value="99"><table><tr><th>모집 명</th><td>상세</td><th>상태</th><td>접수중</td></tr><tr><th>접수 기간</th><td>2026-04-01 ~ 2026-04-02</td><th>강의날짜</th><td>2026-04-03(목) 10:00 ~ 11:00</td></tr><tr><th>장소</th><td>온라인(Webex)</td><th>모집인원</th><td>4명</td></tr><tr><th>작성자</th><td>작성자</td><th>등록일</th><td>2026-04-01</td></tr></table><div data-content><p>본문</p></div>',
|
|
63
152
|
})
|
|
153
|
+
const client = new SomaClient({ http })
|
|
64
154
|
|
|
65
155
|
const result = await client.mentoring.get(99)
|
|
66
156
|
|
|
67
|
-
expect(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
157
|
+
expect(calls).toEqual([
|
|
158
|
+
{
|
|
159
|
+
method: 'get',
|
|
160
|
+
path: '/mypage/mentoLec/view.do',
|
|
161
|
+
data: { menuNo: MENU_NO.MENTORING, qustnrSn: '99' },
|
|
162
|
+
},
|
|
163
|
+
])
|
|
71
164
|
expect(result).toMatchObject({ id: 99, title: '상세', venue: '온라인(Webex)' })
|
|
72
165
|
})
|
|
73
166
|
|
|
74
|
-
|
|
167
|
+
it('posts the expected payloads for create, update, delete, apply, cancel, and reserve', async () => {
|
|
75
168
|
const mentoringDetailHtml =
|
|
76
169
|
'<div class="group"><strong class="t">모집 명</strong><div class="c">[자유 멘토링] 기존 멘토링</div></div><div class="group"><strong class="t">접수 기간</strong><div class="c">2026.03.01 ~ 2026.03.15</div></div><div class="group"><strong class="t">강의날짜</strong><div class="c"><span>2026.03.20 10:00시 ~ 12:00시</span></div></div><div class="group"><strong class="t">장소</strong><div class="c">온라인(Webex)</div></div><div class="group"><strong class="t">모집인원</strong><div class="c">5명</div></div><div class="group"><strong class="t">작성자</strong><div class="c">전수열</div></div><div class="group"><strong class="t">등록일</strong><div class="c">2026.03.01</div></div><div class="cont"><p>기존 내용</p></div>'
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
170
|
+
const mentoringEditFormHtml = buildMentoringEditFormFixture({
|
|
171
|
+
qustnrSn: '42',
|
|
172
|
+
qustnrSj: '기존 멘토링',
|
|
173
|
+
reportCd: 'MRC010',
|
|
174
|
+
receiptType: 'UNTIL_LECTURE',
|
|
175
|
+
bgndeDate: '2026-03-01',
|
|
176
|
+
bgndeTime: '00:00',
|
|
177
|
+
enddeDate: '2026-03-20',
|
|
178
|
+
enddeTime: '10:00',
|
|
179
|
+
eventDt: '2026-03-20',
|
|
180
|
+
eventStime: '10:00',
|
|
181
|
+
eventEtime: '12:00',
|
|
182
|
+
applyCnt: '5',
|
|
183
|
+
place: '온라인(Webex)',
|
|
88
184
|
})
|
|
185
|
+
const { http, calls } = createFakeHttp({
|
|
186
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
187
|
+
getBody: (path) => (path === '/mypage/mentoLec/forUpdate.do' ? mentoringEditFormHtml : mentoringDetailHtml),
|
|
188
|
+
})
|
|
189
|
+
const client = new SomaClient({ http })
|
|
89
190
|
|
|
90
191
|
await client.mentoring.create({
|
|
91
192
|
title: '새 멘토링',
|
|
@@ -113,75 +214,326 @@ describe('SomaClient', () => {
|
|
|
113
214
|
slots: ['10:00', '10:30'],
|
|
114
215
|
title: '회의',
|
|
115
216
|
})
|
|
116
|
-
await client.event.apply(11)
|
|
117
217
|
|
|
118
|
-
|
|
218
|
+
const writes = calls.filter((call) => call.method !== 'get')
|
|
219
|
+
expect(writes.map((call) => `${call.method}:${call.path}`)).toEqual([
|
|
119
220
|
'postForm:/mypage/mentoLec/insert.do',
|
|
120
221
|
'postForm:/mypage/mentoLec/update.do',
|
|
121
222
|
'post:/mypage/mentoLec/delete.do',
|
|
122
223
|
'post:/application/application/application.do',
|
|
123
224
|
'post:/mypage/userAnswer/cancel.do',
|
|
124
225
|
'post:/mypage/itemRent/insert.do',
|
|
125
|
-
'post:/application/application/application.do',
|
|
126
226
|
])
|
|
127
|
-
expect(
|
|
227
|
+
expect(writes[0]?.data).toMatchObject({
|
|
128
228
|
menuNo: MENU_NO.MENTORING,
|
|
129
229
|
reportCd: 'MRC020',
|
|
130
230
|
qustnrSj: '새 멘토링',
|
|
131
231
|
})
|
|
132
|
-
expect(
|
|
232
|
+
expect(writes[1]?.data).toMatchObject({
|
|
133
233
|
menuNo: MENU_NO.MENTORING,
|
|
134
234
|
reportCd: 'MRC010',
|
|
135
235
|
qustnrSj: '수정된 멘토링',
|
|
136
236
|
qustnrSn: '42',
|
|
137
237
|
})
|
|
138
|
-
expect(
|
|
238
|
+
expect(writes[2]?.data).toEqual({
|
|
139
239
|
menuNo: MENU_NO.MENTORING,
|
|
140
240
|
qustnrSn: '7',
|
|
141
241
|
pageQueryString: '',
|
|
142
242
|
})
|
|
143
|
-
expect(
|
|
144
|
-
menuNo: MENU_NO.
|
|
243
|
+
expect(writes[3]?.data).toEqual({
|
|
244
|
+
menuNo: MENU_NO.MENTORING,
|
|
145
245
|
qustnrSn: '8',
|
|
146
246
|
applyGb: 'C',
|
|
147
247
|
stepHeader: '0',
|
|
148
248
|
})
|
|
149
|
-
expect(
|
|
249
|
+
expect(writes[4]?.data).toEqual({
|
|
150
250
|
menuNo: MENU_NO.APPLICATION_HISTORY,
|
|
151
251
|
applySn: '9',
|
|
152
252
|
qustnrSn: '10',
|
|
153
253
|
})
|
|
154
|
-
expect(
|
|
254
|
+
expect(writes[5]?.data).toMatchObject({
|
|
155
255
|
menuNo: MENU_NO.ROOM,
|
|
156
256
|
itemId: '17',
|
|
157
257
|
title: '회의',
|
|
158
258
|
'time[0]': '10:00',
|
|
159
259
|
'time[1]': '10:30',
|
|
160
260
|
})
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('fetches, updates, and cancels room reservations via itemRent endpoints', async () => {
|
|
264
|
+
const detailHtml = `
|
|
265
|
+
<form id="frm" method="post">
|
|
266
|
+
<input type="hidden" name="rentId" value="18718" />
|
|
267
|
+
<input type="hidden" name="itemId" value="17" />
|
|
268
|
+
<input type="hidden" name="receiptStatCd" value="RS001" />
|
|
269
|
+
<input type="hidden" name="title" value="멘토링" />
|
|
270
|
+
<input type="hidden" name="rentDt" value="2026-05-31" />
|
|
271
|
+
<input type="hidden" name="rentBgnde" value="2026-05-31 21:00:00.0" />
|
|
272
|
+
<input type="hidden" name="rentEndde" value="2026-05-31 21:30:00.0" />
|
|
273
|
+
<input type="hidden" name="infoCn" value="" />
|
|
274
|
+
<input type="hidden" name="rentNum" value="4" />
|
|
275
|
+
</form>
|
|
276
|
+
`
|
|
277
|
+
const { http, calls } = createFakeHttp({
|
|
278
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
279
|
+
getBody: (path) => (path === '/mypage/itemRent/view.do' ? detailHtml : ''),
|
|
166
280
|
})
|
|
281
|
+
const client = new SomaClient({ http })
|
|
282
|
+
|
|
283
|
+
const detail = await client.room.get(18718)
|
|
284
|
+
expect(detail).toEqual({
|
|
285
|
+
rentId: 18718,
|
|
286
|
+
itemId: 17,
|
|
287
|
+
title: '멘토링',
|
|
288
|
+
date: '2026-05-31',
|
|
289
|
+
startTime: '21:00',
|
|
290
|
+
endTime: '21:30',
|
|
291
|
+
attendees: 4,
|
|
292
|
+
notes: '',
|
|
293
|
+
status: 'confirmed',
|
|
294
|
+
statusCode: 'RS001',
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
await client.room.update(18718, { title: '스터디', notes: '회고 공유' })
|
|
298
|
+
await client.room.update(18718, { slots: ['22:00', '22:30'] })
|
|
299
|
+
await client.room.cancel(18718)
|
|
300
|
+
|
|
301
|
+
const updateCalls = calls.filter((call) => call.path === '/mypage/itemRent/update.do')
|
|
302
|
+
expect(updateCalls).toHaveLength(3)
|
|
303
|
+
expect(updateCalls[0]?.data).toMatchObject({
|
|
304
|
+
rentId: '18718',
|
|
305
|
+
itemId: '17',
|
|
306
|
+
receiptStatCd: 'RS001',
|
|
307
|
+
title: '스터디',
|
|
308
|
+
infoCn: '회고 공유',
|
|
309
|
+
rentBgnde: '2026-05-31 21:00:00',
|
|
310
|
+
rentEndde: '2026-05-31 21:30:00',
|
|
311
|
+
})
|
|
312
|
+
expect(updateCalls[0]?.data?.['time[0]']).toBeUndefined()
|
|
313
|
+
expect(updateCalls[1]?.data).toMatchObject({
|
|
314
|
+
rentId: '18718',
|
|
315
|
+
title: '멘토링',
|
|
316
|
+
rentBgnde: '2026-05-31 22:00:00',
|
|
317
|
+
rentEndde: '2026-05-31 22:59:00',
|
|
318
|
+
'time[0]': '22:00',
|
|
319
|
+
'time[1]': '22:30',
|
|
320
|
+
chkData_1: '2026-05-31|22:00|17',
|
|
321
|
+
chkData_2: '2026-05-31|22:30|17',
|
|
322
|
+
})
|
|
323
|
+
expect(updateCalls[2]?.data).toMatchObject({
|
|
324
|
+
rentId: '18718',
|
|
325
|
+
receiptStatCd: 'RS002',
|
|
326
|
+
title: '멘토링',
|
|
327
|
+
rentBgnde: '2026-05-31 21:00:00',
|
|
328
|
+
rentEndde: '2026-05-31 21:30:00',
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
const viewCalls = calls.filter((call) => call.path === '/mypage/itemRent/view.do')
|
|
332
|
+
expect(viewCalls).toHaveLength(4)
|
|
167
333
|
})
|
|
168
334
|
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
335
|
+
it('swallows the SWMaestro success-alert thrown by SomaHttp on room update', async () => {
|
|
336
|
+
const detailHtml = `
|
|
337
|
+
<form id="frm" method="post">
|
|
338
|
+
<input type="hidden" name="rentId" value="18718" />
|
|
339
|
+
<input type="hidden" name="itemId" value="17" />
|
|
340
|
+
<input type="hidden" name="receiptStatCd" value="RS001" />
|
|
341
|
+
<input type="hidden" name="title" value="멘토링" />
|
|
342
|
+
<input type="hidden" name="rentDt" value="2026-05-31" />
|
|
343
|
+
<input type="hidden" name="rentBgnde" value="2026-05-31 21:00:00.0" />
|
|
344
|
+
<input type="hidden" name="rentEndde" value="2026-05-31 21:30:00.0" />
|
|
345
|
+
<input type="hidden" name="infoCn" value="" />
|
|
346
|
+
<input type="hidden" name="rentNum" value="4" />
|
|
347
|
+
</form>
|
|
348
|
+
`
|
|
349
|
+
const { http } = createFakeHttp({
|
|
350
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
351
|
+
getBody: () => detailHtml,
|
|
352
|
+
postBody: (path) => {
|
|
353
|
+
if (path === '/mypage/itemRent/update.do') {
|
|
354
|
+
throw new Error('정상적으로 수정하였습니다.')
|
|
355
|
+
}
|
|
179
356
|
return ''
|
|
180
357
|
},
|
|
181
358
|
})
|
|
359
|
+
const client = new SomaClient({ http })
|
|
360
|
+
|
|
361
|
+
await expect(client.room.update(18718, { title: '변경' })).resolves.toBeUndefined()
|
|
362
|
+
await expect(client.room.cancel(18718)).resolves.toBeUndefined()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('lists room reservations filtered by default to confirmed status via itemRent list endpoint', async () => {
|
|
366
|
+
const listHtml = `
|
|
367
|
+
<ul class="bbs-total">
|
|
368
|
+
<li><strong>Total :</strong> 2</li>
|
|
369
|
+
<li><span>1</span>/1 Page</li>
|
|
370
|
+
</ul>
|
|
371
|
+
<table>
|
|
372
|
+
<thead>
|
|
373
|
+
<tr><th>NO.</th><th>회의실 명</th><th>제목</th><th>사용기간</th><th>작성자</th><th>상태</th><th>등록일</th></tr>
|
|
374
|
+
</thead>
|
|
375
|
+
<tbody>
|
|
376
|
+
<tr>
|
|
377
|
+
<td>2</td>
|
|
378
|
+
<td><a href="/sw/mypage/itemRent/view.do?rentId=18618">스페이스 M1</a></td>
|
|
379
|
+
<td>
|
|
380
|
+
<div class="rel">
|
|
381
|
+
<a href="/sw/mypage/itemRent/view.do?rentId=18618">멘토 특강</a>
|
|
382
|
+
<span>예약완료</span>
|
|
383
|
+
</div>
|
|
384
|
+
</td>
|
|
385
|
+
<td>2026.05.31 16:00 ~ 17:30</td>
|
|
386
|
+
<td>전수열</td>
|
|
387
|
+
<td>예약완료</td>
|
|
388
|
+
<td>2026.04.20</td>
|
|
389
|
+
</tr>
|
|
390
|
+
<tr>
|
|
391
|
+
<td>1</td>
|
|
392
|
+
<td><a href="/sw/mypage/itemRent/view.do?rentId=18616">스페이스 A3</a></td>
|
|
393
|
+
<td>
|
|
394
|
+
<div class="rel">
|
|
395
|
+
<a href="/sw/mypage/itemRent/view.do?rentId=18616">자유 멘토링</a>
|
|
396
|
+
<span>예약완료</span>
|
|
397
|
+
</div>
|
|
398
|
+
</td>
|
|
399
|
+
<td>2026.05.24 10:00 ~ 11:00</td>
|
|
400
|
+
<td>전수열</td>
|
|
401
|
+
<td>예약완료</td>
|
|
402
|
+
<td>2026.04.15</td>
|
|
403
|
+
</tr>
|
|
404
|
+
</tbody>
|
|
405
|
+
</table>
|
|
406
|
+
`
|
|
407
|
+
const { http, calls } = createFakeHttp({
|
|
408
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
409
|
+
getBody: (path) => (path === '/mypage/itemRent/list.do' ? listHtml : ''),
|
|
410
|
+
})
|
|
411
|
+
const client = new SomaClient({ http })
|
|
412
|
+
|
|
413
|
+
const result = await client.room.reservations({ startDate: '2026-01-01', endDate: '2026-12-31' })
|
|
414
|
+
|
|
415
|
+
expect(calls).toEqual([
|
|
416
|
+
{
|
|
417
|
+
method: 'get',
|
|
418
|
+
path: '/mypage/itemRent/list.do',
|
|
419
|
+
data: {
|
|
420
|
+
menuNo: MENU_NO.ROOM,
|
|
421
|
+
pageIndex: '1',
|
|
422
|
+
sdate: '2026-01-01',
|
|
423
|
+
edate: '2026-12-31',
|
|
424
|
+
searchStat: 'RS001',
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
])
|
|
428
|
+
expect(result.pagination).toEqual({ total: 2, currentPage: 1, totalPages: 1 })
|
|
429
|
+
expect(result.items).toEqual([
|
|
430
|
+
{
|
|
431
|
+
rentId: 18618,
|
|
432
|
+
venue: '스페이스 M1',
|
|
433
|
+
title: '멘토 특강',
|
|
434
|
+
date: '2026-05-31',
|
|
435
|
+
startTime: '16:00',
|
|
436
|
+
endTime: '17:30',
|
|
437
|
+
author: '전수열',
|
|
438
|
+
status: 'confirmed',
|
|
439
|
+
statusLabel: '예약완료',
|
|
440
|
+
registeredAt: '2026.04.20',
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
rentId: 18616,
|
|
444
|
+
venue: '스페이스 A3',
|
|
445
|
+
title: '자유 멘토링',
|
|
446
|
+
date: '2026-05-24',
|
|
447
|
+
startTime: '10:00',
|
|
448
|
+
endTime: '11:00',
|
|
449
|
+
author: '전수열',
|
|
450
|
+
status: 'confirmed',
|
|
451
|
+
statusLabel: '예약완료',
|
|
452
|
+
registeredAt: '2026.04.15',
|
|
453
|
+
},
|
|
454
|
+
])
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('omits the searchStat filter when status is "all"', async () => {
|
|
458
|
+
const { http, calls } = createFakeHttp({
|
|
459
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
460
|
+
getBody: () => '<ul class="bbs-total"><li>Total : 0</li></ul>',
|
|
461
|
+
})
|
|
462
|
+
const client = new SomaClient({ http })
|
|
463
|
+
|
|
464
|
+
await client.room.reservations({ status: 'all', page: 3 })
|
|
465
|
+
|
|
466
|
+
expect(calls[0]?.data).toEqual({ menuNo: MENU_NO.ROOM, pageIndex: '3' })
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('sends searchStat=RS002 when listing cancelled reservations', async () => {
|
|
470
|
+
const { http, calls } = createFakeHttp({
|
|
471
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
472
|
+
getBody: () => '<ul class="bbs-total"><li>Total : 0</li></ul>',
|
|
473
|
+
})
|
|
474
|
+
const client = new SomaClient({ http })
|
|
475
|
+
|
|
476
|
+
await client.room.reservations({ status: 'cancelled' })
|
|
477
|
+
|
|
478
|
+
expect(calls[0]?.data).toMatchObject({ searchStat: 'RS002' })
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it('re-raises genuine errors thrown by SomaHttp on room update', async () => {
|
|
482
|
+
const detailHtml = `
|
|
483
|
+
<form id="frm" method="post">
|
|
484
|
+
<input type="hidden" name="rentId" value="18718" />
|
|
485
|
+
<input type="hidden" name="itemId" value="17" />
|
|
486
|
+
<input type="hidden" name="receiptStatCd" value="RS001" />
|
|
487
|
+
<input type="hidden" name="title" value="멘토링" />
|
|
488
|
+
<input type="hidden" name="rentDt" value="2026-05-31" />
|
|
489
|
+
<input type="hidden" name="rentBgnde" value="2026-05-31 21:00:00.0" />
|
|
490
|
+
<input type="hidden" name="rentEndde" value="2026-05-31 21:30:00.0" />
|
|
491
|
+
<input type="hidden" name="infoCn" value="" />
|
|
492
|
+
<input type="hidden" name="rentNum" value="4" />
|
|
493
|
+
</form>
|
|
494
|
+
`
|
|
495
|
+
const { http } = createFakeHttp({
|
|
496
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
497
|
+
getBody: () => detailHtml,
|
|
498
|
+
postBody: (path) => {
|
|
499
|
+
if (path === '/mypage/itemRent/update.do') {
|
|
500
|
+
throw new Error('권한이 없습니다.')
|
|
501
|
+
}
|
|
502
|
+
return ''
|
|
503
|
+
},
|
|
504
|
+
})
|
|
505
|
+
const client = new SomaClient({ http })
|
|
506
|
+
|
|
507
|
+
await expect(client.room.update(18718, { title: '변경' })).rejects.toThrow('권한이 없습니다.')
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
it('merges partial update params with the existing mentoring data', async () => {
|
|
511
|
+
const mentoringDetailHtml =
|
|
512
|
+
'<div class="group"><strong class="t">모집 명</strong><div class="c">[멘토 특강] 웹 성능 특강</div></div><div class="group"><strong class="t">접수 기간</strong><div class="c">2026.04.01 ~ 2026.04.10</div></div><div class="group"><strong class="t">강의날짜</strong><div class="c"><span>2026.04.11 14:00시 ~ 15:30시</span></div></div><div class="group"><strong class="t">장소</strong><div class="c">온라인(Webex)</div></div><div class="group"><strong class="t">모집인원</strong><div class="c">20명</div></div><div class="group"><strong class="t">작성자</strong><div class="c">전수열</div></div><div class="group"><strong class="t">등록일</strong><div class="c">2026.04.01</div></div><div class="cont"><p>세션 본문</p></div>'
|
|
513
|
+
const mentoringEditFormHtml = buildMentoringEditFormFixture({
|
|
514
|
+
qustnrSn: '9572',
|
|
515
|
+
qustnrSj: '웹 성능 특강',
|
|
516
|
+
reportCd: 'MRC020',
|
|
517
|
+
receiptType: 'UNTIL_LECTURE',
|
|
518
|
+
bgndeDate: '2026-04-01',
|
|
519
|
+
bgndeTime: '00:00',
|
|
520
|
+
enddeDate: '2026-04-11',
|
|
521
|
+
enddeTime: '14:00',
|
|
522
|
+
eventDt: '2026-04-11',
|
|
523
|
+
eventStime: '14:00',
|
|
524
|
+
eventEtime: '15:30',
|
|
525
|
+
applyCnt: '20',
|
|
526
|
+
place: '온라인(Webex)',
|
|
527
|
+
})
|
|
528
|
+
const { http, calls } = createFakeHttp({
|
|
529
|
+
identity: { userId: 'user@example.com', userNm: 'Test' },
|
|
530
|
+
getBody: (path) => (path === '/mypage/mentoLec/forUpdate.do' ? mentoringEditFormHtml : mentoringDetailHtml),
|
|
531
|
+
})
|
|
532
|
+
const client = new SomaClient({ http })
|
|
182
533
|
|
|
183
534
|
await client.mentoring.update(9572, { title: '변경된 제목' })
|
|
184
535
|
|
|
536
|
+
const postFormCalls = calls.filter((c) => c.method === 'postForm')
|
|
185
537
|
expect(postFormCalls).toHaveLength(1)
|
|
186
538
|
expect(postFormCalls[0]?.data).toMatchObject({
|
|
187
539
|
qustnrSj: '변경된 제목',
|
|
@@ -192,19 +544,21 @@ describe('SomaClient', () => {
|
|
|
192
544
|
eventEtime: '15:30',
|
|
193
545
|
place: '온라인(Webex)',
|
|
194
546
|
applyCnt: '20',
|
|
195
|
-
|
|
196
|
-
|
|
547
|
+
bgndeDate: '2026-04-01',
|
|
548
|
+
bgndeTime: '00:00',
|
|
549
|
+
enddeDate: '2026-04-11',
|
|
550
|
+
enddeTime: '14:00',
|
|
551
|
+
receiptType: 'UNTIL_LECTURE',
|
|
552
|
+
stateCd: 'A',
|
|
553
|
+
qustnrAt: 'N',
|
|
197
554
|
qestnarCn: '<p>세션 본문</p>',
|
|
198
555
|
})
|
|
199
556
|
})
|
|
200
557
|
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
checkLogin: async () => ({ userId: 'neo@example.com', userNm: '전수열' }),
|
|
206
|
-
get: async (path: string, data?: Record<string, string>) => {
|
|
207
|
-
calls.push({ method: 'get', path, data })
|
|
558
|
+
it('routes room, dashboard, notice, team, member, and history calls to the expected endpoints', async () => {
|
|
559
|
+
const { http, calls } = createFakeHttp({
|
|
560
|
+
identity: { userId: 'neo@example.com', userNm: '전수열' },
|
|
561
|
+
getBody: (path) => {
|
|
208
562
|
if (path === '/mypage/myMain/dashboard.do') {
|
|
209
563
|
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> Indent</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-orange label"><span>멘토</span></span><div class="welcome"><strong>전수열</strong>님 안녕하세요.</div></div></div></li></ul><ul class="bbs-dash_w"><li>멘토링 · 멘토특강<li><a href="/sw/mypage/mentoLec/view.do?qustnrSn=9582">게임 개발 AI 활용법 접수중</a></li></li></ul>'
|
|
210
564
|
}
|
|
@@ -223,32 +577,24 @@ describe('SomaClient', () => {
|
|
|
223
577
|
if (path === '/mypage/myInfo/forUpdateMy.do') {
|
|
224
578
|
return '<dl><dt><span class="point">아이디</span></dt><dd>neo@example.com</dd></dl><dl><dt><span class="point">이름</span></dt><dd>전수열</dd></dl><dl><dt><span class="point">성별</span></dt><dd>남자</dd></dl><dl><dt><span class="point">생년월일</span></dt><dd>1995-01-14</dd></dl><dl><dt><span class="point">연락처</span></dt><dd>01012345678</dd></dl><dl><dt><span class="point">소속</span></dt><dd>Indent</dd></dl><dl><dt><span class="point">직책</span></dt><dd></dd></dl>'
|
|
225
579
|
}
|
|
226
|
-
if (path === '/mypage/applicants/list.do') {
|
|
227
|
-
return '<table><tbody><tr><td>1</td><td>행사</td><td>행사</td><td>2026-04-01 ~ 2026-04-02</td><td>2026-04-03 ~ 2026-04-03</td><td>[접수중]</td><td>2026-04-01</td></tr></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>'
|
|
228
|
-
}
|
|
229
|
-
if (path === '/mypage/applicants/view.do') {
|
|
230
|
-
return '<input name="bbsId" value="1"><table><tr><th>제목</th><td>행사 상세</td></tr></table><div data-content><p>본문</p></div>'
|
|
231
|
-
}
|
|
232
580
|
return '<table><tbody><tr><td>1</td><td>멘토 특강</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=1">접수내역</a></td><td>전수열</td><td>2026.04.11</td><td>2026-04-01</td><td>[신청완료]</td><td>[OK]</td><td>승인대기</td><td>-</td></tr></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>'
|
|
233
581
|
},
|
|
234
|
-
|
|
235
|
-
calls.push({ method: 'post', path, data })
|
|
582
|
+
postBody: (path) => {
|
|
236
583
|
if (path === '/mypage/officeMng/rentTime.do') {
|
|
237
584
|
return '<span class="ck-st2 disabled" data-hour="09" data-minute="00"><input type="checkbox" disabled="disabled"><label title="아침 회의<br>예약자 : 김오픈">오전 09:00</label></span>'
|
|
238
585
|
}
|
|
239
586
|
return '<ul class="bbs-reserve"><li class="item"><a href="javascript:void(0);" onclick="location.href=\'/sw/mypage/officeMng/view.do?itemId=17\';"><div class="cont"><h4 class="tit">스페이스 A1</h4><ul class="txt bul-dot grey"><li>이용기간 : 2026-04-01 ~ 2026-12-31</li><li><p>설명 : 4인</p></li><li class="time-list"><div class="time-grid"><span>09:00</span></div></li></ul></div></a></li></ul>'
|
|
240
587
|
},
|
|
241
588
|
})
|
|
589
|
+
const client = new SomaClient({ http })
|
|
242
590
|
|
|
243
591
|
const roomList = await client.room.list({ date: '2026-04-01', room: 'A1', includeReservations: true })
|
|
244
592
|
const roomSlots = await client.room.available(17, '2026-04-01')
|
|
245
593
|
const dashboard = await client.dashboard.get()
|
|
246
594
|
const noticeList = await client.notice.list({ page: 2 })
|
|
247
595
|
const noticeDetail = await client.notice.get(1)
|
|
248
|
-
const team = await client.team.
|
|
596
|
+
const team = await client.team.list()
|
|
249
597
|
const member = await client.member.show()
|
|
250
|
-
const events = await client.event.list({ page: 3 })
|
|
251
|
-
const eventDetail = await client.event.get(1)
|
|
252
598
|
const history = await client.mentoring.history({ page: 4 })
|
|
253
599
|
|
|
254
600
|
expect(roomList[0]?.itemId).toBe(17)
|
|
@@ -270,16 +616,16 @@ describe('SomaClient', () => {
|
|
|
270
616
|
type: '멘토 특강',
|
|
271
617
|
},
|
|
272
618
|
])
|
|
619
|
+
expect(dashboard.teams.map((t) => t.name)).toEqual(['오픈소마'])
|
|
273
620
|
expect(noticeList.items[0]?.title).toBe('공지')
|
|
274
621
|
expect(noticeDetail).toMatchObject({ id: 1, title: '공지' })
|
|
275
622
|
expect(team.teams[0]?.name).toBe('오픈소마')
|
|
276
623
|
expect(member.email).toBe('neo@example.com')
|
|
277
|
-
expect(events.items[0]?.title).toBe('행사')
|
|
278
|
-
expect(eventDetail).toMatchObject({ id: 1, title: '행사 상세' })
|
|
279
624
|
expect(history.items[0]).toEqual({
|
|
280
625
|
id: 1,
|
|
281
626
|
category: '멘토 특강',
|
|
282
627
|
title: '접수내역',
|
|
628
|
+
url: '/sw/mypage/mentoLec/view.do?qustnrSn=1',
|
|
283
629
|
author: '전수열',
|
|
284
630
|
sessionDate: '2026-04-11',
|
|
285
631
|
appliedAt: '2026-04-01',
|
|
@@ -289,8 +635,7 @@ describe('SomaClient', () => {
|
|
|
289
635
|
note: '-',
|
|
290
636
|
})
|
|
291
637
|
|
|
292
|
-
|
|
293
|
-
expect(dashboardCallIndex).toBeGreaterThanOrEqual(0)
|
|
638
|
+
expect(calls.some((c) => c.path === '/mypage/myMain/dashboard.do')).toBe(true)
|
|
294
639
|
expect(calls).toContainEqual({
|
|
295
640
|
method: 'post',
|
|
296
641
|
path: '/mypage/officeMng/rentTime.do',
|
|
@@ -305,53 +650,330 @@ describe('SomaClient', () => {
|
|
|
305
650
|
})
|
|
306
651
|
})
|
|
307
652
|
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
653
|
+
it('fills trainee dashboard mentoring sessions from all upcoming application history pages', async () => {
|
|
654
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
655
|
+
const todayDotted = today.replaceAll('-', '.')
|
|
656
|
+
const { http, calls } = createFakeHttp({
|
|
657
|
+
identity: { userId: 'trainee@example.com', userNm: '김연수' },
|
|
658
|
+
getBody: (path, data) => {
|
|
659
|
+
if (path === '/mypage/myMain/dashboard.do') {
|
|
660
|
+
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> OpenSoma</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-blue label"><span>17기 연수생</span></span><div class="welcome"><strong>김연수</strong>님 안녕하세요.</div></div></div></li></ul><ul class="bbs-dash_w"><li>멘토링 · 멘토특강<li><a href="/sw/mypage/mentoLec/view.do?qustnrSn=999">네이티브 항목 접수중</a></li></li></ul>'
|
|
661
|
+
}
|
|
662
|
+
if (path === '/mypage/userAnswer/history.do') {
|
|
663
|
+
const page = Number(data?.pageIndex ?? '1')
|
|
664
|
+
if (page === 1) {
|
|
665
|
+
return `<table><tbody>
|
|
666
|
+
<tr><td>45</td><td>멘토특강</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=39">미래 특강</a></td><td>김멘토</td><td>2099.01.01(목) 10:00:00 ~ 12:00:00</td><td>2026-04-20 22:41</td><td>[접수완료]</td><td>[OK]</td><td>-</td><td>-</td></tr>
|
|
667
|
+
<tr><td>44</td><td>멘토특강</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=40">이미 지난 특강</a></td><td>장영원</td><td>2000.01.01(토) 20:00:00 ~ 22:30:00</td><td>2026-04-24 22:41</td><td>[접수완료]</td><td>[OK]</td><td>-</td><td>-</td></tr>
|
|
668
|
+
<tr><td>43</td><td>자유멘토링</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=41">취소한 멘토링</a></td><td>이현수</td><td>2099.01.01(목) 20:00:00 ~ 21:30:00</td><td>2026-04-26 03:18</td><td>[접수취소]</td><td>[OK]</td><td>-</td><td>-</td></tr>
|
|
669
|
+
</tbody></table><ul class="bbs-total"><li>Total : 5</li><li>1/2 Page</li></ul>`
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return `<table><tbody>
|
|
673
|
+
<tr><td>42</td><td>자유멘토링</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=44">팀 프로젝트 방향성 피드백</a></td><td>이상철</td><td>${todayDotted}(화) 22:00:00 ~ 24:00:00</td><td>2026-04-27 22:41</td><td>[접수완료]</td><td>[OK]</td><td>-</td><td>-</td></tr>
|
|
674
|
+
<tr><td>41</td><td>행사</td><td><a href="/sw/mypage/applicants/view.do?bbsId=38">멘토링이 아닌 행사</a></td><td>관리자</td><td>2099.01.02(금) 10:00:00 ~ 12:00:00</td><td>2026-04-20 22:41</td><td>[접수완료]</td><td>[OK]</td><td>-</td><td>-</td></tr>
|
|
675
|
+
</tbody></table><ul class="bbs-total"><li>Total : 5</li><li>2/2 Page</li></ul>`
|
|
676
|
+
}
|
|
677
|
+
return ''
|
|
314
678
|
},
|
|
315
|
-
checkLogin: async () => ({ userId: 'neo@example.com', userNm: '전수열' }),
|
|
316
679
|
})
|
|
680
|
+
const client = new SomaClient({ http })
|
|
317
681
|
|
|
318
|
-
await client.
|
|
682
|
+
const dashboard = await client.dashboard.get()
|
|
319
683
|
|
|
320
|
-
expect(
|
|
321
|
-
|
|
684
|
+
expect(dashboard.role).toBe('17기 연수생')
|
|
685
|
+
expect(dashboard.mentoringSessions).toEqual([
|
|
686
|
+
{
|
|
687
|
+
title: '팀 프로젝트 방향성 피드백',
|
|
688
|
+
url: '/sw/mypage/mentoLec/view.do?qustnrSn=44',
|
|
689
|
+
status: '접수완료',
|
|
690
|
+
date: today,
|
|
691
|
+
time: '22:00',
|
|
692
|
+
timeEnd: '24:00',
|
|
693
|
+
type: '자유 멘토링',
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
title: '미래 특강',
|
|
697
|
+
url: '/sw/mypage/mentoLec/view.do?qustnrSn=39',
|
|
698
|
+
status: '접수완료',
|
|
699
|
+
date: '2099-01-01',
|
|
700
|
+
time: '10:00',
|
|
701
|
+
timeEnd: '12:00',
|
|
702
|
+
type: '멘토 특강',
|
|
703
|
+
},
|
|
704
|
+
])
|
|
705
|
+
const historyPages = calls
|
|
706
|
+
.filter((c) => c.path === '/mypage/userAnswer/history.do')
|
|
707
|
+
.map((c) => c.data?.pageIndex ?? '1')
|
|
708
|
+
.sort()
|
|
709
|
+
expect(historyPages).toEqual(['1', '2'])
|
|
710
|
+
expect(calls.some((c) => c.path === '/mypage/mentoLec/list.do')).toBe(false)
|
|
322
711
|
})
|
|
323
712
|
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
713
|
+
it('enriches dashboard with every team the mentor belongs to', async () => {
|
|
714
|
+
const teamCard = (name: string, ownerId: string, teamId: string) =>
|
|
715
|
+
`<li><div class="top"><strong class="t"><a href="javascript:void(0);" onclick="teamPageGo('${name}','${ownerId}','${teamId}');">${name}</a></strong></div><p>팀원 3명</p><button type="button">참여중</button></li>`
|
|
716
|
+
const { http, calls } = createFakeHttp({
|
|
717
|
+
identity: { userId: 'mentor@example.com', userNm: 'Mentor One' },
|
|
718
|
+
getBody: (path) => {
|
|
719
|
+
if (path === '/mypage/myMain/dashboard.do') {
|
|
720
|
+
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> Org</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-orange label"><span>멘토</span></span><div class="welcome"><strong>Mentor One</strong>님 안녕하세요.</div></div><ul class="dash-box"><li><strong class="t">팀명</strong><div class="c">Team Alpha</div></li><li><strong class="t">팀원</strong><div class="c">Member A,Member B,Member C</div></li><li><strong class="t">멘토</strong><div class="c">Mentor One,Mentor Two</div></li></ul></div></li></ul>'
|
|
721
|
+
}
|
|
722
|
+
if (path === '/mypage/myTeam/team.do') {
|
|
723
|
+
return `<ul class="bbs-team">${teamCard('Team Alpha', 'Mentor One', 'team-alpha')}${teamCard('Team Beta', 'Mentor Three', 'team-beta')}</ul><p class="ico-team">현재 참여중인 방은 <strong class="color-blue">2</strong>/100팀 입니다</p>`
|
|
724
|
+
}
|
|
725
|
+
return '<table><tbody></tbody></table><ul class="bbs-total"><li>Total : 0</li><li>1/1 Page</li></ul>'
|
|
332
726
|
},
|
|
333
|
-
|
|
334
|
-
|
|
727
|
+
})
|
|
728
|
+
const client = new SomaClient({ http })
|
|
729
|
+
|
|
730
|
+
const dashboard = await client.dashboard.get()
|
|
731
|
+
|
|
732
|
+
expect(dashboard.team).toEqual({
|
|
733
|
+
name: 'Team Alpha',
|
|
734
|
+
members: 'Member A,Member B,Member C',
|
|
735
|
+
mentor: 'Mentor One,Mentor Two',
|
|
736
|
+
})
|
|
737
|
+
expect(dashboard.teams.map((t) => ({ name: t.name, teamId: t.teamId }))).toEqual([
|
|
738
|
+
{ name: 'Team Alpha', teamId: 'team-alpha' },
|
|
739
|
+
{ name: 'Team Beta', teamId: 'team-beta' },
|
|
740
|
+
])
|
|
741
|
+
const teamCall = calls.find((c) => c.path === '/mypage/myTeam/team.do')
|
|
742
|
+
expect(teamCall?.data).toMatchObject({
|
|
743
|
+
menuNo: MENU_NO.TEAM,
|
|
744
|
+
searchCnd: '2',
|
|
745
|
+
searchWrd: 'Mentor One',
|
|
746
|
+
})
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
it('enriches trainee dashboard with every team the trainee belongs to', async () => {
|
|
750
|
+
const { http, calls } = createFakeHttp({
|
|
751
|
+
identity: { userId: 'trainee@example.com', userNm: 'Trainee One' },
|
|
752
|
+
getBody: (path) => {
|
|
753
|
+
if (path === '/mypage/myMain/dashboard.do') {
|
|
754
|
+
return '<ul class="dash-top"><li class="dash-card"><div class="dash-etc"><span>소속 :<br> Org</span><span>직책 :<br> </span></div><div class="dash-state"><div class="top"><span class="bg-blue label"><span>17기 연수생</span></span><div class="welcome"><strong>Trainee One</strong>님 안녕하세요.</div></div></div></li></ul>'
|
|
755
|
+
}
|
|
756
|
+
if (path === '/mypage/myTeam/team.do') {
|
|
757
|
+
return `<ul class="bbs-team"><li><div class="top"><strong class="t"><a href="javascript:void(0);" onclick="teamPageGo('Mentor One','owner-1','team-1');">Team Alpha</a></strong></div><p>팀원 3명</p><button type="button">참여중</button></li></ul><p class="ico-team">현재 참여중인 방은 <strong class="color-blue">1</strong>/100팀 입니다</p>`
|
|
758
|
+
}
|
|
759
|
+
return '<table><tbody></tbody></table><ul class="bbs-total"><li>Total : 0</li><li>1/1 Page</li></ul>'
|
|
335
760
|
},
|
|
336
|
-
|
|
761
|
+
})
|
|
762
|
+
const client = new SomaClient({ http })
|
|
763
|
+
|
|
764
|
+
const dashboard = await client.dashboard.get()
|
|
765
|
+
|
|
766
|
+
expect(dashboard.role).toBe('17기 연수생')
|
|
767
|
+
expect(dashboard.teams.map((t) => t.name)).toEqual(['Team Alpha'])
|
|
768
|
+
const teamCall = calls.find((c) => c.path === '/mypage/myTeam/team.do')
|
|
769
|
+
expect(teamCall?.data).toMatchObject({
|
|
770
|
+
menuNo: MENU_NO.TEAM,
|
|
771
|
+
searchCnd: '1',
|
|
772
|
+
searchWrd: 'Trainee One',
|
|
773
|
+
})
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
it('routes schedule calls to the expected endpoint', async () => {
|
|
777
|
+
const { http, calls } = createFakeHttp({
|
|
778
|
+
identity: { userId: 'neo@example.com', userNm: '전수열' },
|
|
779
|
+
getBody: (path) => {
|
|
780
|
+
if (path === '/mypage/schedule/list.do') {
|
|
781
|
+
return '<table><tbody><tr><td>1</td><td>팀78</td><td>배준서</td><td>이유제, 이중곤</td><td>-</td><td>-</td><td>-</td><td>-</td></tr></tbody></table><table><thead><tr><th>날짜</th><th>구분</th><th>제목</th></tr></thead><tbody><tr><td>2026-04-01 ~ 2026-04-02</td><td>행사</td><td>행사</td></tr></tbody></table>'
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return ''
|
|
785
|
+
},
|
|
786
|
+
})
|
|
787
|
+
const client = new SomaClient({ http })
|
|
788
|
+
|
|
789
|
+
const schedule = await client.schedule.list({ page: 5 })
|
|
790
|
+
|
|
791
|
+
expect(schedule.items).toEqual([
|
|
792
|
+
{
|
|
793
|
+
id: 1,
|
|
794
|
+
category: '행사',
|
|
795
|
+
title: '행사',
|
|
796
|
+
period: { start: '2026-04-01', end: '2026-04-02' },
|
|
797
|
+
},
|
|
798
|
+
])
|
|
799
|
+
expect(schedule.pagination).toEqual({ total: 1, currentPage: 1, totalPages: 1 })
|
|
800
|
+
expect(calls).toContainEqual({
|
|
801
|
+
method: 'get',
|
|
802
|
+
path: '/mypage/schedule/list.do',
|
|
803
|
+
data: { menuNo: MENU_NO.SCHEDULE, pageIndex: '5' },
|
|
804
|
+
})
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
it('joins a team by POSTing the native payload to updateUserTeamIn.json', async () => {
|
|
808
|
+
const { http, calls } = createFakeHttp({
|
|
809
|
+
identity: {
|
|
810
|
+
userId: 'neo@example.com',
|
|
811
|
+
userNm: '전수열',
|
|
812
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
813
|
+
userGb: UserGb.Mentor,
|
|
814
|
+
},
|
|
815
|
+
postJsonBody: () => ({ resultCode: 'success' }),
|
|
816
|
+
})
|
|
817
|
+
const client = new SomaClient({ http })
|
|
818
|
+
|
|
819
|
+
await client.team.join('60e6785c8c404142b12cf9ed2a3d811f')
|
|
820
|
+
|
|
821
|
+
expect(calls).toContainEqual({
|
|
822
|
+
method: 'postJson',
|
|
823
|
+
path: '/mypage/myTeam/updateUserTeamIn.json',
|
|
824
|
+
data: {
|
|
825
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
826
|
+
userNm: '전수열',
|
|
827
|
+
userGb: UserGb.Mentor,
|
|
828
|
+
teamNo: '60e6785c8c404142b12cf9ed2a3d811f',
|
|
829
|
+
},
|
|
830
|
+
})
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
it('leaves a team by POSTing the native payload to updateUserTeamOut.json', async () => {
|
|
834
|
+
const { http, calls } = createFakeHttp({
|
|
835
|
+
identity: {
|
|
836
|
+
userId: 'neo@example.com',
|
|
837
|
+
userNm: '전수열',
|
|
838
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
839
|
+
userGb: UserGb.Mentor,
|
|
840
|
+
},
|
|
841
|
+
postJsonBody: () => ({ resultCode: 'success' }),
|
|
842
|
+
})
|
|
843
|
+
const client = new SomaClient({ http })
|
|
844
|
+
|
|
845
|
+
await client.team.leave('60e6785c8c404142b12cf9ed2a3d811f')
|
|
846
|
+
|
|
847
|
+
expect(calls).toContainEqual({
|
|
848
|
+
method: 'postJson',
|
|
849
|
+
path: '/mypage/myTeam/updateUserTeamOut.json',
|
|
850
|
+
data: {
|
|
851
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
852
|
+
userNm: '전수열',
|
|
853
|
+
userGb: UserGb.Mentor,
|
|
854
|
+
teamNo: '60e6785c8c404142b12cf9ed2a3d811f',
|
|
855
|
+
},
|
|
856
|
+
})
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
it('throws when team.join receives a non-success resultCode', async () => {
|
|
860
|
+
const { http } = createFakeHttp({
|
|
861
|
+
identity: {
|
|
862
|
+
userId: 'neo@example.com',
|
|
863
|
+
userNm: '전수열',
|
|
864
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
865
|
+
userGb: UserGb.Mentor,
|
|
866
|
+
},
|
|
867
|
+
postJsonBody: () => ({ resultCode: 'fail' }),
|
|
868
|
+
})
|
|
869
|
+
const client = new SomaClient({ http })
|
|
870
|
+
|
|
871
|
+
await expect(client.team.join('team-1')).rejects.toThrow('팀 참여에 실패했습니다.')
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
it('throws when team.leave receives a non-success resultCode', async () => {
|
|
875
|
+
const { http } = createFakeHttp({
|
|
876
|
+
identity: {
|
|
877
|
+
userId: 'neo@example.com',
|
|
878
|
+
userNm: '전수열',
|
|
879
|
+
userNo: 'f6d192ad3b3e4ee29f1d238714ab92c1',
|
|
880
|
+
userGb: UserGb.Mentor,
|
|
881
|
+
},
|
|
882
|
+
postJsonBody: () => ({ resultCode: 'fail' }),
|
|
883
|
+
})
|
|
884
|
+
const client = new SomaClient({ http })
|
|
885
|
+
|
|
886
|
+
await expect(client.team.leave('team-1')).rejects.toThrow('팀 탈퇴에 실패했습니다.')
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
it('throws when team.join cannot resolve userNo', async () => {
|
|
890
|
+
const { http } = createFakeHttp({
|
|
891
|
+
identity: {
|
|
892
|
+
userId: 'neo@example.com',
|
|
893
|
+
userNm: '전수열',
|
|
894
|
+
userNo: '',
|
|
895
|
+
userGb: UserGb.Mentor,
|
|
896
|
+
},
|
|
897
|
+
})
|
|
898
|
+
const client = new SomaClient({ http })
|
|
899
|
+
|
|
900
|
+
await expect(client.team.join('team-1')).rejects.toThrow('userNo')
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
it('exhausts mentoring pagination so dashboard sessions span all pages', async () => {
|
|
904
|
+
const buildMentoringRow = (id: number, sessionDate: string) =>
|
|
905
|
+
`<tr><td>1</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=${id}">[멘토 특강] 세션 ${id} [접수중]</a></td><td>${sessionDate} ~ ${sessionDate}</td><td>${sessionDate}(목) 10:00 ~ 11:00</td><td>1 /4</td><td>OK</td><td>[접수중]</td><td>전수열</td><td>${sessionDate}</td></tr>`
|
|
906
|
+
const buildPaginatedListBody = (rows: string, currentPage: number, totalPages: number, total: number) =>
|
|
907
|
+
`<table><tbody>${rows}</tbody></table><ul class="bbs-total"><li>Total : ${total}</li><li>${currentPage}/${totalPages} Page</li></ul>`
|
|
908
|
+
const { http, calls } = createFakeHttp({
|
|
909
|
+
identity: { userId: 'neo@example.com', userNm: '전수열' },
|
|
910
|
+
getBody: (path, data) => {
|
|
911
|
+
if (path === '/mypage/myMain/dashboard.do') {
|
|
912
|
+
return ''
|
|
913
|
+
}
|
|
914
|
+
if (path === '/mypage/mentoLec/list.do') {
|
|
915
|
+
const page = Number(data?.pageIndex ?? '1')
|
|
916
|
+
if (page === 1) return buildPaginatedListBody(buildMentoringRow(103, '2026-04-17'), 1, 3, 3)
|
|
917
|
+
if (page === 2) return buildPaginatedListBody(buildMentoringRow(101, '2026-04-03'), 2, 3, 3)
|
|
918
|
+
if (page === 3) return buildPaginatedListBody(buildMentoringRow(102, '2026-04-10'), 3, 3, 3)
|
|
919
|
+
}
|
|
920
|
+
return ''
|
|
921
|
+
},
|
|
922
|
+
})
|
|
923
|
+
const client = new SomaClient({ http })
|
|
924
|
+
|
|
925
|
+
const dashboard = await client.dashboard.get()
|
|
926
|
+
|
|
927
|
+
expect(dashboard.mentoringSessions.map((s) => s.url)).toEqual([
|
|
928
|
+
'/mypage/mentoLec/view.do?qustnrSn=101',
|
|
929
|
+
'/mypage/mentoLec/view.do?qustnrSn=102',
|
|
930
|
+
'/mypage/mentoLec/view.do?qustnrSn=103',
|
|
931
|
+
])
|
|
932
|
+
const listPages = calls
|
|
933
|
+
.filter((c) => c.path === '/mypage/mentoLec/list.do')
|
|
934
|
+
.map((c) => c.data?.pageIndex ?? '1')
|
|
935
|
+
.sort()
|
|
936
|
+
expect(listPages).toEqual(['1', '2', '3'])
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
it('delegates login and isLoggedIn to SomaHttp', async () => {
|
|
940
|
+
const loginCalls: string[] = []
|
|
941
|
+
const { http } = createFakeHttp({
|
|
942
|
+
identity: { userId: 'neo@example.com', userNm: '전수열' },
|
|
943
|
+
onLogin: (username, password) => loginCalls.push(`${username}:${password}`),
|
|
944
|
+
})
|
|
945
|
+
const client = new SomaClient({ username: 'neo@example.com', password: 'secret', http })
|
|
946
|
+
|
|
947
|
+
await client.login()
|
|
948
|
+
|
|
949
|
+
expect(loginCalls).toEqual(['neo@example.com:secret'])
|
|
950
|
+
await expect(client.isLoggedIn()).resolves.toBe(true)
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
it('re-logs in automatically when username and password are configured', async () => {
|
|
954
|
+
const loginCalls: string[] = []
|
|
955
|
+
const { http } = createFakeHttp({
|
|
956
|
+
checkLoginSequence: [null, { userId: 'neo@example.com', userNm: '전수열' }],
|
|
957
|
+
onLogin: (username, password) => loginCalls.push(`${username}:${password}`),
|
|
958
|
+
getBody: () =>
|
|
337
959
|
'<table><tbody><tr><td>1</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=123">[자유 멘토링] 제목 [접수중]</a></td><td>2026-04-01 ~ 2026-04-02</td><td>2026-04-03(목) 10:00 ~ 11:00</td><td>1 /4</td><td>OK</td><td>[접수중]</td><td>작성자</td><td>2026-04-01</td></tr></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>',
|
|
338
960
|
})
|
|
961
|
+
const client = new SomaClient({ username: 'neo@example.com', password: 'secret', http })
|
|
339
962
|
|
|
340
963
|
await expect(client.mentoring.list()).resolves.toMatchObject({
|
|
341
964
|
items: [expect.objectContaining({ id: 123, title: '제목' })],
|
|
342
965
|
})
|
|
343
|
-
expect(
|
|
966
|
+
expect(loginCalls).toEqual(['neo@example.com:secret'])
|
|
344
967
|
})
|
|
345
968
|
|
|
346
|
-
|
|
347
|
-
const client = new SomaClient()
|
|
969
|
+
it('persists the credentials used by login() when saveCredentials is called', async () => {
|
|
348
970
|
const dir = await mkdtemp(join(tmpdir(), 'opensoma-client-save-'))
|
|
349
971
|
const manager = new CredentialManager(dir)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
getCsrfToken: () => 'csrf-1',
|
|
972
|
+
const { http } = createFakeHttp({
|
|
973
|
+
sessionCookie: 'session-1',
|
|
974
|
+
csrfToken: 'csrf-1',
|
|
354
975
|
})
|
|
976
|
+
const client = new SomaClient({ http })
|
|
355
977
|
|
|
356
978
|
await client.login('neo@example.com', 'secret')
|
|
357
979
|
await client.saveCredentials(manager)
|
|
@@ -367,25 +989,19 @@ describe('SomaClient', () => {
|
|
|
367
989
|
await manager.remove()
|
|
368
990
|
})
|
|
369
991
|
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
logout: async () => {
|
|
375
|
-
calls.push('logout')
|
|
376
|
-
},
|
|
377
|
-
})
|
|
992
|
+
it('delegates logout to SomaHttp', async () => {
|
|
993
|
+
const logoutCalls: string[] = []
|
|
994
|
+
const { http } = createFakeHttp({ onLogout: () => logoutCalls.push('logout') })
|
|
995
|
+
const client = new SomaClient({ http })
|
|
378
996
|
|
|
379
997
|
await client.logout()
|
|
380
998
|
|
|
381
|
-
expect(
|
|
999
|
+
expect(logoutCalls).toEqual(['logout'])
|
|
382
1000
|
})
|
|
383
1001
|
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
checkLogin: async () => null,
|
|
388
|
-
})
|
|
1002
|
+
it('throws AuthenticationError from every auth-required operation when not logged in', async () => {
|
|
1003
|
+
const { http } = createFakeHttp({ identity: null })
|
|
1004
|
+
const client = new SomaClient({ http })
|
|
389
1005
|
|
|
390
1006
|
await expect(client.mentoring.list()).rejects.toBeInstanceOf(AuthenticationError)
|
|
391
1007
|
await expect(client.mentoring.get(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
@@ -418,21 +1034,22 @@ describe('SomaClient', () => {
|
|
|
418
1034
|
await expect(
|
|
419
1035
|
client.room.reserve({ roomId: 1, date: '2026-04-01', slots: ['10:00'], title: 'Test' }),
|
|
420
1036
|
).rejects.toBeInstanceOf(AuthenticationError)
|
|
1037
|
+
await expect(client.room.get(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
1038
|
+
await expect(client.room.update(1, { title: 'x' })).rejects.toBeInstanceOf(AuthenticationError)
|
|
1039
|
+
await expect(client.room.cancel(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
421
1040
|
await expect(client.dashboard.get()).rejects.toBeInstanceOf(AuthenticationError)
|
|
422
1041
|
await expect(client.notice.list()).rejects.toBeInstanceOf(AuthenticationError)
|
|
423
1042
|
await expect(client.notice.get(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
424
|
-
await expect(client.team.
|
|
1043
|
+
await expect(client.team.list()).rejects.toBeInstanceOf(AuthenticationError)
|
|
1044
|
+
await expect(client.team.join('team-1')).rejects.toBeInstanceOf(AuthenticationError)
|
|
1045
|
+
await expect(client.team.leave('team-1')).rejects.toBeInstanceOf(AuthenticationError)
|
|
425
1046
|
await expect(client.member.show()).rejects.toBeInstanceOf(AuthenticationError)
|
|
426
|
-
await expect(client.
|
|
427
|
-
await expect(client.event.get(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
428
|
-
await expect(client.event.apply(1)).rejects.toBeInstanceOf(AuthenticationError)
|
|
1047
|
+
await expect(client.schedule.list()).rejects.toBeInstanceOf(AuthenticationError)
|
|
429
1048
|
})
|
|
430
1049
|
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
checkLogin: async () => null,
|
|
435
|
-
})
|
|
1050
|
+
it('includes helpful login hints in the AuthenticationError message', async () => {
|
|
1051
|
+
const { http } = createFakeHttp({ identity: null })
|
|
1052
|
+
const client = new SomaClient({ http })
|
|
436
1053
|
|
|
437
1054
|
try {
|
|
438
1055
|
await client.mentoring.list()
|
|
@@ -443,4 +1060,48 @@ describe('SomaClient', () => {
|
|
|
443
1060
|
expect((error as Error).message).toContain('opensoma auth extract')
|
|
444
1061
|
}
|
|
445
1062
|
})
|
|
1063
|
+
|
|
1064
|
+
it('single-flights relogin across concurrent auth-required calls', async () => {
|
|
1065
|
+
const loginCalls: string[] = []
|
|
1066
|
+
let resolveLoginStarted: (() => void) | undefined
|
|
1067
|
+
const loginStarted = new Promise<void>((resolve) => {
|
|
1068
|
+
resolveLoginStarted = resolve
|
|
1069
|
+
})
|
|
1070
|
+
let allowLogin: (() => void) | undefined
|
|
1071
|
+
const loginGate = new Promise<void>((resolve) => {
|
|
1072
|
+
allowLogin = resolve
|
|
1073
|
+
})
|
|
1074
|
+
let loggedIn = false
|
|
1075
|
+
|
|
1076
|
+
const fake = {
|
|
1077
|
+
checkLogin: async () => (loggedIn ? { userId: 'neo@example.com', userNm: '전수열' } : null),
|
|
1078
|
+
get: async () =>
|
|
1079
|
+
'<table><tbody><tr><td>1</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=123">[자유 멘토링] 제목 [접수중]</a></td><td>2026-04-01 ~ 2026-04-02</td><td>2026-04-03(목) 10:00 ~ 11:00</td><td>1 /4</td><td>OK</td><td>[접수중]</td><td>작성자</td><td>2026-04-01</td></tr></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>',
|
|
1080
|
+
post: async () => '',
|
|
1081
|
+
postForm: async () => '',
|
|
1082
|
+
postMultipart: async () => '',
|
|
1083
|
+
login: async (username: string, password: string) => {
|
|
1084
|
+
loginCalls.push(`${username}:${password}`)
|
|
1085
|
+
resolveLoginStarted?.()
|
|
1086
|
+
await loginGate
|
|
1087
|
+
loggedIn = true
|
|
1088
|
+
},
|
|
1089
|
+
logout: async () => {},
|
|
1090
|
+
getSessionCookie: () => 'session-after-relogin',
|
|
1091
|
+
getCsrfToken: () => 'csrf-after-relogin',
|
|
1092
|
+
}
|
|
1093
|
+
const client = new SomaClient({
|
|
1094
|
+
username: 'neo@example.com',
|
|
1095
|
+
password: 'secret',
|
|
1096
|
+
http: fake as unknown as SomaHttp,
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
const parallel = Promise.all([client.mentoring.list(), client.mentoring.list(), client.mentoring.list()])
|
|
1100
|
+
await loginStarted
|
|
1101
|
+
expect(loginCalls).toEqual(['neo@example.com:secret'])
|
|
1102
|
+
allowLogin?.()
|
|
1103
|
+
await parallel
|
|
1104
|
+
|
|
1105
|
+
expect(loginCalls).toEqual(['neo@example.com:secret'])
|
|
1106
|
+
})
|
|
446
1107
|
})
|