agent-messenger 2.36.1 → 2.37.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/.claude-plugin/plugin.json +1 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts +5 -4
- package/dist/src/platforms/kakaotalk/chat-classifier.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/chat-classifier.js +15 -9
- package/dist/src/platforms/kakaotalk/chat-classifier.js.map +1 -1
- package/dist/src/platforms/kakaotalk/client.d.ts +33 -1
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +377 -42
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/index.d.ts +4 -3
- package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/index.js +2 -2
- package/dist/src/platforms/kakaotalk/index.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.js +13 -2
- package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts +1 -0
- package/dist/src/platforms/kakaotalk/protocol/login-response.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/login-response.js +1 -1
- package/dist/src/platforms/kakaotalk/protocol/login-response.js.map +1 -1
- package/dist/src/platforms/kakaotalk/types.d.ts +50 -2
- package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/types.js +18 -1
- package/dist/src/platforms/kakaotalk/types.js.map +1 -1
- package/docs/content/docs/cli/kakaotalk.mdx +7 -1
- package/docs/content/docs/sdk/kakaotalk.mdx +29 -1
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-kakaotalk/references/common-patterns.md +36 -0
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/kakaotalk/chat-classifier.test.ts +23 -1
- package/src/platforms/kakaotalk/chat-classifier.ts +16 -10
- package/src/platforms/kakaotalk/client.test.ts +1225 -88
- package/src/platforms/kakaotalk/client.ts +476 -45
- package/src/platforms/kakaotalk/index.test.ts +39 -0
- package/src/platforms/kakaotalk/index.ts +12 -1
- package/src/platforms/kakaotalk/protocol/connection.test.ts +87 -0
- package/src/platforms/kakaotalk/protocol/connection.ts +15 -2
- package/src/platforms/kakaotalk/protocol/login-response.test.ts +13 -2
- package/src/platforms/kakaotalk/protocol/login-response.ts +1 -1
- package/src/platforms/kakaotalk/types.ts +41 -2
|
@@ -47,6 +47,23 @@ function makeLong(n: number): { low: number; high: number } {
|
|
|
47
47
|
return { low: n, high: 0 }
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function memberChannelInfo(
|
|
51
|
+
activeMembersCount: number,
|
|
52
|
+
displayMembers: Array<Record<string, unknown>> = [],
|
|
53
|
+
chatId = 100,
|
|
54
|
+
) {
|
|
55
|
+
return {
|
|
56
|
+
statusCode: 0,
|
|
57
|
+
body: {
|
|
58
|
+
chatInfo: {
|
|
59
|
+
chatId: makeLong(chatId),
|
|
60
|
+
activeMembersCount,
|
|
61
|
+
displayMembers,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
function resetAllMocks() {
|
|
51
68
|
mockLogin.mockReset()
|
|
52
69
|
mockGetChatList.mockReset()
|
|
@@ -111,6 +128,7 @@ describe('KakaoTalkClient', () => {
|
|
|
111
128
|
})
|
|
112
129
|
|
|
113
130
|
afterEach(() => {
|
|
131
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
114
132
|
resetAllMocks()
|
|
115
133
|
})
|
|
116
134
|
|
|
@@ -224,6 +242,42 @@ describe('KakaoTalkClient', () => {
|
|
|
224
242
|
client.close()
|
|
225
243
|
})
|
|
226
244
|
|
|
245
|
+
it('preserves an unknown non-empty string type from the login snapshot', async () => {
|
|
246
|
+
mockLogin.mockResolvedValue({
|
|
247
|
+
...DEFAULT_LOGIN_RESULT,
|
|
248
|
+
chatDatas: [{ ...DEFAULT_LOGIN_RESULT.chatDatas[0], t: 'FutureChat' }],
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const client = await new KakaoTalkClient().login({
|
|
252
|
+
oauthToken: 'token',
|
|
253
|
+
userId: 'user1',
|
|
254
|
+
deviceUuid: 'device1',
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
await expect(client.getChats()).resolves.toMatchObject([{ type: 'FutureChat' }])
|
|
258
|
+
|
|
259
|
+
client.close()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
for (const invalidType of ['', true] as const) {
|
|
263
|
+
it(`rejects invalid ${JSON.stringify(invalidType)} type from the login snapshot`, async () => {
|
|
264
|
+
mockLogin.mockResolvedValue({
|
|
265
|
+
...DEFAULT_LOGIN_RESULT,
|
|
266
|
+
chatDatas: [{ ...DEFAULT_LOGIN_RESULT.chatDatas[0], t: invalidType }],
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const client = await new KakaoTalkClient().login({
|
|
270
|
+
oauthToken: 'token',
|
|
271
|
+
userId: 'user1',
|
|
272
|
+
deviceUuid: 'device1',
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
await expect(client.getChats()).rejects.toMatchObject({ code: 'get_chats_failed' })
|
|
276
|
+
|
|
277
|
+
client.close()
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
227
281
|
it('populates last_message.author_name from paired chat.i / chat.k arrays', async () => {
|
|
228
282
|
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
229
283
|
const chats = await client.getChats()
|
|
@@ -482,7 +536,9 @@ describe('KakaoTalkClient', () => {
|
|
|
482
536
|
mockLogin.mockResolvedValue(emptyLoginResult)
|
|
483
537
|
|
|
484
538
|
mockGetChatList.mockResolvedValueOnce({
|
|
539
|
+
statusCode: 0,
|
|
485
540
|
body: {
|
|
541
|
+
status: 0,
|
|
486
542
|
chatDatas: [
|
|
487
543
|
{
|
|
488
544
|
c: 100,
|
|
@@ -541,7 +597,9 @@ describe('KakaoTalkClient', () => {
|
|
|
541
597
|
mockLogin.mockResolvedValue(loginResult)
|
|
542
598
|
|
|
543
599
|
mockGetChatList.mockResolvedValueOnce({
|
|
600
|
+
statusCode: 0,
|
|
544
601
|
body: {
|
|
602
|
+
status: 0,
|
|
545
603
|
chatDatas: [
|
|
546
604
|
{
|
|
547
605
|
c: 300,
|
|
@@ -578,7 +636,9 @@ describe('KakaoTalkClient', () => {
|
|
|
578
636
|
|
|
579
637
|
// Return a chat with same ID as login result
|
|
580
638
|
mockGetChatList.mockResolvedValueOnce({
|
|
639
|
+
statusCode: 0,
|
|
581
640
|
body: {
|
|
641
|
+
status: 0,
|
|
582
642
|
chatDatas: [
|
|
583
643
|
{
|
|
584
644
|
c: 100, // Same as first login chat
|
|
@@ -612,7 +672,9 @@ describe('KakaoTalkClient', () => {
|
|
|
612
672
|
mockLogin.mockResolvedValue(loginResult)
|
|
613
673
|
|
|
614
674
|
mockGetChatList.mockResolvedValueOnce({
|
|
675
|
+
statusCode: 0,
|
|
615
676
|
body: {
|
|
677
|
+
status: 0,
|
|
616
678
|
chatDatas: [
|
|
617
679
|
{
|
|
618
680
|
c: makeLong(300),
|
|
@@ -644,6 +706,148 @@ describe('KakaoTalkClient', () => {
|
|
|
644
706
|
client.close()
|
|
645
707
|
})
|
|
646
708
|
|
|
709
|
+
it('collects every LCHATLIST page until an explicit EOF', async () => {
|
|
710
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
711
|
+
mockGetChatList
|
|
712
|
+
.mockResolvedValueOnce({
|
|
713
|
+
statusCode: 0,
|
|
714
|
+
body: {
|
|
715
|
+
status: 0,
|
|
716
|
+
chatDatas: [
|
|
717
|
+
{
|
|
718
|
+
c: 300,
|
|
719
|
+
t: 1,
|
|
720
|
+
k: ['Dave'],
|
|
721
|
+
i: [4],
|
|
722
|
+
a: 1,
|
|
723
|
+
n: 0,
|
|
724
|
+
o: 1698000000,
|
|
725
|
+
l: null,
|
|
726
|
+
ll: makeLong(100),
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
lastTokenId: makeLong(1),
|
|
730
|
+
lastChatId: makeLong(300),
|
|
731
|
+
eof: false,
|
|
732
|
+
},
|
|
733
|
+
})
|
|
734
|
+
.mockResolvedValueOnce({
|
|
735
|
+
statusCode: 0,
|
|
736
|
+
body: {
|
|
737
|
+
status: 0,
|
|
738
|
+
chatDatas: [
|
|
739
|
+
{
|
|
740
|
+
c: 400,
|
|
741
|
+
t: 1,
|
|
742
|
+
k: ['Erin'],
|
|
743
|
+
i: [5],
|
|
744
|
+
a: 1,
|
|
745
|
+
n: 0,
|
|
746
|
+
o: 1697000000,
|
|
747
|
+
l: null,
|
|
748
|
+
ll: makeLong(90),
|
|
749
|
+
},
|
|
750
|
+
],
|
|
751
|
+
lastTokenId: makeLong(2),
|
|
752
|
+
lastChatId: makeLong(400),
|
|
753
|
+
eof: true,
|
|
754
|
+
},
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
758
|
+
const chats = await client.getChats({ all: true })
|
|
759
|
+
|
|
760
|
+
expect(chats.map((chat) => chat.chat_id)).toEqual(['100', '200', '300', '400'])
|
|
761
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(2)
|
|
762
|
+
|
|
763
|
+
client.close()
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
it('fails closed when LCHATLIST has a nonzero response status', async () => {
|
|
767
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
768
|
+
mockGetChatList.mockResolvedValueOnce({
|
|
769
|
+
statusCode: 503,
|
|
770
|
+
body: { status: 0, chatDatas: [], lastTokenId: makeLong(1), lastChatId: makeLong(300), eof: true },
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
774
|
+
|
|
775
|
+
await expect(client.getChats({ all: true })).rejects.toMatchObject({
|
|
776
|
+
code: 'get_chats_failed',
|
|
777
|
+
responseFailureKind: 'provider_rejection',
|
|
778
|
+
serverStatus: 503,
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
client.close()
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
it('fails closed on an empty non-EOF LCHATLIST page', async () => {
|
|
785
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
786
|
+
mockGetChatList.mockResolvedValueOnce({
|
|
787
|
+
statusCode: 0,
|
|
788
|
+
body: { status: 0, chatDatas: [], lastTokenId: makeLong(1), lastChatId: makeLong(300), eof: false },
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
792
|
+
|
|
793
|
+
await expect(client.getChats({ all: true })).rejects.toMatchObject({ code: 'get_chats_failed' })
|
|
794
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(1)
|
|
795
|
+
|
|
796
|
+
client.close()
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
it('fails closed when LCHATLIST reaches the page cap without EOF', async () => {
|
|
800
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
801
|
+
let page = 0
|
|
802
|
+
mockGetChatList.mockImplementation(async () => {
|
|
803
|
+
page++
|
|
804
|
+
return {
|
|
805
|
+
statusCode: 0,
|
|
806
|
+
body: {
|
|
807
|
+
status: 0,
|
|
808
|
+
chatDatas: [
|
|
809
|
+
{
|
|
810
|
+
c: 1000 + page,
|
|
811
|
+
t: 1,
|
|
812
|
+
k: [`Room ${page}`],
|
|
813
|
+
i: [1000 + page],
|
|
814
|
+
a: 1,
|
|
815
|
+
n: 0,
|
|
816
|
+
o: 1600000000 - page,
|
|
817
|
+
l: null,
|
|
818
|
+
ll: makeLong(page),
|
|
819
|
+
},
|
|
820
|
+
],
|
|
821
|
+
lastTokenId: makeLong(page),
|
|
822
|
+
lastChatId: makeLong(1000 + page),
|
|
823
|
+
eof: false,
|
|
824
|
+
},
|
|
825
|
+
}
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
829
|
+
|
|
830
|
+
await expect(client.getChats({ all: true })).rejects.toMatchObject({ code: 'get_chats_failed' })
|
|
831
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(50)
|
|
832
|
+
|
|
833
|
+
client.close()
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
it('preserves non-all search compatibility on an empty non-EOF page', async () => {
|
|
837
|
+
mockLogin.mockResolvedValue({ ...DEFAULT_LOGIN_RESULT, eof: false })
|
|
838
|
+
mockGetChatList.mockResolvedValueOnce({
|
|
839
|
+
body: { chatDatas: [], lastTokenId: makeLong(1), lastChatId: makeLong(300), eof: false },
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
843
|
+
const chats = await client.getChats({ search: 'Alice' })
|
|
844
|
+
|
|
845
|
+
expect(chats.map((chat) => chat.chat_id)).toEqual(['100'])
|
|
846
|
+
expect(mockGetChatList).toHaveBeenCalledTimes(1)
|
|
847
|
+
|
|
848
|
+
client.close()
|
|
849
|
+
})
|
|
850
|
+
|
|
647
851
|
it('wraps errors as KakaoTalkError', async () => {
|
|
648
852
|
mockLogin.mockRejectedValue(new Error('Connection refused'))
|
|
649
853
|
|
|
@@ -769,96 +973,836 @@ describe('KakaoTalkClient', () => {
|
|
|
769
973
|
})
|
|
770
974
|
|
|
771
975
|
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
772
|
-
const title = await client.getChatTitle('500')
|
|
976
|
+
const title = await client.getChatTitle('500')
|
|
977
|
+
|
|
978
|
+
expect(title).toBe('User Set Title')
|
|
979
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
980
|
+
|
|
981
|
+
client.close()
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
it('returns null when INFOLINK fails for open chats', async () => {
|
|
985
|
+
const openChatLogin = {
|
|
986
|
+
chatDatas: [
|
|
987
|
+
{
|
|
988
|
+
c: 500,
|
|
989
|
+
t: 'OD',
|
|
990
|
+
li: makeLong(7777),
|
|
991
|
+
k: [],
|
|
992
|
+
i: [],
|
|
993
|
+
a: 1,
|
|
994
|
+
n: 0,
|
|
995
|
+
o: 1700000000,
|
|
996
|
+
l: null,
|
|
997
|
+
ll: makeLong(1),
|
|
998
|
+
},
|
|
999
|
+
],
|
|
1000
|
+
lastTokenId: makeLong(0),
|
|
1001
|
+
lastChatId: makeLong(0),
|
|
1002
|
+
eof: true,
|
|
1003
|
+
}
|
|
1004
|
+
mockLogin.mockResolvedValue(openChatLogin)
|
|
1005
|
+
mockGetChannelInfo.mockResolvedValueOnce({ body: { chatInfo: { chatMetas: [] } } })
|
|
1006
|
+
mockGetOpenLinkInfo.mockRejectedValueOnce(new Error('INFOLINK timeout'))
|
|
1007
|
+
|
|
1008
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1009
|
+
const title = await client.getChatTitle('500')
|
|
1010
|
+
|
|
1011
|
+
expect(title).toBeNull()
|
|
1012
|
+
|
|
1013
|
+
client.close()
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
it('returns null when open chat has no link id (li missing)', async () => {
|
|
1017
|
+
const openChatLogin = {
|
|
1018
|
+
chatDatas: [
|
|
1019
|
+
{
|
|
1020
|
+
c: 500,
|
|
1021
|
+
t: 'OM',
|
|
1022
|
+
k: [],
|
|
1023
|
+
i: [],
|
|
1024
|
+
a: 1,
|
|
1025
|
+
n: 0,
|
|
1026
|
+
o: 1700000000,
|
|
1027
|
+
l: null,
|
|
1028
|
+
ll: makeLong(1),
|
|
1029
|
+
},
|
|
1030
|
+
],
|
|
1031
|
+
lastTokenId: makeLong(0),
|
|
1032
|
+
lastChatId: makeLong(0),
|
|
1033
|
+
eof: true,
|
|
1034
|
+
}
|
|
1035
|
+
mockLogin.mockResolvedValue(openChatLogin)
|
|
1036
|
+
mockGetChannelInfo.mockResolvedValueOnce({ body: { chatInfo: { chatMetas: [] } } })
|
|
1037
|
+
|
|
1038
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1039
|
+
const title = await client.getChatTitle('500')
|
|
1040
|
+
|
|
1041
|
+
expect(title).toBeNull()
|
|
1042
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1043
|
+
|
|
1044
|
+
client.close()
|
|
1045
|
+
})
|
|
1046
|
+
|
|
1047
|
+
it('wraps getChatList failure as KakaoTalkError with code get_chats_failed', async () => {
|
|
1048
|
+
const loginResult = { ...DEFAULT_LOGIN_RESULT, eof: false }
|
|
1049
|
+
mockLogin.mockResolvedValue(loginResult)
|
|
1050
|
+
mockGetChatList.mockRejectedValue(new Error('Network error'))
|
|
1051
|
+
|
|
1052
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1053
|
+
try {
|
|
1054
|
+
await client.getChats({ all: true })
|
|
1055
|
+
expect.unreachable('should have thrown')
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
expect(e).toBeInstanceOf(KakaoTalkError)
|
|
1058
|
+
expect((e as KakaoTalkError).code).toBe('get_chats_failed')
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
client.close()
|
|
1062
|
+
})
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
|
+
describe('getChat', () => {
|
|
1066
|
+
it('returns one fully normalized late chat from read-only CHATINFO', async () => {
|
|
1067
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1068
|
+
statusCode: 0,
|
|
1069
|
+
body: {
|
|
1070
|
+
chatInfo: {
|
|
1071
|
+
chatId: makeLong(300),
|
|
1072
|
+
type: 11,
|
|
1073
|
+
left: false,
|
|
1074
|
+
activeMembersCount: 2,
|
|
1075
|
+
newMessageCount: 4,
|
|
1076
|
+
invalidNewMessageCount: false,
|
|
1077
|
+
lastLogId: makeLong(77),
|
|
1078
|
+
lastSeenLogId: makeLong(73),
|
|
1079
|
+
lastChatLog: {
|
|
1080
|
+
logId: makeLong(77),
|
|
1081
|
+
authorId: 42,
|
|
1082
|
+
type: 1,
|
|
1083
|
+
message: 'latest',
|
|
1084
|
+
sendAt: 1700000077,
|
|
1085
|
+
},
|
|
1086
|
+
displayMembers: [
|
|
1087
|
+
{ userId: makeLong(42), nickName: 'Alice' },
|
|
1088
|
+
{ userId: makeLong(43), nickName: 'Bob' },
|
|
1089
|
+
],
|
|
1090
|
+
chatMetas: [{ type: 3, content: 'Late room' }],
|
|
1091
|
+
pushAlert: true,
|
|
1092
|
+
},
|
|
1093
|
+
},
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1097
|
+
const chat = await client.getChat('300')
|
|
1098
|
+
|
|
1099
|
+
expect(chat).toEqual({
|
|
1100
|
+
chat_id: '300',
|
|
1101
|
+
type: 11,
|
|
1102
|
+
display_name: 'Alice, Bob',
|
|
1103
|
+
title: 'Late room',
|
|
1104
|
+
active_members: 2,
|
|
1105
|
+
unread_count: 4,
|
|
1106
|
+
last_message: {
|
|
1107
|
+
author_id: 42,
|
|
1108
|
+
author_name: 'Alice',
|
|
1109
|
+
message: 'latest',
|
|
1110
|
+
sent_at: 1700000077,
|
|
1111
|
+
},
|
|
1112
|
+
})
|
|
1113
|
+
|
|
1114
|
+
client.close()
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
it('classifies an explicit CHATINFO header rejection without exposing response contents', async () => {
|
|
1118
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: 403, body: {} })
|
|
1119
|
+
|
|
1120
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1121
|
+
|
|
1122
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1123
|
+
code: 'get_chat_failed',
|
|
1124
|
+
getChatFailureReason: 'provider_rejection',
|
|
1125
|
+
responseFailureKind: 'provider_rejection',
|
|
1126
|
+
responseStatusSource: 'packet',
|
|
1127
|
+
serverStatus: 403,
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
client.close()
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
it('classifies an explicit CHATINFO body rejection and preserves its numeric status', async () => {
|
|
1134
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: 0, body: { status: -404 } })
|
|
1135
|
+
|
|
1136
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1137
|
+
|
|
1138
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1139
|
+
code: 'get_chat_failed',
|
|
1140
|
+
getChatFailureReason: 'provider_rejection',
|
|
1141
|
+
responseFailureKind: 'provider_rejection',
|
|
1142
|
+
responseStatusSource: 'body',
|
|
1143
|
+
serverStatus: -404,
|
|
1144
|
+
})
|
|
1145
|
+
|
|
1146
|
+
client.close()
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
it('does not expose rejected CHATINFO body contents through public error metadata', async () => {
|
|
1150
|
+
const secretSentinel = 'secret-sentinel-do-not-emit'
|
|
1151
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1152
|
+
statusCode: 0,
|
|
1153
|
+
body: { status: -404, secret: secretSentinel },
|
|
1154
|
+
})
|
|
1155
|
+
|
|
1156
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1157
|
+
let error: unknown
|
|
1158
|
+
try {
|
|
1159
|
+
await client.getChat('300')
|
|
1160
|
+
} catch (cause) {
|
|
1161
|
+
error = cause
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1165
|
+
const publicError = error as KakaoTalkError
|
|
1166
|
+
expect(publicError).toMatchObject({
|
|
1167
|
+
code: 'get_chat_failed',
|
|
1168
|
+
getChatFailureReason: 'provider_rejection',
|
|
1169
|
+
responseFailureKind: 'provider_rejection',
|
|
1170
|
+
responseStatusSource: 'body',
|
|
1171
|
+
serverStatus: -404,
|
|
1172
|
+
})
|
|
1173
|
+
expect(publicError.message).not.toContain(secretSentinel)
|
|
1174
|
+
expect(JSON.stringify(publicError)).not.toContain(secretSentinel)
|
|
1175
|
+
expect(JSON.stringify(publicError.cause)).not.toContain(secretSentinel)
|
|
1176
|
+
|
|
1177
|
+
client.close()
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
it('classifies synthetic connection-close status -1 as transient rather than provider rejection', async () => {
|
|
1181
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: -1, body: { error: 'connection closed' } })
|
|
1182
|
+
|
|
1183
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1184
|
+
|
|
1185
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1186
|
+
code: 'get_chat_failed',
|
|
1187
|
+
getChatFailureReason: 'synthetic_connection_close',
|
|
1188
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1189
|
+
responseStatusSource: 'packet',
|
|
1190
|
+
serverStatus: -1,
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
client.close()
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
it('does not classify status -1 with a nonmatching body as a synthetic connection close', async () => {
|
|
1197
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: -1, body: { error: 'provider rejected' } })
|
|
1198
|
+
|
|
1199
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1200
|
+
|
|
1201
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1202
|
+
code: 'get_chat_failed',
|
|
1203
|
+
getChatFailureReason: 'provider_rejection',
|
|
1204
|
+
responseFailureKind: 'provider_rejection',
|
|
1205
|
+
responseStatusSource: 'packet',
|
|
1206
|
+
serverStatus: -1,
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
client.close()
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1212
|
+
it('classifies a thrown CHATINFO transport failure as transient or unknown', async () => {
|
|
1213
|
+
const secretSentinel = 'secret-transport-body-do-not-emit'
|
|
1214
|
+
mockGetChannelInfo.mockRejectedValueOnce(
|
|
1215
|
+
Object.assign(new Error('socket unavailable'), { body: { secret: secretSentinel } }),
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1219
|
+
|
|
1220
|
+
let error: unknown
|
|
1221
|
+
try {
|
|
1222
|
+
await client.getChat('300')
|
|
1223
|
+
} catch (cause) {
|
|
1224
|
+
error = cause
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1228
|
+
expect(error).toMatchObject({
|
|
1229
|
+
code: 'get_chat_failed',
|
|
1230
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1231
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1232
|
+
})
|
|
1233
|
+
const serialized = JSON.stringify(error)
|
|
1234
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1235
|
+
expect(serialized).not.toContain('body')
|
|
1236
|
+
expect(serialized).not.toContain('message')
|
|
1237
|
+
expect(serialized).not.toContain('cause')
|
|
1238
|
+
|
|
1239
|
+
client.close()
|
|
1240
|
+
})
|
|
1241
|
+
|
|
1242
|
+
it('classifies a successful CHATINFO response without chatInfo as structurally absent', async () => {
|
|
1243
|
+
const secretSentinel = 'secret-chat-body-do-not-emit'
|
|
1244
|
+
mockGetChannelInfo.mockResolvedValueOnce({ statusCode: 0, body: { secret: secretSentinel } })
|
|
1245
|
+
|
|
1246
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1247
|
+
|
|
1248
|
+
let error: unknown
|
|
1249
|
+
try {
|
|
1250
|
+
await client.getChat('300')
|
|
1251
|
+
} catch (cause) {
|
|
1252
|
+
error = cause
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1256
|
+
expect(error).toMatchObject({
|
|
1257
|
+
code: 'get_chat_failed',
|
|
1258
|
+
getChatFailureReason: 'chat_info_absent',
|
|
1259
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1260
|
+
})
|
|
1261
|
+
const serialized = JSON.stringify(error)
|
|
1262
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1263
|
+
expect(serialized).not.toContain('body')
|
|
1264
|
+
expect(serialized).not.toContain('message')
|
|
1265
|
+
expect(serialized).not.toContain('cause')
|
|
1266
|
+
|
|
1267
|
+
client.close()
|
|
1268
|
+
})
|
|
1269
|
+
|
|
1270
|
+
it('classifies a successful CHATINFO response with null chatInfo as structurally absent', async () => {
|
|
1271
|
+
const secretSentinel = 'secret-null-chat-body-do-not-emit'
|
|
1272
|
+
const chatIdSentinel = '987654321012345678'
|
|
1273
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1274
|
+
statusCode: 0,
|
|
1275
|
+
body: { chatInfo: null, secret: secretSentinel },
|
|
1276
|
+
})
|
|
1277
|
+
|
|
1278
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1279
|
+
|
|
1280
|
+
let error: unknown
|
|
1281
|
+
try {
|
|
1282
|
+
await client.getChat(chatIdSentinel)
|
|
1283
|
+
} catch (cause) {
|
|
1284
|
+
error = cause
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1288
|
+
expect(error).toMatchObject({
|
|
1289
|
+
code: 'get_chat_failed',
|
|
1290
|
+
getChatFailureReason: 'chat_info_absent',
|
|
1291
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1292
|
+
})
|
|
1293
|
+
const serialized = JSON.stringify(error)
|
|
1294
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1295
|
+
expect(serialized).not.toContain('body')
|
|
1296
|
+
expect(serialized).not.toContain('message')
|
|
1297
|
+
expect(serialized).not.toContain('cause')
|
|
1298
|
+
expect(serialized).not.toContain('chatId')
|
|
1299
|
+
expect(serialized).not.toContain(chatIdSentinel)
|
|
1300
|
+
|
|
1301
|
+
client.close()
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
it('classifies exact CHATINFO left=true as structurally absent without exposing response contents', async () => {
|
|
1305
|
+
const secretSentinel = 'secret-left-chat-body-do-not-emit'
|
|
1306
|
+
const nativeChatIdSentinel = '987654321'
|
|
1307
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1308
|
+
statusCode: 0,
|
|
1309
|
+
body: {
|
|
1310
|
+
chatInfo: {
|
|
1311
|
+
left: true,
|
|
1312
|
+
chatId: makeLong(Number(nativeChatIdSentinel)),
|
|
1313
|
+
secret: secretSentinel,
|
|
1314
|
+
},
|
|
1315
|
+
},
|
|
1316
|
+
})
|
|
1317
|
+
|
|
1318
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1319
|
+
|
|
1320
|
+
let error: unknown
|
|
1321
|
+
try {
|
|
1322
|
+
await client.getChat(nativeChatIdSentinel)
|
|
1323
|
+
} catch (cause) {
|
|
1324
|
+
error = cause
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
expect(error).toBeInstanceOf(KakaoTalkError)
|
|
1328
|
+
expect(error).toMatchObject({
|
|
1329
|
+
code: 'get_chat_failed',
|
|
1330
|
+
getChatFailureReason: 'chat_info_absent',
|
|
1331
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1332
|
+
})
|
|
1333
|
+
const publicError = error as KakaoTalkError
|
|
1334
|
+
const serialized = JSON.stringify(publicError)
|
|
1335
|
+
expect(publicError.message).not.toContain(secretSentinel)
|
|
1336
|
+
expect(publicError.message).not.toContain(nativeChatIdSentinel)
|
|
1337
|
+
expect(serialized).not.toContain(secretSentinel)
|
|
1338
|
+
expect(serialized).not.toContain(nativeChatIdSentinel)
|
|
1339
|
+
|
|
1340
|
+
client.close()
|
|
1341
|
+
})
|
|
1342
|
+
|
|
1343
|
+
for (const [label, chatInfo] of [
|
|
1344
|
+
['mismatched chatId', { left: true, chatId: makeLong(301) }],
|
|
1345
|
+
['missing chatId', { left: true }],
|
|
1346
|
+
['invalid chatId', { left: true, chatId: { invalid: true } }],
|
|
1347
|
+
] as const) {
|
|
1348
|
+
it(`does not classify CHATINFO left=true with ${label} as structurally absent`, async () => {
|
|
1349
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1350
|
+
statusCode: 0,
|
|
1351
|
+
body: { chatInfo },
|
|
1352
|
+
})
|
|
1353
|
+
|
|
1354
|
+
const client = await new KakaoTalkClient().login({
|
|
1355
|
+
oauthToken: 'token',
|
|
1356
|
+
userId: 'user1',
|
|
1357
|
+
deviceUuid: 'device1',
|
|
1358
|
+
})
|
|
1359
|
+
|
|
1360
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1361
|
+
code: 'get_chat_failed',
|
|
1362
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1363
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1364
|
+
})
|
|
1365
|
+
|
|
1366
|
+
client.close()
|
|
1367
|
+
})
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
for (const [label, chatInfo] of [
|
|
1371
|
+
['left=false', { left: false }],
|
|
1372
|
+
['left missing', {}],
|
|
1373
|
+
['left nonboolean', { left: 'true' }],
|
|
1374
|
+
] as const) {
|
|
1375
|
+
it(`does not classify malformed successful CHATINFO with ${label} as structurally absent`, async () => {
|
|
1376
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1377
|
+
statusCode: 0,
|
|
1378
|
+
body: { chatInfo },
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
const client = await new KakaoTalkClient().login({
|
|
1382
|
+
oauthToken: 'token',
|
|
1383
|
+
userId: 'user1',
|
|
1384
|
+
deviceUuid: 'device1',
|
|
1385
|
+
})
|
|
1386
|
+
|
|
1387
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1388
|
+
code: 'get_chat_failed',
|
|
1389
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1390
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1391
|
+
})
|
|
1392
|
+
|
|
1393
|
+
client.close()
|
|
1394
|
+
})
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
it('classifies malformed successful CHATINFO chatInfo as transport or unknown', async () => {
|
|
1398
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1399
|
+
statusCode: 0,
|
|
1400
|
+
body: { chatInfo: { chatId: makeLong(300) } },
|
|
1401
|
+
})
|
|
1402
|
+
|
|
1403
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1404
|
+
|
|
1405
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1406
|
+
code: 'get_chat_failed',
|
|
1407
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1408
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1409
|
+
})
|
|
1410
|
+
|
|
1411
|
+
client.close()
|
|
1412
|
+
})
|
|
1413
|
+
|
|
1414
|
+
for (const openChatType of ['OM', 'OD'] as const) {
|
|
1415
|
+
it(`preserves ${openChatType} and falls back to INFOLINK when CHATINFO has no title`, async () => {
|
|
1416
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1417
|
+
statusCode: 0,
|
|
1418
|
+
body: {
|
|
1419
|
+
chatInfo: {
|
|
1420
|
+
chatId: makeLong(300),
|
|
1421
|
+
type: openChatType,
|
|
1422
|
+
activeMembersCount: 2,
|
|
1423
|
+
newMessageCount: 0,
|
|
1424
|
+
invalidNewMessageCount: false,
|
|
1425
|
+
lastLogId: makeLong(77),
|
|
1426
|
+
lastSeenLogId: makeLong(73),
|
|
1427
|
+
lastChatLog: null,
|
|
1428
|
+
displayMembers: [
|
|
1429
|
+
{ userId: makeLong(42), nickName: 'Alice' },
|
|
1430
|
+
{ userId: makeLong(43), nickName: 'Bob' },
|
|
1431
|
+
],
|
|
1432
|
+
chatMetas: [],
|
|
1433
|
+
li: makeLong(7777),
|
|
1434
|
+
pushAlert: true,
|
|
1435
|
+
},
|
|
1436
|
+
},
|
|
1437
|
+
})
|
|
1438
|
+
mockGetOpenLinkInfo.mockResolvedValueOnce({ body: { ols: [{ ln: 'Open Group Title' }] } })
|
|
1439
|
+
|
|
1440
|
+
const client = await new KakaoTalkClient().login({
|
|
1441
|
+
oauthToken: 'token',
|
|
1442
|
+
userId: 'user1',
|
|
1443
|
+
deviceUuid: 'device1',
|
|
1444
|
+
})
|
|
1445
|
+
const chat = await client.getChat('300')
|
|
1446
|
+
|
|
1447
|
+
expect(chat).toEqual({
|
|
1448
|
+
chat_id: '300',
|
|
1449
|
+
type: openChatType,
|
|
1450
|
+
display_name: 'Alice, Bob',
|
|
1451
|
+
title: 'Open Group Title',
|
|
1452
|
+
active_members: 2,
|
|
1453
|
+
unread_count: 0,
|
|
1454
|
+
last_message: null,
|
|
1455
|
+
})
|
|
1456
|
+
expect(mockGetOpenLinkInfo).toHaveBeenCalledTimes(1)
|
|
1457
|
+
|
|
1458
|
+
client.close()
|
|
1459
|
+
})
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
it('falls back to INFOLINK for a numeric open-chat code', async () => {
|
|
1463
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1464
|
+
statusCode: 0,
|
|
1465
|
+
body: {
|
|
1466
|
+
chatInfo: {
|
|
1467
|
+
chatId: makeLong(300),
|
|
1468
|
+
type: 13,
|
|
1469
|
+
activeMembersCount: 2,
|
|
1470
|
+
newMessageCount: 0,
|
|
1471
|
+
invalidNewMessageCount: false,
|
|
1472
|
+
lastLogId: makeLong(77),
|
|
1473
|
+
lastSeenLogId: makeLong(73),
|
|
1474
|
+
lastChatLog: null,
|
|
1475
|
+
displayMembers: [],
|
|
1476
|
+
chatMetas: [],
|
|
1477
|
+
li: makeLong(7777),
|
|
1478
|
+
pushAlert: true,
|
|
1479
|
+
},
|
|
1480
|
+
},
|
|
1481
|
+
})
|
|
1482
|
+
mockGetOpenLinkInfo.mockResolvedValueOnce({ body: { ols: [{ ln: 'Numeric Open Title' }] } })
|
|
1483
|
+
|
|
1484
|
+
const client = await new KakaoTalkClient().login({
|
|
1485
|
+
oauthToken: 'token',
|
|
1486
|
+
userId: 'user1',
|
|
1487
|
+
deviceUuid: 'device1',
|
|
1488
|
+
})
|
|
1489
|
+
const chat = await client.getChat('300')
|
|
1490
|
+
|
|
1491
|
+
expect(chat.type).toBe(13)
|
|
1492
|
+
expect(chat.title).toBe('Numeric Open Title')
|
|
1493
|
+
expect(mockGetOpenLinkInfo).toHaveBeenCalledTimes(1)
|
|
1494
|
+
|
|
1495
|
+
client.close()
|
|
1496
|
+
})
|
|
1497
|
+
|
|
1498
|
+
for (const normalChatType of ['DirectChat', 'MultiChat', 'PlusChat', 'MemoChat'] as const) {
|
|
1499
|
+
it(`preserves donor-defined ${normalChatType} without contacting INFOLINK`, async () => {
|
|
1500
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1501
|
+
statusCode: 0,
|
|
1502
|
+
body: {
|
|
1503
|
+
chatInfo: {
|
|
1504
|
+
chatId: makeLong(300),
|
|
1505
|
+
type: normalChatType,
|
|
1506
|
+
activeMembersCount: 2,
|
|
1507
|
+
newMessageCount: 0,
|
|
1508
|
+
invalidNewMessageCount: false,
|
|
1509
|
+
lastLogId: makeLong(1),
|
|
1510
|
+
lastSeenLogId: makeLong(0),
|
|
1511
|
+
displayMembers: [],
|
|
1512
|
+
chatMetas: [],
|
|
1513
|
+
li: makeLong(7777),
|
|
1514
|
+
pushAlert: true,
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
const client = await new KakaoTalkClient().login({
|
|
1520
|
+
oauthToken: 'token',
|
|
1521
|
+
userId: 'user1',
|
|
1522
|
+
deviceUuid: 'device1',
|
|
1523
|
+
})
|
|
1524
|
+
|
|
1525
|
+
await expect(client.getChat('300')).resolves.toEqual({
|
|
1526
|
+
chat_id: '300',
|
|
1527
|
+
type: normalChatType,
|
|
1528
|
+
display_name: null,
|
|
1529
|
+
title: null,
|
|
1530
|
+
active_members: 2,
|
|
1531
|
+
unread_count: 0,
|
|
1532
|
+
last_message: null,
|
|
1533
|
+
})
|
|
1534
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1535
|
+
|
|
1536
|
+
client.close()
|
|
1537
|
+
})
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
it('rejects an empty string CHATINFO type without contacting INFOLINK', async () => {
|
|
1541
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1542
|
+
statusCode: 0,
|
|
1543
|
+
body: {
|
|
1544
|
+
chatInfo: {
|
|
1545
|
+
chatId: makeLong(300),
|
|
1546
|
+
type: '',
|
|
1547
|
+
activeMembersCount: 2,
|
|
1548
|
+
newMessageCount: 0,
|
|
1549
|
+
invalidNewMessageCount: false,
|
|
1550
|
+
lastLogId: makeLong(1),
|
|
1551
|
+
lastSeenLogId: makeLong(0),
|
|
1552
|
+
displayMembers: [],
|
|
1553
|
+
chatMetas: [],
|
|
1554
|
+
pushAlert: true,
|
|
1555
|
+
},
|
|
1556
|
+
},
|
|
1557
|
+
})
|
|
1558
|
+
|
|
1559
|
+
const client = await new KakaoTalkClient().login({
|
|
1560
|
+
oauthToken: 'token',
|
|
1561
|
+
userId: 'user1',
|
|
1562
|
+
deviceUuid: 'device1',
|
|
1563
|
+
})
|
|
1564
|
+
|
|
1565
|
+
await expect(client.getChat('300')).rejects.toMatchObject({
|
|
1566
|
+
code: 'get_chat_failed',
|
|
1567
|
+
getChatFailureReason: 'transport_or_unknown',
|
|
1568
|
+
responseFailureKind: 'transient_or_unknown',
|
|
1569
|
+
})
|
|
1570
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1571
|
+
|
|
1572
|
+
client.close()
|
|
1573
|
+
})
|
|
1574
|
+
|
|
1575
|
+
it('preserves an unknown non-empty string CHATINFO type without contacting INFOLINK', async () => {
|
|
1576
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1577
|
+
statusCode: 0,
|
|
1578
|
+
body: {
|
|
1579
|
+
chatInfo: {
|
|
1580
|
+
chatId: makeLong(300),
|
|
1581
|
+
type: 'UnknownChat',
|
|
1582
|
+
activeMembersCount: 2,
|
|
1583
|
+
newMessageCount: 0,
|
|
1584
|
+
invalidNewMessageCount: false,
|
|
1585
|
+
lastLogId: makeLong(1),
|
|
1586
|
+
lastSeenLogId: makeLong(0),
|
|
1587
|
+
displayMembers: [],
|
|
1588
|
+
chatMetas: [],
|
|
1589
|
+
pushAlert: true,
|
|
1590
|
+
},
|
|
1591
|
+
},
|
|
1592
|
+
})
|
|
1593
|
+
|
|
1594
|
+
const client = await new KakaoTalkClient().login({
|
|
1595
|
+
oauthToken: 'token',
|
|
1596
|
+
userId: 'user1',
|
|
1597
|
+
deviceUuid: 'device1',
|
|
1598
|
+
})
|
|
1599
|
+
|
|
1600
|
+
await expect(client.getChat('300')).resolves.toEqual({
|
|
1601
|
+
chat_id: '300',
|
|
1602
|
+
type: 'UnknownChat',
|
|
1603
|
+
display_name: null,
|
|
1604
|
+
title: null,
|
|
1605
|
+
active_members: 2,
|
|
1606
|
+
unread_count: 0,
|
|
1607
|
+
last_message: null,
|
|
1608
|
+
})
|
|
1609
|
+
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1610
|
+
|
|
1611
|
+
client.close()
|
|
1612
|
+
})
|
|
1613
|
+
|
|
1614
|
+
it('rejects a CHATINFO response bound to a different chat id', async () => {
|
|
1615
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1616
|
+
statusCode: 0,
|
|
1617
|
+
body: {
|
|
1618
|
+
chatInfo: {
|
|
1619
|
+
chatId: makeLong(999),
|
|
1620
|
+
type: 11,
|
|
1621
|
+
activeMembersCount: 2,
|
|
1622
|
+
newMessageCount: 0,
|
|
1623
|
+
invalidNewMessageCount: false,
|
|
1624
|
+
lastLogId: makeLong(1),
|
|
1625
|
+
lastSeenLogId: makeLong(0),
|
|
1626
|
+
displayMembers: [],
|
|
1627
|
+
chatMetas: [],
|
|
1628
|
+
pushAlert: true,
|
|
1629
|
+
},
|
|
1630
|
+
},
|
|
1631
|
+
})
|
|
1632
|
+
|
|
1633
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1634
|
+
|
|
1635
|
+
await expect(client.getChat('300')).rejects.toMatchObject({ code: 'get_chat_failed' })
|
|
1636
|
+
|
|
1637
|
+
client.close()
|
|
1638
|
+
})
|
|
1639
|
+
|
|
1640
|
+
it('rejects an invalid chat id before opening a LOCO session', async () => {
|
|
1641
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1642
|
+
|
|
1643
|
+
await expect(client.getChat('not-a-number')).rejects.toMatchObject({ code: 'invalid_chat_id' })
|
|
1644
|
+
expect(mockLogin).not.toHaveBeenCalled()
|
|
1645
|
+
expect(mockGetChannelInfo).not.toHaveBeenCalled()
|
|
1646
|
+
|
|
1647
|
+
client.close()
|
|
1648
|
+
})
|
|
1649
|
+
})
|
|
1650
|
+
|
|
1651
|
+
describe('getMessages', () => {
|
|
1652
|
+
it('returns a lossless forward page without discarding the oldest messages', async () => {
|
|
1653
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
1654
|
+
statusCode: 0,
|
|
1655
|
+
body: {
|
|
1656
|
+
status: 0,
|
|
1657
|
+
chatLogs: Array.from({ length: 5 }, (_, index) => ({
|
|
1658
|
+
logId: makeLong(index + 1),
|
|
1659
|
+
chatId: 100,
|
|
1660
|
+
type: 1,
|
|
1661
|
+
authorId: 42,
|
|
1662
|
+
message: `message-${index + 1}`,
|
|
1663
|
+
sendAt: 1700000000 + index,
|
|
1664
|
+
})),
|
|
1665
|
+
eof: true,
|
|
1666
|
+
},
|
|
1667
|
+
})
|
|
1668
|
+
|
|
1669
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1670
|
+
const page = await client.getMessagePage('100', { count: 2 })
|
|
1671
|
+
|
|
1672
|
+
expect(page.messages.map((message) => message.log_id)).toEqual(['1', '2'])
|
|
1673
|
+
expect(page.next_cursor).toBe('2')
|
|
1674
|
+
expect(page.complete).toBe(false)
|
|
1675
|
+
|
|
1676
|
+
client.close()
|
|
1677
|
+
})
|
|
1678
|
+
|
|
1679
|
+
it('returns a complete forward page when the protocol page is exhausted', async () => {
|
|
1680
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
1681
|
+
statusCode: 0,
|
|
1682
|
+
body: {
|
|
1683
|
+
status: 0,
|
|
1684
|
+
chatLogs: [
|
|
1685
|
+
{ logId: makeLong(11), chatId: 100, type: 1, authorId: 42, message: 'eleven', sendAt: 11 },
|
|
1686
|
+
{ logId: makeLong(10), chatId: 100, type: 1, authorId: 42, message: 'ten', sendAt: 10 },
|
|
1687
|
+
],
|
|
1688
|
+
eof: true,
|
|
1689
|
+
},
|
|
1690
|
+
})
|
|
1691
|
+
|
|
1692
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1693
|
+
const page = await client.getMessagePage('100', { from: '9', count: 10 })
|
|
773
1694
|
|
|
774
|
-
expect(
|
|
775
|
-
expect(
|
|
1695
|
+
expect(page.messages.map((message) => message.log_id)).toEqual(['10', '11'])
|
|
1696
|
+
expect(page.next_cursor).toBe('11')
|
|
1697
|
+
expect(page.complete).toBe(true)
|
|
776
1698
|
|
|
777
1699
|
client.close()
|
|
778
1700
|
})
|
|
779
1701
|
|
|
780
|
-
it('
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
ll: makeLong(1),
|
|
794
|
-
},
|
|
795
|
-
],
|
|
796
|
-
lastTokenId: makeLong(0),
|
|
797
|
-
lastChatId: makeLong(0),
|
|
798
|
-
eof: true,
|
|
799
|
-
}
|
|
800
|
-
mockLogin.mockResolvedValue(openChatLogin)
|
|
801
|
-
mockGetChannelInfo.mockResolvedValueOnce({ body: { chatInfo: { chatMetas: [] } } })
|
|
802
|
-
mockGetOpenLinkInfo.mockRejectedValueOnce(new Error('INFOLINK timeout'))
|
|
1702
|
+
it('continues exclusively after the supplied cursor when the server repeats it', async () => {
|
|
1703
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
1704
|
+
statusCode: 0,
|
|
1705
|
+
body: {
|
|
1706
|
+
status: 0,
|
|
1707
|
+
chatLogs: [
|
|
1708
|
+
{ logId: makeLong(2), chatId: 100, type: 1, authorId: 42, message: 'repeat', sendAt: 2 },
|
|
1709
|
+
{ logId: makeLong(3), chatId: 100, type: 1, authorId: 42, message: 'three', sendAt: 3 },
|
|
1710
|
+
{ logId: makeLong(4), chatId: 100, type: 1, authorId: 42, message: 'four', sendAt: 4 },
|
|
1711
|
+
],
|
|
1712
|
+
eof: true,
|
|
1713
|
+
},
|
|
1714
|
+
})
|
|
803
1715
|
|
|
804
1716
|
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
805
|
-
const
|
|
1717
|
+
const page = await client.getMessagePage('100', { from: '2', count: 10 })
|
|
806
1718
|
|
|
807
|
-
expect(
|
|
1719
|
+
expect(page.messages.map((message) => message.log_id)).toEqual(['3', '4'])
|
|
1720
|
+
expect(page.next_cursor).toBe('4')
|
|
1721
|
+
expect(page.complete).toBe(true)
|
|
808
1722
|
|
|
809
1723
|
client.close()
|
|
810
1724
|
})
|
|
811
1725
|
|
|
812
|
-
it('
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1726
|
+
it('does not report completion when the protocol makes no cursor progress', async () => {
|
|
1727
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
1728
|
+
statusCode: 0,
|
|
1729
|
+
body: { status: 0, chatLogs: [], eof: false },
|
|
1730
|
+
})
|
|
1731
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1732
|
+
statusCode: 0,
|
|
1733
|
+
body: {
|
|
1734
|
+
chatInfo: {
|
|
1735
|
+
chatId: makeLong(100),
|
|
1736
|
+
type: 11,
|
|
1737
|
+
activeMembersCount: 2,
|
|
1738
|
+
newMessageCount: 0,
|
|
1739
|
+
invalidNewMessageCount: false,
|
|
1740
|
+
lastLogId: makeLong(999),
|
|
1741
|
+
lastSeenLogId: makeLong(0),
|
|
1742
|
+
displayMembers: [],
|
|
1743
|
+
chatMetas: [],
|
|
1744
|
+
pushAlert: true,
|
|
825
1745
|
},
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
mockGetChannelInfo.mockResolvedValueOnce({ body: { chatInfo: { chatMetas: [] } } })
|
|
1746
|
+
},
|
|
1747
|
+
})
|
|
1748
|
+
mockSyncMessages.mockResolvedValueOnce({
|
|
1749
|
+
statusCode: 0,
|
|
1750
|
+
body: { status: 0, chatLogs: [], isOK: false },
|
|
1751
|
+
})
|
|
833
1752
|
|
|
834
1753
|
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
835
|
-
const
|
|
1754
|
+
const page = await client.getMessagePage('100', { from: '9', count: 10 })
|
|
836
1755
|
|
|
837
|
-
expect(
|
|
838
|
-
expect(mockGetOpenLinkInfo).not.toHaveBeenCalled()
|
|
1756
|
+
expect(page).toEqual({ messages: [], next_cursor: null, complete: false })
|
|
839
1757
|
|
|
840
1758
|
client.close()
|
|
841
1759
|
})
|
|
842
1760
|
|
|
843
|
-
it('
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1761
|
+
it('uses read-only CHATINFO to paginate a late room missing from the login snapshot', async () => {
|
|
1762
|
+
mockLogin.mockResolvedValueOnce({ ...structuredClone(DEFAULT_LOGIN_RESULT), chatDatas: [] })
|
|
1763
|
+
mockGetChatLogs.mockResolvedValueOnce({
|
|
1764
|
+
statusCode: 0,
|
|
1765
|
+
body: { status: 0, chatLogs: [], eof: true },
|
|
1766
|
+
})
|
|
1767
|
+
mockGetChannelInfo.mockResolvedValueOnce({
|
|
1768
|
+
statusCode: 0,
|
|
1769
|
+
body: {
|
|
1770
|
+
chatInfo: {
|
|
1771
|
+
chatId: makeLong(300),
|
|
1772
|
+
type: 11,
|
|
1773
|
+
activeMembersCount: 2,
|
|
1774
|
+
newMessageCount: 1,
|
|
1775
|
+
invalidNewMessageCount: false,
|
|
1776
|
+
lastLogId: makeLong(12),
|
|
1777
|
+
lastSeenLogId: makeLong(0),
|
|
1778
|
+
displayMembers: [],
|
|
1779
|
+
chatMetas: [],
|
|
1780
|
+
pushAlert: true,
|
|
1781
|
+
},
|
|
1782
|
+
},
|
|
1783
|
+
})
|
|
1784
|
+
mockSyncMessages.mockResolvedValueOnce({
|
|
1785
|
+
statusCode: 0,
|
|
1786
|
+
body: {
|
|
1787
|
+
status: 0,
|
|
1788
|
+
isOK: true,
|
|
1789
|
+
chatLogs: [{ logId: makeLong(12), chatId: 300, type: 1, authorId: 42, message: 'late', sendAt: 12 }],
|
|
1790
|
+
},
|
|
1791
|
+
})
|
|
847
1792
|
|
|
848
1793
|
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1794
|
+
const page = await client.getMessagePage('300', { from: '11', count: 10 })
|
|
1795
|
+
|
|
1796
|
+
expect(page.messages.map((message) => message.log_id)).toEqual(['12'])
|
|
1797
|
+
expect(page.next_cursor).toBe('12')
|
|
1798
|
+
expect(page.complete).toBe(true)
|
|
1799
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1800
|
+
expect(mockSyncMessages.mock.calls[0]?.[2]?.toString()).toBe('11')
|
|
1801
|
+
expect(mockSyncMessages.mock.calls[0]?.[3]?.toString()).toBe('12')
|
|
856
1802
|
|
|
857
1803
|
client.close()
|
|
858
1804
|
})
|
|
859
|
-
})
|
|
860
1805
|
|
|
861
|
-
describe('getMessages', () => {
|
|
862
1806
|
it('falls back to SYNCMSG when MCHATLOGS succeeds with an empty result', async () => {
|
|
863
1807
|
const loginResult = structuredClone(DEFAULT_LOGIN_RESULT)
|
|
864
1808
|
loginResult.chatDatas[0]!.ll = makeLong(10)
|
|
@@ -1610,7 +2554,8 @@ describe('KakaoTalkClient', () => {
|
|
|
1610
2554
|
|
|
1611
2555
|
describe('getMembers / getMembersByIds', () => {
|
|
1612
2556
|
it('returns formatted members from GETMEM with normalized fields', async () => {
|
|
1613
|
-
|
|
2557
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(2)).mockResolvedValueOnce(memberChannelInfo(2))
|
|
2558
|
+
mockGetAllMembers.mockResolvedValue({
|
|
1614
2559
|
statusCode: 0,
|
|
1615
2560
|
body: {
|
|
1616
2561
|
members: [
|
|
@@ -1670,12 +2615,14 @@ describe('KakaoTalkClient', () => {
|
|
|
1670
2615
|
open_profile_link_id: '99',
|
|
1671
2616
|
open_permission: 4,
|
|
1672
2617
|
})
|
|
2618
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1673
2619
|
|
|
1674
2620
|
client.close()
|
|
1675
2621
|
})
|
|
1676
2622
|
|
|
1677
|
-
it('
|
|
1678
|
-
|
|
2623
|
+
it('rejects a partial GETMEM snapshot without entering the room', async () => {
|
|
2624
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(3)).mockResolvedValueOnce(memberChannelInfo(3))
|
|
2625
|
+
mockGetAllMembers.mockResolvedValue({
|
|
1679
2626
|
statusCode: 0,
|
|
1680
2627
|
body: {
|
|
1681
2628
|
members: [
|
|
@@ -1689,43 +2636,223 @@ describe('KakaoTalkClient', () => {
|
|
|
1689
2636
|
token: 0,
|
|
1690
2637
|
},
|
|
1691
2638
|
})
|
|
1692
|
-
|
|
2639
|
+
|
|
2640
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
2641
|
+
await expect(client.getMembers('100')).rejects.toMatchObject({
|
|
2642
|
+
code: 'get_members_failed',
|
|
2643
|
+
})
|
|
2644
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
2645
|
+
|
|
2646
|
+
client.close()
|
|
2647
|
+
})
|
|
2648
|
+
|
|
2649
|
+
it('returns a stable empty snapshot for a zero-member room without CHATONROOM', async () => {
|
|
2650
|
+
mockGetChannelInfo.mockResolvedValue(memberChannelInfo(0))
|
|
2651
|
+
mockGetAllMembers.mockResolvedValue({
|
|
2652
|
+
statusCode: 0,
|
|
2653
|
+
body: { members: [] },
|
|
2654
|
+
})
|
|
2655
|
+
|
|
2656
|
+
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
2657
|
+
const snapshot = await client.getMemberSnapshot('100')
|
|
2658
|
+
|
|
2659
|
+
expect(snapshot).toMatchObject({
|
|
2660
|
+
chat_id: '100',
|
|
2661
|
+
active_members: 0,
|
|
2662
|
+
complete: true,
|
|
2663
|
+
consistency_basis: 'stable_double_read_chatinfo_getmem',
|
|
2664
|
+
})
|
|
2665
|
+
expect(snapshot.members).toEqual([])
|
|
2666
|
+
expect(mockGetAllMembers).toHaveBeenCalledTimes(2)
|
|
2667
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
2668
|
+
|
|
2669
|
+
client.close()
|
|
2670
|
+
})
|
|
2671
|
+
|
|
2672
|
+
it('returns a stable CHATINFO GETMEM snapshot without CHATONROOM', async () => {
|
|
2673
|
+
const visible = [
|
|
2674
|
+
{
|
|
2675
|
+
userId: makeLong(42),
|
|
2676
|
+
nickName: 'Alice',
|
|
2677
|
+
type: 100,
|
|
2678
|
+
},
|
|
2679
|
+
]
|
|
2680
|
+
mockGetChannelInfo
|
|
2681
|
+
.mockResolvedValueOnce(memberChannelInfo(2, visible))
|
|
2682
|
+
.mockResolvedValueOnce(memberChannelInfo(2, visible))
|
|
2683
|
+
mockGetAllMembers.mockResolvedValue({
|
|
2684
|
+
statusCode: 0,
|
|
2685
|
+
body: {
|
|
2686
|
+
members: [
|
|
2687
|
+
visible[0],
|
|
2688
|
+
{
|
|
2689
|
+
userId: makeLong(43),
|
|
2690
|
+
nickName: 'Bob',
|
|
2691
|
+
type: 100,
|
|
2692
|
+
},
|
|
2693
|
+
],
|
|
2694
|
+
},
|
|
2695
|
+
})
|
|
2696
|
+
|
|
2697
|
+
const client = await new KakaoTalkClient().login({
|
|
2698
|
+
oauthToken: 'token',
|
|
2699
|
+
userId: 'user1',
|
|
2700
|
+
deviceUuid: 'device1',
|
|
2701
|
+
})
|
|
2702
|
+
const snapshot = await client.getMemberSnapshot('100')
|
|
2703
|
+
|
|
2704
|
+
expect(snapshot).toMatchObject({
|
|
2705
|
+
chat_id: '100',
|
|
2706
|
+
active_members: 2,
|
|
2707
|
+
complete: true,
|
|
2708
|
+
consistency_basis: 'stable_double_read_chatinfo_getmem',
|
|
2709
|
+
})
|
|
2710
|
+
expect(snapshot.members.map((member) => member.user_id)).toEqual(['42', '43'])
|
|
2711
|
+
expect(mockGetAllMembers).toHaveBeenCalledTimes(2)
|
|
2712
|
+
expect(mockGetChannelInfo).toHaveBeenCalledTimes(2)
|
|
2713
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
2714
|
+
|
|
2715
|
+
client.close()
|
|
2716
|
+
})
|
|
2717
|
+
|
|
2718
|
+
it('fails closed when active membership changes across the snapshot', async () => {
|
|
2719
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(2)).mockResolvedValueOnce(memberChannelInfo(3))
|
|
2720
|
+
mockGetAllMembers.mockResolvedValue({
|
|
1693
2721
|
statusCode: 0,
|
|
1694
2722
|
body: {
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
{ userId: makeLong(
|
|
1698
|
-
{ userId: makeLong(43), nickName: 'Bob', type: 100 },
|
|
1699
|
-
{ userId: makeLong(44), nickName: 'Carol', type: 100 },
|
|
2723
|
+
members: [
|
|
2724
|
+
{ userId: makeLong(42), nickName: 'Alice' },
|
|
2725
|
+
{ userId: makeLong(43), nickName: 'Bob' },
|
|
1700
2726
|
],
|
|
1701
2727
|
},
|
|
1702
2728
|
})
|
|
1703
2729
|
|
|
1704
|
-
const client = await new KakaoTalkClient().login({
|
|
1705
|
-
|
|
2730
|
+
const client = await new KakaoTalkClient().login({
|
|
2731
|
+
oauthToken: 'token',
|
|
2732
|
+
userId: 'user1',
|
|
2733
|
+
deviceUuid: 'device1',
|
|
2734
|
+
})
|
|
2735
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2736
|
+
code: 'get_member_snapshot_failed',
|
|
2737
|
+
})
|
|
2738
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1706
2739
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2740
|
+
client.close()
|
|
2741
|
+
})
|
|
2742
|
+
|
|
2743
|
+
it('fails closed on a same-cardinality hidden member swap', async () => {
|
|
2744
|
+
const visible = [{ userId: makeLong(42), nickName: 'Visible' }]
|
|
2745
|
+
mockGetChannelInfo
|
|
2746
|
+
.mockResolvedValueOnce(memberChannelInfo(2, visible))
|
|
2747
|
+
.mockResolvedValueOnce(memberChannelInfo(2, visible))
|
|
2748
|
+
mockGetAllMembers
|
|
2749
|
+
.mockResolvedValueOnce({
|
|
2750
|
+
statusCode: 0,
|
|
2751
|
+
body: {
|
|
2752
|
+
members: [visible[0], { userId: makeLong(43), nickName: 'Departed' }],
|
|
2753
|
+
},
|
|
2754
|
+
})
|
|
2755
|
+
.mockResolvedValueOnce({
|
|
2756
|
+
statusCode: 0,
|
|
2757
|
+
body: {
|
|
2758
|
+
members: [visible[0], { userId: makeLong(44), nickName: 'Joined' }],
|
|
2759
|
+
},
|
|
2760
|
+
})
|
|
2761
|
+
|
|
2762
|
+
const client = await new KakaoTalkClient().login({
|
|
2763
|
+
oauthToken: 'token',
|
|
2764
|
+
userId: 'user1',
|
|
2765
|
+
deviceUuid: 'device1',
|
|
2766
|
+
})
|
|
2767
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2768
|
+
code: 'get_member_snapshot_failed',
|
|
2769
|
+
})
|
|
1712
2770
|
|
|
1713
2771
|
client.close()
|
|
1714
2772
|
})
|
|
1715
2773
|
|
|
1716
|
-
it('
|
|
1717
|
-
|
|
2774
|
+
it('fails closed on malformed CHATINFO member identity', async () => {
|
|
2775
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1, [{ userId: {}, nickName: 'Malformed' }]))
|
|
1718
2776
|
|
|
1719
|
-
const client = await new KakaoTalkClient().login({
|
|
1720
|
-
|
|
2777
|
+
const client = await new KakaoTalkClient().login({
|
|
2778
|
+
oauthToken: 'token',
|
|
2779
|
+
userId: 'user1',
|
|
2780
|
+
deviceUuid: 'device1',
|
|
2781
|
+
})
|
|
2782
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2783
|
+
code: 'get_member_snapshot_failed',
|
|
2784
|
+
})
|
|
2785
|
+
expect(mockGetAllMembers).not.toHaveBeenCalled()
|
|
2786
|
+
|
|
2787
|
+
client.close()
|
|
2788
|
+
})
|
|
2789
|
+
|
|
2790
|
+
it('fails closed on malformed GETMEM member identity', async () => {
|
|
2791
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1))
|
|
2792
|
+
mockGetAllMembers.mockResolvedValueOnce({
|
|
2793
|
+
statusCode: 0,
|
|
2794
|
+
body: {
|
|
2795
|
+
members: [{ userId: '42', nickName: 'Malformed string id' }],
|
|
2796
|
+
},
|
|
2797
|
+
})
|
|
2798
|
+
|
|
2799
|
+
const client = await new KakaoTalkClient().login({
|
|
2800
|
+
oauthToken: 'token',
|
|
2801
|
+
userId: 'user1',
|
|
2802
|
+
deviceUuid: 'device1',
|
|
2803
|
+
})
|
|
2804
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2805
|
+
code: 'get_member_snapshot_failed',
|
|
2806
|
+
})
|
|
2807
|
+
|
|
2808
|
+
client.close()
|
|
2809
|
+
})
|
|
2810
|
+
|
|
2811
|
+
it('fails closed on duplicate GETMEM identity without CHATONROOM', async () => {
|
|
2812
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(2))
|
|
2813
|
+
mockGetAllMembers.mockResolvedValue({
|
|
2814
|
+
statusCode: 0,
|
|
2815
|
+
body: {
|
|
2816
|
+
members: [
|
|
2817
|
+
{ userId: makeLong(42), nickName: 'Alice' },
|
|
2818
|
+
{ userId: makeLong(42), nickName: 'Duplicate' },
|
|
2819
|
+
],
|
|
2820
|
+
},
|
|
2821
|
+
})
|
|
2822
|
+
|
|
2823
|
+
const client = await new KakaoTalkClient().login({
|
|
2824
|
+
oauthToken: 'token',
|
|
2825
|
+
userId: 'user1',
|
|
2826
|
+
deviceUuid: 'device1',
|
|
2827
|
+
})
|
|
2828
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2829
|
+
code: 'get_member_snapshot_failed',
|
|
2830
|
+
})
|
|
2831
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
2832
|
+
|
|
2833
|
+
client.close()
|
|
2834
|
+
})
|
|
2835
|
+
|
|
2836
|
+
it('fails closed on CHATINFO chat id mismatch before GETMEM', async () => {
|
|
2837
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1, [], 101))
|
|
1721
2838
|
|
|
1722
|
-
|
|
2839
|
+
const client = await new KakaoTalkClient().login({
|
|
2840
|
+
oauthToken: 'token',
|
|
2841
|
+
userId: 'user1',
|
|
2842
|
+
deviceUuid: 'device1',
|
|
2843
|
+
})
|
|
2844
|
+
await expect(client.getMemberSnapshot('100')).rejects.toMatchObject({
|
|
2845
|
+
code: 'get_member_snapshot_failed',
|
|
2846
|
+
})
|
|
2847
|
+
expect(mockGetAllMembers).not.toHaveBeenCalled()
|
|
2848
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1723
2849
|
|
|
1724
2850
|
client.close()
|
|
1725
2851
|
})
|
|
1726
2852
|
|
|
1727
2853
|
it('normalizes missing user_type to null and treats pli=0 as absent', async () => {
|
|
1728
|
-
|
|
2854
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(3)).mockResolvedValueOnce(memberChannelInfo(3))
|
|
2855
|
+
mockGetAllMembers.mockResolvedValue({
|
|
1729
2856
|
statusCode: 0,
|
|
1730
2857
|
body: {
|
|
1731
2858
|
members: [
|
|
@@ -1749,6 +2876,7 @@ describe('KakaoTalkClient', () => {
|
|
|
1749
2876
|
})
|
|
1750
2877
|
|
|
1751
2878
|
it('wraps GETMEM failures as KakaoTalkError get_members_failed', async () => {
|
|
2879
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1))
|
|
1752
2880
|
mockGetAllMembers.mockRejectedValueOnce(new Error('Network error'))
|
|
1753
2881
|
|
|
1754
2882
|
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
@@ -1764,6 +2892,7 @@ describe('KakaoTalkClient', () => {
|
|
|
1764
2892
|
})
|
|
1765
2893
|
|
|
1766
2894
|
it('throws on synthetic disconnect packet from GETMEM (statusCode != 0)', async () => {
|
|
2895
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1))
|
|
1767
2896
|
mockGetAllMembers.mockResolvedValueOnce({
|
|
1768
2897
|
statusCode: -1,
|
|
1769
2898
|
body: { error: 'connection closed' },
|
|
@@ -1782,6 +2911,7 @@ describe('KakaoTalkClient', () => {
|
|
|
1782
2911
|
})
|
|
1783
2912
|
|
|
1784
2913
|
it('throws on GETMEM body.status nonzero', async () => {
|
|
2914
|
+
mockGetChannelInfo.mockResolvedValueOnce(memberChannelInfo(1))
|
|
1785
2915
|
mockGetAllMembers.mockResolvedValueOnce({
|
|
1786
2916
|
statusCode: 0,
|
|
1787
2917
|
body: { status: -500 },
|
|
@@ -1893,6 +3023,8 @@ describe('KakaoTalkClient', () => {
|
|
|
1893
3023
|
expect((e as KakaoTalkError).code).toBe('invalid_chat_id')
|
|
1894
3024
|
}
|
|
1895
3025
|
expect(mockGetAllMembers).not.toHaveBeenCalled()
|
|
3026
|
+
expect(mockGetChannelInfo).not.toHaveBeenCalled()
|
|
3027
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1896
3028
|
expect(mockLogin).not.toHaveBeenCalled()
|
|
1897
3029
|
|
|
1898
3030
|
client.close()
|
|
@@ -1959,17 +3091,22 @@ describe('KakaoTalkClient', () => {
|
|
|
1959
3091
|
if (handler) handler()
|
|
1960
3092
|
return Promise.resolve({ statusCode: -1, body: { error: 'connection closed' } })
|
|
1961
3093
|
})
|
|
1962
|
-
mockGetAllMembers.
|
|
3094
|
+
mockGetAllMembers.mockResolvedValue({
|
|
1963
3095
|
statusCode: 0,
|
|
1964
3096
|
body: { members: [{ userId: makeLong(42), nickName: 'Alice', type: 100 }] },
|
|
1965
3097
|
})
|
|
3098
|
+
mockGetChannelInfo
|
|
3099
|
+
.mockResolvedValueOnce(memberChannelInfo(1))
|
|
3100
|
+
.mockResolvedValueOnce(memberChannelInfo(1))
|
|
3101
|
+
.mockResolvedValueOnce(memberChannelInfo(1))
|
|
1966
3102
|
|
|
1967
3103
|
const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
|
|
1968
3104
|
const members = await client.getMembers('100')
|
|
1969
3105
|
|
|
1970
3106
|
expect(members).toHaveLength(1)
|
|
1971
3107
|
expect(members[0].nickname).toBe('Alice')
|
|
1972
|
-
expect(mockGetAllMembers).toHaveBeenCalledTimes(
|
|
3108
|
+
expect(mockGetAllMembers).toHaveBeenCalledTimes(3)
|
|
3109
|
+
expect(mockGetChatInfo).not.toHaveBeenCalled()
|
|
1973
3110
|
// Login fires twice: once for the initial connect, once for the reconnect
|
|
1974
3111
|
// after the captured onClose handler invalidated this.state.
|
|
1975
3112
|
expect(mockLogin).toHaveBeenCalledTimes(2)
|