screeps-connectivity 0.2.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/eslint.config.js +54 -0
  3. package/package.json +45 -0
  4. package/src/ScreepsClient.ts +172 -0
  5. package/src/badge/colors.ts +83 -0
  6. package/src/badge/generateSvg.ts +70 -0
  7. package/src/badge/index.ts +1 -0
  8. package/src/badge/paths.ts +385 -0
  9. package/src/cache/Cache.ts +71 -0
  10. package/src/cache/Map2Storage.ts +112 -0
  11. package/src/file-storage.ts +2 -0
  12. package/src/http/HttpClient.ts +160 -0
  13. package/src/http/auth/AuthStrategy.ts +5 -0
  14. package/src/http/auth/GuestAuth.ts +13 -0
  15. package/src/http/auth/PasswordAuth.ts +17 -0
  16. package/src/http/auth/SteamTicketAuth.ts +17 -0
  17. package/src/http/auth/TokenAuth.ts +14 -0
  18. package/src/http/decompress.ts +37 -0
  19. package/src/http/endpoints/auth.ts +23 -0
  20. package/src/http/endpoints/experimental.ts +13 -0
  21. package/src/http/endpoints/game.ts +103 -0
  22. package/src/http/endpoints/leaderboard.ts +16 -0
  23. package/src/http/endpoints/power-creeps.ts +24 -0
  24. package/src/http/endpoints/register.ts +19 -0
  25. package/src/http/endpoints/user-messages.ts +20 -0
  26. package/src/http/endpoints/user.ts +95 -0
  27. package/src/http/fetchServerVersion.ts +151 -0
  28. package/src/index.ts +55 -0
  29. package/src/logger.ts +25 -0
  30. package/src/socket/MessageParser.ts +44 -0
  31. package/src/socket/SocketClient.ts +203 -0
  32. package/src/storage/FileStorage.ts +44 -0
  33. package/src/storage/IndexedDBStorage.ts +77 -0
  34. package/src/storage/NullStorage.ts +8 -0
  35. package/src/storage/StorageAdapter.ts +6 -0
  36. package/src/stores/MapStatsStore.ts +115 -0
  37. package/src/stores/MapStore.ts +254 -0
  38. package/src/stores/NavigationStore.ts +61 -0
  39. package/src/stores/RoomStore.ts +264 -0
  40. package/src/stores/ServerStore.ts +128 -0
  41. package/src/stores/TypedStore.ts +31 -0
  42. package/src/stores/UserStore.ts +189 -0
  43. package/src/subscription/index.ts +18 -0
  44. package/src/types/api.ts +252 -0
  45. package/src/types/events.ts +72 -0
  46. package/src/types/game.ts +160 -0
  47. package/tests/.gitkeep +0 -0
  48. package/tests/ScreepsClient.test.ts +229 -0
  49. package/tests/badge/generateSvg.test.ts +174 -0
  50. package/tests/cache/Cache.test.ts +99 -0
  51. package/tests/cache/Map2Storage.test.ts +130 -0
  52. package/tests/http/HttpClient.test.ts +188 -0
  53. package/tests/http/decompress.test.ts +52 -0
  54. package/tests/http/endpoints/auth.test.ts +126 -0
  55. package/tests/http/endpoints/game.test.ts +210 -0
  56. package/tests/http/endpoints/power-creeps.test.ts +81 -0
  57. package/tests/http/endpoints/user-messages.test.ts +68 -0
  58. package/tests/http/endpoints/user.test.ts +139 -0
  59. package/tests/socket/MessageParser.test.ts +55 -0
  60. package/tests/socket/SocketClient.test.ts +144 -0
  61. package/tests/storage/FileStorage.test.ts +64 -0
  62. package/tests/storage/IndexedDBStorage.test.ts +36 -0
  63. package/tests/storage/NullStorage.test.ts +24 -0
  64. package/tests/stores/MapStatsStore.test.ts +234 -0
  65. package/tests/stores/MapStore.test.ts +537 -0
  66. package/tests/stores/NavigationStore.test.ts +166 -0
  67. package/tests/stores/RoomStore.test.ts +130 -0
  68. package/tests/stores/ServerStore.test.ts +48 -0
  69. package/tests/stores/TypedStore.test.ts +54 -0
  70. package/tests/stores/UserStore.test.ts +136 -0
  71. package/tests/subscription/SubscriptionGroup.test.ts +34 -0
  72. package/tests/types/game.test.ts +42 -0
  73. package/tsconfig.json +17 -0
  74. package/tsup.config.ts +9 -0
  75. package/vitest.config.ts +7 -0
