Skip to content

Commit 74c84b8

Browse files
joehannohe427
andauthored
Jules unit tests for auth mcp tools (#9031)
* Jules unit tests for auth mcp tools * Formats * Fix disable_user_spec file. * Update get_user spec * Update any to ServerToolContext * Update set_claims spec * Update set_sms_region_policy * Fixing broken error behavior - thanks unit tests! --------- Co-authored-by: Alexander Nohe <nohe@google.com>
1 parent c7b1015 commit 74c84b8

File tree

8 files changed

+309
-5
lines changed

8 files changed

+309
-5
lines changed

src/gcp/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ interface MfaEnrollment {
1212
unobfuscatedPhoneInfo?: string;
1313
}
1414

15-
interface UserInfo {
15+
export interface UserInfo {
1616
uid?: string;
1717
localId?: string;
1818
email: string;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
import { disable_user } from "./disable_user";
4+
import * as auth from "../../../gcp/auth";
5+
import { toContent } from "../../util";
6+
import { ServerToolContext } from "../../tool";
7+
8+
describe("disable_user tool", () => {
9+
const projectId = "test-project";
10+
const uid = "test-uid";
11+
12+
let disableUserStub: sinon.SinonStub;
13+
14+
beforeEach(() => {
15+
disableUserStub = sinon.stub(auth, "disableUser");
16+
});
17+
18+
afterEach(() => {
19+
sinon.restore();
20+
});
21+
22+
it("should disable a user successfully", async () => {
23+
disableUserStub.resolves(true);
24+
25+
const result = await disable_user.fn({ uid, disabled: true }, {
26+
projectId,
27+
} as ServerToolContext);
28+
29+
expect(disableUserStub).to.be.calledWith(projectId, uid, true);
30+
expect(result).to.deep.equal(toContent(`User ${uid} has been disabled`));
31+
});
32+
33+
it("should enable a user successfully", async () => {
34+
disableUserStub.resolves(true);
35+
36+
const result = await disable_user.fn({ uid, disabled: false }, {
37+
projectId,
38+
} as ServerToolContext);
39+
40+
expect(disableUserStub).to.be.calledWith(projectId, uid, false);
41+
expect(result).to.deep.equal(toContent(`User ${uid} has been enabled`));
42+
});
43+
44+
it("should handle failure to disable a user", async () => {
45+
disableUserStub.resolves(false);
46+
47+
const result = await disable_user.fn({ uid, disabled: true }, {
48+
projectId,
49+
} as ServerToolContext);
50+
51+
expect(result).to.deep.equal(toContent(`Failed to disable user ${uid}`));
52+
});
53+
54+
it("should handle failure to enable a user", async () => {
55+
disableUserStub.resolves(false);
56+
57+
const result = await disable_user.fn({ uid, disabled: false }, {
58+
projectId,
59+
} as ServerToolContext);
60+
61+
expect(result).to.deep.equal(toContent(`Failed to enable user ${uid}`));
62+
});
63+
});

src/mcp/tools/auth/disable_user.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const disable_user = tool(
2424
async ({ uid, disabled }, { projectId }) => {
2525
const res = await disableUser(projectId, uid, disabled);
2626
if (res) {
27-
return toContent(`User ${uid} as been ${disabled ? "disabled" : "enabled"}`);
27+
return toContent(`User ${uid} has been ${disabled ? "disabled" : "enabled"}`);
2828
}
2929
return toContent(`Failed to ${disabled ? "disable" : "enable"} user ${uid}`);
3030
},
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
import { get_user } from "./get_user";
4+
import * as auth from "../../../gcp/auth";
5+
import * as util from "../../util";
6+
import { ServerToolContext } from "../../tool";
7+
8+
describe("get_user tool", () => {
9+
const projectId = "test-project";
10+
const email = "test@example.com";
11+
const phoneNumber = "+11234567890";
12+
const uid = "test-uid";
13+
const user = { uid, email, phoneNumber };
14+
15+
let findUserStub: sinon.SinonStub;
16+
let mcpErrorStub: sinon.SinonStub;
17+
18+
beforeEach(() => {
19+
findUserStub = sinon.stub(auth, "findUser");
20+
mcpErrorStub = sinon.stub(util, "mcpError");
21+
});
22+
23+
afterEach(() => {
24+
sinon.restore();
25+
});
26+
27+
it("should return an error if no identifier is provided", async () => {
28+
await get_user.fn({}, { projectId } as ServerToolContext);
29+
expect(mcpErrorStub).to.be.calledWith("No user identifier supplied in auth_get_user tool");
30+
});
31+
32+
it("should get a user by email", async () => {
33+
findUserStub.resolves(user);
34+
const result = await get_user.fn({ email }, { projectId } as ServerToolContext);
35+
expect(findUserStub).to.be.calledWith(projectId, email, undefined, undefined);
36+
expect(result).to.deep.equal(util.toContent(user));
37+
});
38+
39+
it("should get a user by phone number", async () => {
40+
findUserStub.resolves(user);
41+
const result = await get_user.fn({ phone_number: phoneNumber }, {
42+
projectId,
43+
} as ServerToolContext);
44+
expect(findUserStub).to.be.calledWith(projectId, undefined, phoneNumber, undefined);
45+
expect(result).to.deep.equal(util.toContent(user));
46+
});
47+
48+
it("should get a user by UID", async () => {
49+
findUserStub.resolves(user);
50+
const result = await get_user.fn({ uid }, { projectId } as ServerToolContext);
51+
expect(findUserStub).to.be.calledWith(projectId, undefined, undefined, uid);
52+
expect(result).to.deep.equal(util.toContent(user));
53+
});
54+
55+
it("returns an error when no user exists", async () => {
56+
findUserStub.rejects(new Error("No users found"));
57+
await get_user.fn({ uid: "nonexistant@email.com" }, { projectId } as ServerToolContext);
58+
expect(mcpErrorStub).to.be.calledWith("Unable to find user");
59+
});
60+
});

src/mcp/tools/auth/get_user.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from "zod";
22
import { tool } from "../../tool";
33
import { mcpError, toContent } from "../../util";
4-
import { findUser } from "../../../gcp/auth";
4+
import { findUser, UserInfo } from "../../../gcp/auth";
55

66
export const get_user = tool(
77
{
@@ -36,8 +36,14 @@ export const get_user = tool(
3636
},
3737
async ({ email, phone_number, uid }, { projectId }) => {
3838
if (email === undefined && phone_number === undefined && uid === undefined) {
39-
return mcpError(`No user identifier supplied in auth_get_user tool`);
39+
return mcpError("No user identifier supplied in auth_get_user tool");
4040
}
41-
return toContent(await findUser(projectId, email, phone_number, uid));
41+
let user: UserInfo;
42+
try {
43+
user = await findUser(projectId, email, phone_number, uid);
44+
} catch (err: any) {
45+
return mcpError("Unable to find user");
46+
}
47+
return toContent(user);
4248
},
4349
);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
import { list_users } from "./list_users";
4+
import * as auth from "../../../gcp/auth";
5+
import { toContent } from "../../util";
6+
import { ServerToolContext } from "../../tool";
7+
8+
describe("list_users tool", () => {
9+
const projectId = "test-project";
10+
const users = [
11+
{ uid: "uid1", email: "user1@example.com", passwordHash: "hash", salt: "salt" },
12+
{ uid: "uid2", email: "user2@example.com", passwordHash: "hash", salt: "salt" },
13+
];
14+
const prunedUsers = [
15+
{ uid: "uid1", email: "user1@example.com" },
16+
{ uid: "uid2", email: "user2@example.com" },
17+
];
18+
19+
let listUsersStub: sinon.SinonStub;
20+
21+
beforeEach(() => {
22+
listUsersStub = sinon.stub(auth, "listUsers");
23+
});
24+
25+
afterEach(() => {
26+
sinon.restore();
27+
});
28+
29+
it("should list users with the default limit", async () => {
30+
listUsersStub.resolves(users);
31+
32+
const result = await list_users.fn({}, { projectId } as ServerToolContext);
33+
34+
expect(listUsersStub).to.be.calledWith(projectId, 100);
35+
expect(result).to.deep.equal(toContent(prunedUsers));
36+
});
37+
38+
it("should list users with a specified limit", async () => {
39+
listUsersStub.resolves(users);
40+
41+
const result = await list_users.fn({ limit: 10 }, { projectId } as ServerToolContext);
42+
43+
expect(listUsersStub).to.be.calledWith(projectId, 10);
44+
expect(result).to.deep.equal(toContent(prunedUsers));
45+
});
46+
47+
it("should handle an empty list of users", async () => {
48+
listUsersStub.resolves([]);
49+
50+
const result = await list_users.fn({}, { projectId } as ServerToolContext);
51+
52+
expect(listUsersStub).to.be.calledWith(projectId, 100);
53+
expect(result).to.deep.equal(toContent([]));
54+
});
55+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
import { set_claim } from "./set_claims";
4+
import * as auth from "../../../gcp/auth";
5+
import * as util from "../../util";
6+
import { ServerToolContext } from "../../tool";
7+
8+
describe("set_claim tool", () => {
9+
const projectId = "test-project";
10+
const uid = "test-uid";
11+
const claim = "admin";
12+
13+
let setCustomClaimStub: sinon.SinonStub;
14+
let mcpErrorStub: sinon.SinonStub;
15+
16+
beforeEach(() => {
17+
setCustomClaimStub = sinon.stub(auth, "setCustomClaim");
18+
mcpErrorStub = sinon.stub(util, "mcpError");
19+
});
20+
21+
afterEach(() => {
22+
sinon.restore();
23+
});
24+
25+
it("should set a simple claim", async () => {
26+
const value = true;
27+
setCustomClaimStub.resolves({ success: true });
28+
29+
const result = await set_claim.fn({ uid, claim, value }, { projectId } as ServerToolContext);
30+
31+
expect(setCustomClaimStub).to.be.calledWith(
32+
projectId,
33+
uid,
34+
{ [claim]: value },
35+
{ merge: true },
36+
);
37+
expect(result).to.deep.equal(util.toContent({ success: true }));
38+
});
39+
40+
it("should set a JSON claim", async () => {
41+
const json_value = '{"role": "editor"}';
42+
const parsedValue = { role: "editor" };
43+
setCustomClaimStub.resolves({ success: true });
44+
45+
const result = await set_claim.fn({ uid, claim, json_value }, {
46+
projectId,
47+
} as ServerToolContext);
48+
49+
expect(setCustomClaimStub).to.be.calledWith(
50+
projectId,
51+
uid,
52+
{ [claim]: parsedValue },
53+
{ merge: true },
54+
);
55+
expect(result).to.deep.equal(util.toContent({ success: true }));
56+
});
57+
58+
it("should return an error for invalid JSON", async () => {
59+
const json_value = "invalid-json";
60+
await set_claim.fn({ uid, claim, json_value }, { projectId } as ServerToolContext);
61+
expect(mcpErrorStub).to.be.calledWith(
62+
`Provided \`json_value\` was not valid JSON: ${json_value}`,
63+
);
64+
});
65+
66+
it("should return an error if both value and json_value are provided", async () => {
67+
const value = "simple";
68+
const json_value = '{"complex": true}';
69+
await set_claim.fn({ uid, claim, value, json_value }, { projectId } as ServerToolContext);
70+
expect(mcpErrorStub).to.be.calledWith("Must supply only `value` or `json_value`, not both.");
71+
});
72+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
import { set_sms_region_policy } from "./set_sms_region_policy";
4+
import * as auth from "../../../gcp/auth";
5+
import { toContent } from "../../util";
6+
import { ServerToolContext } from "../../tool";
7+
8+
describe("set_sms_region_policy tool", () => {
9+
const projectId = "test-project";
10+
const country_codes = ["us", "ca"];
11+
const upperCaseCountryCodes = ["US", "CA"];
12+
13+
let setAllowSmsRegionPolicyStub: sinon.SinonStub;
14+
let setDenySmsRegionPolicyStub: sinon.SinonStub;
15+
16+
beforeEach(() => {
17+
setAllowSmsRegionPolicyStub = sinon.stub(auth, "setAllowSmsRegionPolicy");
18+
setDenySmsRegionPolicyStub = sinon.stub(auth, "setDenySmsRegionPolicy");
19+
});
20+
21+
afterEach(() => {
22+
sinon.restore();
23+
});
24+
25+
it("should set an ALLOW policy", async () => {
26+
setAllowSmsRegionPolicyStub.resolves(true);
27+
28+
const result = await set_sms_region_policy.fn({ policy_type: "ALLOW", country_codes }, {
29+
projectId,
30+
} as ServerToolContext);
31+
32+
expect(setAllowSmsRegionPolicyStub).to.be.calledWith(projectId, upperCaseCountryCodes);
33+
expect(setDenySmsRegionPolicyStub).to.not.be.called;
34+
expect(result).to.deep.equal(toContent(true));
35+
});
36+
37+
it("should set a DENY policy", async () => {
38+
setDenySmsRegionPolicyStub.resolves(true);
39+
40+
const result = await set_sms_region_policy.fn({ policy_type: "DENY", country_codes }, {
41+
projectId,
42+
} as ServerToolContext);
43+
44+
expect(setDenySmsRegionPolicyStub).to.be.calledWith(projectId, upperCaseCountryCodes);
45+
expect(setAllowSmsRegionPolicyStub).to.not.be.called;
46+
expect(result).to.deep.equal(toContent(true));
47+
});
48+
});

0 commit comments

Comments
 (0)