Skip to content

Commit 2590773

Browse files
Copilothotlong
andcommitted
Add test files for Phase 12 (Security) and Phase 13 (Advanced) hooks
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 54a9534 commit 2590773

File tree

6 files changed

+719
-0
lines changed

6 files changed

+719
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* Tests for useAuditLog – validates audit log fetching
3+
* with optional pagination options.
4+
*/
5+
import { renderHook, act } from "@testing-library/react-native";
6+
7+
/* ---- Mock useClient from SDK ---- */
8+
const mockList = jest.fn();
9+
10+
const mockClient = {
11+
system: {
12+
audit: { list: mockList },
13+
},
14+
};
15+
16+
jest.mock("@objectstack/client-react", () => ({
17+
useClient: () => mockClient,
18+
}));
19+
20+
import { useAuditLog } from "~/hooks/useAuditLog";
21+
22+
beforeEach(() => {
23+
mockList.mockReset();
24+
});
25+
26+
describe("useAuditLog", () => {
27+
it("getAuditLog fetches and stores entries", async () => {
28+
const entries = [
29+
{
30+
id: "audit-1",
31+
action: "create",
32+
object: "tasks",
33+
recordId: "task-1",
34+
userId: "user-1",
35+
timestamp: "2026-01-01T00:00:00Z",
36+
changes: [{ field: "status", oldValue: null, newValue: "open" }],
37+
},
38+
{
39+
id: "audit-2",
40+
action: "update",
41+
object: "tasks",
42+
recordId: "task-1",
43+
userId: "user-1",
44+
timestamp: "2026-01-02T00:00:00Z",
45+
changes: [{ field: "status", oldValue: "open", newValue: "closed" }],
46+
},
47+
];
48+
mockList.mockResolvedValue(entries);
49+
50+
const { result } = renderHook(() => useAuditLog());
51+
52+
let returned: unknown;
53+
await act(async () => {
54+
returned = await result.current.getAuditLog("tasks", "task-1");
55+
});
56+
57+
expect(mockList).toHaveBeenCalledWith({ object: "tasks", recordId: "task-1" });
58+
expect(returned).toEqual(entries);
59+
expect(result.current.entries).toEqual(entries);
60+
expect(result.current.isLoading).toBe(false);
61+
expect(result.current.error).toBeNull();
62+
});
63+
64+
it("getAuditLog with options (limit, offset)", async () => {
65+
const entries = [
66+
{
67+
id: "audit-3",
68+
action: "delete",
69+
object: "tasks",
70+
recordId: "task-2",
71+
userId: "user-2",
72+
timestamp: "2026-01-03T00:00:00Z",
73+
},
74+
];
75+
mockList.mockResolvedValue(entries);
76+
77+
const { result } = renderHook(() => useAuditLog());
78+
79+
let returned: unknown;
80+
await act(async () => {
81+
returned = await result.current.getAuditLog("tasks", "task-2", {
82+
limit: 10,
83+
offset: 5,
84+
});
85+
});
86+
87+
expect(mockList).toHaveBeenCalledWith({
88+
object: "tasks",
89+
recordId: "task-2",
90+
limit: 10,
91+
offset: 5,
92+
});
93+
expect(returned).toEqual(entries);
94+
expect(result.current.entries).toEqual(entries);
95+
expect(result.current.isLoading).toBe(false);
96+
expect(result.current.error).toBeNull();
97+
});
98+
99+
it("handles getAuditLog error", async () => {
100+
mockList.mockRejectedValue(new Error("Access denied"));
101+
102+
const { result } = renderHook(() => useAuditLog());
103+
104+
await act(async () => {
105+
await expect(result.current.getAuditLog("tasks", "task-1")).rejects.toThrow("Access denied");
106+
});
107+
108+
expect(result.current.entries).toEqual([]);
109+
expect(result.current.isLoading).toBe(false);
110+
expect(result.current.error?.message).toBe("Access denied");
111+
});
112+
});
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Tests for useCollaboration – validates joining, leaving,
3+
* and cursor updates in a collaboration session.
4+
*/
5+
import { renderHook, act } from "@testing-library/react-native";
6+
7+
/* ---- Mock useClient from SDK ---- */
8+
const mockJoin = jest.fn();
9+
const mockLeave = jest.fn();
10+
const mockGetParticipants = jest.fn();
11+
const mockUpdateCursor = jest.fn();
12+
13+
const mockClient = {
14+
realtime: {
15+
collaboration: {
16+
join: mockJoin,
17+
leave: mockLeave,
18+
getParticipants: mockGetParticipants,
19+
updateCursor: mockUpdateCursor,
20+
},
21+
},
22+
};
23+
24+
jest.mock("@objectstack/client-react", () => ({
25+
useClient: () => mockClient,
26+
}));
27+
28+
import { useCollaboration } from "~/hooks/useCollaboration";
29+
30+
beforeEach(() => {
31+
mockJoin.mockReset();
32+
mockLeave.mockReset();
33+
mockGetParticipants.mockReset();
34+
mockUpdateCursor.mockReset();
35+
});
36+
37+
describe("useCollaboration", () => {
38+
it("join connects and fetches participants", async () => {
39+
const participants = [
40+
{ userId: "user-1", status: "active", color: "#ff0000", lastSeen: "2026-01-01T00:00:00Z" },
41+
{ userId: "user-2", status: "idle", color: "#00ff00", lastSeen: "2026-01-01T00:00:00Z" },
42+
];
43+
mockJoin.mockResolvedValue(undefined);
44+
mockGetParticipants.mockResolvedValue(participants);
45+
46+
const { result } = renderHook(() => useCollaboration());
47+
48+
await act(async () => {
49+
await result.current.join("tasks", "task-123");
50+
});
51+
52+
expect(mockJoin).toHaveBeenCalledWith("tasks", "task-123");
53+
expect(mockGetParticipants).toHaveBeenCalled();
54+
expect(result.current.participants).toEqual(participants);
55+
expect(result.current.isConnected).toBe(true);
56+
expect(result.current.isLoading).toBe(false);
57+
expect(result.current.error).toBeNull();
58+
});
59+
60+
it("leave disconnects and clears participants", async () => {
61+
mockJoin.mockResolvedValue(undefined);
62+
mockGetParticipants.mockResolvedValue([
63+
{ userId: "user-1", status: "active", color: "#ff0000", lastSeen: "2026-01-01T00:00:00Z" },
64+
]);
65+
mockLeave.mockResolvedValue(undefined);
66+
67+
const { result } = renderHook(() => useCollaboration());
68+
69+
// First join
70+
await act(async () => {
71+
await result.current.join("tasks", "task-123");
72+
});
73+
expect(result.current.isConnected).toBe(true);
74+
75+
// Then leave
76+
await act(async () => {
77+
await result.current.leave();
78+
});
79+
80+
expect(mockLeave).toHaveBeenCalled();
81+
expect(result.current.participants).toEqual([]);
82+
expect(result.current.isConnected).toBe(false);
83+
expect(result.current.isLoading).toBe(false);
84+
expect(result.current.error).toBeNull();
85+
});
86+
87+
it("updateCursor sends cursor data", async () => {
88+
mockUpdateCursor.mockResolvedValue(undefined);
89+
90+
const { result } = renderHook(() => useCollaboration());
91+
92+
const cursor = { x: 100, y: 200, field: "name" };
93+
await act(async () => {
94+
await result.current.updateCursor(cursor);
95+
});
96+
97+
expect(mockUpdateCursor).toHaveBeenCalledWith(cursor);
98+
expect(result.current.error).toBeNull();
99+
});
100+
101+
it("handles join error", async () => {
102+
mockJoin.mockRejectedValue(new Error("Connection refused"));
103+
104+
const { result } = renderHook(() => useCollaboration());
105+
106+
await act(async () => {
107+
await expect(result.current.join("tasks", "task-123")).rejects.toThrow("Connection refused");
108+
});
109+
110+
expect(result.current.isConnected).toBe(false);
111+
expect(result.current.isLoading).toBe(false);
112+
expect(result.current.error?.message).toBe("Connection refused");
113+
});
114+
115+
it("handles leave error", async () => {
116+
mockLeave.mockRejectedValue(new Error("Disconnect failed"));
117+
118+
const { result } = renderHook(() => useCollaboration());
119+
120+
await act(async () => {
121+
await expect(result.current.leave()).rejects.toThrow("Disconnect failed");
122+
});
123+
124+
expect(result.current.isLoading).toBe(false);
125+
expect(result.current.error?.message).toBe("Disconnect failed");
126+
});
127+
});

