diff --git a/meta/testing/jest.config.cjs b/meta/testing/jest.config.cjs index ed3115a..535bb50 100644 --- a/meta/testing/jest.config.cjs +++ b/meta/testing/jest.config.cjs @@ -1,8 +1,9 @@ module.exports = { collectCoverageFrom: ["src/**/*.ts", "!**/__tests__/**/*"], coveragePathIgnorePatterns : [ + "/src/index.ts", "/src/constants.ts", - "/src/index.ts" + "/src/helpers/cache/migrations/index.ts" ], moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1', diff --git a/src/__tests__/__mocks__/ora.ts b/src/__tests__/__mocks__/ora.ts index 95747bc..44b2b16 100644 --- a/src/__tests__/__mocks__/ora.ts +++ b/src/__tests__/__mocks__/ora.ts @@ -1,12 +1,19 @@ +const methods = { + fail: jest.fn(), + info: jest.fn(), + stop: jest.fn(), + succeed: jest.fn(), + warn: jest.fn(), +}; + module.exports = { name: "ora", default: jest.fn(() => ({ - start: jest - .fn() - .mockImplementation(() => ({ text: "", succeed: jest.fn() })), - succeed: jest.fn(), - fail: jest.fn(), - warn: jest.fn(), + start: jest.fn().mockImplementation(() => ({ + text: "", + ...methods, + })), + ...methods, })), __esModule: true, }; diff --git a/src/configuration/__tests__/build-configuration-rules.spec.ts b/src/configuration/__tests__/build-configuration-rules.spec.ts new file mode 100644 index 0000000..37ca9e4 --- /dev/null +++ b/src/configuration/__tests__/build-configuration-rules.spec.ts @@ -0,0 +1,21 @@ +import { buildConfigurationRules } from "../build-configuration-rules.js"; + +jest.mock("../../constants.js", () => ({ + BLUESKY_IDENTIFIER: "username", + BLUESKY_INSTANCE: "bsky.social", + BLUESKY_PASSWORD: "app-password", + MASTODON_ACCESS_TOKEN: "access-token", + MASTODON_INSTANCE: "mastodon.social", + SYNC_BLUESKY: true, + SYNC_MASTODON: true, + TWITTER_HANDLE: "username", +})); + +describe("buildConfigurationRules", () => { + it("should return an array of configuration rules", () => { + const result = buildConfigurationRules(); + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(Array); + expect(result).toHaveLength(6); + }); +}); diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index aeda525..e6da580 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -19,7 +19,7 @@ import { TOUITOMAMOUT_VERSION, TWITTER_HANDLE, } from "../constants.js"; -import { handleTwitterAuth } from "../helpers/auth/index.js"; +import { handleTwitterAuth } from "../helpers/auth/handle-twitter-auth.js"; import { createCacheFile } from "../helpers/cache/create-cache.js"; import { getCachedPosts } from "../helpers/cache/get-cached-posts.js"; import { runMigrations } from "../helpers/cache/run-migrations.js"; diff --git a/src/helpers/auth/__tests__/handle-twitter-auth.spec.ts b/src/helpers/auth/__tests__/handle-twitter-auth.spec.ts new file mode 100644 index 0000000..ccf9617 --- /dev/null +++ b/src/helpers/auth/__tests__/handle-twitter-auth.spec.ts @@ -0,0 +1,77 @@ +import { Scraper } from "@the-convocation/twitter-scraper"; + +import { handleTwitterAuth } from "../handle-twitter-auth.js"; +import { restorePreviousSession } from "../restore-previous-session.js"; + +const constantsMock = jest.requireMock("../../../constants.js"); +jest.mock("../../../constants.js", () => ({})); +jest.mock("../restore-previous-session.js", () => ({ + restorePreviousSession: jest.fn(), +})); + +const restorePreviousSessionSpy = restorePreviousSession as jest.Mock; + +const isLoggedInSpy = jest.fn(); +const loginSpy = jest.fn(); +const getCookiesSpy = jest.fn(); + +const twitterClient = { + isLoggedIn: isLoggedInSpy, + login: loginSpy, + getCookies: getCookiesSpy, +} as unknown as Scraper; + +describe("handleTwitterAuth", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("when constants are not set", () => { + beforeEach(() => { + constantsMock.TWITTER_USERNAME = undefined; + constantsMock.TWITTER_PASSWORD = undefined; + }); + + it("should not log in", async () => { + const result = await handleTwitterAuth(twitterClient); + + expect(restorePreviousSessionSpy).not.toHaveBeenCalled(); + expect(loginSpy).not.toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); + }); + + describe("when constants are set", () => { + beforeEach(() => { + constantsMock.TWITTER_USERNAME = "username"; + constantsMock.TWITTER_PASSWORD = "password"; + }); + + describe("when cookies are set", () => { + beforeEach(() => { + getCookiesSpy.mockResolvedValue(["cookies"]); + }); + + it("should restore the previous session", async () => { + await handleTwitterAuth(twitterClient); + + expect(restorePreviousSessionSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe("when cookies are not set", () => { + beforeEach(() => { + getCookiesSpy.mockResolvedValue(undefined); + }); + + it("should login", async () => { + await handleTwitterAuth(twitterClient); + + expect(restorePreviousSessionSpy).toHaveBeenCalledTimes(1); + expect(isLoggedInSpy).toHaveBeenCalledTimes(2); + expect(loginSpy).toHaveBeenCalledTimes(1); + expect(loginSpy).toHaveBeenCalledWith("username", "password"); + }); + }); + }); +}); diff --git a/src/helpers/auth/index.ts b/src/helpers/auth/index.ts deleted file mode 100644 index 9f3b633..0000000 --- a/src/helpers/auth/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./handle-twitter-auth.js"; diff --git a/src/helpers/cache/__tests__/run-migrations.spec.ts b/src/helpers/cache/__tests__/run-migrations.spec.ts new file mode 100644 index 0000000..b13cc23 --- /dev/null +++ b/src/helpers/cache/__tests__/run-migrations.spec.ts @@ -0,0 +1,36 @@ +import { runMigrations } from "../run-migrations.js"; +import { writeToCacheFile } from "../write-to-cache-file.js"; + +jest.mock("../write-to-cache-file.js", () => ({ + writeToCacheFile: jest.fn(), +})); + +jest.mock("../migrations/index.js", () => ({ + __esModule: true, + default: [ + jest.fn().mockImplementation(() => Promise.resolve()), + jest.fn().mockImplementation(() => Promise.resolve()), + ], +})); + +jest.mock("../get-cache.js", () => ({ + getCache: jest.fn().mockResolvedValue({ + posts: {}, + profile: {}, + instance: {}, + }), +})); + +describe("runMigrations", () => { + it("should run all migrations", async () => { + await runMigrations(); + expect(writeToCacheFile).toHaveBeenCalledTimes(2); + }); + + describe("when a migration fails", () => { + it("should throw an error", async () => { + (writeToCacheFile as jest.Mock).mockRejectedValueOnce(new Error("test")); + await expect(runMigrations()).rejects.toThrow(); + }); + }); +}); diff --git a/src/helpers/post/__tests__/make-bluesky-post.spec.ts b/src/helpers/post/__tests__/make-bluesky-post.spec.ts index 298d2bc..f0f93f9 100644 --- a/src/helpers/post/__tests__/make-bluesky-post.spec.ts +++ b/src/helpers/post/__tests__/make-bluesky-post.spec.ts @@ -1,7 +1,6 @@ import { AppBskyFeedPost, BskyAgent } from "@atproto/api"; import { makeTweetMock } from "../../../services/__tests__/helpers/make-tweet-mock.js"; -import { getCachedPostChunk } from "../../cache/get-cached-post-chunk.js"; import { makeBlueskyPost } from "../make-bluesky-post.js"; jest.mock("../../../constants.js", () => ({ diff --git a/src/helpers/post/__tests__/make-post.spec.ts b/src/helpers/post/__tests__/make-post.spec.ts new file mode 100644 index 0000000..d55a0ca --- /dev/null +++ b/src/helpers/post/__tests__/make-post.spec.ts @@ -0,0 +1,43 @@ +import { BskyAgent } from "@atproto/api"; +import { mastodon } from "masto"; +import ora from "ora"; + +import { makeTweetMock } from "../../../services/__tests__/helpers/make-tweet-mock.js"; +import { makeBlueskyPost } from "../make-bluesky-post.js"; +import { makeMastodonPost } from "../make-mastodon-post.js"; +import { makePost } from "../make-post.js"; + +jest.mock("../../../constants.js", () => ({})); +jest.mock("../make-mastodon-post.js", () => ({ + makeMastodonPost: jest.fn(), +})); +jest.mock("../make-bluesky-post.js", () => ({ + makeBlueskyPost: jest.fn(), +})); + +const mastodonClient = {} as unknown as mastodon.rest.Client; +const blueskyClient = {} as unknown as BskyAgent; + +const tweet = makeTweetMock(); +const madePostMock = { + tweet, + chunks: [tweet.text], + username: "username", +}; + +(makeMastodonPost as jest.Mock).mockResolvedValue(madePostMock); +(makeBlueskyPost as jest.Mock).mockResolvedValue(madePostMock); + +describe("makePost", () => { + it("should make a post", async () => { + const result = await makePost(tweet, mastodonClient, blueskyClient, ora(), { + current: 0, + total: 0, + }); + + expect(result).toStrictEqual({ + mastodon: madePostMock, + bluesky: madePostMock, + }); + }); +}); diff --git a/src/services/__tests__/posts-synchronizer-service.spec.ts b/src/services/__tests__/posts-synchronizer-service.spec.ts new file mode 100644 index 0000000..3b0b5ce --- /dev/null +++ b/src/services/__tests__/posts-synchronizer-service.spec.ts @@ -0,0 +1,80 @@ +import { BskyAgent } from "@atproto/api"; +import Counter from "@pm2/io/build/main/utils/metrics/counter.js"; +import { Scraper } from "@the-convocation/twitter-scraper"; +import { mastodon } from "masto"; + +import { blueskySenderService } from "../bluesky-sender.service.js"; +import { mastodonSenderService } from "../mastodon-sender.service.js"; +import { postsSynchronizerService } from "../posts-synchronizer.service.js"; +import { MockTwitterClient } from "./mocks/twitter-client.js"; + +jest.mock("../../constants.js", () => ({})); + +jest.mock("../../helpers/cache/get-cached-posts.js", () => { + return { + getCachedPosts: jest.fn().mockResolvedValue({ + "1234567891234567891": {}, + "1234567891234567892": {}, + "1234567891234567893": {}, + }), + }; +}); + +jest.mock("../../helpers/post/make-post.js", () => ({ + makePost: jest.fn().mockImplementation((tweet) => ({ + mastodon: { + tweet, + chunks: [tweet.text], + username: "username", + }, + bluesky: { + tweet, + chunks: [tweet.text], + username: "username", + }, + })), +})); + +jest.mock("../bluesky-sender.service.js", () => ({ + blueskySenderService: jest.fn(), +})); +jest.mock("../mastodon-sender.service.js", () => ({ + mastodonSenderService: jest.fn(), +})); + +const mastodonSenderServiceMock = ( + mastodonSenderService as jest.Mock +).mockImplementation(() => Promise.resolve()); +const blueskySenderServiceMock = ( + blueskySenderService as jest.Mock +).mockImplementation(() => Promise.resolve()); + +describe("postsSynchronizerService", () => { + it("should return a response with the expected shape", async () => { + const twitterClient = new MockTwitterClient(3) as unknown as Scraper; + const mastodonClient = {} as mastodon.rest.Client; + const blueskyClient = {} as BskyAgent; + const synchronizedPostsCountThisRun = { + inc: jest.fn(), + } as unknown as Counter.default; + + const response = await postsSynchronizerService( + twitterClient, + mastodonClient, + blueskyClient, + synchronizedPostsCountThisRun, + ); + + expect(mastodonSenderServiceMock).toHaveBeenCalledTimes(3); + expect(blueskySenderServiceMock).toHaveBeenCalledTimes(3); + expect(response).toStrictEqual({ + twitterClient, + mastodonClient, + blueskyClient, + metrics: { + totalSynced: 3, + justSynced: 3, + }, + }); + }); +}); diff --git a/src/services/posts-synchronizer.service.ts b/src/services/posts-synchronizer.service.ts index 7454dd1..c881471 100644 --- a/src/services/posts-synchronizer.service.ts +++ b/src/services/posts-synchronizer.service.ts @@ -10,8 +10,8 @@ import { oraPrefixer } from "../helpers/logs/index.js"; import { makePost } from "../helpers/post/make-post.js"; import { Media, Metrics, SynchronizerResponse } from "../types/index.js"; import { blueskySenderService } from "./bluesky-sender.service.js"; -import { tweetsGetterService } from "./index.js"; import { mastodonSenderService } from "./mastodon-sender.service.js"; +import { tweetsGetterService } from "./tweets-getter.service.js"; /** * An async method in charge of dispatching posts synchronization tasks for each received tweets.