opensoma 0.5.0 → 0.5.1
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 +1 -1
- package/dist/src/client.d.ts +7 -1
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +13 -11
- package/dist/src/client.js.map +1 -1
- package/dist/src/shared/utils/swmaestro.d.ts.map +1 -1
- package/dist/src/shared/utils/swmaestro.js +1 -5
- package/dist/src/shared/utils/swmaestro.js.map +1 -1
- package/package.json +1 -1
- package/src/client.test.ts +135 -117
- package/src/client.ts +16 -12
- package/src/commands/auth.test.ts +7 -7
- package/src/commands/helpers.test.ts +8 -8
- package/src/commands/report.test.ts +7 -7
- package/src/credential-manager.test.ts +5 -5
- package/src/formatters.test.ts +22 -22
- package/src/http.test.ts +37 -37
- package/src/shared/utils/mentoring-params.test.ts +16 -16
- package/src/shared/utils/swmaestro.test.ts +87 -8
- package/src/shared/utils/swmaestro.ts +1 -6
- 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 +220 -204
|
@@ -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',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import { resolveVenue } from './swmaestro'
|
|
3
|
+
import { buildRoomReservationPayload, resolveVenue } from './swmaestro'
|
|
4
4
|
|
|
5
5
|
describe('resolveVenue', () => {
|
|
6
|
-
|
|
6
|
+
it('prepends "토즈-" to bare TOZ location names', () => {
|
|
7
7
|
expect(resolveVenue('광화문점')).toBe('토즈-광화문점')
|
|
8
8
|
expect(resolveVenue('양재점')).toBe('토즈-양재점')
|
|
9
9
|
expect(resolveVenue('강남컨퍼런스센터점')).toBe('토즈-강남컨퍼런스센터점')
|
|
@@ -14,17 +14,17 @@ describe('resolveVenue', () => {
|
|
|
14
14
|
expect(resolveVenue('홍대점')).toBe('토즈-홍대점')
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
it('passes through TOZ locations that already have the prefix', () => {
|
|
18
18
|
expect(resolveVenue('토즈-광화문점')).toBe('토즈-광화문점')
|
|
19
19
|
expect(resolveVenue('토즈-강남역토즈타워점')).toBe('토즈-강남역토즈타워점')
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
it('resolves "신촌비즈니스센터점" to "연수센터-7"', () => {
|
|
23
23
|
expect(resolveVenue('신촌비즈니스센터점')).toBe('연수센터-7')
|
|
24
24
|
expect(resolveVenue('토즈-신촌비즈니스센터점')).toBe('연수센터-7')
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
it('passes through non-TOZ venues unchanged', () => {
|
|
28
28
|
expect(resolveVenue('온라인(Webex)')).toBe('온라인(Webex)')
|
|
29
29
|
expect(resolveVenue('스페이스 A1')).toBe('스페이스 A1')
|
|
30
30
|
expect(resolveVenue('스페이스 M1')).toBe('스페이스 M1')
|
|
@@ -33,12 +33,91 @@ describe('resolveVenue', () => {
|
|
|
33
33
|
expect(resolveVenue('(엑스퍼트) 외부_카페')).toBe('(엑스퍼트) 외부_카페')
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
it('trims surrounding whitespace from the input', () => {
|
|
37
37
|
expect(resolveVenue(' 강남역토즈타워점 ')).toBe('토즈-강남역토즈타워점')
|
|
38
38
|
expect(resolveVenue(' 스페이스 A1 ')).toBe('스페이스 A1')
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
it('passes through unknown venues unchanged', () => {
|
|
42
42
|
expect(resolveVenue('기타 장소')).toBe('기타 장소')
|
|
43
43
|
})
|
|
44
44
|
})
|
|
45
|
+
|
|
46
|
+
describe('buildRoomReservationPayload', () => {
|
|
47
|
+
it('sets rentEndde to the last selected slot so the server reserves only the chosen slots', () => {
|
|
48
|
+
const payload = buildRoomReservationPayload({
|
|
49
|
+
roomId: 17,
|
|
50
|
+
date: '2026-04-20',
|
|
51
|
+
slots: ['13:00', '13:30'],
|
|
52
|
+
title: '회의',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect(payload.rentBgnde).toBe('2026-04-20 13:00:00')
|
|
56
|
+
expect(payload.rentEndde).toBe('2026-04-20 13:30:00')
|
|
57
|
+
expect(payload['time[0]']).toBe('13:00')
|
|
58
|
+
expect(payload['time[1]']).toBe('13:30')
|
|
59
|
+
expect(payload['time[2]']).toBeUndefined()
|
|
60
|
+
expect(payload['chkData_1']).toBe('2026-04-20|13:00|17')
|
|
61
|
+
expect(payload['chkData_2']).toBe('2026-04-20|13:30|17')
|
|
62
|
+
expect(payload['chkData_3']).toBeUndefined()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('handles a single-slot reservation', () => {
|
|
66
|
+
const payload = buildRoomReservationPayload({
|
|
67
|
+
roomId: 17,
|
|
68
|
+
date: '2026-04-20',
|
|
69
|
+
slots: ['13:00'],
|
|
70
|
+
title: '회의',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
expect(payload.rentBgnde).toBe('2026-04-20 13:00:00')
|
|
74
|
+
expect(payload.rentEndde).toBe('2026-04-20 13:00:00')
|
|
75
|
+
expect(payload['time[0]']).toBe('13:00')
|
|
76
|
+
expect(payload['time[1]']).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('handles a reservation ending at the last available slot', () => {
|
|
80
|
+
const payload = buildRoomReservationPayload({
|
|
81
|
+
roomId: 17,
|
|
82
|
+
date: '2026-04-20',
|
|
83
|
+
slots: ['23:00', '23:30'],
|
|
84
|
+
title: '회의',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
expect(payload.rentBgnde).toBe('2026-04-20 23:00:00')
|
|
88
|
+
expect(payload.rentEndde).toBe('2026-04-20 23:30:00')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('rejects non-consecutive slots', () => {
|
|
92
|
+
expect(() =>
|
|
93
|
+
buildRoomReservationPayload({
|
|
94
|
+
roomId: 17,
|
|
95
|
+
date: '2026-04-20',
|
|
96
|
+
slots: ['13:00', '14:00'],
|
|
97
|
+
title: '회의',
|
|
98
|
+
}),
|
|
99
|
+
).toThrow('Time slots must be consecutive')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('rejects invalid time slots', () => {
|
|
103
|
+
expect(() =>
|
|
104
|
+
buildRoomReservationPayload({
|
|
105
|
+
roomId: 17,
|
|
106
|
+
date: '2026-04-20',
|
|
107
|
+
slots: ['25:00'],
|
|
108
|
+
title: '회의',
|
|
109
|
+
}),
|
|
110
|
+
).toThrow('Invalid time slot')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('rejects empty slot lists', () => {
|
|
114
|
+
expect(() =>
|
|
115
|
+
buildRoomReservationPayload({
|
|
116
|
+
roomId: 17,
|
|
117
|
+
date: '2026-04-20',
|
|
118
|
+
slots: [],
|
|
119
|
+
title: '회의',
|
|
120
|
+
}),
|
|
121
|
+
).toThrow('At least one time slot is required')
|
|
122
|
+
})
|
|
123
|
+
})
|
|
@@ -142,17 +142,12 @@ export function buildRoomReservationPayload(params: {
|
|
|
142
142
|
|
|
143
143
|
const firstSlot = params.slots[0]
|
|
144
144
|
const lastSlot = params.slots[params.slots.length - 1]
|
|
145
|
-
const endSlot = TIME_SLOTS[TIME_SLOTS.indexOf(lastSlot) + 1]
|
|
146
|
-
|
|
147
|
-
if (!endSlot) {
|
|
148
|
-
throw new Error('Reservation end time is out of range')
|
|
149
|
-
}
|
|
150
145
|
|
|
151
146
|
const payload: Record<string, string> = {
|
|
152
147
|
menuNo: MENU_NO.ROOM,
|
|
153
148
|
itemId: String(params.roomId),
|
|
154
149
|
rentBgnde: `${params.date} ${firstSlot}:00`,
|
|
155
|
-
rentEndde: `${params.date} ${
|
|
150
|
+
rentEndde: `${params.date} ${lastSlot}:00`,
|
|
156
151
|
title: params.title,
|
|
157
152
|
rentDt: params.date,
|
|
158
153
|
rentNum: String(params.attendees ?? 1),
|
|
@@ -52,9 +52,12 @@ describe('formatStartTime / buildStartTimeParam', () => {
|
|
|
52
52
|
expect(buildStartTimeParam('9:05')).toBe('0905')
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
-
it('rejects
|
|
55
|
+
it('rejects time without colon separator', () => {
|
|
56
56
|
expect(() => formatStartTime('1000')).toThrow()
|
|
57
|
-
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('accepts out-of-range hours because the format matches', () => {
|
|
60
|
+
expect(formatStartTime('25:00')).toEqual({ hour: '25', minute: '00' })
|
|
58
61
|
})
|
|
59
62
|
})
|
|
60
63
|
|
|
@@ -120,13 +123,15 @@ describe('parseEmail', () => {
|
|
|
120
123
|
})
|
|
121
124
|
|
|
122
125
|
describe('assertDurationInRange', () => {
|
|
123
|
-
it('accepts
|
|
124
|
-
expect(
|
|
125
|
-
expect(
|
|
126
|
-
expect(
|
|
126
|
+
it('accepts boundary and interior values', () => {
|
|
127
|
+
expect(assertDurationInRange(120)).toBeUndefined()
|
|
128
|
+
expect(assertDurationInRange(150)).toBeUndefined()
|
|
129
|
+
expect(assertDurationInRange(180)).toBeUndefined()
|
|
127
130
|
})
|
|
128
131
|
|
|
129
|
-
it('rejects
|
|
132
|
+
it('rejects values just outside the boundary', () => {
|
|
133
|
+
expect(() => assertDurationInRange(119)).toThrow()
|
|
134
|
+
expect(() => assertDurationInRange(181)).toThrow()
|
|
130
135
|
expect(() => assertDurationInRange(60)).toThrow()
|
|
131
136
|
expect(() => assertDurationInRange(210)).toThrow()
|
|
132
137
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite'
|
|
2
|
-
import { afterEach, describe, expect,
|
|
2
|
+
import { afterEach, describe, expect, it } from 'bun:test'
|
|
3
3
|
import { execSync } from 'node:child_process'
|
|
4
4
|
import { createCipheriv, pbkdf2Sync } from 'node:crypto'
|
|
5
5
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
@@ -22,7 +22,7 @@ afterEach(() => {
|
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
describe('TokenExtractor', () => {
|
|
25
|
-
|
|
25
|
+
it('finds cookie databases for every supported browser on macOS', async () => {
|
|
26
26
|
const home = await makeTempDir()
|
|
27
27
|
|
|
28
28
|
for (const browser of BROWSERS) {
|
|
@@ -38,7 +38,7 @@ describe('TokenExtractor', () => {
|
|
|
38
38
|
}
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
it('finds cookie databases for every supported browser on Linux', async () => {
|
|
42
42
|
const home = await makeTempDir()
|
|
43
43
|
|
|
44
44
|
for (const browser of BROWSERS) {
|
|
@@ -51,12 +51,12 @@ describe('TokenExtractor', () => {
|
|
|
51
51
|
expect(paths).toHaveLength(BROWSERS.length)
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
it('returns null when no cookie databases exist', async () => {
|
|
55
55
|
const extractor = new TokenExtractor('linux', await makeTempDir())
|
|
56
56
|
expect(await extractor.extract()).toBeNull()
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
it('extracts a plaintext cookie value', async () => {
|
|
60
60
|
const home = await makeTempDir()
|
|
61
61
|
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
62
62
|
createCookieDbWithPlaintext(dbPath, 'my-session-id')
|
|
@@ -65,7 +65,7 @@ describe('TokenExtractor', () => {
|
|
|
65
65
|
expect(await extractor.extract()).toEqual({ sessionCookie: 'my-session-id' })
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
it('finds cookie databases across numbered browser profiles', async () => {
|
|
69
69
|
const home = await makeTempDir()
|
|
70
70
|
createCookieFile(join(home, '.config', 'google-chrome', 'Default', 'Cookies'))
|
|
71
71
|
createCookieFile(join(home, '.config', 'google-chrome', 'Profile 1', 'Cookies'))
|
|
@@ -80,7 +80,7 @@ describe('TokenExtractor', () => {
|
|
|
80
80
|
])
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
it('keeps the newest unique cookie across profiles when collecting candidates', async () => {
|
|
84
84
|
const home = await makeTempDir()
|
|
85
85
|
createCookieDbWithPlaintext(join(home, '.config', 'google-chrome', 'Default', 'Cookies'), 'stale-session', 10)
|
|
86
86
|
createCookieDbWithPlaintext(join(home, '.config', 'google-chrome', 'Profile 1', 'Cookies'), 'valid-session', 20)
|
|
@@ -104,7 +104,7 @@ describe('TokenExtractor', () => {
|
|
|
104
104
|
])
|
|
105
105
|
})
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
it('extracts a plaintext cookie from the opensoma.dev host', async () => {
|
|
108
108
|
const home = await makeTempDir()
|
|
109
109
|
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
110
110
|
createCookieDbWithPlaintext(dbPath, 'opensoma-dev-session', 0, 'opensoma.dev')
|
|
@@ -113,7 +113,7 @@ describe('TokenExtractor', () => {
|
|
|
113
113
|
expect(await extractor.extract()).toEqual({ sessionCookie: 'opensoma-dev-session' })
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
it('decrypts an encrypted cookie from the opensoma.dev host on Linux', async () => {
|
|
117
117
|
const home = await makeTempDir()
|
|
118
118
|
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
119
119
|
const encrypted = encryptLinuxCookie('opensoma-dev-encrypted')
|
|
@@ -123,7 +123,7 @@ describe('TokenExtractor', () => {
|
|
|
123
123
|
expect(await extractor.extract()).toEqual({ sessionCookie: 'opensoma-dev-encrypted' })
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
it('decrypts an encrypted cookie on Linux', async () => {
|
|
127
127
|
const home = await makeTempDir()
|
|
128
128
|
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
129
129
|
const encrypted = encryptLinuxCookie('decrypted-session')
|
|
@@ -133,7 +133,7 @@ describe('TokenExtractor', () => {
|
|
|
133
133
|
expect(await extractor.extract()).toEqual({ sessionCookie: 'decrypted-session' })
|
|
134
134
|
})
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
it('extracts a plaintext cookie when running under Node.js (compiled ESM)', async () => {
|
|
137
137
|
execSync('bun run build', { cwd: join(import.meta.dir, '..'), stdio: 'pipe' })
|
|
138
138
|
const home = await makeTempDir()
|
|
139
139
|
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
@@ -144,7 +144,7 @@ describe('TokenExtractor', () => {
|
|
|
144
144
|
expect(JSON.parse(result.trim())).toEqual({ sessionCookie: 'node-test-session' })
|
|
145
145
|
})
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
it('extracts an encrypted cookie when running under Node.js (compiled ESM)', async () => {
|
|
148
148
|
execSync('bun run build', { cwd: join(import.meta.dir, '..'), stdio: 'pipe' })
|
|
149
149
|
const home = await makeTempDir()
|
|
150
150
|
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
package/src/toz-http.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, describe, expect,
|
|
1
|
+
import { afterEach, describe, expect, it, mock } from 'bun:test'
|
|
2
2
|
|
|
3
3
|
import { TOZ_BASE_URL } from './constants'
|
|
4
4
|
import { TozHttp } from './toz-http'
|
|
@@ -11,7 +11,7 @@ afterEach(() => {
|
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
describe('TozHttp', () => {
|
|
14
|
-
|
|
14
|
+
it('seeds JSESSIONID by GETting index.htm during bootstrap', async () => {
|
|
15
15
|
const fetchMock = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
16
16
|
expect(String(input)).toBe(`${TOZ_BASE_URL}/index.htm`)
|
|
17
17
|
expect(init?.method).toBe('GET')
|
|
@@ -26,7 +26,7 @@ describe('TozHttp', () => {
|
|
|
26
26
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
it('skips bootstrap when JSESSIONID is already set', async () => {
|
|
30
30
|
const fetchMock = mock(async () => createResponse('ok'))
|
|
31
31
|
globalThis.fetch = fetchMock as typeof fetch
|
|
32
32
|
|
|
@@ -37,7 +37,7 @@ describe('TozHttp', () => {
|
|
|
37
37
|
expect(http.getSessionCookie()).toBe('EXISTING')
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
it('sends URL-encoded POST bodies with the required headers and cookie', async () => {
|
|
41
41
|
const fetchMock = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
42
42
|
expect(String(input)).toBe(`${TOZ_BASE_URL}/ajaxGetEnableBoothes.htm`)
|
|
43
43
|
expect(init?.method).toBe('POST')
|
|
@@ -65,7 +65,7 @@ describe('TozHttp', () => {
|
|
|
65
65
|
expect(text).toBe('[]')
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
it('parses the JSON body returned by postJson', async () => {
|
|
69
69
|
globalThis.fetch = mock(async () => createResponse('{"result":"SUCCESS","resultMsg":"abc"}')) as typeof fetch
|
|
70
70
|
|
|
71
71
|
const http = new TozHttp({ sessionCookie: 'ABC' })
|
|
@@ -74,25 +74,25 @@ describe('TozHttp', () => {
|
|
|
74
74
|
expect(json).toEqual({ result: 'SUCCESS', resultMsg: 'abc' })
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
it('trims the plain-text body returned by postText', async () => {
|
|
78
78
|
globalThis.fetch = mock(async () => createResponse('SUCCESS\n')) as typeof fetch
|
|
79
79
|
|
|
80
80
|
const http = new TozHttp({ sessionCookie: 'ABC' })
|
|
81
81
|
expect(await http.postText('/ajaxHpVerify.htm', {})).toBe('SUCCESS')
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
it('strips the "JSESSIONID=" prefix when constructed with a full cookie string', async () => {
|
|
85
85
|
const http = new TozHttp({ sessionCookie: 'JSESSIONID=XYZ789' })
|
|
86
86
|
expect(http.getSessionCookie()).toBe('XYZ789')
|
|
87
87
|
expect(http.getCookies()).toEqual({ JSESSIONID: 'XYZ789' })
|
|
88
88
|
})
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
it('preserves every cookie when constructed with a cookies map', async () => {
|
|
91
91
|
const http = new TozHttp({ cookies: { JSESSIONID: 'A', other: 'B' } })
|
|
92
92
|
expect(http.getCookies()).toEqual({ JSESSIONID: 'A', other: 'B' })
|
|
93
93
|
})
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
it('throws on a non-2xx response', async () => {
|
|
96
96
|
globalThis.fetch = mock(async () =>
|
|
97
97
|
createResponse('Server Error', [], 'text/html', { status: 500 }),
|
|
98
98
|
) as typeof fetch
|
|
@@ -101,7 +101,7 @@ describe('TozHttp', () => {
|
|
|
101
101
|
await expect(http.get('/index.htm')).rejects.toThrow(/HTTP 500/)
|
|
102
102
|
})
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
it('refuses to follow cross-origin redirects to prevent cookie leaks', async () => {
|
|
105
105
|
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
106
106
|
const url = String(input)
|
|
107
107
|
if (url.endsWith('/index.htm')) {
|
|
@@ -114,7 +114,7 @@ describe('TozHttp', () => {
|
|
|
114
114
|
await expect(http.get('/index.htm')).rejects.toThrow(/cross-origin redirect/)
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
it('follows same-origin redirects normally', async () => {
|
|
118
118
|
let hit = 0
|
|
119
119
|
globalThis.fetch = mock(async (input: RequestInfo | URL) => {
|
|
120
120
|
const url = String(input)
|