__tests__/hooks/useRLS.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* Tests for useRLS – validates RLS policy fetching
3+
* and access evaluation.
4+
*/
5+
import { renderHook, act } from "@testing-library/react-native";
6+
7+
/* ---- Mock useClient from SDK ---- */
8+
const mockList = jest.fn();
9+
const mockEvaluate = jest.fn();
10+
11+
const mockClient = {
12+
security: {
13+
rls: { list: mockList, evaluate: mockEvaluate },
14+
},
15+
};
16+
17+
jest.mock("@objectstack/client-react", () => ({
18+
useClient: () => mockClient,
19+
}));
20+
21+
import { useRLS } from "~/hooks/useRLS";
22+
23+
beforeEach(() => {
24+
mockList.mockReset();
25+
mockEvaluate.mockReset();
26+
});
27+
28+
describe("useRLS", () => {
29+
it("getPolicies fetches and stores policies", async () => {
30+
const policies = [
31+
{
32+
id: "pol-1",
33+
object: "Account",
34+
name: "Account Select",
35+
operation: "select",
36+
condition: "owner_id = current_user()",
37+
enabled: true,
38+
},
39+
{
40+
id: "pol-2",
41+
object: "Account",
42+
name: "Account Update",
43+
operation: "update",
44+
condition: "owner_id = current_user()",
45+
enabled: false,
46+
},
47+
];
48+
mockList.mockResolvedValue(policies);
49+
50+
const { result } = renderHook(() => useRLS());
51+
52+
let returned: unknown;
53+
await act(async () => {
54+
returned = await result.current.getPolicies("Account");
55+
});
56+
57+
expect(mockList).toHaveBeenCalledWith({ object: "Account" });
58+
expect(returned).toEqual(policies);
59+
expect(result.current.policies).toEqual(policies);
60+
expect(result.current.isLoading).toBe(false);
61+
expect(result.current.error).toBeNull();
62+
});
63+
64+
it("evaluate checks access for record", async () => {
65+
const evaluation = {
66+
allowed: true,
67+
appliedPolicies: ["pol-1"],
68+
};
69+
mockEvaluate.mockResolvedValue(evaluation);
70+
71+
const { result } = renderHook(() => useRLS());
72+
73+
let returned: unknown;
74+
await act(async () => {
75+
returned = await result.current.evaluate("Account", "001", "select");
76+
});
77+
78+
expect(mockEvaluate).toHaveBeenCalledWith({
79+
object: "Account",
80+
recordId: "001",
81+
operation: "select",
82+
});
83+
expect(returned).toEqual(evaluation);
84+
expect(result.current.isLoading).toBe(false);
85+
expect(result.current.error).toBeNull();
86+
});
87+
88+
it("handles getPolicies error", async () => {
89+
mockList.mockRejectedValue(new Error("Forbidden"));
90+
91+
const { result } = renderHook(() => useRLS());
92+
93+
await act(async () => {
94+
await expect(result.current.getPolicies("Account")).rejects.toThrow("Forbidden");
95+
});
96+
97+
expect(result.current.policies).toEqual([]);
98+
expect(result.current.isLoading).toBe(false);
99+
expect(result.current.error?.message).toBe("Forbidden");
100+
});
101+
102+
it("handles evaluate error", async () => {
103+
mockEvaluate.mockRejectedValue(new Error("Evaluation failed"));
104+
105+
const { result } = renderHook(() => useRLS());
106+
107+
await act(async () => {
108+
await expect(result.current.evaluate("Account", "001")).rejects.toThrow("Evaluation failed");
109+
});
110+
111+
expect(result.current.isLoading).toBe(false);
112+
expect(result.current.error?.message).toBe("Evaluation failed");
113+
});
114+
});

0 commit comments

Comments
 (0)