feat(browser-ext): move browser extension to browser-ext/ and update API client to tRPC
- Create browser-ext/ with full extension code (MV3 manifest, background service worker, content script, popup, options page) - Add tRPC API client that communicates with unified monolith endpoints - Implement cache, settings, and phishing detection utilities - Create extension tRPC router in web app (getAuthStatus, linkDevice, reportPhishing) - Configure Vite build with manifest V3 support - Write unit tests for cache, phishing detector, and API client - All 20 tests passing, TypeScript lint clean
This commit is contained in:
90
browser-ext/tests/api-client.test.ts
Normal file
90
browser-ext/tests/api-client.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { createApiClient } from "../src/lib/api-client";
|
||||
|
||||
vi.mock("@trpc/client", () => ({
|
||||
createTRPCProxyClient: vi.fn(() => ({
|
||||
spamshield: {
|
||||
checkNumber: {
|
||||
query: vi.fn().mockResolvedValue({
|
||||
isSpam: true,
|
||||
score: 0.85,
|
||||
category: "scam",
|
||||
}),
|
||||
},
|
||||
classifySMS: {
|
||||
query: vi.fn().mockResolvedValue({
|
||||
isSpam: false,
|
||||
score: 0.1,
|
||||
category: "legitimate",
|
||||
}),
|
||||
},
|
||||
},
|
||||
extension: {
|
||||
getAuthStatus: {
|
||||
query: vi.fn().mockResolvedValue({ linked: false }),
|
||||
},
|
||||
linkDevice: {
|
||||
mutate: vi.fn().mockResolvedValue({
|
||||
linked: true,
|
||||
deviceId: "dev-123",
|
||||
}),
|
||||
},
|
||||
reportPhishing: {
|
||||
mutate: vi.fn().mockResolvedValue({
|
||||
reported: true,
|
||||
url: "https://phishing.com",
|
||||
}),
|
||||
},
|
||||
},
|
||||
})),
|
||||
httpBatchLink: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("createApiClient", () => {
|
||||
let client: ReturnType<typeof createApiClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
client = createApiClient("https://api.shieldai.com/api/trpc", "test-key");
|
||||
});
|
||||
|
||||
it("should create a client", () => {
|
||||
expect(client).toBeDefined();
|
||||
});
|
||||
|
||||
it("should check phone number reputation", async () => {
|
||||
const result = await client.spamshield.checkNumber.query({
|
||||
phoneNumber: "+1234567890",
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
expect(result.isSpam).toBe(true);
|
||||
expect(result.score).toBe(0.85);
|
||||
});
|
||||
|
||||
it("should classify SMS text", async () => {
|
||||
const result = await client.spamshield.classifySMS.query({
|
||||
text: "Hello, this is a test message",
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
expect(result.isSpam).toBe(false);
|
||||
});
|
||||
|
||||
it("should get auth status", async () => {
|
||||
const result = await client.extension.getAuthStatus.query();
|
||||
expect(result).toEqual({ linked: false });
|
||||
});
|
||||
|
||||
it("should link device", async () => {
|
||||
const result = await client.extension.linkDevice.mutate({
|
||||
extensionId: "ext-123",
|
||||
});
|
||||
expect(result.linked).toBe(true);
|
||||
expect(result.deviceId).toBe("dev-123");
|
||||
});
|
||||
|
||||
it("should report phishing", async () => {
|
||||
const result = await client.extension.reportPhishing.mutate({
|
||||
url: "https://phishing.com",
|
||||
});
|
||||
expect(result.reported).toBe(true);
|
||||
});
|
||||
});
|
||||
53
browser-ext/tests/cache.test.ts
Normal file
53
browser-ext/tests/cache.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { Cache } from "../src/lib/cache";
|
||||
|
||||
describe("Cache", () => {
|
||||
let cache: Cache<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
cache = new Cache(1000);
|
||||
});
|
||||
|
||||
it("should store and retrieve values", () => {
|
||||
cache.set("key1", "value1");
|
||||
expect(cache.get("key1")).toBe("value1");
|
||||
});
|
||||
|
||||
it("should return null for missing keys", () => {
|
||||
expect(cache.get("nonexistent")).toBeNull();
|
||||
});
|
||||
|
||||
it("should expire entries after TTL", () => {
|
||||
cache.set("key1", "value1");
|
||||
vi.advanceTimersByTime(1500);
|
||||
expect(cache.get("key1")).toBeNull();
|
||||
});
|
||||
|
||||
it("should not expire entries before TTL", () => {
|
||||
cache.set("key1", "value1");
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(cache.get("key1")).toBe("value1");
|
||||
});
|
||||
|
||||
it("should delete entries", () => {
|
||||
cache.set("key1", "value1");
|
||||
cache.delete("key1");
|
||||
expect(cache.get("key1")).toBeNull();
|
||||
});
|
||||
|
||||
it("should clear all entries", () => {
|
||||
cache.set("key1", "value1");
|
||||
cache.set("key2", "value2");
|
||||
cache.clear();
|
||||
expect(cache.get("key1")).toBeNull();
|
||||
expect(cache.get("key2")).toBeNull();
|
||||
});
|
||||
|
||||
it("should not return expired entries when they are cleaned up on get", () => {
|
||||
cache.set("key1", "value1");
|
||||
vi.advanceTimersByTime(1001);
|
||||
const result = cache.get("key1");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
48
browser-ext/tests/phishing-detector.test.ts
Normal file
48
browser-ext/tests/phishing-detector.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { checkUrlForPhishing } from "../src/lib/phishing-detector";
|
||||
|
||||
describe("checkUrlForPhishing", () => {
|
||||
it("should return isPhishing: false for a normal URL", () => {
|
||||
const result = checkUrlForPhishing("https://www.google.com");
|
||||
expect(result.isPhishing).toBe(false);
|
||||
expect(result.confidence).toBe(0);
|
||||
expect(result.reasons).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should detect known phishing domains", () => {
|
||||
const result = checkUrlForPhishing("https://shieldai-secure.com/login");
|
||||
expect(result.isPhishing).toBe(true);
|
||||
expect(result.reasons).toContain("Known phishing domain: shieldai-secure.com");
|
||||
});
|
||||
|
||||
it("should detect suspicious URL patterns", () => {
|
||||
const result = checkUrlForPhishing("https://login-secure.example.com");
|
||||
expect(result.isPhishing).toBe(true);
|
||||
expect(result.reasons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should detect phishing with multiple signals for high confidence", () => {
|
||||
const result = checkUrlForPhishing(
|
||||
"https://shieldai-verify.com/account-update/verify",
|
||||
);
|
||||
expect(result.isPhishing).toBe(true);
|
||||
expect(result.confidence).toBeGreaterThan(0.5);
|
||||
expect(result.reasons.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("should handle invalid URLs gracefully", () => {
|
||||
const result = checkUrlForPhishing("not-a-valid-url");
|
||||
expect(result.isPhishing).toBe(true);
|
||||
expect(result.reasons).toContain("Invalid URL format");
|
||||
});
|
||||
|
||||
it("should detect phishing with subdomain of known domain", () => {
|
||||
const result = checkUrlForPhishing("https://login.shieldai-secure.com");
|
||||
expect(result.isPhishing).toBe(true);
|
||||
});
|
||||
|
||||
it("should not flag legitimate shieldai.com URLs", () => {
|
||||
const result = checkUrlForPhishing("https://api.shieldai.com/api/trpc");
|
||||
expect(result.isPhishing).toBe(false);
|
||||
});
|
||||
});
|
||||
40
browser-ext/tests/setup.ts
Normal file
40
browser-ext/tests/setup.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
const mockStorage: Record<string, unknown> = {};
|
||||
|
||||
vi.stubGlobal("chrome", {
|
||||
runtime: {
|
||||
id: "test-extension-id",
|
||||
onInstalled: {
|
||||
addListener: vi.fn(),
|
||||
},
|
||||
onMessage: {
|
||||
addListener: vi.fn(),
|
||||
},
|
||||
sendMessage: vi.fn(),
|
||||
openOptionsPage: vi.fn(),
|
||||
},
|
||||
storage: {
|
||||
sync: {
|
||||
get: vi.fn((keys: string | string[] | Record<string, unknown>) => {
|
||||
if (typeof keys === "string") {
|
||||
return Promise.resolve({ [keys]: mockStorage[keys] ?? null });
|
||||
}
|
||||
if (Array.isArray(keys)) {
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const key of keys) result[key] = mockStorage[key] ?? null;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
return Promise.resolve({});
|
||||
}),
|
||||
set: vi.fn((items: Record<string, unknown>) => {
|
||||
Object.assign(mockStorage, items);
|
||||
return Promise.resolve();
|
||||
}),
|
||||
},
|
||||
local: {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user