diff --git a/src/adapters/supabase/helpers/user.ts b/src/adapters/supabase/helpers/user.ts index 77fafc8..8bf62ce 100644 --- a/src/adapters/supabase/helpers/user.ts +++ b/src/adapters/supabase/helpers/user.ts @@ -1,6 +1,7 @@ import { SupabaseClient } from "@supabase/supabase-js"; import { Super } from "./supabase"; import { Context } from "../../../types/context"; +import { logAndComment } from "../../../utils/logger"; type Wallet = { address: string; @@ -25,8 +26,7 @@ export class User extends Super { async getWalletByUserId(userId: number, issueNumber: number) { const { data, error } = (await this.supabase.from("users").select("wallets(*)").eq("id", userId).single()) as { data: { wallets: Wallet }; error: unknown }; if ((error && !data) || !data.wallets?.address) { - const log = this.context.logger.error("No wallet address found", { userId, issueNumber }); - await addCommentToIssue(this.context, log.logMessage.diff); + throw logAndComment(this.context, "error", "No wallet address found", { userId, issueNumber }); } else { this.context.logger.info("Successfully fetched wallet", { userId, address: data.wallets?.address }); } @@ -34,29 +34,3 @@ export class User extends Super { return data?.wallets?.address || null; } } - -async function addCommentToIssue(context: Context, message: string) { - const { payload, octokit } = context; - const { - repository: { full_name }, - issue, - } = payload; - - if (!full_name) { - context.logger.error("No issue found to comment on"); - return; - } - - const [owner, repo] = full_name.split("/"); - - try { - await octokit.rest.issues.createComment({ - owner, - repo, - issue_number: issue.number, - body: message, - }); - } catch (e) { - context.logger.error("Error adding comment to issue", { owner, repo, issue_number: issue.number }); - } -} diff --git a/src/handlers/faucet.ts b/src/handlers/faucet.ts index db0a91d..3234da1 100644 --- a/src/handlers/faucet.ts +++ b/src/handlers/faucet.ts @@ -5,7 +5,7 @@ import { NetworkId, RPCHandler } from "@ubiquity-dao/rpc-handler"; function isEthAddress(address: string) { if (!address) return false; - return /^0x[a-fA-F0-9]{40}$/.test(address); + return /^0x[a-fA-F0-9]{40}$/.test(address.trim()); } export async function faucet(context: Context, args: Args) { diff --git a/src/plugin.ts b/src/plugin.ts index 667d78f..83bf5ed 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -29,6 +29,7 @@ export async function runPlugin(context: Context) { })}`; await logAndComment(context, "info", comment, { txs }); + return txs; } logger.info(`Ignoring event ${eventName}`); diff --git a/tests/__mocks__/handlers.ts b/tests/__mocks__/handlers.ts index f9a25a6..ab4c73e 100644 --- a/tests/__mocks__/handlers.ts +++ b/tests/__mocks__/handlers.ts @@ -1,7 +1,6 @@ import { http, HttpResponse } from "msw"; import { db } from "./db"; import issueTemplate from "./issue-template"; -import { StorageLayout } from "../../src/adapters/storage"; /** * Intercepts the routes and returns a custom payload */ @@ -47,29 +46,6 @@ export const handlers = [ db.issueComments.create(newItem); return HttpResponse.json(newItem); }), - - http.get("https://api.github.com/repos/:owner/:repo/contents/:path", () => { - const storage: StorageLayout = { - keyrxng: { - claimed: 0, - lastClaim: null, - wallet: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - }, - ubiquity: { - claimed: 0, - lastClaim: null, - wallet: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", - }, - }; - return HttpResponse.json({ - content: Buffer.from(JSON.stringify(storage)).toString("base64"), - }); - }), - http.put("https://api.github.com/repos/:owner/:repo/contents/:path", async ({ request }) => { - const { content } = await getValue(request.body); - const storage = JSON.parse(Buffer.from(content, "base64").toString()); - return HttpResponse.json(storage); - }), ]; async function getValue(body: ReadableStream | null) { diff --git a/tests/__mocks__/helpers.ts b/tests/__mocks__/helpers.ts index 3018e0f..e9ff3b6 100644 --- a/tests/__mocks__/helpers.ts +++ b/tests/__mocks__/helpers.ts @@ -35,6 +35,8 @@ export async function setupTests() { id: 2, number: 2, labels: [], + state: "closed", + state_reason: "completed", }); } diff --git a/tests/main.test.ts b/tests/main.test.ts index 46eeaef..ff0c84e 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -5,120 +5,160 @@ import { expect, describe, beforeAll, beforeEach, afterAll, afterEach, it } from import { Context } from "../src/types/context"; import { Octokit } from "@octokit/rest"; import { STRINGS } from "./__mocks__/strings"; -import { createComment, setupTests } from "./__mocks__/helpers"; +import { setupTests } from "./__mocks__/helpers"; import manifest from "../manifest.json"; import dotenv from "dotenv"; import { Logs } from "@ubiquity-dao/ubiquibot-logger"; -import { Env } from "../src/types"; import { runPlugin } from "../src/plugin"; -import { HandlerConstructorConfig, RPCHandler } from "@ubiquity-dao/rpc-handler"; -import { Storage } from "../src/adapters/storage"; +import { RPCHandler } from "@ubiquity-dao/rpc-handler"; import { ethers } from "ethers"; +import { createAdapters } from "../src/adapters"; +import { createClient } from "@supabase/supabase-js"; + +import usersGet from "./__mocks__/users-get.json"; dotenv.config(); jest.requireActual("@octokit/rest"); - jest.mock("@ubiquity-dao/rpc-handler"); + const mockRpcHandler = { getFastestRpcProvider: jest.fn().mockResolvedValue(new ethers.providers.JsonRpcProvider("http://localhost:8545")), }; -(RPCHandler as unknown as jest.Mock).mockImplementation(() => mockRpcHandler); -const octokit = new Octokit(); +(RPCHandler as unknown as jest.Mock).mockImplementation(() => mockRpcHandler); -export const testConfig: HandlerConstructorConfig = { - networkId: "1337", - autoStorage: false, - cacheRefreshCycles: 3, - networkName: null, - networkRpcs: null, - rpcTimeout: 600, - runtimeRpcs: null, - proxySettings: { - retryCount: 3, - retryDelay: 10, - logTier: "info", - logger: null, - strictLogs: true, - }, +let supabaseMock = { + getWalletByUserId: jest.fn().mockResolvedValue("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), + hasClaimedBefore: jest.fn().mockResolvedValue(false), }; +jest.mock("../src/adapters/supabase/helpers/user", () => { + return { + User: jest.fn().mockImplementation(() => supabaseMock), + }; +}); + +const octokit = new Octokit(); + beforeAll(() => { server.listen(); }); afterEach(() => { server.resetHandlers(); - jest.clearAllMocks(); }); afterAll(() => server.close()); -describe("Plugin tests", () => { - beforeEach(async () => { - drop(db); - await setupTests(); - }); +beforeEach(async () => { + jest.resetModules(); + jest.clearAllMocks(); + drop(db); + await setupTests(); +}); +describe("Plugin tests", () => { it("Should serve the manifest file", async () => { const worker = (await import("../src/worker")).default; - const response = await worker.fetch(new Request("http://localhost/manifest.json"), {}); + const response = await worker.fetch(new Request("http://localhost/manifest.json"), { + SUPABASE_KEY: "", + SUPABASE_URL: "", + }); const content = await response.json(); expect(content).toEqual(manifest); }); it("Should successfully distribute gas tokens", async () => { - const { context } = createContext("/faucet keyrxng 100 1 native"); + const { context } = createContext(); const result = await runPlugin(context); - expect(result).toHaveProperty("status", 1); const account = new ethers.Wallet(context.config.fundingWalletPrivateKey).address; - expect(result).toHaveProperty("from", account); - }); - it("Should handle the /register command", async () => { - const { context } = createContext("/register", 1, 3); - const result = await runPlugin(context); + expect(result).toBeDefined(); + if (!result) { - throw new Error("Expected LogReturn"); + throw new Error(); } - if ("logMessage" in result) { - expect(result.logMessage.raw).toContain("Please go to https://safe.ubq.fi to finalize registering your account."); - } else { - throw new Error("Expected LogReturn"); - } + const userOne = result[usersGet[0].login]; + const userTwo = result[usersGet[1].login]; + + expect(userOne).toHaveLength(1); + expect(userTwo).toHaveLength(1); + + const tx = userOne[0]; + const tx2 = userTwo[0]; + + verifyTx(tx); + verifyTx(tx2); + + expect(tx.from).toEqual(account); + expect(tx2.from).toEqual(account); }); it("Should handle an issues.closed event", async () => { - const context = createIssuesClosedContext( + const context = createContextInner( db.repo.findFirst({ where: { id: { equals: 1 } } }) as unknown as Context["payload"]["repository"], db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as Context["payload"]["sender"], db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Context<"issues.closed">["payload"]["issue"] ); const result = await runPlugin(context); - if (result && Array.isArray(result)) { - expect(result).toHaveLength(2); - expect(result[0]).toHaveProperty("status", 1); - expect(result[1]).toHaveProperty("status", 1); - - expect(result[0]).toHaveProperty("from", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); - expect(result[0]).toHaveProperty("to", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); - - expect(result[1]).toHaveProperty("from", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); - expect(result[1]).toHaveProperty("to", "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"); - } else { - throw new Error("Expected array of transactions"); + + expect(result).toBeDefined(); + + if (!result) { + throw new Error(); } + + const userOne = result[usersGet[0].login]; + const userTwo = result[usersGet[1].login]; + + expect(userOne).toHaveLength(1); + expect(userTwo).toHaveLength(1); + + const tx = userOne[0]; + const tx2 = userTwo[0]; + + verifyTx(tx); + verifyTx(tx2); }); }); -function createContext(commentBody: string, repoId: number = 1, payloadSenderId: number = 1, commentId: number = 1, issueOne: number = 1) { +describe("", () => { + beforeEach(() => { + supabaseMock = { + getWalletByUserId: jest.fn().mockResolvedValue("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), + hasClaimedBefore: jest.fn().mockResolvedValue(true), + }; + }); + + it("Should not distribute if a permit exists in the DB for the user", async () => { + const { context } = createContext(1, 1, 2); + const result = await runPlugin(context); + expect(result).toBeDefined(); + + if (!result) { + throw new Error(); + } + + expect(result).toEqual({}); + }); +}); + +function verifyTx(tx: ethers.providers.TransactionReceipt) { + expect(tx).toHaveProperty("status", 1); + expect(tx).toHaveProperty("from", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); + expect(tx).toHaveProperty("to", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); + expect(tx).toHaveProperty("transactionHash"); + const txHash = tx.transactionHash; + expect(txHash).toBeDefined(); + expect(txHash).toHaveLength(66); + expect(txHash).not.toEqual("0x" + "0".repeat(64)); +} + +function createContext(repoId: number = 1, payloadSenderId: number = 1, issueOne: number = 1) { const repo = db.repo.findFirst({ where: { id: { equals: repoId } } }) as unknown as Context["payload"]["repository"]; const sender = db.users.findFirst({ where: { id: { equals: payloadSenderId } } }) as unknown as Context["payload"]["sender"]; const issue1 = db.issue.findFirst({ where: { id: { equals: issueOne } } }) as unknown as Context["payload"]["issue"]; - createComment(commentBody, commentId, 3, "test"); - const comment = db.issueComments.findFirst({ where: { id: { equals: commentId } } }); - - const context = createContextInner(repo, sender, issue1, comment); + const context = createContextInner(repo, sender, issue1); const infoSpy = jest.spyOn(context.logger, "info"); const errorSpy = jest.spyOn(context.logger, "error"); const debugSpy = jest.spyOn(context.logger, "debug"); @@ -137,46 +177,8 @@ function createContext(commentBody: string, repoId: number = 1, payloadSenderId: }; } -function createContextInner( - repo: Context["payload"]["repository"], - sender: Context["payload"]["sender"], - issue: Context["payload"]["issue"], - comment: unknown -) { +function createContextInner(repo: Context["payload"]["repository"], sender: Context["payload"]["sender"], issue: Context["payload"]["issue"]) { const ctx: Context = { - eventName: "issue_comment.created", - payload: { - action: "created", - sender: sender, - repository: repo, - issue: issue, - comment, - installation: { id: 1 } as Context["payload"]["installation"], - organization: { login: STRINGS.UBIQUITY } as Context["payload"]["organization"], - } as Context["payload"], - storage: {} as Storage, - logger: new Logs("debug"), - config: { - fundingWalletPrivateKey: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", - networkIds: [100, 1], - nativeGasToken: BigInt(1e18), - howManyTimesUserCanClaim: 1, - // distributionTokens: {} - }, - env: {} as Env, - octokit: octokit, - }; - - ctx.storage = new Storage(ctx); - return ctx; -} - -function createIssuesClosedContext( - repo: Context["payload"]["repository"], - sender: Context["payload"]["sender"], - issue: Context<"issues.closed">["payload"]["issue"] -) { - const ctx: Context<"issues.closed"> = { eventName: "issues.closed", payload: { action: "closed", @@ -185,20 +187,22 @@ function createIssuesClosedContext( issue: issue, installation: { id: 1 } as Context["payload"]["installation"], organization: { login: STRINGS.UBIQUITY } as Context["payload"]["organization"], - }, - storage: {} as Storage, + } as Context["payload"], logger: new Logs("debug"), config: { fundingWalletPrivateKey: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", - networkIds: [1337], - nativeGasToken: BigInt(1e18), - howManyTimesUserCanClaim: 1, - // distributionTokens: {} + networkId: "100", + gasSubsidyAmount: BigInt(1e18), + }, + env: { + SUPABASE_KEY: "test", + SUPABASE_URL: "test", }, - env: {} as Env, octokit: octokit, + adapters: {} as ReturnType, }; - ctx.storage = new Storage(ctx); + ctx.adapters = createAdapters(createClient("http://localhost:8545", "test"), ctx); + return ctx; }