opensoma 0.8.0 → 0.9.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.
@@ -654,10 +654,10 @@ describe('SomaClient', () => {
654
654
  const today = new Date().toISOString().slice(0, 10)
655
655
  const todayDotted = today.replaceAll('-', '.')
656
656
  const { http, calls } = createFakeHttp({
657
- identity: { userId: 'trainee@example.com', userNm: '김연수' },
657
+ identity: { userId: 'trainee@example.com', userNm: 'Trainee One', userNo: 'trainee-1', userGb: UserGb.Trainee },
658
658
  getBody: (path, data) => {
659
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>'
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>Trainee One</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
661
  }
662
662
  if (path === '/mypage/userAnswer/history.do') {
663
663
  const page = Number(data?.pageIndex ?? '1')
@@ -710,6 +710,58 @@ describe('SomaClient', () => {
710
710
  expect(calls.some((c) => c.path === '/mypage/mentoLec/list.do')).toBe(false)
711
711
  })
712
712
 
713
+ it('uses checkLogin userGb instead of dashboard role to choose the trainee dashboard source', async () => {
714
+ const { http, calls } = createFakeHttp({
715
+ identity: { userId: 'trainee@example.com', userNm: 'Trainee One', userNo: 'trainee-1', userGb: UserGb.Trainee },
716
+ getBody: (path) => {
717
+ if (path === '/mypage/myMain/dashboard.do') {
718
+ 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>Trainee One</strong>님 안녕하세요.</div></div></div></li></ul>'
719
+ }
720
+ if (path === '/mypage/userAnswer/history.do') {
721
+ return '<table><tbody><tr><td>1</td><td>멘토특강</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=39">미래 특강</a></td><td>Mentor One</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></tbody></table><ul class="bbs-total"><li>Total : 1</li><li>1/1 Page</li></ul>'
722
+ }
723
+ if (path === '/mypage/myTeam/team.do') {
724
+ return '<ul class="bbs-team"></ul><p class="ico-team">현재 참여중인 방은 <strong class="color-blue">0</strong>/100팀 입니다</p>'
725
+ }
726
+ return ''
727
+ },
728
+ })
729
+ const client = new SomaClient({ http })
730
+
731
+ const dashboard = await client.dashboard.get()
732
+
733
+ expect(dashboard.role).toBe('멘토')
734
+ expect(dashboard.mentoringSessions.map((item) => item.url)).toEqual(['/sw/mypage/mentoLec/view.do?qustnrSn=39'])
735
+ expect(calls.some((c) => c.path === '/mypage/userAnswer/history.do')).toBe(true)
736
+ expect(calls.some((c) => c.path === '/mypage/mentoLec/list.do')).toBe(false)
737
+ })
738
+
739
+ it('excludes deleted application history rows from trainee dashboard mentoring sessions', async () => {
740
+ const { http } = createFakeHttp({
741
+ identity: { userId: 'trainee@example.com', userNm: 'Trainee One', userNo: 'trainee-1', userGb: UserGb.Trainee },
742
+ getBody: (path) => {
743
+ if (path === '/mypage/myMain/dashboard.do') {
744
+ 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>'
745
+ }
746
+ if (path === '/mypage/userAnswer/history.do') {
747
+ return `<table><tbody>
748
+ <tr><td>2</td><td>멘토특강</td><td>Deleted Lecture</td><td>Mentor One</td><td>2099.01.01(목) 19:00:00 ~ 22:00:00</td><td>2026-04-20 22:41</td><td>[접수완료]</td><td>[OK]</td><td>삭제</td><td>-</td></tr>
749
+ <tr><td>1</td><td>멘토특강</td><td><a href="/sw/mypage/mentoLec/view.do?qustnrSn=39">Active Lecture</a></td><td>Mentor Two</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>
750
+ </tbody></table><ul class="bbs-total"><li>Total : 2</li><li>1/1 Page</li></ul>`
751
+ }
752
+ if (path === '/mypage/myTeam/team.do') {
753
+ return '<ul class="bbs-team"></ul><p class="ico-team">현재 참여중인 방은 <strong class="color-blue">0</strong>/100팀 입니다</p>'
754
+ }
755
+ return ''
756
+ },
757
+ })
758
+ const client = new SomaClient({ http })
759
+
760
+ const dashboard = await client.dashboard.get()
761
+
762
+ expect(dashboard.mentoringSessions.map((item) => item.title)).toEqual(['Active Lecture'])
763
+ })
764
+
713
765
  it('enriches dashboard with every team the mentor belongs to', async () => {
714
766
  const teamCard = (name: string, ownerId: string, teamId: string) =>
715
767
  `<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>`
@@ -748,7 +800,7 @@ describe('SomaClient', () => {
748
800
 
749
801
  it('enriches trainee dashboard with every team the trainee belongs to', async () => {
750
802
  const { http, calls } = createFakeHttp({
751
- identity: { userId: 'trainee@example.com', userNm: 'Trainee One' },
803
+ identity: { userId: 'trainee@example.com', userNm: 'Trainee One', userNo: 'trainee-1', userGb: UserGb.Trainee },
752
804
  getBody: (path) => {
753
805
  if (path === '/mypage/myMain/dashboard.do') {
754
806
  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>'
@@ -1057,7 +1109,6 @@ describe('SomaClient', () => {
1057
1109
  } catch (error) {
1058
1110
  expect(error).toBeInstanceOf(AuthenticationError)
1059
1111
  expect((error as Error).message).toContain('opensoma auth login')
1060
- expect((error as Error).message).toContain('opensoma auth extract')
1061
1112
  }
1062
1113
  })
1063
1114
 
package/src/client.ts CHANGED
@@ -4,7 +4,7 @@ import { MENU_NO } from './constants'
4
4
  import { CredentialManager } from './credential-manager'
5
5
  import { AuthenticationError } from './errors'
6
6
  import * as formatters from './formatters'
7
- import { SomaHttp, type UserIdentity } from './http'
7
+ import { SomaHttp, UserGb, type UserIdentity } from './http'
8
8
  import { buildMentoringListParams, type MentoringSearchQuery } from './shared/utils/mentoring-params'
9
9
  import {
10
10
  buildApplicationPayload,
@@ -357,11 +357,11 @@ export class SomaClient {
357
357
 
358
358
  this.dashboard = {
359
359
  get: async () => {
360
- await this.requireAuth()
360
+ const identity = await this.requireAuth()
361
361
  const dashboard = formatters.parseDashboard(
362
362
  await this.http.get('/mypage/myMain/dashboard.do', { menuNo: MENU_NO.DASHBOARD }),
363
363
  )
364
- const trainee = isTraineeRole(dashboard.role)
364
+ const trainee = identity.userGb === UserGb.Trainee
365
365
  const teamSearchField = trainee ? ('member' as const) : ('mentor' as const)
366
366
  if (trainee) {
367
367
  const [firstPage, teams] = await Promise.all([
@@ -598,7 +598,7 @@ export class SomaClient {
598
598
  }
599
599
  }
600
600
 
601
- private async requireAuth(): Promise<void> {
601
+ private async requireAuth(): Promise<UserIdentity> {
602
602
  let identity = await this.http.checkLogin()
603
603
  if (!identity && this.loginCredentials) {
604
604
  await this.relogin()
@@ -608,6 +608,8 @@ export class SomaClient {
608
608
  if (!identity) {
609
609
  throw new AuthenticationError()
610
610
  }
611
+
612
+ return identity
611
613
  }
612
614
 
613
615
  private async relogin(): Promise<void> {
@@ -730,14 +732,11 @@ export class SomaClient {
730
732
  }
731
733
  }
732
734
 
733
- function isTraineeRole(role: string): boolean {
734
- return role.includes('연수생')
735
- }
736
-
737
735
  function applicationHistoryToDashboardItem(
738
736
  item: ApplicationHistoryItem,
739
737
  ): Dashboard['mentoringSessions'][number] | null {
740
738
  if (item.applicationStatus.includes('취소')) return null
739
+ if (item.applicationDetail.includes('삭제')) return null
741
740
 
742
741
  const type = applicationCategoryToMentoringType(item.category)
743
742
  if (!type) return null
@@ -1,65 +1,11 @@
1
1
  import { describe, expect, it } from 'bun:test'
2
2
 
3
- import { inspectStoredAuthStatus, resolveExtractedCredentials } from './auth'
4
-
5
- const noBrowserExtraction = async () => null
6
-
7
- describe('resolveExtractedCredentials', () => {
8
- it('returns the first candidate that validates successfully', async () => {
9
- const calls: string[] = []
10
-
11
- const credentials = await resolveExtractedCredentials(
12
- [
13
- { browser: 'Chrome', lastAccessUtc: 30, profile: 'Default', sessionCookie: 'stale-session' },
14
- { browser: 'Chrome', lastAccessUtc: 20, profile: 'Profile 1', sessionCookie: 'valid-session' },
15
- ],
16
- (sessionCookie) => ({
17
- checkLogin: async () => {
18
- calls.push(`check:${sessionCookie}`)
19
- return sessionCookie === 'valid-session' ? { userId: 'neo', userNm: 'Neo' } : null
20
- },
21
- extractCsrfToken: async () => {
22
- calls.push(`csrf:${sessionCookie}`)
23
- return `${sessionCookie}-csrf`
24
- },
25
- }),
26
- )
27
-
28
- expect(credentials).toEqual({
29
- sessionCookie: 'valid-session',
30
- csrfToken: 'valid-session-csrf',
31
- })
32
- expect(calls).toEqual(['check:stale-session', 'check:valid-session', 'csrf:valid-session'])
33
- })
34
-
35
- it('returns null when every candidate is invalid or throws', async () => {
36
- const credentials = await resolveExtractedCredentials(
37
- [
38
- { browser: 'Chrome', lastAccessUtc: 30, profile: 'Default', sessionCookie: 'stale-session' },
39
- { browser: 'Edge', lastAccessUtc: 20, profile: 'Profile 1', sessionCookie: 'broken-session' },
40
- ],
41
- (sessionCookie) => ({
42
- checkLogin: async () => {
43
- if (sessionCookie === 'broken-session') {
44
- throw new Error('network error')
45
- }
46
-
47
- return null
48
- },
49
- extractCsrfToken: async () => {
50
- throw new Error('should not be called')
51
- },
52
- }),
53
- )
54
-
55
- expect(credentials).toBeNull()
56
- })
57
- })
3
+ import { inspectStoredAuthStatus } from './auth'
58
4
 
59
5
  describe('inspectStoredAuthStatus', () => {
60
- it('clears session state but preserves saved id/password when both recovery methods fail', async () => {
6
+ it('clears session state but preserves saved id/password when re-login fails', async () => {
61
7
  let cleared = false
62
- let postClearCredentials = {
8
+ const postClearCredentials = {
63
9
  sessionCookie: '',
64
10
  csrfToken: '',
65
11
  username: 'mentor@example.com',
@@ -93,7 +39,6 @@ describe('inspectStoredAuthStatus', () => {
93
39
  getSessionCookie: () => null,
94
40
  getCsrfToken: () => null,
95
41
  }),
96
- noBrowserExtraction,
97
42
  )
98
43
 
99
44
  expect(status).toEqual({
@@ -101,7 +46,7 @@ describe('inspectStoredAuthStatus', () => {
101
46
  credentials: null,
102
47
  clearedStaleSession: true,
103
48
  preservedRecoveryCredentials: true,
104
- hint: 'Session expired. Run: opensoma auth login or opensoma auth extract',
49
+ hint: 'Session expired. Run: opensoma auth login',
105
50
  })
106
51
  expect(cleared).toBe(true)
107
52
  })
@@ -128,8 +73,6 @@ describe('inspectStoredAuthStatus', () => {
128
73
  () => ({
129
74
  checkLogin: async () => null,
130
75
  }),
131
- undefined,
132
- noBrowserExtraction,
133
76
  )
134
77
 
135
78
  expect(status).toEqual({
@@ -137,7 +80,7 @@ describe('inspectStoredAuthStatus', () => {
137
80
  credentials: null,
138
81
  clearedStaleSession: true,
139
82
  preservedRecoveryCredentials: false,
140
- hint: 'Session expired. Run: opensoma auth login or opensoma auth extract',
83
+ hint: 'Session expired. Run: opensoma auth login',
141
84
  })
142
85
  })
143
86
 
@@ -171,46 +114,11 @@ describe('inspectStoredAuthStatus', () => {
171
114
  valid: false,
172
115
  username: 'mentor@example.com',
173
116
  loggedInAt: '2026-04-13T00:00:00.000Z',
174
- hint: 'Could not verify session. Try again or run: opensoma auth login or opensoma auth extract',
117
+ hint: 'Could not verify session. Try again or run: opensoma auth login',
175
118
  })
176
119
  expect(cleared).toBe(false)
177
120
  })
178
121
 
179
- it('recovers via browser extraction when no stored password is available', async () => {
180
- let savedCredentials: Record<string, unknown> | null = null
181
-
182
- const status = await inspectStoredAuthStatus(
183
- {
184
- getCredentials: async () => ({
185
- sessionCookie: 'stale-session',
186
- csrfToken: 'stale-csrf',
187
- }),
188
- setCredentials: async (credentials: Record<string, unknown>) => {
189
- savedCredentials = credentials
190
- },
191
- clearSessionState: async () => {
192
- throw new Error('should not clear session state when browser extraction succeeds')
193
- },
194
- },
195
- () => ({
196
- checkLogin: async () => null,
197
- }),
198
- undefined,
199
- async () => ({ sessionCookie: 'browser-session', csrfToken: 'browser-csrf' }),
200
- )
201
-
202
- expect(status).toMatchObject({
203
- authenticated: true,
204
- valid: true,
205
- username: null,
206
- })
207
- expect(savedCredentials).toMatchObject({
208
- sessionCookie: 'browser-session',
209
- csrfToken: 'browser-csrf',
210
- })
211
- expect(savedCredentials).toHaveProperty('loggedInAt')
212
- })
213
-
214
122
  it('refreshes the session automatically when stored username and password are available', async () => {
215
123
  let savedCredentials: Record<string, string> | null = null
216
124
 
@@ -7,7 +7,6 @@ import { SomaHttp } from '../http'
7
7
  import { recoverSession } from '../session-recovery'
8
8
  import { handleError } from '../shared/utils/error-handler'
9
9
  import { formatOutput } from '../shared/utils/output'
10
- import type { ExtractedSessionCandidate } from '../token-extractor'
11
10
 
12
11
  function ask(rl: ReadlineInterface, message: string): Promise<string> {
13
12
  return new Promise((resolve) => {
@@ -101,62 +100,12 @@ async function promptCredentials(
101
100
 
102
101
  type LoginOptions = { username?: string; password?: string; pretty?: boolean }
103
102
  type StatusOptions = { pretty?: boolean }
104
- type ExtractOptions = { debug?: boolean; pretty?: boolean }
105
- type ExtractedSessionValidator = Pick<SomaHttp, 'checkLogin' | 'extractCsrfToken'>
106
103
  type CredentialStore = Pick<CredentialManager, 'clearSessionState' | 'getCredentials' | 'setCredentials'>
107
104
  type StatusValidator = Pick<SomaHttp, 'checkLogin'>
108
105
  type ReloginHttp = Pick<SomaHttp, 'checkLogin' | 'getCsrfToken' | 'getSessionCookie' | 'login'>
109
- type BrowserExtractor = () => Promise<{ csrfToken: string; sessionCookie: string } | null>
110
106
 
111
- async function defaultExtractBrowserCredentials(): Promise<{ csrfToken: string; sessionCookie: string } | null> {
112
- const { TokenExtractor } = (await import('../token-extractor')) as {
113
- TokenExtractor: new () => { extractCandidates: () => Promise<ExtractedSessionCandidate[]> }
114
- }
115
- const candidates = await new TokenExtractor().extractCandidates()
116
- if (candidates.length === 0) return null
117
- return resolveExtractedCredentials(candidates)
118
- }
119
-
120
- const EXPIRED_SESSION_HINT = 'Session expired. Run: opensoma auth login or opensoma auth extract'
121
- const UNVERIFIED_SESSION_HINT =
122
- 'Could not verify session. Try again or run: opensoma auth login or opensoma auth extract'
123
-
124
- export async function resolveExtractedCredentials(
125
- candidates: ExtractedSessionCandidate[],
126
- createValidator: (sessionCookie: string) => ExtractedSessionValidator = (sessionCookie) =>
127
- new SomaHttp({ sessionCookie }),
128
- debug?: (message: string) => void,
129
- ): Promise<{ csrfToken: string; sessionCookie: string } | null> {
130
- debug?.(`Validating ${candidates.length} candidate(s) against server...`)
131
-
132
- for (const candidate of candidates) {
133
- const http = createValidator(candidate.sessionCookie)
134
- debug?.(` ${candidate.browser} / ${candidate.profile}: checkLogin...`)
135
-
136
- try {
137
- const valid = Boolean(await http.checkLogin())
138
- if (!valid) {
139
- debug?.(` ${candidate.browser} / ${candidate.profile}: session invalid`)
140
- continue
141
- }
142
-
143
- debug?.(` ${candidate.browser} / ${candidate.profile}: valid! Extracting CSRF token...`)
144
- const csrfToken = await http.extractCsrfToken()
145
- debug?.(` CSRF token obtained (${csrfToken.length} chars)`)
146
-
147
- return {
148
- sessionCookie: candidate.sessionCookie,
149
- csrfToken,
150
- }
151
- } catch (error) {
152
- debug?.(
153
- ` ${candidate.browser} / ${candidate.profile}: error: ${error instanceof Error ? error.message : String(error)}`,
154
- )
155
- }
156
- }
157
-
158
- return null
159
- }
107
+ const EXPIRED_SESSION_HINT = 'Session expired. Run: opensoma auth login'
108
+ const UNVERIFIED_SESSION_HINT = 'Could not verify session. Try again or run: opensoma auth login'
160
109
 
161
110
  async function loginAction(options: LoginOptions): Promise<void> {
162
111
  try {
@@ -230,7 +179,6 @@ export async function inspectStoredAuthStatus(
230
179
  createValidator: (credentials: { sessionCookie: string; csrfToken: string }) => StatusValidator = (credentials) =>
231
180
  new SomaHttp({ sessionCookie: credentials.sessionCookie, csrfToken: credentials.csrfToken }),
232
181
  createReloginHttp: () => ReloginHttp = () => new SomaHttp(),
233
- recoverViaBrowser: BrowserExtractor = defaultExtractBrowserCredentials,
234
182
  ): Promise<Record<string, boolean | null | string>> {
235
183
  const creds = await manager.getCredentials()
236
184
  if (!creds) {
@@ -271,21 +219,6 @@ export async function inspectStoredAuthStatus(
271
219
  }
272
220
  }
273
221
 
274
- try {
275
- const extracted = await recoverViaBrowser()
276
- if (extracted) {
277
- const loggedInAt = new Date().toISOString()
278
- await manager.setCredentials({
279
- sessionCookie: extracted.sessionCookie,
280
- csrfToken: extracted.csrfToken,
281
- loggedInAt,
282
- })
283
- return { authenticated: true, valid: true, username: null, loggedInAt }
284
- }
285
- } catch {
286
- // Browser extraction failed — fall through to credential removal
287
- }
288
-
289
222
  await manager.clearSessionState()
290
223
  const post = await manager.getCredentials()
291
224
  return {
@@ -313,45 +246,6 @@ async function statusAction(options: StatusOptions): Promise<void> {
313
246
  }
314
247
  }
315
248
 
316
- async function extractAction(options: ExtractOptions): Promise<void> {
317
- const log = options.debug ? (message: string) => process.stderr.write(`[extract] ${message}\n`) : undefined
318
-
319
- try {
320
- const { TokenExtractor } = (await import('../token-extractor')) as {
321
- TokenExtractor: new (options?: { debug?: boolean }) => {
322
- extractCandidates: () => Promise<ExtractedSessionCandidate[]>
323
- }
324
- }
325
- const extractor = new TokenExtractor({ debug: options.debug })
326
- const candidates = await extractor.extractCandidates()
327
- if (candidates.length === 0) {
328
- throw new Error(
329
- 'No SWMaestro session found in any browser. Login to swmaestro.ai or opensoma.dev in a supported Chromium browser (Chrome, Edge, Brave, Arc, Vivaldi) first.',
330
- )
331
- }
332
-
333
- log?.(
334
- `Extracted ${candidates.length} candidate(s): ${candidates.map((c) => `${c.browser}/${c.profile}`).join(', ')}`,
335
- )
336
-
337
- const credentials = await resolveExtractedCredentials(candidates, undefined, log)
338
- if (!credentials) {
339
- throw new Error(
340
- 'Found SWMaestro session cookies in supported browsers, but none are valid. Refresh your swmaestro.ai or opensoma.dev login in a supported Chromium browser and try again.',
341
- )
342
- }
343
-
344
- await new CredentialManager().setCredentials({
345
- sessionCookie: credentials.sessionCookie,
346
- csrfToken: credentials.csrfToken,
347
- loggedInAt: new Date().toISOString(),
348
- })
349
- console.log(formatOutput({ ok: true, extracted: true, valid: true }, options.pretty))
350
- } catch (error) {
351
- handleError(error)
352
- }
353
- }
354
-
355
249
  export const authCommand = new Command('auth')
356
250
  .description('Manage authentication')
357
251
  .addCommand(
@@ -374,10 +268,3 @@ export const authCommand = new Command('auth')
374
268
  .option('--pretty', 'Pretty print JSON output')
375
269
  .action(statusAction),
376
270
  )
377
- .addCommand(
378
- new Command('extract')
379
- .description('Extract browser credentials')
380
- .option('--debug', 'Show debug output')
381
- .option('--pretty', 'Pretty print JSON output')
382
- .action(extractAction),
383
- )
@@ -2,8 +2,6 @@ import { describe, expect, it } from 'bun:test'
2
2
 
3
3
  import { createAuthenticatedHttp } from './helpers'
4
4
 
5
- const noBrowserExtraction = async () => null
6
-
7
5
  describe('createAuthenticatedHttp', () => {
8
6
  it('throws a login hint when no credentials are stored', async () => {
9
7
  const manager = {
@@ -11,12 +9,10 @@ describe('createAuthenticatedHttp', () => {
11
9
  clearSessionState: async () => {},
12
10
  }
13
11
 
14
- await expect(createAuthenticatedHttp(manager)).rejects.toThrow(
15
- 'Not logged in. Run: opensoma auth login or opensoma auth extract',
16
- )
12
+ await expect(createAuthenticatedHttp(manager)).rejects.toThrow('Not logged in. Run: opensoma auth login')
17
13
  })
18
14
 
19
- it('clears only session state (not username/password) when both recovery methods fail', async () => {
15
+ it('clears only session state (not username/password) when re-login fails', async () => {
20
16
  let cleared = false
21
17
  const manager = {
22
18
  getCredentials: async () => ({
@@ -33,15 +29,13 @@ describe('createAuthenticatedHttp', () => {
33
29
  },
34
30
  }
35
31
 
36
- await expect(
37
- createAuthenticatedHttp(manager, () => ({ checkLogin: async () => null }), undefined, noBrowserExtraction),
38
- ).rejects.toThrow(
39
- 'Session expired. Run: opensoma auth login or opensoma auth extract (saved id/password were preserved)',
32
+ await expect(createAuthenticatedHttp(manager, () => ({ checkLogin: async () => null }))).rejects.toThrow(
33
+ 'Session expired. Run: opensoma auth login (saved id/password were preserved)',
40
34
  )
41
35
  expect(cleared).toBe(true)
42
36
  })
43
37
 
44
- it('preserves stored id/password on disk when session expires and recovery fails', async () => {
38
+ it('preserves stored id/password on disk when session expires and re-login fails', async () => {
45
39
  const { CredentialManager } = await import('../credential-manager')
46
40
  const { mkdtemp, rm } = await import('node:fs/promises')
47
41
  const { tmpdir } = await import('node:os')
@@ -70,7 +64,6 @@ describe('createAuthenticatedHttp', () => {
70
64
  getSessionCookie: () => null,
71
65
  getCsrfToken: () => null,
72
66
  }),
73
- noBrowserExtraction,
74
67
  ),
75
68
  ).rejects.toThrow('Session expired')
76
69
 
@@ -160,108 +153,4 @@ describe('createAuthenticatedHttp', () => {
160
153
  tozPhone: '010-1234-5678',
161
154
  })
162
155
  })
163
-
164
- it('recovers via browser extraction when no stored password is available', async () => {
165
- let savedCredentials: Record<string, unknown> | null = null
166
- const manager = {
167
- getCredentials: async () => ({
168
- sessionCookie: 'stale-session',
169
- csrfToken: 'stale-csrf',
170
- }),
171
- setCredentials: async (credentials: Record<string, unknown>) => {
172
- savedCredentials = credentials
173
- },
174
- clearSessionState: async () => {
175
- throw new Error('should not clear session state when browser extraction succeeds')
176
- },
177
- }
178
- const recoveredHttp = {
179
- checkLogin: async () => ({ userId: 'mentor@example.com', userNm: 'Mentor One' }),
180
- }
181
-
182
- const result = await createAuthenticatedHttp(
183
- manager,
184
- (credentials) => {
185
- if (credentials.sessionCookie === 'browser-session') return recoveredHttp
186
- return { checkLogin: async () => null }
187
- },
188
- undefined,
189
- async () => ({ sessionCookie: 'browser-session', csrfToken: 'browser-csrf' }),
190
- )
191
-
192
- expect(result).toBe(recoveredHttp)
193
- expect(savedCredentials).toMatchObject({
194
- sessionCookie: 'browser-session',
195
- csrfToken: 'browser-csrf',
196
- })
197
- expect(savedCredentials).toHaveProperty('loggedInAt')
198
- })
199
-
200
- it('falls back to browser extraction when password re-login fails', async () => {
201
- let savedCredentials: Record<string, unknown> | null = null
202
- const manager = {
203
- getCredentials: async () => ({
204
- sessionCookie: 'stale-session',
205
- csrfToken: 'stale-csrf',
206
- username: 'mentor@example.com',
207
- password: 'wrong-password',
208
- }),
209
- setCredentials: async (credentials: Record<string, unknown>) => {
210
- savedCredentials = credentials
211
- },
212
- clearSessionState: async () => {
213
- throw new Error('should not clear session state when browser extraction succeeds')
214
- },
215
- }
216
- const recoveredHttp = {
217
- checkLogin: async () => ({ userId: 'mentor@example.com', userNm: 'Mentor One' }),
218
- }
219
-
220
- const result = await createAuthenticatedHttp(
221
- manager,
222
- (credentials) => {
223
- if (credentials.sessionCookie === 'browser-session') return recoveredHttp
224
- return { checkLogin: async () => null }
225
- },
226
- () => ({
227
- login: async () => {
228
- throw new Error('wrong password')
229
- },
230
- checkLogin: async () => null,
231
- getSessionCookie: () => null,
232
- getCsrfToken: () => null,
233
- }),
234
- async () => ({ sessionCookie: 'browser-session', csrfToken: 'browser-csrf' }),
235
- )
236
-
237
- expect(result).toBe(recoveredHttp)
238
- expect(savedCredentials).toMatchObject({
239
- sessionCookie: 'browser-session',
240
- csrfToken: 'browser-csrf',
241
- })
242
- })
243
-
244
- it('skips browser extraction when no credentials exist', async () => {
245
- let browserExtractionCalled = false
246
- const manager = {
247
- getCredentials: async () => null,
248
- setCredentials: async () => {
249
- throw new Error('should not save')
250
- },
251
- clearSessionState: async () => {},
252
- }
253
-
254
- await expect(
255
- createAuthenticatedHttp(
256
- manager,
257
- () => ({ checkLogin: async () => null }),
258
- undefined,
259
- async () => {
260
- browserExtractionCalled = true
261
- return { sessionCookie: 's', csrfToken: 'c' }
262
- },
263
- ),
264
- ).rejects.toThrow('Not logged in')
265
- expect(browserExtractionCalled).toBe(false)
266
- })
267
156
  })