@@ -0,0 +1,210 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { HttpClient } from '../../../src/http/HttpClient.js'
3
+ import { TokenAuth } from '../../../src/http/auth/TokenAuth.js'
4
+
5
+ function mockResponse(body: unknown, opts: ResponseInit = {}): Response {
6
+ return new Response(JSON.stringify(body), {
7
+ status: 200,
8
+ headers: { 'content-type': 'application/json', ...opts.headers },
9
+ ...opts,
10
+ })
11
+ }
12
+
13
+ describe('game endpoints', () => {
14
+ let fetchMock: ReturnType<typeof vi.fn>
15
+
16
+ beforeEach(() => {
17
+ fetchMock = vi.fn()
18
+ vi.stubGlobal('fetch', fetchMock)
19
+ })
20
+
21
+ afterEach(() => {
22
+ vi.unstubAllGlobals()
23
+ })
24
+
25
+ it('createFlag sends POST to /api/game/create-flag', async () => {
26
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
27
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
28
+
29
+ await http.game.createFlag('E2N2', 15, 25, 'MyFlag', 1, 2, 'shard1')
30
+
31
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
32
+ expect(url).toContain('/api/game/create-flag')
33
+ expect(init.method).toBe('POST')
34
+ expect(JSON.parse(init.body as string)).toEqual({
35
+ room: 'E2N2',
36
+ x: 15,
37
+ y: 25,
38
+ name: 'MyFlag',
39
+ color: 1,
40
+ secondaryColor: 2,
41
+ shard: 'shard1',
42
+ })
43
+ })
44
+
45
+ it('createFlag omits shard when not provided', async () => {
46
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
47
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
48
+
49
+ await http.game.createFlag('E2N2', 15, 25, 'MyFlag', 1, 2)
50
+
51
+ const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
52
+ expect(JSON.parse(init.body as string)).toEqual({
53
+ room: 'E2N2',
54
+ x: 15,
55
+ y: 25,
56
+ name: 'MyFlag',
57
+ color: 1,
58
+ secondaryColor: 2,
59
+ })
60
+ })
61
+
62
+ it('genUniqueFlagName sends POST to /api/game/gen-unique-flag-name', async () => {
63
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1, name: 'Flag1' }))
64
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
65
+
66
+ const res = await http.game.genUniqueFlagName()
67
+
68
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
69
+ expect(url).toContain('/api/game/gen-unique-flag-name')
70
+ expect(init.method).toBe('POST')
71
+ expect(res.name).toBe('Flag1')
72
+ })
73
+
74
+ it('checkUniqueFlagName sends POST to /api/game/check-unique-flag-name', async () => {
75
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
76
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
77
+
78
+ await http.game.checkUniqueFlagName('MyFlag')
79
+
80
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
81
+ expect(url).toContain('/api/game/check-unique-flag-name')
82
+ expect(init.method).toBe('POST')
83
+ expect(JSON.parse(init.body as string)).toEqual({ name: 'MyFlag' })
84
+ })
85
+
86
+ it('genUniqueObjectName sends POST with type', async () => {
87
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1, name: 'Spawn1' }))
88
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
89
+ const res = await http.game.genUniqueObjectName('spawn')
90
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
91
+ expect(init.method).toBe('POST')
92
+ expect(url).toContain('/api/game/gen-unique-object-name')
93
+ expect(JSON.parse(init.body as string)).toEqual({ type: 'spawn' })
94
+ expect(res.name).toBe('Spawn1')
95
+ })
96
+
97
+ it('checkUniqueObjectName sends POST with type and name', async () => {
98
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
99
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
100
+ await http.game.checkUniqueObjectName('spawn', 'Spawn1')
101
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
102
+ expect(init.method).toBe('POST')
103
+ expect(url).toContain('/api/game/check-unique-object-name')
104
+ expect(JSON.parse(init.body as string)).toEqual({ type: 'spawn', name: 'Spawn1' })
105
+ })
106
+
107
+ it('placeSpawn sends POST with room, x, y', async () => {
108
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
109
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
110
+ await http.game.placeSpawn('W1N1', 10, 20)
111
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
112
+ expect(init.method).toBe('POST')
113
+ expect(url).toContain('/api/game/place-spawn')
114
+ expect(JSON.parse(init.body as string)).toEqual({ room: 'W1N1', x: 10, y: 20 })
115
+ })
116
+
117
+ it('placeSpawn includes name when provided', async () => {
118
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
119
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
120
+ await http.game.placeSpawn('W1N1', 10, 20, 'Spawn1')
121
+ const body = JSON.parse(fetchMock.mock.calls[0][1].body as string)
122
+ expect(body).toEqual({ room: 'W1N1', x: 10, y: 20, name: 'Spawn1' })
123
+ })
124
+
125
+ it('placeSpawn includes shard when provided', async () => {
126
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
127
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
128
+ await http.game.placeSpawn('W1N1', 10, 20, 'Spawn1', 'shard0')
129
+ const body = JSON.parse(fetchMock.mock.calls[0][1].body as string)
130
+ expect(body).toEqual({ room: 'W1N1', x: 10, y: 20, name: 'Spawn1', shard: 'shard0' })
131
+ })
132
+
133
+ it('createConstruction sends POST with required fields', async () => {
134
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
135
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
136
+ await http.game.createConstruction('W1N1', 5, 5, 'extension')
137
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
138
+ expect(init.method).toBe('POST')
139
+ expect(url).toContain('/api/game/create-construction')
140
+ expect(JSON.parse(init.body as string)).toEqual({ room: 'W1N1', x: 5, y: 5, structureType: 'extension' })
141
+ })
142
+
143
+ it('addObjectIntent sends POST with _id mapping', async () => {
144
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
145
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
146
+ await http.game.addObjectIntent('obj-id', 'W1N1', 'attack', { targetId: 'x' })
147
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
148
+ expect(init.method).toBe('POST')
149
+ expect(url).toContain('/api/game/add-object-intent')
150
+ expect(JSON.parse(init.body as string)).toEqual({ _id: 'obj-id', room: 'W1N1', name: 'attack', intent: { targetId: 'x' } })
151
+ })
152
+
153
+ it('addGlobalIntent sends POST with name and intent', async () => {
154
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
155
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
156
+ await http.game.addGlobalIntent('respawn', {})
157
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
158
+ expect(init.method).toBe('POST')
159
+ expect(url).toContain('/api/game/add-global-intent')
160
+ expect(JSON.parse(init.body as string)).toEqual({ name: 'respawn', intent: {} })
161
+ })
162
+
163
+ it('setNotifyWhenAttacked sends POST with _id mapping', async () => {
164
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
165
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
166
+ await http.game.setNotifyWhenAttacked('struct-id', true)
167
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
168
+ expect(init.method).toBe('POST')
169
+ expect(url).toContain('/api/game/set-notify-when-attacked')
170
+ expect(JSON.parse(init.body as string)).toEqual({ _id: 'struct-id', enabled: true })
171
+ })
172
+
173
+ it('createInvader sends POST with required fields', async () => {
174
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
175
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
176
+ await http.game.createInvader('W1N1', 10, 10, 1, 'melee')
177
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
178
+ expect(init.method).toBe('POST')
179
+ expect(url).toContain('/api/game/create-invader')
180
+ expect(JSON.parse(init.body as string)).toEqual({ room: 'W1N1', x: 10, y: 10, size: 1, type: 'melee' })
181
+ })
182
+
183
+ it('createInvader includes boosted when provided', async () => {
184
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
185
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
186
+ await http.game.createInvader('W1N1', 10, 10, 1, 'melee', true)
187
+ const body = JSON.parse(fetchMock.mock.calls[0][1].body as string)
188
+ expect(body.boosted).toBe(true)
189
+ })
190
+
191
+ it('removeInvader sends POST with _id mapping', async () => {
192
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1 }))
193
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
194
+ await http.game.removeInvader('inv-id')
195
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
196
+ expect(init.method).toBe('POST')
197
+ expect(url).toContain('/api/game/remove-invader')
198
+ expect(JSON.parse(init.body as string)).toEqual({ _id: 'inv-id' })
199
+ })
200
+
201
+ it('tick sends GET to /api/game/tick', async () => {
202
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1, tick: 500 }))
203
+ const http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
204
+ const res = await http.game.tick()
205
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
206
+ expect(init.method).toBe('GET')
207
+ expect(url).toContain('/api/game/tick')
208
+ expect(res.tick).toBe(500)
209
+ })
210
+ })
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { HttpClient } from '../../../src/http/HttpClient.js'
3
+ import { TokenAuth } from '../../../src/http/auth/TokenAuth.js'
4
+
5
+ function mockResponse(body: unknown, opts: ResponseInit = {}): Response {
6
+ return new Response(JSON.stringify(body), {
7
+ status: 200,
8
+ headers: { 'content-type': 'application/json', ...opts.headers },
9
+ ...opts,
10
+ })
11
+ }
12
+
13
+ describe('game.powerCreeps endpoints', () => {
14
+ let fetchMock: ReturnType<typeof vi.fn>
15
+ let http: HttpClient
16
+
17
+ beforeEach(() => {
18
+ fetchMock = vi.fn().mockResolvedValue(mockResponse({ ok: 1 }))
19
+ vi.stubGlobal('fetch', fetchMock)
20
+ http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
21
+ })
22
+
23
+ afterEach(() => {
24
+ vi.unstubAllGlobals()
25
+ })
26
+
27
+ it('list sends GET to /api/game/power-creeps/list', async () => {
28
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1, list: [] }))
29
+ await http.game.powerCreeps.list()
30
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
31
+ expect(init.method).toBe('GET')
32
+ expect(url).toContain('/api/game/power-creeps/list')
33
+ })
34
+
35
+ it('create sends POST with name and className', async () => {
36
+ await http.game.powerCreeps.create('MyPC', 'operator')
37
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
38
+ expect(init.method).toBe('POST')
39
+ expect(url).toContain('/api/game/power-creeps/create')
40
+ expect(JSON.parse(init.body as string)).toEqual({ name: 'MyPC', className: 'operator' })
41
+ })
42
+
43
+ it('delete sends POST with id', async () => {
44
+ await http.game.powerCreeps.delete('pc-id')
45
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
46
+ expect(init.method).toBe('POST')
47
+ expect(url).toContain('/api/game/power-creeps/delete')
48
+ expect(JSON.parse(init.body as string)).toEqual({ id: 'pc-id' })
49
+ })
50
+
51
+ it('cancelDelete sends POST with id', async () => {
52
+ await http.game.powerCreeps.cancelDelete('pc-id')
53
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
54
+ expect(init.method).toBe('POST')
55
+ expect(url).toContain('/api/game/power-creeps/cancel-delete')
56
+ expect(JSON.parse(init.body as string)).toEqual({ id: 'pc-id' })
57
+ })
58
+
59
+ it('upgrade sends POST with id and powers', async () => {
60
+ await http.game.powerCreeps.upgrade('pc-id', { OPERATE_SPAWN: 2 })
61
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
62
+ expect(init.method).toBe('POST')
63
+ expect(url).toContain('/api/game/power-creeps/upgrade')
64
+ expect(JSON.parse(init.body as string)).toEqual({ id: 'pc-id', powers: { OPERATE_SPAWN: 2 } })
65
+ })
66
+
67
+ it('rename sends POST with id and name', async () => {
68
+ await http.game.powerCreeps.rename('pc-id', 'NewName')
69
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
70
+ expect(init.method).toBe('POST')
71
+ expect(url).toContain('/api/game/power-creeps/rename')
72
+ expect(JSON.parse(init.body as string)).toEqual({ id: 'pc-id', name: 'NewName' })
73
+ })
74
+
75
+ it('experimentation sends POST to /api/game/power-creeps/experimentation', async () => {
76
+ await http.game.powerCreeps.experimentation()
77
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
78
+ expect(init.method).toBe('POST')
79
+ expect(url).toContain('/api/game/power-creeps/experimentation')
80
+ })
81
+ })
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { HttpClient } from '../../../src/http/HttpClient.js'
3
+ import { TokenAuth } from '../../../src/http/auth/TokenAuth.js'
4
+
5
+ function mockResponse(body: unknown, opts: ResponseInit = {}): Response {
6
+ return new Response(JSON.stringify(body), {
7
+ status: 200,
8
+ headers: { 'content-type': 'application/json', ...opts.headers },
9
+ ...opts,
10
+ })
11
+ }
12
+
13
+ describe('user.messages endpoints', () => {
14
+ let fetchMock: ReturnType<typeof vi.fn>
15
+ let http: HttpClient
16
+
17
+ beforeEach(() => {
18
+ fetchMock = vi.fn().mockResolvedValue(mockResponse({ ok: 1 }))
19
+ vi.stubGlobal('fetch', fetchMock)
20
+ http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
21
+ })
22
+
23
+ afterEach(() => {
24
+ vi.unstubAllGlobals()
25
+ })
26
+
27
+ it('send sends POST with respondent and text', async () => {
28
+ await http.user.messages.send('Tigga', 'hello')
29
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
30
+ expect(init.method).toBe('POST')
31
+ expect(url).toContain('/api/user/messages/send')
32
+ expect(JSON.parse(init.body as string)).toEqual({ respondent: 'Tigga', text: 'hello' })
33
+ })
34
+
35
+ it('list sends GET with respondent query param', async () => {
36
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1, messages: [] }))
37
+ await http.user.messages.list('Tigga')
38
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
39
+ expect(init.method).toBe('GET')
40
+ expect(url).toContain('/api/user/messages/list')
41
+ expect(url).toContain('respondent=Tigga')
42
+ })
43
+
44
+ it('index sends GET to /api/user/messages/index', async () => {
45
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1, list: [] }))
46
+ await http.user.messages.index()
47
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
48
+ expect(init.method).toBe('GET')
49
+ expect(url).toContain('/api/user/messages/index')
50
+ })
51
+
52
+ it('markRead sends POST with id', async () => {
53
+ await http.user.messages.markRead('msg-id-123')
54
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
55
+ expect(init.method).toBe('POST')
56
+ expect(url).toContain('/api/user/messages/mark-read')
57
+ expect(JSON.parse(init.body as string)).toEqual({ id: 'msg-id-123' })
58
+ })
59
+
60
+ it('unreadCount sends GET to /api/user/messages/unread-count', async () => {
61
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1, count: 3 }))
62
+ const res = await http.user.messages.unreadCount()
63
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
64
+ expect(init.method).toBe('GET')
65
+ expect(url).toContain('/api/user/messages/unread-count')
66
+ expect(res.count).toBe(3)
67
+ })
68
+ })
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { HttpClient } from '../../../src/http/HttpClient.js'
3
+ import { TokenAuth } from '../../../src/http/auth/TokenAuth.js'
4
+
5
+ function mockResponse(body: unknown, opts: ResponseInit = {}): Response {
6
+ return new Response(JSON.stringify(body), {
7
+ status: 200,
8
+ headers: { 'content-type': 'application/json', ...opts.headers },
9
+ ...opts,
10
+ })
11
+ }
12
+
13
+ describe('user endpoints', () => {
14
+ let fetchMock: ReturnType<typeof vi.fn>
15
+ let http: HttpClient
16
+
17
+ beforeEach(() => {
18
+ fetchMock = vi.fn().mockResolvedValue(mockResponse({ ok: 1 }))
19
+ vi.stubGlobal('fetch', fetchMock)
20
+ http = new HttpClient({ url: 'http://test.local', auth: new TokenAuth({ token: 't' }) })
21
+ })
22
+
23
+ afterEach(() => {
24
+ vi.unstubAllGlobals()
25
+ })
26
+
27
+ it('find sends GET with username query param', async () => {
28
+ await http.user.find({ username: 'Tigga' })
29
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
30
+ expect(init.method).toBe('GET')
31
+ expect(url).toContain('/api/user/find')
32
+ expect(url).toContain('username=Tigga')
33
+ })
34
+
35
+ it('find sends GET with id query param', async () => {
36
+ await http.user.find({ id: 'abc123' })
37
+ const [url] = fetchMock.mock.calls[0] as [string]
38
+ expect(url).toContain('id=abc123')
39
+ })
40
+
41
+ it('moneyHistory sends GET without page when omitted', async () => {
42
+ await http.user.moneyHistory()
43
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
44
+ expect(init.method).toBe('GET')
45
+ expect(url).toContain('/api/user/money-history')
46
+ expect(url).not.toContain('page')
47
+ })
48
+
49
+ it('moneyHistory sends GET with page param', async () => {
50
+ await http.user.moneyHistory(2)
51
+ const [url] = fetchMock.mock.calls[0] as [string]
52
+ expect(url).toContain('page=2')
53
+ })
54
+
55
+ it('respawn sends POST to /api/user/respawn', async () => {
56
+ await http.user.respawn()
57
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
58
+ expect(init.method).toBe('POST')
59
+ expect(url).toContain('/api/user/respawn')
60
+ })
61
+
62
+ it('respawnProhibitedRooms sends GET', async () => {
63
+ fetchMock.mockResolvedValue(mockResponse({ ok: 1, rooms: [] }))
64
+ await http.user.respawnProhibitedRooms()
65
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
66
+ expect(init.method).toBe('GET')
67
+ expect(url).toContain('/api/user/respawn-prohibited-rooms')
68
+ })
69
+
70
+ it('badge sends POST with badge body', async () => {
71
+ const badge = { type: 1, color1: '#ff0000', color2: '#00ff00', color3: '#0000ff', flip: false }
72
+ await http.user.badge(badge)
73
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
74
+ expect(init.method).toBe('POST')
75
+ expect(url).toContain('/api/user/badge')
76
+ expect(JSON.parse(init.body as string)).toEqual({ badge })
77
+ })
78
+
79
+ it('setActiveBranch sends POST with activeName and branch', async () => {
80
+ await http.user.setActiveBranch('activeWorld', 'main')
81
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
82
+ expect(init.method).toBe('POST')
83
+ expect(url).toContain('/api/user/set-active-branch')
84
+ expect(JSON.parse(init.body as string)).toEqual({ activeName: 'activeWorld', branch: 'main' })
85
+ })
86
+
87
+ it('cloneBranch sends POST with newName', async () => {
88
+ await http.user.cloneBranch('backup')
89
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
90
+ expect(init.method).toBe('POST')
91
+ expect(url).toContain('/api/user/clone-branch')
92
+ expect(JSON.parse(init.body as string)).toEqual({ newName: 'backup' })
93
+ })
94
+
95
+ it('cloneBranch includes branch and defaultModules when provided', async () => {
96
+ await http.user.cloneBranch('backup', 'main', true)
97
+ const body = JSON.parse(fetchMock.mock.calls[0][1].body as string)
98
+ expect(body).toEqual({ newName: 'backup', branch: 'main', defaultModules: true })
99
+ })
100
+
101
+ it('deleteBranch sends POST with branch', async () => {
102
+ await http.user.deleteBranch('old-branch')
103
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
104
+ expect(init.method).toBe('POST')
105
+ expect(url).toContain('/api/user/delete-branch')
106
+ expect(JSON.parse(init.body as string)).toEqual({ branch: 'old-branch' })
107
+ })
108
+
109
+ it('notifyPrefs sends POST with partial prefs', async () => {
110
+ await http.user.notifyPrefs({ disabled: true, interval: 60 })
111
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
112
+ expect(init.method).toBe('POST')
113
+ expect(url).toContain('/api/user/notify-prefs')
114
+ expect(JSON.parse(init.body as string)).toEqual({ disabled: true, interval: 60 })
115
+ })
116
+
117
+ it('tutorialDone sends POST', async () => {
118
+ await http.user.tutorialDone()
119
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
120
+ expect(init.method).toBe('POST')
121
+ expect(url).toContain('/api/user/tutorial-done')
122
+ })
123
+
124
+ it('email sends POST with email', async () => {
125
+ await http.user.email('test@example.com')
126
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
127
+ expect(init.method).toBe('POST')
128
+ expect(url).toContain('/api/user/email')
129
+ expect(JSON.parse(init.body as string)).toEqual({ email: 'test@example.com' })
130
+ })
131
+
132
+ it('setSteamVisible sends POST with visible flag', async () => {
133
+ await http.user.setSteamVisible(false)
134
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
135
+ expect(init.method).toBe('POST')
136
+ expect(url).toContain('/api/user/set-steam-visible')
137
+ expect(JSON.parse(init.body as string)).toEqual({ visible: false })
138
+ })
139
+ })
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { parseMessage } from '../../src/socket/MessageParser.js'
3
+
4
+ describe('parseMessage', () => {
5
+ it('parses auth ok with token', async () => {
6
+ expect(await parseMessage('auth ok abc123')).toEqual({
7
+ kind: 'server',
8
+ command: { type: 'auth', status: 'ok', token: 'abc123' },
9
+ })
10
+ })
11
+
12
+ it('parses auth failed', async () => {
13
+ expect(await parseMessage('auth failed')).toEqual({
14
+ kind: 'server',
15
+ command: { type: 'auth', status: 'failed', token: undefined },
16
+ })
17
+ })
18
+
19
+ it('parses time command', async () => {
20
+ expect(await parseMessage('time 99999')).toEqual({
21
+ kind: 'server',
22
+ command: { type: 'time', time: 99999 },
23
+ })
24
+ })
25
+
26
+ it('parses protocol command', async () => {
27
+ expect(await parseMessage('protocol 13')).toEqual({
28
+ kind: 'server',
29
+ command: { type: 'protocol', protocol: 13 },
30
+ })
31
+ })
32
+
33
+ it('parses package command', async () => {
34
+ expect(await parseMessage('package 42')).toEqual({
35
+ kind: 'server',
36
+ command: { type: 'package', package: 42 },
37
+ })
38
+ })
39
+
40
+ it('parses JSON array channel message', async () => {
41
+ const raw = JSON.stringify(['user:uid123/cpu', { cpu: 30, memory: 1024 }])
42
+ expect(await parseMessage(raw)).toEqual({
43
+ kind: 'channel',
44
+ message: { channel: 'user:uid123/cpu', data: { cpu: 30, memory: 1024 } },
45
+ })
46
+ })
47
+
48
+ it('accepts MessageEvent (browser WS format)', async () => {
49
+ const event = { data: 'time 500' } as MessageEvent
50
+ expect(await parseMessage(event)).toEqual({
51
+ kind: 'server',
52
+ command: { type: 'time', time: 500 },
53
+ })
54
+ })
55
+ })