@nadohq/mobile-client 0.64.0 → 0.66.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 (225) hide show
  1. package/README.md +124 -7
  2. package/dist/MobileClient-M4oC4lrX.cjs +1101 -0
  3. package/dist/MobileClient-M4oC4lrX.cjs.map +1 -0
  4. package/dist/MobileClient.cjs +1 -1
  5. package/dist/MobileClient.d.cts +306 -10
  6. package/dist/MobileClient.d.cts.map +1 -1
  7. package/dist/MobileClient.d.ts +306 -10
  8. package/dist/MobileClient.d.ts.map +1 -1
  9. package/dist/MobileClient.js +544 -5
  10. package/dist/MobileClient.js.map +1 -1
  11. package/dist/{MobileServerFailureError-Dd3c6P_Z.d.ts → MobileServerFailureError-C9iqNsxI.d.ts} +2 -3
  12. package/dist/MobileServerFailureError-C9iqNsxI.d.ts.map +1 -0
  13. package/dist/{MobileServerFailureError-Bpa_WA9f.d.cts → MobileServerFailureError-MtS2reel.d.cts} +2 -3
  14. package/dist/MobileServerFailureError-MtS2reel.d.cts.map +1 -0
  15. package/dist/MobileUnsupportedFeedItemError-BsZvW8i2.d.cts +15 -0
  16. package/dist/MobileUnsupportedFeedItemError-BsZvW8i2.d.cts.map +1 -0
  17. package/dist/MobileUnsupportedFeedItemError-BsZvW8i2.d.ts +15 -0
  18. package/dist/MobileUnsupportedFeedItemError-BsZvW8i2.d.ts.map +1 -0
  19. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.cts +15 -0
  20. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.cts.map +1 -0
  21. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.ts +15 -0
  22. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.ts.map +1 -0
  23. package/dist/{buildSignedMobileRequest-Cb1T74xV.d.ts → buildSignedMobileRequest-BR-KJhIF.d.ts} +2 -2
  24. package/dist/{buildSignedMobileRequest-Cb1T74xV.d.ts.map → buildSignedMobileRequest-BR-KJhIF.d.ts.map} +1 -1
  25. package/dist/{buildSignedMobileRequest-NHbMAPzd.d.cts → buildSignedMobileRequest-DyKCK34Y.d.cts} +2 -2
  26. package/dist/{buildSignedMobileRequest-NHbMAPzd.d.cts.map → buildSignedMobileRequest-DyKCK34Y.d.cts.map} +1 -1
  27. package/dist/{canonicalize-BBYGh17w.d.ts → canonicalize-Bv6UIAbR.d.ts} +2 -2
  28. package/dist/canonicalize-Bv6UIAbR.d.ts.map +1 -0
  29. package/dist/{canonicalize-BF_SiNvO.d.cts → canonicalize-CWh3XZzj.d.cts} +2 -2
  30. package/dist/canonicalize-CWh3XZzj.d.cts.map +1 -0
  31. package/dist/clientTypes-BUn5_OzK.d.ts +1665 -0
  32. package/dist/clientTypes-BUn5_OzK.d.ts.map +1 -0
  33. package/dist/clientTypes-DOpUcu80.d.cts +1665 -0
  34. package/dist/clientTypes-DOpUcu80.d.cts.map +1 -0
  35. package/dist/dataMappers.cjs +691 -14
  36. package/dist/dataMappers.cjs.map +1 -1
  37. package/dist/dataMappers.d.cts +254 -8
  38. package/dist/dataMappers.d.cts.map +1 -1
  39. package/dist/dataMappers.d.ts +254 -8
  40. package/dist/dataMappers.d.ts.map +1 -1
  41. package/dist/dataMappers.js +651 -15
  42. package/dist/dataMappers.js.map +1 -1
  43. package/dist/{eip712MethodByType-BSx11xG-.d.cts → eip712MethodByType-CJ6BoayT.d.cts} +2 -2
  44. package/dist/eip712MethodByType-CJ6BoayT.d.cts.map +1 -0
  45. package/dist/{eip712MethodByType-DkiAjCLs.d.ts → eip712MethodByType-Dx4mvgni.d.ts} +2 -2
  46. package/dist/eip712MethodByType-Dx4mvgni.d.ts.map +1 -0
  47. package/dist/{feed-DTJlsg8W.d.ts → feed-DErOLaoY.d.cts} +4 -2
  48. package/dist/feed-DErOLaoY.d.cts.map +1 -0
  49. package/dist/{feed-DTJlsg8W.d.cts → feed-DErOLaoY.d.ts} +4 -2
  50. package/dist/feed-DErOLaoY.d.ts.map +1 -0
  51. package/dist/index.cjs +9 -2
  52. package/dist/index.d.cts +15 -16
  53. package/dist/index.d.ts +15 -16
  54. package/dist/index.js +5 -3
  55. package/dist/{mobileErrorCodes-3LtreeZM.d.ts → mobileErrorCodes-vD6rbFuF.d.cts} +25 -6
  56. package/dist/mobileErrorCodes-vD6rbFuF.d.cts.map +1 -0
  57. package/dist/{mobileErrorCodes-3LtreeZM.d.cts → mobileErrorCodes-vD6rbFuF.d.ts} +25 -6
  58. package/dist/mobileErrorCodes-vD6rbFuF.d.ts.map +1 -0
  59. package/dist/{payloadHash-BOWGjqdb.d.cts → payloadHash-CdkF2Ocr.d.cts} +2 -2
  60. package/dist/payloadHash-CdkF2Ocr.d.cts.map +1 -0
  61. package/dist/{payloadHash-Bzf4CtjV.d.ts → payloadHash-HD29K77M.d.ts} +2 -2
  62. package/dist/payloadHash-HD29K77M.d.ts.map +1 -0
  63. package/dist/serverExecuteTypes-5JyyIXRK.d.ts +178 -0
  64. package/dist/serverExecuteTypes-5JyyIXRK.d.ts.map +1 -0
  65. package/dist/serverExecuteTypes-CHrQ9Rtq.d.cts +178 -0
  66. package/dist/serverExecuteTypes-CHrQ9Rtq.d.cts.map +1 -0
  67. package/dist/serverModelTypes-CGUZCi0y.d.cts +938 -0
  68. package/dist/serverModelTypes-CGUZCi0y.d.cts.map +1 -0
  69. package/dist/serverModelTypes-CGUZCi0y.d.ts +938 -0
  70. package/dist/serverModelTypes-CGUZCi0y.d.ts.map +1 -0
  71. package/dist/{serverResponseGuards-BU1Xv_tG.d.ts → serverResponseGuards-Bvw8ltYJ.d.ts} +2 -2
  72. package/dist/{serverResponseGuards-BU1Xv_tG.d.ts.map → serverResponseGuards-Bvw8ltYJ.d.ts.map} +1 -1
  73. package/dist/{serverResponseGuards-Dkdok0Sm.d.cts → serverResponseGuards-i1njcYKN.d.cts} +2 -2
  74. package/dist/{serverResponseGuards-Dkdok0Sm.d.cts.map → serverResponseGuards-i1njcYKN.d.cts.map} +1 -1
  75. package/dist/signing/buildSignedMobileRequest.d.cts +1 -1
  76. package/dist/signing/buildSignedMobileRequest.d.ts +1 -1
  77. package/dist/signing/canonicalize.cjs +86 -0
  78. package/dist/signing/canonicalize.cjs.map +1 -1
  79. package/dist/signing/canonicalize.d.cts +1 -1
  80. package/dist/signing/canonicalize.d.ts +1 -1
  81. package/dist/signing/canonicalize.js +86 -0
  82. package/dist/signing/canonicalize.js.map +1 -1
  83. package/dist/signing/eip712MethodByType.cjs +12 -0
  84. package/dist/signing/eip712MethodByType.cjs.map +1 -1
  85. package/dist/signing/eip712MethodByType.d.cts +1 -1
  86. package/dist/signing/eip712MethodByType.d.ts +1 -1
  87. package/dist/signing/eip712MethodByType.js +12 -0
  88. package/dist/signing/eip712MethodByType.js.map +1 -1
  89. package/dist/signing/index.d.cts +5 -5
  90. package/dist/signing/index.d.ts +5 -5
  91. package/dist/signing/payloadHash.cjs +1 -1
  92. package/dist/signing/payloadHash.d.cts +1 -1
  93. package/dist/signing/payloadHash.d.ts +1 -1
  94. package/dist/signing/payloadHash.js +1 -1
  95. package/dist/signing/types.d.cts +1 -1
  96. package/dist/signing/types.d.ts +1 -1
  97. package/dist/types/MobileServerFailureError.d.cts +1 -1
  98. package/dist/types/MobileServerFailureError.d.ts +1 -1
  99. package/dist/types/MobileUnsupportedFeedItemError.cjs +19 -0
  100. package/dist/types/MobileUnsupportedFeedItemError.cjs.map +1 -0
  101. package/dist/types/MobileUnsupportedFeedItemError.d.cts +2 -0
  102. package/dist/types/MobileUnsupportedFeedItemError.d.ts +2 -0
  103. package/dist/types/MobileUnsupportedFeedItemError.js +18 -0
  104. package/dist/types/MobileUnsupportedFeedItemError.js.map +1 -0
  105. package/dist/types/MobileUnsupportedNotificationRouteError.cjs +19 -0
  106. package/dist/types/MobileUnsupportedNotificationRouteError.cjs.map +1 -0
  107. package/dist/types/MobileUnsupportedNotificationRouteError.d.cts +2 -0
  108. package/dist/types/MobileUnsupportedNotificationRouteError.d.ts +2 -0
  109. package/dist/types/MobileUnsupportedNotificationRouteError.js +18 -0
  110. package/dist/types/MobileUnsupportedNotificationRouteError.js.map +1 -0
  111. package/dist/types/clientTypes.d.cts +2 -2
  112. package/dist/types/clientTypes.d.ts +2 -2
  113. package/dist/types/index.cjs +6 -0
  114. package/dist/types/index.d.cts +9 -9
  115. package/dist/types/index.d.ts +9 -9
  116. package/dist/types/index.js +4 -2
  117. package/dist/types/mobileErrorCodes.cjs +39 -1
  118. package/dist/types/mobileErrorCodes.cjs.map +1 -1
  119. package/dist/types/mobileErrorCodes.d.cts +1 -1
  120. package/dist/types/mobileErrorCodes.d.ts +1 -1
  121. package/dist/types/mobileErrorCodes.js +39 -1
  122. package/dist/types/mobileErrorCodes.js.map +1 -1
  123. package/dist/types/serverBaseTypes.d.cts +1 -1
  124. package/dist/types/serverBaseTypes.d.ts +1 -1
  125. package/dist/types/serverExecuteTypes.d.cts +2 -2
  126. package/dist/types/serverExecuteTypes.d.ts +2 -2
  127. package/dist/types/serverModelTypes.cjs +15 -0
  128. package/dist/types/serverModelTypes.cjs.map +1 -1
  129. package/dist/types/serverModelTypes.d.cts +2 -2
  130. package/dist/types/serverModelTypes.d.ts +2 -2
  131. package/dist/types/serverModelTypes.js +14 -1
  132. package/dist/types/serverModelTypes.js.map +1 -1
  133. package/dist/types/serverQueryTypes.d.cts +2 -2
  134. package/dist/types/serverQueryTypes.d.ts +2 -2
  135. package/dist/types/serverRequestTypes.d.cts +2 -2
  136. package/dist/types/serverRequestTypes.d.ts +2 -2
  137. package/dist/types-C9YE4EIt.d.cts +794 -0
  138. package/dist/types-C9YE4EIt.d.cts.map +1 -0
  139. package/dist/types-Cwy1y_9x.d.ts +794 -0
  140. package/dist/types-Cwy1y_9x.d.ts.map +1 -0
  141. package/dist/utils/buildMobileFeedImageMultipart.cjs +31 -0
  142. package/dist/utils/buildMobileFeedImageMultipart.cjs.map +1 -0
  143. package/dist/utils/buildMobileFeedImageMultipart.d.cts +15 -0
  144. package/dist/utils/buildMobileFeedImageMultipart.d.cts.map +1 -0
  145. package/dist/utils/buildMobileFeedImageMultipart.d.ts +15 -0
  146. package/dist/utils/buildMobileFeedImageMultipart.d.ts.map +1 -0
  147. package/dist/utils/buildMobileFeedImageMultipart.js +30 -0
  148. package/dist/utils/buildMobileFeedImageMultipart.js.map +1 -0
  149. package/dist/utils/feed.cjs +3 -0
  150. package/dist/utils/feed.cjs.map +1 -1
  151. package/dist/utils/feed.d.cts +2 -2
  152. package/dist/utils/feed.d.ts +2 -2
  153. package/dist/utils/feed.js +3 -1
  154. package/dist/utils/feed.js.map +1 -1
  155. package/dist/utils/index.cjs +2 -1
  156. package/dist/utils/index.d.cts +3 -3
  157. package/dist/utils/index.d.ts +3 -3
  158. package/dist/utils/index.js +2 -2
  159. package/dist/utils/serverResponseGuards.d.cts +1 -1
  160. package/dist/utils/serverResponseGuards.d.ts +1 -1
  161. package/package.json +3 -3
  162. package/src/MobileClient.ts +789 -7
  163. package/src/dataMappers.test.ts +848 -0
  164. package/src/dataMappers.ts +924 -17
  165. package/src/feedImages.test.ts +309 -0
  166. package/src/notifications.test.ts +950 -0
  167. package/src/signing/canonicalize.ts +119 -0
  168. package/src/signing/eip712MethodByType.ts +15 -0
  169. package/src/signing/signing.test.ts +362 -0
  170. package/src/signing/types.ts +108 -13
  171. package/src/socialFeed.test.ts +38 -2
  172. package/src/types/MobileUnsupportedFeedItemError.ts +18 -0
  173. package/src/types/MobileUnsupportedNotificationRouteError.ts +16 -0
  174. package/src/types/clientTypes.ts +1012 -34
  175. package/src/types/index.ts +2 -0
  176. package/src/types/mobileErrorCodes.ts +38 -0
  177. package/src/types/serverExecuteTypes.ts +112 -3
  178. package/src/types/serverModelTypes.ts +581 -28
  179. package/src/types/serverQueryTypes.ts +212 -5
  180. package/src/types/serverRequestTypes.ts +213 -13
  181. package/src/utils/buildMobileFeedImageMultipart.test.ts +45 -0
  182. package/src/utils/buildMobileFeedImageMultipart.ts +36 -0
  183. package/src/utils/feed.ts +3 -0
  184. package/dist/MobileClient-aDxiM_Kf.cjs +0 -562
  185. package/dist/MobileClient-aDxiM_Kf.cjs.map +0 -1
  186. package/dist/MobileServerFailureError-Bpa_WA9f.d.cts.map +0 -1
  187. package/dist/MobileServerFailureError-Dd3c6P_Z.d.ts.map +0 -1
  188. package/dist/canonicalize-BBYGh17w.d.ts.map +0 -1
  189. package/dist/canonicalize-BF_SiNvO.d.cts.map +0 -1
  190. package/dist/clientTypes-BWFKo9oE.d.cts +0 -844
  191. package/dist/clientTypes-BWFKo9oE.d.cts.map +0 -1
  192. package/dist/clientTypes-DCTiUV0H.d.ts +0 -844
  193. package/dist/clientTypes-DCTiUV0H.d.ts.map +0 -1
  194. package/dist/eip712MethodByType-BSx11xG-.d.cts.map +0 -1
  195. package/dist/eip712MethodByType-DkiAjCLs.d.ts.map +0 -1
  196. package/dist/feed-DTJlsg8W.d.cts.map +0 -1
  197. package/dist/feed-DTJlsg8W.d.ts.map +0 -1
  198. package/dist/mobileErrorCodes-3LtreeZM.d.cts.map +0 -1
  199. package/dist/mobileErrorCodes-3LtreeZM.d.ts.map +0 -1
  200. package/dist/payloadHash-BOWGjqdb.d.cts.map +0 -1
  201. package/dist/payloadHash-Bzf4CtjV.d.ts.map +0 -1
  202. package/dist/serverBaseTypes-DWf3iGWy.d.ts +0 -25
  203. package/dist/serverBaseTypes-DWf3iGWy.d.ts.map +0 -1
  204. package/dist/serverBaseTypes-KREDe2M9.d.cts +0 -25
  205. package/dist/serverBaseTypes-KREDe2M9.d.cts.map +0 -1
  206. package/dist/serverExecuteTypes-DVRa6ifu.d.cts +0 -89
  207. package/dist/serverExecuteTypes-DVRa6ifu.d.cts.map +0 -1
  208. package/dist/serverExecuteTypes-DlYZCp1f.d.ts +0 -89
  209. package/dist/serverExecuteTypes-DlYZCp1f.d.ts.map +0 -1
  210. package/dist/serverModelTypes-DAqEyYtB.d.cts +0 -508
  211. package/dist/serverModelTypes-DAqEyYtB.d.cts.map +0 -1
  212. package/dist/serverModelTypes-DAqEyYtB.d.ts +0 -508
  213. package/dist/serverModelTypes-DAqEyYtB.d.ts.map +0 -1
  214. package/dist/serverQueryTypes-BFdwq0Z2.d.ts +0 -171
  215. package/dist/serverQueryTypes-BFdwq0Z2.d.ts.map +0 -1
  216. package/dist/serverQueryTypes-DNdMpmDr.d.cts +0 -171
  217. package/dist/serverQueryTypes-DNdMpmDr.d.cts.map +0 -1
  218. package/dist/serverRequestTypes-BNA7FCY6.d.ts +0 -200
  219. package/dist/serverRequestTypes-BNA7FCY6.d.ts.map +0 -1
  220. package/dist/serverRequestTypes-CxDRFKfv.d.cts +0 -200
  221. package/dist/serverRequestTypes-CxDRFKfv.d.cts.map +0 -1
  222. package/dist/types-BCr2G5gS.d.cts +0 -84
  223. package/dist/types-BCr2G5gS.d.cts.map +0 -1
  224. package/dist/types-C18k-9Vy.d.ts +0 -84
  225. package/dist/types-C18k-9Vy.d.ts.map +0 -1
