forked from elliotBraem/efizzybot
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33 from PotLock/feat/test-branch
Adds test route and mock twitter submission service
- Loading branch information
Showing
13 changed files
with
558 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,100 @@ | ||
import { Tweet } from "agent-twitter-client"; | ||
import { SearchMode, Tweet } from "agent-twitter-client"; | ||
import { TwitterService } from "../../services/twitter/client"; | ||
import { logger } from "../../utils/logger"; | ||
|
||
export class MockTwitterService { | ||
export class MockTwitterService extends TwitterService { | ||
private mockTweets: Tweet[] = []; | ||
private mockUserIds: Map<string, string> = new Map(); | ||
private lastCheckedTweetId: string | null = null; | ||
private tweetIdCounter: bigint = BigInt(Date.now()); | ||
private testLastCheckedTweetId: string | null = null; | ||
|
||
public addMockTweet(tweet: Tweet) { | ||
this.mockTweets.push(tweet); | ||
constructor() { | ||
// Pass config with the bot's username so mentions are found | ||
super({ | ||
username: "test_bot", | ||
password: "mock_pass", | ||
email: "[email protected]", | ||
}); | ||
// Override the client with a basic mock | ||
(this as any).client = { | ||
isLoggedIn: async () => true, | ||
login: async () => {}, | ||
logout: async () => {}, | ||
getCookies: async () => [], | ||
setCookies: async () => {}, | ||
fetchSearchTweets: async ( | ||
query: string, | ||
count: number, | ||
mode: SearchMode, | ||
) => { | ||
// Filter tweets that match the query (mentions @test_bot) | ||
const matchingTweets = this.mockTweets.filter((tweet) => | ||
tweet.text?.includes("@test_bot"), | ||
); | ||
|
||
// Sort by ID descending (newest first) to match Twitter search behavior | ||
const sortedTweets = [...matchingTweets].sort((a, b) => { | ||
const aId = BigInt(a.id); | ||
const bId = BigInt(b.id); | ||
return bId > aId ? 1 : bId < aId ? -1 : 0; | ||
}); | ||
|
||
return { | ||
tweets: sortedTweets.slice(0, count), | ||
}; | ||
}, | ||
likeTweet: async (tweetId: string) => { | ||
logger.info(`Mock: Liked tweet ${tweetId}`); | ||
return true; | ||
}, | ||
sendTweet: async (message: string, replyToId?: string) => { | ||
const newTweet = this.addMockTweet({ | ||
text: message, | ||
username: "test_bot", | ||
inReplyToStatusId: replyToId, | ||
}); | ||
return { | ||
json: async () => ({ | ||
data: { | ||
create_tweet: { | ||
tweet_results: { | ||
result: { | ||
rest_id: newTweet.id, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}), | ||
} as Response; | ||
}, | ||
}; | ||
} | ||
|
||
private getNextTweetId(): string { | ||
this.tweetIdCounter = this.tweetIdCounter + BigInt(1); | ||
return this.tweetIdCounter.toString(); | ||
} | ||
|
||
public addMockTweet(tweet: Partial<Tweet> & { inReplyToStatusId?: string }) { | ||
const fullTweet: Tweet = { | ||
id: tweet.id || this.getNextTweetId(), | ||
text: tweet.text || "", | ||
username: tweet.username || "test_user", | ||
userId: tweet.userId || `mock-user-id-${tweet.username || "test_user"}`, | ||
timeParsed: tweet.timeParsed || new Date(), | ||
hashtags: tweet.hashtags || [], | ||
mentions: tweet.mentions || [], | ||
photos: tweet.photos || [], | ||
urls: tweet.urls || [], | ||
videos: tweet.videos || [], | ||
thread: [], | ||
inReplyToStatusId: tweet.inReplyToStatusId, | ||
}; | ||
this.mockTweets.push(fullTweet); | ||
logger.info( | ||
`Mock: Added tweet "${fullTweet.text}" from @${fullTweet.username}${tweet.inReplyToStatusId ? ` as reply to ${tweet.inReplyToStatusId}` : ""}`, | ||
); | ||
return fullTweet; | ||
} | ||
|
||
public addMockUserId(username: string, userId: string) { | ||
|
@@ -15,41 +103,77 @@ export class MockTwitterService { | |
|
||
public clearMockTweets() { | ||
this.mockTweets = []; | ||
logger.info("Mock: Cleared all tweets"); | ||
} | ||
|
||
async initialize(): Promise<void> { | ||
// No-op for mock | ||
logger.info("Mock Twitter service initialized"); | ||
} | ||
|
||
async stop(): Promise<void> { | ||
// No-op for mock | ||
logger.info("Mock Twitter service stopped"); | ||
} | ||
|
||
async getUserIdByScreenName(screenName: string): Promise<string> { | ||
return this.mockUserIds.get(screenName) || `mock-user-id-${screenName}`; | ||
} | ||
|
||
async fetchAllNewMentions(): Promise<Tweet[]> { | ||
return this.mockTweets; | ||
} | ||
const BATCH_SIZE = 200; | ||
|
||
async getTweet(tweetId: string): Promise<Tweet | null> { | ||
return this.mockTweets.find((t) => t.id === tweetId) || null; | ||
} | ||
// Get the last tweet ID we processed | ||
const lastCheckedId = this.testLastCheckedTweetId | ||
? BigInt(this.testLastCheckedTweetId) | ||
: null; | ||
|
||
async replyToTweet(tweetId: string, message: string): Promise<string | null> { | ||
return `mock-reply-${Date.now()}`; | ||
} | ||
// Get latest tweets first (up to batch size), excluding already checked tweets | ||
const latestTweets = [...this.mockTweets] | ||
.sort((a, b) => { | ||
const aId = BigInt(a.id); | ||
const bId = BigInt(b.id); | ||
return bId > aId ? -1 : bId < aId ? 1 : 0; // Descending order (newest first) | ||
}) | ||
.filter((tweet) => { | ||
const tweetId = BigInt(tweet.id); | ||
return !lastCheckedId || tweetId > lastCheckedId; | ||
}) | ||
.slice(0, BATCH_SIZE); | ||
|
||
if (latestTweets.length === 0) { | ||
logger.info("No tweets found"); | ||
return []; | ||
} | ||
|
||
// Filter for mentions | ||
const newMentions = latestTweets.filter( | ||
(tweet) => | ||
tweet.text?.includes("@test_bot") || | ||
tweet.mentions?.some((m) => m.username === "test_bot"), | ||
); | ||
|
||
async setLastCheckedTweetId(tweetId: string): Promise<void> { | ||
this.lastCheckedTweetId = tweetId; | ||
// Sort chronologically (oldest to newest) to match real service | ||
newMentions.sort((a, b) => { | ||
const aId = BigInt(a.id); | ||
const bId = BigInt(b.id); | ||
return aId > bId ? 1 : aId < bId ? -1 : 0; | ||
}); | ||
|
||
// Update last checked ID if we found new tweets | ||
if (latestTweets.length > 0) { | ||
// Use the first tweet from latestTweets since it's the newest (they're in descending order) | ||
const highestId = latestTweets[0].id; | ||
await this.setLastCheckedTweetId(highestId); | ||
} | ||
|
||
return newMentions; | ||
} | ||
|
||
async likeTweet(tweetId: string): Promise<void> { | ||
return; | ||
async setLastCheckedTweetId(tweetId: string) { | ||
this.testLastCheckedTweetId = tweetId; | ||
logger.info(`Last checked tweet ID updated to: ${tweetId}`); | ||
} | ||
|
||
getLastCheckedTweetId(): string | null { | ||
return this.lastCheckedTweetId; | ||
async getTweet(tweetId: string): Promise<Tweet | null> { | ||
return this.mockTweets.find((t) => t.id === tweetId) || null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { Tweet } from "agent-twitter-client"; | ||
import { Elysia } from "elysia"; | ||
import { MockTwitterService } from "../__tests__/mocks/twitter-service.mock"; | ||
|
||
// Create a single mock instance to maintain state | ||
const mockTwitterService = new MockTwitterService(); | ||
|
||
// Helper to create a tweet object | ||
const createTweet = ( | ||
id: string, | ||
text: string, | ||
username: string, | ||
inReplyToStatusId?: string, | ||
hashtags?: string[], | ||
): Tweet => { | ||
return { | ||
id, | ||
text, | ||
username, | ||
userId: `mock-user-id-${username}`, | ||
timeParsed: new Date(), | ||
hashtags: hashtags ?? [], | ||
mentions: [], | ||
photos: [], | ||
urls: [], | ||
videos: [], | ||
thread: [], | ||
inReplyToStatusId, | ||
}; | ||
}; | ||
|
||
export const testRoutes = new Elysia({ prefix: "/api/test" }) | ||
.guard({ | ||
beforeHandle: ({ request }) => { | ||
// Only allow in development and test environments | ||
if (process.env.NODE_ENV === "production") { | ||
return new Response("Not found", { status: 404 }); | ||
} | ||
}, | ||
}) | ||
.post("/tweets", async ({ body }) => { | ||
const { id, text, username, inReplyToStatusId, hashtags } = body as { | ||
id: string; | ||
text: string; | ||
username: string; | ||
inReplyToStatusId?: string; | ||
hashtags?: string[]; | ||
}; | ||
const tweet = createTweet(id, text, username, inReplyToStatusId, hashtags); | ||
mockTwitterService.addMockTweet(tweet); | ||
return tweet; | ||
}) | ||
.post("/reset", () => { | ||
mockTwitterService.clearMockTweets(); | ||
return { success: true }; | ||
}); | ||
|
||
// Export for use in tests and for replacing the real service | ||
export { mockTwitterService }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.