diff --git a/biome.json b/biome.json index a30eb57..38190cb 100644 --- a/biome.json +++ b/biome.json @@ -22,6 +22,8 @@ "includes": [ "**/packages/client/src/**/*.{ts,tsx,js,jsx}", "**/packages/client/*.{ts,tsx,js,jsx}", + "**/packages/admin/src/**/*.{ts,tsx,js,jsx}", + "**/packages/admin/*.{ts,tsx,js,jsx}", "**/packages/api/src/**/*.{ts,tsx,js,jsx}", "**/packages/indexer/src/**/*.{ts,tsx,js,jsx}", "**/packages/contracts/src/**/*.{ts,tsx,js,jsx}", @@ -56,6 +58,7 @@ "files": { "includes": [ "**/packages/client/src/**/*.{ts,tsx,js,jsx}", + "**/packages/admin/src/**/*.{ts,tsx,js,jsx}", "**/packages/api/src/**/*.{ts,tsx,js,jsx}", "**/packages/indexer/src/**/*.{ts,tsx,js,jsx}", "**/packages/contracts/src/**/*.{ts,tsx,js,jsx}", diff --git a/packages/admin/src/App.tsx b/packages/admin/src/App.tsx index 411f7fa..776a0f0 100644 --- a/packages/admin/src/App.tsx +++ b/packages/admin/src/App.tsx @@ -15,7 +15,7 @@ import Contracts from "@/views/Contracts"; function App() { return ( - + } /> }> @@ -75,9 +75,9 @@ function App() { }, }} /> - + ); } -export default App; \ No newline at end of file +export default App; diff --git a/packages/admin/src/__mocks__/index.ts b/packages/admin/src/__mocks__/index.ts index 4d4fecc..19a79af 100644 --- a/packages/admin/src/__mocks__/index.ts +++ b/packages/admin/src/__mocks__/index.ts @@ -31,7 +31,7 @@ vi.mock("@privy-io/react-auth", () => ({ }, ], }), - PrivyProvider: ({ children }: { children: React.ReactNode }) => + PrivyProvider: ({ children }: { children: React.ReactNode }) => React.createElement("div", { "data-testid": "privy-provider" }, children), })); diff --git a/packages/admin/src/__mocks__/privy.ts b/packages/admin/src/__mocks__/privy.ts index c0feec7..26e1b74 100644 --- a/packages/admin/src/__mocks__/privy.ts +++ b/packages/admin/src/__mocks__/privy.ts @@ -41,7 +41,7 @@ export const MockPrivyProvider = ({ children }: { children: React.ReactNode }) = export const createMockPrivyUser = (role: "admin" | "operator" | "unauthorized") => { const addressMap = { admin: "0x2aa64E6d80390F5C017F0313cB908051BE2FD35e", - operator: "0x04D60647836bcA09c37B379550038BdaaFD82503", + operator: "0x04D60647836bcA09c37B379550038BdaaFD82503", unauthorized: "0x1234567890123456789012345678901234567890", }; diff --git a/packages/admin/src/__mocks__/server.ts b/packages/admin/src/__mocks__/server.ts index 1549abd..4fd7ea6 100644 --- a/packages/admin/src/__mocks__/server.ts +++ b/packages/admin/src/__mocks__/server.ts @@ -6,51 +6,51 @@ const handlers = [ // Mock GetGardens query graphql.query("GetGardens", ({ variables }) => { const chainId = variables.chainId as number; - + // Filter gardens by chainId const allGardens = [ - { - id: "0x1234567890123456789012345678901234567890", - chainId: 84532, - tokenAddress: "0xabcd1234567890123456789012345678901234ef", - tokenID: "1", - name: "Test Garden 1", - description: "A test garden for unit testing", - location: "Test Location", - bannerImage: "https://example.com/banner1.jpg", - createdAt: "2024-01-01T00:00:00Z", - gardeners: ["0x2aa64E6d80390F5C017F0313cB908051BE2FD35e"], - operators: ["0x04D60647836bcA09c37B379550038BdaaFD82503"], - }, - { - id: "0x2345678901234567890123456789012345678901", - chainId: 84532, - tokenAddress: "0xbcde2345678901234567890123456789012345f0", - tokenID: "2", - name: "Test Garden 2", - description: "Another test garden", - location: "Test Location 2", - bannerImage: "https://example.com/banner2.jpg", - createdAt: "2024-01-02T00:00:00Z", - gardeners: ["0x04D60647836bcA09c37B379550038BdaaFD82503"], - operators: ["0x04D60647836bcA09c37B379550038BdaaFD82503"], - }, - ]; + { + id: "0x1234567890123456789012345678901234567890", + chainId: 84532, + tokenAddress: "0xabcd1234567890123456789012345678901234ef", + tokenID: "1", + name: "Test Garden 1", + description: "A test garden for unit testing", + location: "Test Location", + bannerImage: "https://example.com/banner1.jpg", + createdAt: "2024-01-01T00:00:00Z", + gardeners: ["0x2aa64E6d80390F5C017F0313cB908051BE2FD35e"], + operators: ["0x04D60647836bcA09c37B379550038BdaaFD82503"], + }, + { + id: "0x2345678901234567890123456789012345678901", + chainId: 84532, + tokenAddress: "0xbcde2345678901234567890123456789012345f0", + tokenID: "2", + name: "Test Garden 2", + description: "Another test garden", + location: "Test Location 2", + bannerImage: "https://example.com/banner2.jpg", + createdAt: "2024-01-02T00:00:00Z", + gardeners: ["0x04D60647836bcA09c37B379550038BdaaFD82503"], + operators: ["0x04D60647836bcA09c37B379550038BdaaFD82503"], + }, + ]; - // Filter by chainId - const filteredGardens = allGardens.filter(garden => garden.chainId === chainId); + // Filter by chainId + const filteredGardens = allGardens.filter((garden) => garden.chainId === chainId); - return HttpResponse.json({ - data: { - Garden: filteredGardens, - }, - }); + return HttpResponse.json({ + data: { + Garden: filteredGardens, + }, + }); }), // Mock GetDashboardStats query graphql.query("GetDashboardStats", ({ variables }) => { const chainId = variables.chainId as number; - + const allGardens = [ { id: "0x1234567890123456789012345678901234567890", @@ -68,7 +68,7 @@ const handlers = [ }, ]; - const filteredGardens = allGardens.filter(garden => garden.chainId === chainId); + const filteredGardens = allGardens.filter((garden) => garden.chainId === chainId); return HttpResponse.json({ data: { @@ -80,7 +80,7 @@ const handlers = [ // Mock GetOperatorGardens query graphql.query("GetOperatorGardens", ({ variables }) => { const operator = variables.operator as string[]; - + // Return gardens where the operator is listed const operatorGardens = [ { @@ -104,22 +104,24 @@ const handlers = [ // Mock GetGardenDetail query graphql.query("GetGardenDetail", ({ variables }) => { const id = variables.id as string; - + return HttpResponse.json({ data: { - Garden: [{ - id, - chainId: 84532, - tokenAddress: "0xabcd1234567890123456789012345678901234ef", - tokenID: "1", - name: "Test Garden Detail", - description: "Detailed test garden", - location: "Test Location", - bannerImage: "https://example.com/banner.jpg", - createdAt: "2024-01-01T00:00:00Z", - gardeners: ["0x2aa64E6d80390F5C017F0313cB908051BE2FD35e"], - operators: ["0x04D60647836bcA09c37B379550038BdaaFD82503"], - }], + Garden: [ + { + id, + chainId: 84532, + tokenAddress: "0xabcd1234567890123456789012345678901234ef", + tokenID: "1", + name: "Test Garden Detail", + description: "Detailed test garden", + location: "Test Location", + bannerImage: "https://example.com/banner.jpg", + createdAt: "2024-01-01T00:00:00Z", + gardeners: ["0x2aa64E6d80390F5C017F0313cB908051BE2FD35e"], + operators: ["0x04D60647836bcA09c37B379550038BdaaFD82503"], + }, + ], }, }); }), diff --git a/packages/admin/src/__tests__/components/RequireAuth.test.tsx b/packages/admin/src/__tests__/components/RequireAuth.test.tsx index cac377e..3e2271e 100644 --- a/packages/admin/src/__tests__/components/RequireAuth.test.tsx +++ b/packages/admin/src/__tests__/components/RequireAuth.test.tsx @@ -21,10 +21,11 @@ vi.mock("react-router-dom", async () => { const actual = await vi.importActual("react-router-dom"); return { ...actual, - Navigate: ({ to }: { to: string }) => React.createElement("div", { - "data-testid": "navigate", - "data-to": to - }), + Navigate: ({ to }: { to: string }) => + React.createElement("div", { + "data-testid": "navigate", + "data-to": to, + }), Outlet: () => React.createElement("div", { "data-testid": "outlet" }, "Protected Content"), useLocation: () => mockUseLocation(), }; diff --git a/packages/admin/src/__tests__/components/RequireRole.test.tsx b/packages/admin/src/__tests__/components/RequireRole.test.tsx index b25c476..2e721eb 100644 --- a/packages/admin/src/__tests__/components/RequireRole.test.tsx +++ b/packages/admin/src/__tests__/components/RequireRole.test.tsx @@ -114,7 +114,9 @@ describe("RequireRole", () => { expect(screen.getByText("Unauthorized")).toBeInTheDocument(); expect(screen.getByText("You don't have permission to access this area.")).toBeInTheDocument(); - expect(screen.queryByText("Contact an admin to be added as an operator.")).not.toBeInTheDocument(); + expect( + screen.queryByText("Contact an admin to be added as an operator.") + ).not.toBeInTheDocument(); expect(screen.queryByTestId("outlet")).not.toBeInTheDocument(); }); @@ -129,6 +131,8 @@ describe("RequireRole", () => { render(); - expect(screen.queryByText("Contact an admin to be added as an operator.")).not.toBeInTheDocument(); + expect( + screen.queryByText("Contact an admin to be added as an operator.") + ).not.toBeInTheDocument(); }); }); diff --git a/packages/admin/src/__tests__/components/auth.simple.test.tsx b/packages/admin/src/__tests__/components/auth.simple.test.tsx index 2958700..1c686f9 100644 --- a/packages/admin/src/__tests__/components/auth.simple.test.tsx +++ b/packages/admin/src/__tests__/components/auth.simple.test.tsx @@ -42,10 +42,10 @@ describe("Authentication Logic", () => { search: "?tab=details", hash: "#section1", }; - + const redirectTo = encodeURIComponent(location.pathname + location.search + location.hash); const expectedRedirect = `/login?redirectTo=${redirectTo}`; - + expect(expectedRedirect).toBe("/login?redirectTo=%2Fgardens%2F123%3Ftab%3Ddetails%23section1"); }); diff --git a/packages/admin/src/__tests__/hooks/useGardenOperations.test.ts b/packages/admin/src/__tests__/hooks/useGardenOperations.test.ts index 40b9d94..87d165f 100644 --- a/packages/admin/src/__tests__/hooks/useGardenOperations.test.ts +++ b/packages/admin/src/__tests__/hooks/useGardenOperations.test.ts @@ -36,7 +36,7 @@ describe("useGardenOperations", () => { beforeEach(() => { vi.clearAllMocks(); - + mockUseWallets.mockReturnValue({ wallets: [ { @@ -62,7 +62,7 @@ describe("useGardenOperations", () => { it("should add gardener successfully", async () => { mockWalletClient.writeContract.mockResolvedValue(MOCK_TX_HASH); - + const { result } = renderHook(() => useGardenOperations(gardenId)); await act(async () => { @@ -70,14 +70,11 @@ describe("useGardenOperations", () => { expect(txHash).toBe(MOCK_TX_HASH); }); - expect(mockExecuteWithToast).toHaveBeenCalledWith( - expect.any(Function), - { - loadingMessage: "Adding gardener...", - successMessage: "Gardener added successfully", - errorMessage: "Failed to add gardener", - } - ); + expect(mockExecuteWithToast).toHaveBeenCalledWith(expect.any(Function), { + loadingMessage: "Adding gardener...", + successMessage: "Gardener added successfully", + errorMessage: "Failed to add gardener", + }); expect(mockWalletClient.writeContract).toHaveBeenCalledWith({ address: gardenId, @@ -90,27 +87,26 @@ describe("useGardenOperations", () => { it("should remove gardener successfully", async () => { mockWalletClient.writeContract.mockResolvedValue(MOCK_TX_HASH); - + const { result } = renderHook(() => useGardenOperations(gardenId)); await act(async () => { - const txHash = await result.current.removeGardener("0x1234567890123456789012345678901234567890"); + const txHash = await result.current.removeGardener( + "0x1234567890123456789012345678901234567890" + ); expect(txHash).toBe(MOCK_TX_HASH); }); - expect(mockExecuteWithToast).toHaveBeenCalledWith( - expect.any(Function), - { - loadingMessage: "Removing gardener...", - successMessage: "Gardener removed successfully", - errorMessage: "Failed to remove gardener", - } - ); + expect(mockExecuteWithToast).toHaveBeenCalledWith(expect.any(Function), { + loadingMessage: "Removing gardener...", + successMessage: "Gardener removed successfully", + errorMessage: "Failed to remove gardener", + }); }); it("should handle wallet not connected error", async () => { mockUseWallets.mockReturnValue({ wallets: [] }); - + const { result } = renderHook(() => useGardenOperations(gardenId)); await act(async () => { @@ -132,7 +128,7 @@ describe("useGardenOperations", () => { throw error; } }); - + const { result } = renderHook(() => useGardenOperations(gardenId)); await act(async () => { @@ -148,9 +144,9 @@ describe("useGardenOperations", () => { it("should set loading state during operations", async () => { mockWalletClient.writeContract.mockImplementation( - () => new Promise(resolve => setTimeout(() => resolve(MOCK_TX_HASH), 100)) + () => new Promise((resolve) => setTimeout(() => resolve(MOCK_TX_HASH), 100)) ); - + const { result } = renderHook(() => useGardenOperations(gardenId)); expect(result.current.isLoading).toBe(false); @@ -165,7 +161,7 @@ describe("useGardenOperations", () => { it("should add operator successfully", async () => { mockWalletClient.writeContract.mockResolvedValue(MOCK_TX_HASH); - + const { result } = renderHook(() => useGardenOperations(gardenId)); await act(async () => { @@ -173,33 +169,29 @@ describe("useGardenOperations", () => { expect(txHash).toBe(MOCK_TX_HASH); }); - expect(mockExecuteWithToast).toHaveBeenCalledWith( - expect.any(Function), - { - loadingMessage: "Adding operator...", - successMessage: "Operator added successfully", - errorMessage: "Failed to add operator", - } - ); + expect(mockExecuteWithToast).toHaveBeenCalledWith(expect.any(Function), { + loadingMessage: "Adding operator...", + successMessage: "Operator added successfully", + errorMessage: "Failed to add operator", + }); }); it("should remove operator successfully", async () => { mockWalletClient.writeContract.mockResolvedValue(MOCK_TX_HASH); - + const { result } = renderHook(() => useGardenOperations(gardenId)); await act(async () => { - const txHash = await result.current.removeOperator("0x1111111111111111111111111111111111111111"); + const txHash = await result.current.removeOperator( + "0x1111111111111111111111111111111111111111" + ); expect(txHash).toBe(MOCK_TX_HASH); }); - expect(mockExecuteWithToast).toHaveBeenCalledWith( - expect.any(Function), - { - loadingMessage: "Removing operator...", - successMessage: "Operator removed successfully", - errorMessage: "Failed to remove operator", - } - ); + expect(mockExecuteWithToast).toHaveBeenCalledWith(expect.any(Function), { + loadingMessage: "Removing operator...", + successMessage: "Operator removed successfully", + errorMessage: "Failed to remove operator", + }); }); }); diff --git a/packages/admin/src/__tests__/hooks/useRole.simple.test.ts b/packages/admin/src/__tests__/hooks/useRole.simple.test.ts index 47a901a..e7e0305 100644 --- a/packages/admin/src/__tests__/hooks/useRole.simple.test.ts +++ b/packages/admin/src/__tests__/hooks/useRole.simple.test.ts @@ -9,7 +9,7 @@ describe("useRole Hook", () => { const deployerAddress = "0x2aa64E6d80390F5C017F0313cB908051BE2FD35e"; // Mock deployment registry check would happen here const canDeploy = true; // Simulating deployment registry allowlist check - + expect(canDeploy).toBe(true); }); @@ -17,7 +17,7 @@ describe("useRole Hook", () => { const operatorAddress = "0x04D60647836bcA09c37B379550038BdaaFD82503"; const operatorGardens = [{ id: "0x123", name: "Test Garden" }]; const isOperator = operatorGardens.length > 0; - + expect(isOperator).toBe(true); }); @@ -25,17 +25,17 @@ describe("useRole Hook", () => { const unknownAddress = "0x1234567890123456789012345678901234567890"; const canDeploy = false; // Not in deployment registry const operatorGardens: any[] = []; // Not operator of any gardens - + const isDeployer = canDeploy; const isOperator = operatorGardens.length > 0; - + let role = "user"; if (isDeployer) { role = "deployer"; } else if (isOperator) { role = "operator"; } - + expect(role).toBe("user"); expect(isDeployer).toBe(false); expect(isOperator).toBe(false); @@ -45,17 +45,17 @@ describe("useRole Hook", () => { const deployerAddress = "0x2aa64E6d80390F5C017F0313cB908051BE2FD35e"; const canDeploy = true; // In deployment registry allowlist const operatorGardens = [{ id: "0x123", name: "Test Garden" }]; // Also operator of gardens - + const isDeployer = canDeploy; const isOperator = operatorGardens.length > 0; - + let role = "user"; if (isDeployer) { role = "deployer"; } else if (isOperator) { role = "operator"; } - + expect(role).toBe("deployer"); // Deployer takes precedence expect(isDeployer).toBe(true); expect(isOperator).toBe(true); // Can still be operator diff --git a/packages/admin/src/__tests__/integration/end-to-end.test.ts b/packages/admin/src/__tests__/integration/end-to-end.test.ts index 9340c24..6c2164d 100644 --- a/packages/admin/src/__tests__/integration/end-to-end.test.ts +++ b/packages/admin/src/__tests__/integration/end-to-end.test.ts @@ -14,7 +14,7 @@ describe.skipIf(!isIntegrationEnabled)("End-to-End Garden Management", () => { beforeAll(async () => { // Deploy test contracts contractAddresses = await setupIntegrationTest(); - + // Update environment with deployed contract addresses Object.defineProperty(import.meta, "env", { value: { @@ -68,9 +68,7 @@ describe.skipIf(!isIntegrationEnabled)("End-to-End Garden Management", () => { role: "operator", isAdmin: false, isOperator: true, - operatorGardens: [ - { id: contractAddresses.gardenAccount, name: "Test Garden" } - ], + operatorGardens: [{ id: contractAddresses.gardenAccount, name: "Test Garden" }], loading: false, })); @@ -86,8 +84,8 @@ describe.skipIf(!isIntegrationEnabled)("End-to-End Garden Management", () => { // 1. Perform a blockchain transaction // 2. Wait for indexer to process the event // 3. Verify UI reflects the update - - const mockWaitForIndexerUpdate = vi.fn(() => + + const mockWaitForIndexerUpdate = vi.fn(() => Promise.resolve({ garden: { id: contractAddresses.gardenAccount, @@ -103,9 +101,7 @@ describe.skipIf(!isIntegrationEnabled)("End-to-End Garden Management", () => { it("should handle network failures gracefully", async () => { // Mock network failure - const mockNetworkFailure = vi.fn(() => - Promise.reject(new Error("Network request failed")) - ); + const mockNetworkFailure = vi.fn(() => Promise.reject(new Error("Network request failed"))); try { await mockNetworkFailure(); @@ -132,11 +128,11 @@ describe.skipIf(!isIntegrationEnabled)("End-to-End Garden Management", () => { // Helper function to wait for blockchain confirmation async function waitForTransaction(txHash: string, maxWait = 30000): Promise { const startTime = Date.now(); - + while (Date.now() - startTime < maxWait) { // Mock transaction receipt check const mockGetReceipt = vi.fn(() => Promise.resolve({ status: "success" })); - + try { const receipt = await mockGetReceipt(); if (receipt.status === "success") { @@ -145,9 +141,9 @@ async function waitForTransaction(txHash: string, maxWait = 30000): Promise setTimeout(resolve, 2000)); + + await new Promise((resolve) => setTimeout(resolve, 2000)); } - + return false; } diff --git a/packages/admin/src/__tests__/integration/foundry-setup.ts b/packages/admin/src/__tests__/integration/foundry-setup.ts index 7b367b0..81cd607 100644 --- a/packages/admin/src/__tests__/integration/foundry-setup.ts +++ b/packages/admin/src/__tests__/integration/foundry-setup.ts @@ -14,9 +14,11 @@ export interface TestContractAddresses { */ export async function deployTestContracts(): Promise { const contractsDir = join(process.cwd(), "../contracts"); - + if (!existsSync(contractsDir)) { - throw new Error("Contracts directory not found. Make sure you're running from the admin package."); + throw new Error( + "Contracts directory not found. Make sure you're running from the admin package." + ); } // Create a test deployment script @@ -45,7 +47,9 @@ echo "{\\"deploymentRegistry\\": \\"$DEPLOYMENT_REGISTRY\\", \\"gardenToken\\": env: { ...process.env, BASE_SEPOLIA_RPC: process.env.VITE_BASE_SEPOLIA_RPC || "https://sepolia.base.org", - TEST_PRIVATE_KEY: process.env.TEST_PRIVATE_KEY || "0x1234567890123456789012345678901234567890123456789012345678901234", + TEST_PRIVATE_KEY: + process.env.TEST_PRIVATE_KEY || + "0x1234567890123456789012345678901234567890123456789012345678901234", }, stdio: ["pipe", "pipe", "pipe"], }); diff --git a/packages/admin/src/__tests__/integration/garden-lifecycle.test.ts b/packages/admin/src/__tests__/integration/garden-lifecycle.test.ts index 24ee4bd..4c4b65b 100644 --- a/packages/admin/src/__tests__/integration/garden-lifecycle.test.ts +++ b/packages/admin/src/__tests__/integration/garden-lifecycle.test.ts @@ -5,7 +5,8 @@ import { privateKeyToAccount } from "viem/accounts"; import { GardenAccountABI, DeploymentRegistryABI } from "@/utils/contracts"; // Integration test configuration -const TEST_PRIVATE_KEY = "0x1234567890123456789012345678901234567890123456789012345678901234" as const; +const TEST_PRIVATE_KEY = + "0x1234567890123456789012345678901234567890123456789012345678901234" as const; const BASE_SEPOLIA_RPC = process.env.VITE_BASE_SEPOLIA_RPC || "https://sepolia.base.org"; // Skip integration tests if not configured @@ -29,14 +30,15 @@ describe.skipIf(!isIntegrationEnabled)("Garden Lifecycle Integration Tests", () .extend(walletActions); // Get deployment registry address from environment or deploy a test one - deploymentRegistryAddress = process.env.VITE_DEPLOYMENT_REGISTRY_ADDRESS || "0x1234567890123456789012345678901234567890"; + deploymentRegistryAddress = + process.env.VITE_DEPLOYMENT_REGISTRY_ADDRESS || "0x1234567890123456789012345678901234567890"; }); it("should create a garden successfully", async () => { // This test requires actual contract deployment and interaction // For now, we'll mock the expected behavior const mockCreateGarden = vi.fn(() => Promise.resolve("0xabcdef1234567890")); - + const gardenParams = { name: "Integration Test Garden", description: "A garden created during integration testing", @@ -53,28 +55,29 @@ describe.skipIf(!isIntegrationEnabled)("Garden Lifecycle Integration Tests", () it("should add gardener to existing garden", async () => { // Mock garden operations const mockAddGardener = vi.fn(() => Promise.resolve("0xabcdef1234567890")); - + const newGardenerAddress = "0x2345678901234567890123456789012345678901"; const txHash = await mockAddGardener(testGardenAddress, newGardenerAddress); - + expect(txHash).toMatch(/^0x[a-fA-F0-9]{64}$/); }); it("should remove gardener from garden", async () => { // Mock garden operations const mockRemoveGardener = vi.fn(() => Promise.resolve("0xabcdef1234567890")); - + const gardenerToRemove = "0x2345678901234567890123456789012345678901"; const txHash = await mockRemoveGardener(testGardenAddress, gardenerToRemove); - + expect(txHash).toMatch(/^0x[a-fA-F0-9]{64}$/); }); it("should fail when unauthorized user tries to perform admin actions", async () => { // Create unauthorized account - const unauthorizedKey = "0x9876543210987654321098765432109876543210987654321098765432109876" as const; + const unauthorizedKey = + "0x9876543210987654321098765432109876543210987654321098765432109876" as const; const unauthorizedAccount = privateKeyToAccount(unauthorizedKey); - + const unauthorizedClient = createTestClient({ chain: baseSepolia, transport: http(BASE_SEPOLIA_RPC), @@ -84,10 +87,10 @@ describe.skipIf(!isIntegrationEnabled)("Garden Lifecycle Integration Tests", () .extend(walletActions); // Mock contract call that should fail - const mockUnauthorizedAction = vi.fn(() => + const mockUnauthorizedAction = vi.fn(() => Promise.reject(new Error("Unauthorized: Only admins can create gardens")) ); - + try { await mockUnauthorizedAction(); } catch (error) { @@ -97,19 +100,21 @@ describe.skipIf(!isIntegrationEnabled)("Garden Lifecycle Integration Tests", () it("should verify indexer updates after garden creation", async () => { // Mock indexer query to verify garden was indexed - const mockIndexerQuery = vi.fn(() => Promise.resolve({ - data: { - gardens: [ - { - id: testGardenAddress, - name: "Integration Test Garden", - description: "A garden created during integration testing", - operators: [testAccount.address], - gardeners: [testAccount.address], - }, - ], - }, - })); + const mockIndexerQuery = vi.fn(() => + Promise.resolve({ + data: { + gardens: [ + { + id: testGardenAddress, + name: "Integration Test Garden", + description: "A garden created during integration testing", + operators: [testAccount.address], + gardeners: [testAccount.address], + }, + ], + }, + }) + ); const result = await mockIndexerQuery(); expect(result.data.gardens).toHaveLength(1); @@ -118,7 +123,7 @@ describe.skipIf(!isIntegrationEnabled)("Garden Lifecycle Integration Tests", () it("should handle contract deployment failures gracefully", async () => { // Mock deployment failure - const mockFailedDeployment = vi.fn(() => + const mockFailedDeployment = vi.fn(() => Promise.reject(new Error("Insufficient funds for gas")) ); @@ -132,7 +137,7 @@ describe.skipIf(!isIntegrationEnabled)("Garden Lifecycle Integration Tests", () it("should verify gas estimation for garden operations", async () => { // Mock gas estimation const mockEstimateGas = vi.fn(() => Promise.resolve(BigInt(150000))); - + const gasEstimate = await mockEstimateGas(); expect(gasEstimate).toBeGreaterThan(BigInt(100000)); // Reasonable gas limit expect(gasEstimate).toBeLessThan(BigInt(500000)); // Not excessive @@ -145,14 +150,14 @@ async function waitForIndexerUpdate(gardenId: string, maxAttempts = 10): Promise // Mock indexer check const mockCheck = vi.fn(() => Promise.resolve(true)); const found = await mockCheck(); - + if (found) { return true; } - - await new Promise(resolve => setTimeout(resolve, 1000)); + + await new Promise((resolve) => setTimeout(resolve, 1000)); } - + return false; } diff --git a/packages/admin/src/__tests__/setup.ts b/packages/admin/src/__tests__/setup.ts index 615a5ea..f81ee30 100644 --- a/packages/admin/src/__tests__/setup.ts +++ b/packages/admin/src/__tests__/setup.ts @@ -36,4 +36,4 @@ vi.mock("react-hot-toast", () => ({ loading: vi.fn(() => "toast-id"), }, Toaster: () => null, -})); \ No newline at end of file +})); diff --git a/packages/admin/src/__tests__/summary.test.ts b/packages/admin/src/__tests__/summary.test.ts index b257413..916319b 100644 --- a/packages/admin/src/__tests__/summary.test.ts +++ b/packages/admin/src/__tests__/summary.test.ts @@ -23,7 +23,7 @@ describe("Test Suite Summary", () => { const environments = ["unit", "integration"]; const hasUnitTests = environments.includes("unit"); const hasIntegrationTests = environments.includes("integration"); - + expect(hasUnitTests).toBe(true); expect(hasIntegrationTests).toBe(true); }); @@ -44,7 +44,7 @@ describe("Test Suite Summary", () => { it("should validate all required user roles", () => { const supportedRoles = ["admin", "operator", "unauthorized"]; - + expect(supportedRoles).toContain("admin"); expect(supportedRoles).toContain("operator"); expect(supportedRoles).toContain("unauthorized"); @@ -54,7 +54,7 @@ describe("Test Suite Summary", () => { it("should test blockchain operations", () => { const blockchainOperations = [ "addGardener", - "removeGardener", + "removeGardener", "addOperator", "removeOperator", "createGarden", diff --git a/packages/admin/src/__tests__/utils/mock-providers.tsx b/packages/admin/src/__tests__/utils/mock-providers.tsx index 91199e4..c2c41a2 100644 --- a/packages/admin/src/__tests__/utils/mock-providers.tsx +++ b/packages/admin/src/__tests__/utils/mock-providers.tsx @@ -1,8 +1,11 @@ import React from "react"; // Create simplified mock providers for component testing -export const MockUserProvider = ({ children, userRole = "admin" }: { - children: React.ReactNode; +export const MockUserProvider = ({ + children, + userRole = "admin", +}: { + children: React.ReactNode; userRole?: "admin" | "operator" | "unauthorized"; }) => { const addressMap = { diff --git a/packages/admin/src/__tests__/utils/test-utils.tsx b/packages/admin/src/__tests__/utils/test-utils.tsx index f8adbbd..3ce8881 100644 --- a/packages/admin/src/__tests__/utils/test-utils.tsx +++ b/packages/admin/src/__tests__/utils/test-utils.tsx @@ -29,9 +29,7 @@ function createTestWrapper({ - - {children} - + {children} @@ -41,12 +39,9 @@ function createTestWrapper({ } // Custom render function with providers -export function renderWithProviders( - ui: React.ReactElement, - options: CustomRenderOptions = {} -) { +export function renderWithProviders(ui: React.ReactElement, options: CustomRenderOptions = {}) { const { userRole = "admin", _initialEntries, urqlClient, ...renderOptions } = options; - + const Wrapper = createTestWrapper({ userRole, _initialEntries, urqlClient }); return { diff --git a/packages/admin/src/__tests__/utils/urql-mock.ts b/packages/admin/src/__tests__/utils/urql-mock.ts index 45bf5e1..c93dbd9 100644 --- a/packages/admin/src/__tests__/utils/urql-mock.ts +++ b/packages/admin/src/__tests__/utils/urql-mock.ts @@ -70,12 +70,13 @@ export function createMockUrqlClient(customHandlers?: Record) { // Setup query mock to return appropriate responses mockClient.executeQuery.mockImplementation(({ query }) => { const queryName = query.definitions[0]?.name?.value; - const response = customHandlers?.[queryName] || defaultResponses[queryName] || { - data: null, - fetching: false, - error: new Error(`Unmocked query: ${queryName}`), - }; - + const response = customHandlers?.[queryName] || + defaultResponses[queryName] || { + data: null, + fetching: false, + error: new Error(`Unmocked query: ${queryName}`), + }; + return Promise.resolve(response); }); diff --git a/packages/admin/src/__tests__/views/Gardens.test.tsx b/packages/admin/src/__tests__/views/Gardens.test.tsx index 278eb7c..e57f6d7 100644 --- a/packages/admin/src/__tests__/views/Gardens.test.tsx +++ b/packages/admin/src/__tests__/views/Gardens.test.tsx @@ -19,9 +19,13 @@ vi.mock("urql", () => ({ // Mock CreateGardenModal component vi.mock("@/components/Garden/CreateGardenModal", () => ({ CreateGardenModal: ({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) => - isOpen ? React.createElement("div", { "data-testid": "create-garden-modal" }, - React.createElement("button", { onClick: onClose }, "Close Modal") - ) : null, + isOpen + ? React.createElement( + "div", + { "data-testid": "create-garden-modal" }, + React.createElement("button", { onClick: onClose }, "Close Modal") + ) + : null, })); describe("Gardens View", () => { @@ -211,7 +215,7 @@ describe("Gardens View", () => { // Close modal const closeButton = screen.getByText("Close Modal"); await user.click(closeButton); - + await waitFor(() => { expect(screen.queryByTestId("create-garden-modal")).not.toBeInTheDocument(); }); diff --git a/packages/admin/src/__tests__/workflows/createGarden.test.ts b/packages/admin/src/__tests__/workflows/createGarden.test.ts index 3fec8cd..45d6158 100644 --- a/packages/admin/src/__tests__/workflows/createGarden.test.ts +++ b/packages/admin/src/__tests__/workflows/createGarden.test.ts @@ -124,9 +124,9 @@ describe("createGarden workflow", () => { actor.send({ type: "START", params: validParams }); actor.send({ type: "SUBMIT" }); actor.send({ type: "FAILURE", error: "Transaction failed" }); - + expect(actor.getSnapshot().value).toBe("error"); - + actor.send({ type: "RETRY" }); // After retry, should go back to submitting state diff --git a/packages/admin/src/__tests__/workflows/unauthorized-actions.test.tsx b/packages/admin/src/__tests__/workflows/unauthorized-actions.test.tsx index 5f8b804..d1395b4 100644 --- a/packages/admin/src/__tests__/workflows/unauthorized-actions.test.tsx +++ b/packages/admin/src/__tests__/workflows/unauthorized-actions.test.tsx @@ -26,17 +26,25 @@ vi.mock("@/hooks/useGardenOperations", () => ({ // Mock CreateGardenModal vi.mock("@/components/Garden/CreateGardenModal", () => ({ CreateGardenModal: ({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) => - isOpen ? React.createElement("div", { "data-testid": "create-garden-modal" }, - React.createElement("button", { - "data-testid": "create-garden-submit", - onClick: () => { - // Simulate unauthorized action - toast.error("Unauthorized: Admin role required"); - onClose(); - } - }, "Create Garden"), - React.createElement("button", { onClick: onClose }, "Cancel") - ) : null, + isOpen + ? React.createElement( + "div", + { "data-testid": "create-garden-modal" }, + React.createElement( + "button", + { + "data-testid": "create-garden-submit", + onClick: () => { + // Simulate unauthorized action + toast.error("Unauthorized: Admin role required"); + onClose(); + }, + }, + "Create Garden" + ), + React.createElement("button", { onClick: onClose }, "Cancel") + ) + : null, })); describe("Unauthorized Actions", () => { @@ -51,7 +59,7 @@ describe("Unauthorized Actions", () => { it("should show error toast when unauthorized user tries to create garden", async () => { const user = userEvent.setup(); - + mockUseRole.mockReturnValue({ isAdmin: false, isOperator: false, @@ -75,7 +83,7 @@ describe("Unauthorized Actions", () => { it("should show error toast when operator tries admin-only actions", async () => { const user = userEvent.setup(); - + mockUseRole.mockReturnValue({ isAdmin: false, isOperator: true, @@ -103,8 +111,10 @@ describe("Unauthorized Actions", () => { }); it("should prevent unauthorized garden operations", async () => { - const mockAddGardener = vi.fn(() => Promise.reject(new Error("Unauthorized: Only operators can add gardeners"))); - + const mockAddGardener = vi.fn(() => + Promise.reject(new Error("Unauthorized: Only operators can add gardeners")) + ); + mockUseGardenOperations.mockReturnValue({ addGardener: mockAddGardener, removeGardener: vi.fn(), diff --git a/packages/admin/src/components/ConnectButton.tsx b/packages/admin/src/components/ConnectButton.tsx index d5ab9ba..6b0a85f 100644 --- a/packages/admin/src/components/ConnectButton.tsx +++ b/packages/admin/src/components/ConnectButton.tsx @@ -9,45 +9,54 @@ interface ConnectButtonProps { size?: "sm" | "md" | "lg"; } -export function ConnectButton({ - className, - children, +export function ConnectButton({ + className, + children, variant = "primary", - size = "md" + size = "md", }: ConnectButtonProps) { const { isConnecting } = useAccount(); const { open } = useAppKit(); - const baseStyles = "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; - + const baseStyles = + "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"; + const variantStyles = { - primary: "border border-transparent text-white bg-green-600 hover:bg-green-700 focus:ring-green-500", - secondary: "border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:ring-green-500" + primary: + "border border-transparent text-white bg-green-600 hover:bg-green-700 focus:ring-green-500", + secondary: + "border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:ring-green-500", }; const sizeStyles = { sm: "px-3 py-1.5 text-sm", md: "px-4 py-2 text-sm", - lg: "px-6 py-3 text-base" + lg: "px-6 py-3 text-base", }; return ( - {error && ( -

{error}

- )} + {error &&

{error}

} {/* Buttons */} @@ -147,11 +154,13 @@ export function AddMemberModal({ isOpen, onClose, memberType, onAdd, isLoading } : "bg-green-600 hover:bg-green-700" )} > - {isLoading ? "Adding..." : `Add ${memberType === "gardener" ? "Gardener" : "Operator"}`} + {isLoading + ? "Adding..." + : `Add ${memberType === "gardener" ? "Gardener" : "Operator"}`} ); -} \ No newline at end of file +} diff --git a/packages/admin/src/components/Garden/CreateGardenModal.tsx b/packages/admin/src/components/Garden/CreateGardenModal.tsx index 1d70311..16aa3a7 100644 --- a/packages/admin/src/components/Garden/CreateGardenModal.tsx +++ b/packages/admin/src/components/Garden/CreateGardenModal.tsx @@ -23,7 +23,7 @@ export function CreateGardenModal({ isOpen, onClose }: CreateGardenModalProps) { const [gardenerInput, setGardenerInput] = useState(""); const [operatorInput, setOperatorInput] = useState(""); const { state, startCreation, submitCreation } = useCreateGardenWorkflow(); - const isCreating = state.matches('creating'); + const isCreating = state.matches("creating"); const { register, @@ -52,7 +52,10 @@ export function CreateGardenModal({ isOpen, onClose }: CreateGardenModalProps) { }; const removeGardener = (index: number) => { - setValue("gardeners", gardeners.filter((_, i) => i !== index)); + setValue( + "gardeners", + gardeners.filter((_, i) => i !== index) + ); }; const addOperator = () => { @@ -63,7 +66,10 @@ export function CreateGardenModal({ isOpen, onClose }: CreateGardenModalProps) { }; const removeOperator = (index: number) => { - setValue("operators", operators.filter((_, i) => i !== index)); + setValue( + "operators", + operators.filter((_, i) => i !== index) + ); }; const onSubmit = async (data: CreateGardenForm) => { @@ -108,10 +114,10 @@ export function CreateGardenModal({ isOpen, onClose }: CreateGardenModalProps) { return (
-
e.key === 'Escape' && onClose()} + onKeyDown={(e) => e.key === "Escape" && onClose()} role="button" tabIndex={0} aria-label="Close modal" @@ -120,10 +126,7 @@ export function CreateGardenModal({ isOpen, onClose }: CreateGardenModalProps) {

Create New Garden

-
@@ -132,7 +135,10 @@ export function CreateGardenModal({ isOpen, onClose }: CreateGardenModalProps) { {/* Basic Information */}
-
-