@@ -0,0 +1,309 @@
1
+ import { afterEach, describe, expect, it, jest } from '@jest/globals';
2
+ import { createWalletClient, http, keccak256, stringToBytes } from 'viem';
3
+ import { privateKeyToAccount } from 'viem/accounts';
4
+ import { ink } from 'viem/chains';
5
+ import { MobileClient } from './MobileClient';
6
+ import {
7
+ MOBILE_ERROR_CODES,
8
+ MobileServerFailureError,
9
+ MobileServerFailureResponse,
10
+ MobileServerSocialFeedImage,
11
+ } from './types';
12
+ import { MOBILE_FEED_IMAGE_MAX_REQUEST_BYTES } from './utils/feed';
13
+
14
+ const imagePath = '/v1/mobile/feed/images/test/standard';
15
+ const session = { sessionId: 'Session', viewerToken: 'Viewer' };
16
+ const imageBytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0, 127, 128, 255]);
17
+
18
+ function mockImageClient(data: ArrayBuffer | Uint8Array, status = 200) {
19
+ const client = new MobileClient({ url: 'https://mobile.example/mobile/v1' });
20
+ client.axiosInstance.defaults.adapter = (config) => {
21
+ expect(config.url).toBe(
22
+ 'https://mobile.example/mobile/v1/feed/images/test/standard',
23
+ );
24
+ expect(config.method).toBe('get');
25
+ expect(config.responseType).toBe('arraybuffer');
26
+ expect(config.headers.get('x-feed-session-id')).toBe(session.sessionId);
27
+ expect(config.headers.get('x-feed-viewer-token')).toBe(session.viewerToken);
28
+ return Promise.resolve({
29
+ data,
30
+ status,
31
+ statusText: 'mock',
32
+ headers: {},
33
+ config,
34
+ });
35
+ };
36
+ return client;
37
+ }
38
+
39
+ describe('[mobile-client]: feed image reads', () => {
40
+ afterEach(() => {
41
+ jest.restoreAllMocks();
42
+ });
43
+
44
+ it.each([
45
+ '/v1/mobile/feed/images/test/standard',
46
+ '/feed/images/test/standard',
47
+ '/v1/mobile/feed/comment-images/test/thumbnail',
48
+ ])('exposes a native image request without fetching bytes: %s', (path) => {
49
+ const client = new MobileClient({
50
+ url: 'https://mobile.example/mobile/v1',
51
+ });
52
+ const get = jest.spyOn(client.axiosInstance, 'get');
53
+ expect(client.getFeedImageRequest({ ...session, path })).toEqual({
54
+ url: client.resolveFeedImageUrl(path),
55
+ headers: {
56
+ 'x-feed-session-id': session.sessionId,
57
+ 'x-feed-viewer-token': session.viewerToken,
58
+ },
59
+ });
60
+ expect(get).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it.each(['uint8array', 'arraybuffer'] as const)(
64
+ 'preserves successful image bytes from %s responses',
65
+ async (format) => {
66
+ const data =
67
+ format === 'uint8array' ? imageBytes : imageBytes.slice().buffer;
68
+ const client = mockImageClient(data);
69
+ const getRequest = jest.spyOn(client, 'getFeedImageRequest');
70
+ await expect(
71
+ client.getFeedImageBytes({ ...session, path: imagePath }),
72
+ ).resolves.toEqual(imageBytes);
73
+ expect(getRequest).toHaveBeenCalledWith({ ...session, path: imagePath });
74
+ },
75
+ );
76
+
77
+ describe.each(['uint8array', 'arraybuffer'] as const)(
78
+ '%s error responses',
79
+ (format) => {
80
+ it.each([
81
+ [401, MOBILE_ERROR_CODES.SOCIAL_FEED_SESSION_EXPIRED, 'feed_image'],
82
+ [403, MOBILE_ERROR_CODES.SOCIAL_FEED_SESSION_MISMATCH, 'feed_image'],
83
+ [404, MOBILE_ERROR_CODES.SOCIAL_FEED_CONTENT_UNAVAILABLE, 'feed_image'],
84
+ [
85
+ 404,
86
+ MOBILE_ERROR_CODES.SOCIAL_FEED_CONTENT_UNAVAILABLE,
87
+ 'feed_comment_image',
88
+ ],
89
+ ] as const)(
90
+ 'preserves the failure envelope for HTTP %i',
91
+ async (status, errorCode, requestType) => {
92
+ const failure: MobileServerFailureResponse = {
93
+ status: 'failure',
94
+ error: 'Image unavailable: \u56fe\u7247\u4e0d\u53ef\u7528',
95
+ error_code: errorCode,
96
+ request_type: requestType,
97
+ };
98
+ const encoded = stringToBytes(JSON.stringify(failure));
99
+ const data =
100
+ format === 'uint8array' ? encoded : Uint8Array.from(encoded).buffer;
101
+ const client = mockImageClient(data, status);
102
+ const result = client.getFeedImageBytes({
103
+ ...session,
104
+ path: imagePath,
105
+ });
106
+ await expect(result).rejects.toBeInstanceOf(MobileServerFailureError);
107
+ await expect(result).rejects.toMatchObject({
108
+ errorCode,
109
+ requestType,
110
+ httpStatus: status,
111
+ responseData: failure,
112
+ });
113
+ },
114
+ );
115
+ },
116
+ );
117
+
118
+ it.each([
119
+ '',
120
+ '<html>Bad gateway</html>',
121
+ '{invalid',
122
+ '{"message":"unavailable"}',
123
+ ])('keeps non-envelope failures as transport errors: %s', async (body) => {
124
+ const client = mockImageClient(stringToBytes(body), 502);
125
+ const result = client.getFeedImageBytes({ ...session, path: imagePath });
126
+ await expect(result).rejects.not.toBeInstanceOf(MobileServerFailureError);
127
+ await expect(result).rejects.toThrow(
128
+ 'Unexpected response from mobile service: 502',
129
+ );
130
+ });
131
+
132
+ it.each([
133
+ '/v1/mobile/feed/images/test/standard',
134
+ '/feed/images/test/standard',
135
+ ])('resolves the metadata path %s', (path) => {
136
+ const client = new MobileClient({
137
+ url: 'https://mobile.example/mobile/v1',
138
+ });
139
+ expect(client.resolveFeedImageUrl(path)).toBe(
140
+ 'https://mobile.example/mobile/v1/feed/images/test/standard',
141
+ );
142
+ });
143
+ });
144
+
145
+ describe('[mobile-client]: feed image uploads', () => {
146
+ it.each([
147
+ ['createFeedPostImageUpload', 'create_feed_post_image_uploads'],
148
+ ['createFeedCommentImageUpload', 'create_feed_comment_image_uploads'],
149
+ ] as const)('sends signed multipart bytes for %s', async (method, type) => {
150
+ const account = privateKeyToAccount(
151
+ '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b786907',
152
+ );
153
+ const client = new MobileClient({
154
+ url: 'https://mobile.example/mobile/v1',
155
+ walletClient: createWalletClient({
156
+ account,
157
+ chain: ink,
158
+ transport: http(),
159
+ }),
160
+ });
161
+ const image: MobileServerSocialFeedImage = {
162
+ image_id: '88888888-8888-4888-8888-888888888888',
163
+ url: imagePath,
164
+ width: 1,
165
+ height: 1,
166
+ content_type: 'image/jpeg',
167
+ byte_size: imageBytes.length,
168
+ thumbnail: {
169
+ url: '/v1/mobile/feed/images/test/thumbnail',
170
+ width: 1,
171
+ height: 1,
172
+ },
173
+ };
174
+ client.axiosInstance.defaults.adapter = (config) => {
175
+ expect(config.url).toBe('https://mobile.example/mobile/v1/execute');
176
+ expect(config.method).toBe('post');
177
+ expect(config.data).toBeInstanceOf(ArrayBuffer);
178
+ const body = new Uint8Array(config.data as ArrayBuffer);
179
+ const text = String.fromCharCode(...body);
180
+ const contentType = config.headers.get('Content-Type');
181
+ if (typeof contentType !== 'string')
182
+ throw new Error('Expected multipart Content-Type');
183
+ expect(contentType).toMatch(/^multipart\/form-data; boundary=/);
184
+ const boundary = contentType.split('boundary=')[1];
185
+ const payloadStart = text.indexOf('\r\n\r\n') + 4;
186
+ const payloadEnd = text.indexOf(`\r\n--${boundary}`, payloadStart);
187
+ const payload = JSON.parse(
188
+ text.slice(payloadStart, payloadEnd),
189
+ ) as Record<string, unknown>;
190
+ expect(payload).toMatchObject({
191
+ type,
192
+ content_hash: keccak256(imageBytes),
193
+ });
194
+ expect(payload.signature).toEqual(expect.stringMatching(/^0x[\da-f]+$/));
195
+ const fileHeader = text.indexOf('name="file"; filename="image"');
196
+ expect(fileHeader).toBeGreaterThan(payloadEnd);
197
+ const fileStart = text.indexOf('\r\n\r\n', fileHeader) + 4;
198
+ expect(
199
+ new Uint8Array(body.subarray(fileStart, fileStart + imageBytes.length)),
200
+ ).toEqual(imageBytes);
201
+ expect(text.slice(fileStart + imageBytes.length)).toBe(
202
+ `\r\n--${boundary}--\r\n`,
203
+ );
204
+ return Promise.resolve({
205
+ data: { status: 'success', image },
206
+ status: 200,
207
+ statusText: 'OK',
208
+ headers: {},
209
+ config,
210
+ });
211
+ };
212
+ await expect(
213
+ client[method]({
214
+ subaccountOwner: account.address,
215
+ subaccountName: 'default',
216
+ chainId: ink.id,
217
+ verifyingAddr: '0x0000000000000000000000000000000000000001',
218
+ fileBytes: imageBytes,
219
+ }),
220
+ ).resolves.toEqual({
221
+ imageId: image.image_id,
222
+ url: imagePath,
223
+ width: 1,
224
+ height: 1,
225
+ contentType: 'image/jpeg',
226
+ byteSize: imageBytes.length,
227
+ thumbnail: image.thumbnail,
228
+ });
229
+ });
230
+ });
231
+
232
+ describe.each([
233
+ 'createFeedPostImageUpload',
234
+ 'createFeedCommentImageUpload',
235
+ ] as const)('[mobile-client]: %s request size', (method) => {
236
+ afterEach(() => {
237
+ jest.restoreAllMocks();
238
+ });
239
+
240
+ it.each(
241
+ [-1, 0, 1].flatMap((offset) =>
242
+ [1n, undefined].map((nonce) => ({ offset, nonce })),
243
+ ),
244
+ )(
245
+ 'checks the complete body at offset $offset with nonce $nonce',
246
+ async ({ offset, nonce }) => {
247
+ const account = privateKeyToAccount(
248
+ '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b786907',
249
+ );
250
+ const walletClient = createWalletClient({
251
+ account,
252
+ chain: ink,
253
+ transport: http(),
254
+ });
255
+ const sign = jest.spyOn(walletClient, 'signTypedData');
256
+ const client = new MobileClient({
257
+ url: 'https://mobile.example/mobile/v1',
258
+ walletClient,
259
+ });
260
+ const params = {
261
+ subaccountOwner: account.address,
262
+ subaccountName: 'default',
263
+ chainId: ink.id,
264
+ verifyingAddr: '0x0000000000000000000000000000000000000001',
265
+ nonce,
266
+ } as const;
267
+ const post = jest.spyOn(client.axiosInstance, 'post').mockResolvedValue({
268
+ data: {
269
+ status: 'success',
270
+ image: {
271
+ image_id: 'test',
272
+ url: imagePath,
273
+ width: 1,
274
+ height: 1,
275
+ content_type: 'image/jpeg',
276
+ byte_size: 0,
277
+ thumbnail: { url: imagePath, width: 1, height: 1 },
278
+ },
279
+ },
280
+ status: 200,
281
+ });
282
+ await client[method]({ ...params, fileBytes: new Uint8Array() });
283
+ const emptyBody: unknown = post.mock.calls[0][1];
284
+ if (!(emptyBody instanceof Uint8Array))
285
+ throw new Error('Expected multipart bytes');
286
+ post.mockClear();
287
+ sign.mockClear();
288
+ const fileBytes = new Uint8Array(
289
+ MOBILE_FEED_IMAGE_MAX_REQUEST_BYTES - emptyBody.byteLength + offset,
290
+ );
291
+ const result = client[method]({ ...params, fileBytes });
292
+ if (offset > 0) {
293
+ await expect(result).rejects.toBeInstanceOf(RangeError);
294
+ await expect(result).rejects.toThrow('2097152 bytes (2 MiB)');
295
+ expect(post).not.toHaveBeenCalled();
296
+ expect(sign).not.toHaveBeenCalled();
297
+ } else {
298
+ await expect(result).resolves.toMatchObject({ imageId: 'test' });
299
+ expect(sign).toHaveBeenCalledTimes(1);
300
+ expect(post).toHaveBeenCalledTimes(1);
301
+ const body: unknown = post.mock.calls[0][1];
302
+ expect(body).toHaveProperty(
303
+ 'byteLength',
304
+ MOBILE_FEED_IMAGE_MAX_REQUEST_BYTES + offset,
305
+ );
306
+ }
307
+ },
308
+ );
309
+ });