Skip to content

Commit 75ed1ca

Browse files
gdauber1claude
andcommitted
Fix mobile module based on Claude Code review feedback
Addresses all issues identified in the code review: 1. Type Safety: Changed metadata type from `any` to `Record<string, unknown>` for better type safety in mobile.types.ts 2. Input Validation: Added validateNotificationParams() function to enforce documented character limits: - Title: max 100 characters - Content: max 500 characters - Action label: max 50 characters 3. Service Role Support: Added mobile module to serviceRoleModules in client.ts to enable `base44.asServiceRole.mobile` for server-side bulk notifications 4. Test Coverage: Created comprehensive unit tests (mobile.test.ts) with 15 test cases covering: - Basic notification sending - Parameter validation (all character limits) - Error handling (API errors, 404s, partial failures) - Channel selection - Metadata and HTML content handling All tests pass (127/127) and build succeeds without errors. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent cd6c3ea commit 75ed1ca

4 files changed

Lines changed: 300 additions & 1 deletion

File tree

src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export function createClient(config: CreateClientConfig): Base44Client {
190190
token,
191191
}),
192192
appLogs: createAppLogsModule(serviceRoleAxiosClient, appId),
193+
mobile: createMobileModule(serviceRoleAxiosClient, appId),
193194
cleanup: () => {
194195
if (socket) {
195196
socket.disconnect();

src/modules/mobile.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,31 @@ import {
1111
SendNotificationParams,
1212
} from "./mobile.types";
1313

14+
/**
15+
* Validates notification parameters against character limits.
16+
* @param params - Notification parameters to validate
17+
* @throws Error if any parameter exceeds its limit
18+
*/
19+
function validateNotificationParams(params: SendNotificationParams): void {
20+
if (params.title.length > 100) {
21+
throw new Error(
22+
`Title must be 100 characters or less (current: ${params.title.length})`
23+
);
24+
}
25+
26+
if (params.content.length > 500) {
27+
throw new Error(
28+
`Content must be 500 characters or less (current: ${params.content.length})`
29+
);
30+
}
31+
32+
if (params.actionLabel && params.actionLabel.length > 50) {
33+
throw new Error(
34+
`Action label must be 50 characters or less (current: ${params.actionLabel.length})`
35+
);
36+
}
37+
}
38+
1439
/**
1540
* Creates the mobile module for the Base44 SDK.
1641
*
@@ -27,6 +52,9 @@ export function createMobileModule(
2752
async sendNotification(
2853
params: SendNotificationParams
2954
): Promise<NotificationResult> {
55+
// Validate input parameters
56+
validateNotificationParams(params);
57+
3058
const response = await axios.post<NotificationResult>(
3159
`/api/apps/${appId}/mobile/notifications`,
3260
params

src/modules/mobile.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export interface SendNotificationParams {
2929
/** Optional list of channels. If not specified, uses all channels (mobile_push + in_app) */
3030
channels?: NotificationChannel[];
3131
/** Optional custom metadata */
32-
metadata?: Record<string, any>;
32+
metadata?: Record<string, unknown>;
3333
}
3434

3535
/**

tests/unit/mobile.test.ts

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { describe, test, expect, beforeEach, vi } from "vitest";
2+
import { createMobileModule } from "../../src/modules/mobile";
3+
import { MobileModule, NotificationResult } from "../../src/modules/mobile.types";
4+
import { AxiosInstance } from "axios";
5+
6+
describe("Mobile Module", () => {
7+
let mobileModule: MobileModule;
8+
let mockAxios: AxiosInstance;
9+
const appId = "test-app-id";
10+
11+
beforeEach(() => {
12+
// Create mock axios instance
13+
mockAxios = {
14+
post: vi.fn().mockResolvedValue({
15+
data: {
16+
success: true,
17+
notificationId: "notif_123",
18+
channels: {
19+
mobile_push: { success: true },
20+
in_app: { success: true },
21+
},
22+
},
23+
}),
24+
} as unknown as AxiosInstance;
25+
26+
mobileModule = createMobileModule(mockAxios, appId);
27+
});
28+
29+
describe("sendNotification", () => {
30+
test("should send notification with required params", async () => {
31+
const result = await mobileModule.sendNotification({
32+
userId: "user_123",
33+
title: "Test Notification",
34+
content: "This is a test message",
35+
});
36+
37+
expect(mockAxios.post).toHaveBeenCalledWith(
38+
`/api/apps/${appId}/mobile/notifications`,
39+
{
40+
userId: "user_123",
41+
title: "Test Notification",
42+
content: "This is a test message",
43+
}
44+
);
45+
expect(result.success).toBe(true);
46+
expect(result.notificationId).toBe("notif_123");
47+
});
48+
49+
test("should send notification with all optional params", async () => {
50+
const result = await mobileModule.sendNotification({
51+
userId: "user_123",
52+
title: "Test Notification",
53+
content: "This is a test message",
54+
actionLabel: "View",
55+
actionUrl: "/messages/456",
56+
channels: ["mobile_push"],
57+
metadata: { orderId: "order_789" },
58+
});
59+
60+
expect(mockAxios.post).toHaveBeenCalledWith(
61+
`/api/apps/${appId}/mobile/notifications`,
62+
{
63+
userId: "user_123",
64+
title: "Test Notification",
65+
content: "This is a test message",
66+
actionLabel: "View",
67+
actionUrl: "/messages/456",
68+
channels: ["mobile_push"],
69+
metadata: { orderId: "order_789" },
70+
}
71+
);
72+
expect(result.success).toBe(true);
73+
});
74+
75+
test("should handle specific channel selection", async () => {
76+
await mobileModule.sendNotification({
77+
userId: "user_123",
78+
title: "Test",
79+
content: "Content",
80+
channels: ["in_app"],
81+
});
82+
83+
expect(mockAxios.post).toHaveBeenCalledWith(
84+
`/api/apps/${appId}/mobile/notifications`,
85+
expect.objectContaining({
86+
channels: ["in_app"],
87+
})
88+
);
89+
});
90+
91+
test("should throw error when title exceeds 100 characters", async () => {
92+
const longTitle = "a".repeat(101);
93+
94+
await expect(
95+
mobileModule.sendNotification({
96+
userId: "user_123",
97+
title: longTitle,
98+
content: "Content",
99+
})
100+
).rejects.toThrow("Title must be 100 characters or less");
101+
});
102+
103+
test("should allow title with exactly 100 characters", async () => {
104+
const maxTitle = "a".repeat(100);
105+
106+
await mobileModule.sendNotification({
107+
userId: "user_123",
108+
title: maxTitle,
109+
content: "Content",
110+
});
111+
112+
expect(mockAxios.post).toHaveBeenCalled();
113+
});
114+
115+
test("should throw error when content exceeds 500 characters", async () => {
116+
const longContent = "a".repeat(501);
117+
118+
await expect(
119+
mobileModule.sendNotification({
120+
userId: "user_123",
121+
title: "Title",
122+
content: longContent,
123+
})
124+
).rejects.toThrow("Content must be 500 characters or less");
125+
});
126+
127+
test("should allow content with exactly 500 characters", async () => {
128+
const maxContent = "a".repeat(500);
129+
130+
await mobileModule.sendNotification({
131+
userId: "user_123",
132+
title: "Title",
133+
content: maxContent,
134+
});
135+
136+
expect(mockAxios.post).toHaveBeenCalled();
137+
});
138+
139+
test("should throw error when actionLabel exceeds 50 characters", async () => {
140+
const longActionLabel = "a".repeat(51);
141+
142+
await expect(
143+
mobileModule.sendNotification({
144+
userId: "user_123",
145+
title: "Title",
146+
content: "Content",
147+
actionLabel: longActionLabel,
148+
})
149+
).rejects.toThrow("Action label must be 50 characters or less");
150+
});
151+
152+
test("should allow actionLabel with exactly 50 characters", async () => {
153+
const maxActionLabel = "a".repeat(50);
154+
155+
await mobileModule.sendNotification({
156+
userId: "user_123",
157+
title: "Title",
158+
content: "Content",
159+
actionLabel: maxActionLabel,
160+
});
161+
162+
expect(mockAxios.post).toHaveBeenCalled();
163+
});
164+
165+
test("should handle API error responses", async () => {
166+
mockAxios.post = vi.fn().mockRejectedValue(new Error("API Error"));
167+
168+
await expect(
169+
mobileModule.sendNotification({
170+
userId: "user_123",
171+
title: "Title",
172+
content: "Content",
173+
})
174+
).rejects.toThrow("API Error");
175+
});
176+
177+
test("should handle 404 user not found", async () => {
178+
mockAxios.post = vi.fn().mockRejectedValue({
179+
response: { status: 404 },
180+
message: "User not found",
181+
});
182+
183+
await expect(
184+
mobileModule.sendNotification({
185+
userId: "nonexistent_user",
186+
title: "Title",
187+
content: "Content",
188+
})
189+
).rejects.toMatchObject({
190+
response: { status: 404 },
191+
});
192+
});
193+
194+
test("should handle partial channel failures", async () => {
195+
mockAxios.post = vi.fn().mockResolvedValue({
196+
data: {
197+
success: true,
198+
channels: {
199+
mobile_push: { success: false, error: "Push token not found" },
200+
in_app: { success: true },
201+
},
202+
},
203+
});
204+
205+
const result = await mobileModule.sendNotification({
206+
userId: "user_123",
207+
title: "Title",
208+
content: "Content",
209+
});
210+
211+
expect(result.success).toBe(true);
212+
expect(result.channels.mobile_push?.success).toBe(false);
213+
expect(result.channels.in_app?.success).toBe(true);
214+
});
215+
216+
test("should pass metadata correctly", async () => {
217+
const metadata = {
218+
orderId: "order_123",
219+
priority: "high",
220+
customField: 42,
221+
};
222+
223+
await mobileModule.sendNotification({
224+
userId: "user_123",
225+
title: "Order Update",
226+
content: "Your order has shipped",
227+
metadata,
228+
});
229+
230+
expect(mockAxios.post).toHaveBeenCalledWith(
231+
`/api/apps/${appId}/mobile/notifications`,
232+
expect.objectContaining({
233+
metadata,
234+
})
235+
);
236+
});
237+
238+
test("should handle HTML content", async () => {
239+
const htmlContent = "<strong>Bold</strong> and <em>italic</em> text";
240+
241+
await mobileModule.sendNotification({
242+
userId: "user_123",
243+
title: "Formatted Message",
244+
content: htmlContent,
245+
});
246+
247+
expect(mockAxios.post).toHaveBeenCalledWith(
248+
`/api/apps/${appId}/mobile/notifications`,
249+
expect.objectContaining({
250+
content: htmlContent,
251+
})
252+
);
253+
});
254+
255+
test("should validate before making API call", async () => {
256+
const longTitle = "a".repeat(101);
257+
258+
await expect(
259+
mobileModule.sendNotification({
260+
userId: "user_123",
261+
title: longTitle,
262+
content: "Content",
263+
})
264+
).rejects.toThrow();
265+
266+
// API should not be called if validation fails
267+
expect(mockAxios.post).not.toHaveBeenCalled();
268+
});
269+
});
270+
});

0 commit comments

Comments
 (0)