diff --git a/iam/src/index.ts b/iam/src/index.ts index ef669116ed..8a298a79ec 100644 --- a/iam/src/index.ts +++ b/iam/src/index.ts @@ -40,15 +40,15 @@ import { Providers } from "./utils/providers"; // ---- Identity Providers import { SimpleProvider } from "./providers/simple"; import { GoogleProvider } from "./providers/google"; -// import { TwitterProvider } from "./providers/twitter"; + import { TwitterAuthProvider, TwitterFollowerGT100Provider, + TwitterFollowerGT5000Provider, TwitterFollowerGT500Provider, TwitterFollowerGTE1000Provider, - TwitterFollowerGT5000Provider, TwitterTweetGT10Provider, -} from "@gitcoin/passport-platforms"; +} from "@gitcoin/passport-platforms/dist/commonjs/src/Twitter"; import { PohProvider } from "./providers/poh"; import { POAPProvider } from "./providers/poap"; diff --git a/platforms/__tests__/Twitter/twitter.test.ts b/platforms/__tests__/Twitter/twitter.test.ts new file mode 100644 index 0000000000..43ab56d9bc --- /dev/null +++ b/platforms/__tests__/Twitter/twitter.test.ts @@ -0,0 +1,103 @@ +// ---- Test subject +import TwitterProvider from "../../src/Twitter/Providers/TwitterAuthProvider"; + +import { RequestPayload } from "@gitcoin/passport-types"; +import { auth } from "twitter-api-sdk"; +import { + deleteClient, + getClient, + requestFindMyUser, + TwitterFindMyUserResponse, +} from "../../src/Twitter/procedures/twitterOauth"; + +jest.mock("../../src/Twitter/procedures/twitterOauth", () => ({ + getClient: jest.fn(), + deleteClient: jest.fn(), + requestFindMyUser: jest.fn(), +})); + +const MOCK_TWITTER_OAUTH_CLIENT = {} as auth.OAuth2User; + +const MOCK_TWITTER_USER: TwitterFindMyUserResponse = { + id: "123", + name: "Userguy McTesterson", + username: "DpoppDev", +}; + +const sessionKey = "twitter-myOAuthSession"; +const code = "ABC123_ACCESSCODE"; + +beforeEach(() => { + jest.clearAllMocks(); + (getClient as jest.Mock).mockReturnValue(MOCK_TWITTER_OAUTH_CLIENT); +}); + +describe("Attempt verification", function () { + it("handles valid verification attempt", async () => { + (requestFindMyUser as jest.Mock).mockResolvedValue(MOCK_TWITTER_USER); + + const twitter = new TwitterProvider(); + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(getClient).toBeCalledWith(sessionKey); + expect(requestFindMyUser).toBeCalledWith(MOCK_TWITTER_OAUTH_CLIENT, code); + expect(deleteClient).toBeCalledWith(sessionKey); + expect(verifiedPayload).toEqual({ + valid: true, + record: { + username: MOCK_TWITTER_USER.username, + }, + }); + }); + + it("should return invalid payload when unable to retrieve twitter oauth client", async () => { + (getClient as jest.Mock).mockResolvedValueOnce(undefined); + (requestFindMyUser as jest.Mock).mockResolvedValueOnce((client: TwitterFindMyUserResponse | undefined) => { + return client ? MOCK_TWITTER_USER : {}; + }); + + const twitter = new TwitterProvider(); + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + + it("should return invalid payload when there is no username in requestFindMyUser response", async () => { + (requestFindMyUser as jest.Mock).mockResolvedValue({ username: undefined }); + + const twitter = new TwitterProvider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + + it("should return invalid payload when requestFindMyUser throws", async () => { + (requestFindMyUser as jest.Mock).mockRejectedValue("unauthorized"); + + const twitter = new TwitterProvider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); +}); diff --git a/platforms/__tests__/Twitter/twitterFollower.test.ts b/platforms/__tests__/Twitter/twitterFollower.test.ts new file mode 100644 index 0000000000..800f1e4f0f --- /dev/null +++ b/platforms/__tests__/Twitter/twitterFollower.test.ts @@ -0,0 +1,233 @@ +// ---- Test subject +import { + TwitterFollowerGT100Provider, + TwitterFollowerGT500Provider, + TwitterFollowerGTE1000Provider, + TwitterFollowerGT5000Provider, +} from "../../src/Twitter/Providers/TwitterFollowerProvider"; + +import { RequestPayload } from "@gitcoin/passport-types"; +import { auth } from "twitter-api-sdk"; +import { + deleteClient, + getClient, + getFollowerCount, + TwitterFollowerResponse, +} from "../../src/Twitter/procedures/twitterOauth"; + +jest.mock("../../src/Twitter/procedures/twitterOauth", () => ({ + getClient: jest.fn(), + deleteClient: jest.fn(), + getFollowerCount: jest.fn(), +})); + +const MOCK_TWITTER_OAUTH_CLIENT = {} as auth.OAuth2User; + +const MOCK_TWITTER_USER: TwitterFollowerResponse = { + username: "DpoppDev", + followerCount: 200, +}; + +const sessionKey = "twitter-myOAuthSession"; +const code = "ABC123_ACCESSCODE"; + +beforeEach(() => { + jest.clearAllMocks(); + (getClient as jest.Mock).mockReturnValue(MOCK_TWITTER_OAUTH_CLIENT); +}); + +describe("Attempt verification", function () { + it("handles valid verification attempt", async () => { + (getFollowerCount as jest.Mock).mockResolvedValue(MOCK_TWITTER_USER); + + const twitter = new TwitterFollowerGT100Provider(); + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(getClient).toBeCalledWith(sessionKey); + expect(getFollowerCount).toBeCalledWith(MOCK_TWITTER_OAUTH_CLIENT, code); + expect(deleteClient).toBeCalledWith(sessionKey); + expect(verifiedPayload).toEqual({ + valid: true, + record: { + username: "DpoppDev", + followerCount: "gt100", + }, + }); + }); + + it("should return invalid payload when unable to retrieve twitter oauth client", async () => { + (getClient as jest.Mock).mockReturnValue(undefined); + (getFollowerCount as jest.Mock).mockImplementationOnce(async (client) => { + return Promise.resolve(client ? MOCK_TWITTER_USER : {}); + }); + + const twitter = new TwitterFollowerGT100Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + + it("should return invalid payload when there is no username in requestFindMyUser response", async () => { + (getFollowerCount as jest.Mock).mockResolvedValue({ username: undefined }); + + const twitter = new TwitterFollowerGT100Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + + it("should return invalid payload when requestFindMyUser throws", async () => { + (getFollowerCount as jest.Mock).mockRejectedValue("unauthorized"); + + const twitter = new TwitterFollowerGT100Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + + describe("Check invalid cases for follower ranges", function () { + it("Expected Greater than 100 and Follower Count is 50", async () => { + (getFollowerCount as jest.Mock).mockResolvedValue({ followerCount: 50 }); + + const twitter = new TwitterFollowerGT100Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + + it("Expected Greater than 500 and Follower Count is 150", async () => { + (getFollowerCount as jest.Mock).mockResolvedValue({ followerCount: 150 }); + + const twitter = new TwitterFollowerGT500Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + + it("Expected Greater than or equal to 1000 and Follower Count is 900", async () => { + (getFollowerCount as jest.Mock).mockResolvedValue({ followerCount: 900 }); + + const twitter = new TwitterFollowerGTE1000Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + + it("Expected Greater than 5000 and Follower Count is 2500", async () => { + (getFollowerCount as jest.Mock).mockResolvedValue({ followerCount: 2500 }); + + const twitter = new TwitterFollowerGT5000Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + }); + describe("Check valid cases for follower ranges", function () { + it("Expected Greater than 100 and Follower Count is 150", async () => { + (getFollowerCount as jest.Mock).mockResolvedValue({ followerCount: 150 }); + + const twitter = new TwitterFollowerGT100Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: true }); + }); + + it("Expected Greater than 500 and Follower Count is 700", async () => { + (getFollowerCount as jest.Mock).mockResolvedValue({ followerCount: 700 }); + + const twitter = new TwitterFollowerGT500Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: true }); + }); + + it("Expected Greater than or equal to 1000 and Follower Count is 1500", async () => { + (getFollowerCount as jest.Mock).mockResolvedValue({ followerCount: 1500 }); + + const twitter = new TwitterFollowerGTE1000Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: true }); + }); + + it("Expected Greater than 5000 and Follower Count is 7500", async () => { + (getFollowerCount as jest.Mock).mockResolvedValue({ followerCount: 7500 }); + + const twitter = new TwitterFollowerGT5000Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: true }); + }); + }); +}); diff --git a/platforms/__tests__/Twitter/twitterTweets.test.ts b/platforms/__tests__/Twitter/twitterTweets.test.ts new file mode 100644 index 0000000000..2dd06a70d0 --- /dev/null +++ b/platforms/__tests__/Twitter/twitterTweets.test.ts @@ -0,0 +1,133 @@ +// ---- Test subject +import { TwitterTweetGT10Provider } from "../../src/Twitter/Providers/TwitterTweetsProvider"; + +import { RequestPayload } from "@gitcoin/passport-types"; +import { auth } from "twitter-api-sdk"; +import { + deleteClient, + getClient, + getTweetCount, + TwitterTweetResponse, +} from "../../src/Twitter/procedures/twitterOauth"; + +jest.mock("../../src/Twitter/procedures/twitterOauth", () => ({ + getClient: jest.fn(), + deleteClient: jest.fn(), + getTweetCount: jest.fn(), +})); + +const MOCK_TWITTER_OAUTH_CLIENT = {} as auth.OAuth2User; + +const MOCK_TWITTER_USER: TwitterTweetResponse = { + username: "DpoppDev", + tweetCount: 200, +}; + +const sessionKey = "twitter-myOAuthSession"; +const code = "ABC123_ACCESSCODE"; + +beforeEach(() => { + jest.clearAllMocks(); + (getClient as jest.Mock).mockReturnValue(MOCK_TWITTER_OAUTH_CLIENT); +}); + +describe("Attempt verification", function () { + it("handles valid verification attempt", async () => { + (getTweetCount as jest.Mock).mockResolvedValue(MOCK_TWITTER_USER); + + const twitter = new TwitterTweetGT10Provider(); + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(getClient).toBeCalledWith(sessionKey); + expect(getTweetCount).toBeCalledWith(MOCK_TWITTER_OAUTH_CLIENT, code); + expect(deleteClient).toBeCalledWith(sessionKey); + expect(verifiedPayload).toEqual({ + valid: true, + record: { + username: "DpoppDev", + tweetCount: "gt10", + }, + }); + }); + + it("should return invalid payload when unable to retrieve twitter oauth client", async () => { + (getClient as jest.Mock).mockReturnValue(undefined); + (getTweetCount as jest.Mock).mockImplementationOnce(async (client) => { + return Promise.resolve(client ? MOCK_TWITTER_USER : {}); + }); + + const twitter = new TwitterTweetGT10Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + + it("should return invalid payload when there is no username in requestFindMyUser response", async () => { + (getTweetCount as jest.Mock).mockResolvedValue({ username: undefined }); + + const twitter = new TwitterTweetGT10Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + + it("should return invalid payload when requestFindMyUser throws", async () => { + (getTweetCount as jest.Mock).mockRejectedValue("unauthorized"); + + const twitter = new TwitterTweetGT10Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + it("should return invalid payload when tweet count is 5", async () => { + (getTweetCount as jest.Mock).mockResolvedValue({ tweetCount: 5 }); + + const twitter = new TwitterTweetGT10Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: false }); + }); + it("should return valid payload when tweet count is 20", async () => { + (getTweetCount as jest.Mock).mockResolvedValue({ tweetCount: 20 }); + + const twitter = new TwitterTweetGT10Provider(); + + const verifiedPayload = await twitter.verify({ + proofs: { + sessionKey, + code, + }, + } as unknown as RequestPayload); + + expect(verifiedPayload).toMatchObject({ valid: true }); + }); +}); diff --git a/platforms/__tests__/twitterFollower.test.ts b/platforms/__tests__/twitterFollower.test.ts deleted file mode 100644 index fa88b97538..0000000000 --- a/platforms/__tests__/twitterFollower.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe("Attempt verification", function () { - it("should auth with twitter", async () => { - expect(true).toBe(true); - }); -}); diff --git a/platforms/index.ts b/platforms/index.ts new file mode 100644 index 0000000000..04e6dea156 --- /dev/null +++ b/platforms/index.ts @@ -0,0 +1 @@ +export * as Twitter from "./src/Twitter"; diff --git a/platforms/src/Twitter/index.ts b/platforms/src/Twitter/index.ts new file mode 100644 index 0000000000..7d8dfb1312 --- /dev/null +++ b/platforms/src/Twitter/index.ts @@ -0,0 +1,11 @@ +// Twitter Platform +export { TwitterPlatform } from "./App-Bindings"; +export { default as TwitterAuthProvider } from "./Providers/TwitterAuthProvider"; +export { + TwitterFollowerGT100Provider, + TwitterFollowerGT500Provider, + TwitterFollowerGTE1000Provider, + TwitterFollowerGT5000Provider, +} from "./Providers/TwitterFollowerProvider"; +export { TwitterTweetGT10Provider } from "./Providers/TwitterTweetsProvider"; +export { TwitterPlatformDetails, TwitterProviderConfig } from "./Providers-config"; diff --git a/platforms/src/index.ts b/platforms/src/index.ts deleted file mode 100644 index 1f4b5e9e74..0000000000 --- a/platforms/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Twitter Platform -export { TwitterPlatform } from "./Twitter/App-Bindings"; -export { default as TwitterAuthProvider } from "./Twitter/Providers/TwitterAuthProvider"; -export { - TwitterFollowerGT100Provider, - TwitterFollowerGT500Provider, - TwitterFollowerGTE1000Provider, - TwitterFollowerGT5000Provider, -} from "./Twitter/Providers/TwitterFollowerProvider"; -export { TwitterTweetGT10Provider } from "./Twitter/Providers/TwitterTweetsProvider"; -export { TwitterPlatformDetails, TwitterProviderConfig } from "./Twitter/Providers-config"; diff --git a/platforms/tsconfig.json b/platforms/tsconfig.json index 6c0a5ab87b..2719d4a77c 100644 --- a/platforms/tsconfig.json +++ b/platforms/tsconfig.json @@ -17,5 +17,5 @@ }, "jsx": "react" }, - "include": ["./*", "__tests__", "src/platforms-config.ts", "src/procedure-router.ts", "src/Twitter", "Twitter/types.ts", "src/index.ts", "src/Brightid/procedures/brightidProcedures.ts"] + "include": ["src/**/*", "__tests__/**/*", "index.ts"] }