forked from elizaOS/eliza
-
Notifications
You must be signed in to change notification settings - Fork 0
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 #25 from blockydevs/feat/linkedin-client-posts-aut…
…omation-tests feat: add tests
- Loading branch information
Showing
9 changed files
with
725 additions
and
16 deletions.
There are no files selected for viewing
133 changes: 133 additions & 0 deletions
133
packages/client-linkedin/__tests__/LinkedInPostScheduler.test.ts
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,133 @@ | ||
import { describe, it, expect, beforeEach, vi } from 'vitest'; | ||
import { LinkedInPostScheduler } from "../src/services/LinkedInPostScheduler"; | ||
import { LinkedInPostPublisher } from "../src/repositories/LinkedinPostPublisher"; | ||
import { LinkedInUserInfoFetcher } from "../src/repositories/LinkedinUserInfoFetcher"; | ||
import { PostContentCreator } from "../src/services/PostContentCreator"; | ||
import { IAgentRuntime } from "@elizaos/core"; | ||
import { PublisherConfig } from "../src/interfaces"; | ||
import axios from "axios"; | ||
|
||
vi.mock("../src/repositories/LinkedinPostPublisher"); | ||
vi.mock("../src/services/PostContentCreator"); | ||
vi.useFakeTimers(); | ||
|
||
describe("LinkedInPostScheduler", () => { | ||
let mockRuntime: IAgentRuntime; | ||
let mockPostPublisher: LinkedInPostPublisher; | ||
let mockPostContentCreator: PostContentCreator; | ||
let mockUserInfoFetcher: LinkedInUserInfoFetcher; | ||
let config: PublisherConfig; | ||
|
||
beforeEach(() => { | ||
mockRuntime = { | ||
cacheManager: { | ||
get: vi.fn(), | ||
set: vi.fn(), | ||
}, | ||
messageManager: { | ||
createMemory: vi.fn(), | ||
}, | ||
agentId: "test-agent-id", | ||
} as unknown as IAgentRuntime; | ||
|
||
mockPostPublisher = new LinkedInPostPublisher(axios.create(), "test-user"); | ||
vi.spyOn(mockPostPublisher, 'publishPost'); | ||
|
||
mockPostContentCreator = new PostContentCreator(mockRuntime); | ||
vi.spyOn(mockPostContentCreator, 'createPostContent'); | ||
|
||
mockUserInfoFetcher = { | ||
getUserInfo: vi.fn().mockResolvedValue({ sub: "test-user" }), | ||
} as unknown as LinkedInUserInfoFetcher; | ||
|
||
config = { | ||
LINKEDIN_POST_INTERVAL_MIN: 60, | ||
LINKEDIN_POST_INTERVAL_MAX: 120, | ||
LINKEDIN_DRY_RUN: false, | ||
}; | ||
}); | ||
|
||
describe("createPostScheduler", () => { | ||
it("should create a new instance of LinkedInPostScheduler", async () => { | ||
const scheduler = await LinkedInPostScheduler.createPostScheduler({ | ||
axiosInstance: axios.create(), | ||
userInfoFetcher: mockUserInfoFetcher, | ||
runtime: mockRuntime, | ||
config, | ||
}); | ||
|
||
expect(scheduler).toBeInstanceOf(LinkedInPostScheduler); | ||
expect(mockUserInfoFetcher.getUserInfo).toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe("createPostPublicationLoop", () => { | ||
let scheduler: LinkedInPostScheduler; | ||
|
||
beforeEach(() => { | ||
scheduler = new LinkedInPostScheduler( | ||
mockRuntime, | ||
mockPostPublisher, | ||
mockPostContentCreator, | ||
"test-user", | ||
config | ||
); | ||
}); | ||
|
||
it("should publish post when enough time has passed", async () => { | ||
vi.spyOn(mockRuntime.cacheManager, 'get').mockResolvedValue(null); | ||
vi.spyOn(mockPostContentCreator, 'createPostContent').mockResolvedValue("Test post content"); | ||
vi.spyOn(mockPostPublisher, 'publishPost').mockResolvedValue(); | ||
|
||
await scheduler.createPostPublicationLoop(); | ||
|
||
expect(mockPostContentCreator.createPostContent).toHaveBeenCalledWith("test-user"); | ||
expect(mockPostPublisher.publishPost).toHaveBeenCalledWith({ | ||
postText: "Test post content", | ||
}); | ||
expect(mockRuntime.cacheManager.set).toHaveBeenCalled(); | ||
expect(mockRuntime.messageManager.createMemory).toHaveBeenCalled(); | ||
}); | ||
|
||
it("should not publish post when in dry run mode", async () => { | ||
const dryRunConfig = { ...config, LINKEDIN_DRY_RUN: true }; | ||
scheduler = new LinkedInPostScheduler( | ||
mockRuntime, | ||
mockPostPublisher, | ||
mockPostContentCreator, | ||
"test-user", | ||
dryRunConfig | ||
); | ||
|
||
vi.spyOn(mockRuntime.cacheManager, 'get').mockResolvedValue(null); | ||
vi.spyOn(mockPostContentCreator, 'createPostContent').mockResolvedValue("Test post content"); | ||
|
||
await scheduler.createPostPublicationLoop(); | ||
|
||
expect(mockPostContentCreator.createPostContent).toHaveBeenCalled(); | ||
expect(mockPostPublisher.publishPost).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("should not publish post when not enough time has passed", async () => { | ||
const currentTime = Date.now(); | ||
vi.spyOn(mockRuntime.cacheManager, 'get').mockResolvedValue({ | ||
timestamp: currentTime, | ||
}); | ||
|
||
await scheduler.createPostPublicationLoop(); | ||
|
||
expect(mockPostContentCreator.createPostContent).not.toHaveBeenCalled(); | ||
expect(mockPostPublisher.publishPost).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("should schedule next execution", async () => { | ||
const setTimeoutSpy = vi.spyOn(global, 'setTimeout'); | ||
vi.spyOn(mockRuntime.cacheManager, 'get').mockResolvedValue(null); | ||
vi.spyOn(mockPostContentCreator, 'createPostContent').mockResolvedValue("Test post content"); | ||
|
||
await scheduler.createPostPublicationLoop(); | ||
|
||
expect(setTimeoutSpy).toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
107 changes: 107 additions & 0 deletions
107
packages/client-linkedin/__tests__/LinkedinFileUploader.test.ts
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,107 @@ | ||
import { describe, it, expect, vi } from 'vitest'; | ||
import { LinkedInFileUploader } from '../src/repositories/LinkedinFileUploader'; | ||
import { AxiosInstance, AxiosError } from 'axios'; | ||
import { API_VERSION, API_VERSION_HEADER } from '../src/interfaces'; | ||
|
||
describe('LinkedInFileUploader', () => { | ||
const mockAxios = { | ||
post: vi.fn(), | ||
put: vi.fn(), | ||
} as unknown as AxiosInstance; | ||
|
||
const userId = 'test-user-id'; | ||
const uploader = new LinkedInFileUploader(mockAxios, userId); | ||
|
||
it('should upload asset successfully', async () => { | ||
const mockUploadUrl = 'https://example.com/upload'; | ||
const mockImageId = 'image123'; | ||
const mockBlob = new Blob(['test'], { type: 'image/jpeg' }); | ||
|
||
vi.mocked(mockAxios.post).mockResolvedValueOnce({ | ||
data: { | ||
value: { | ||
uploadUrl: mockUploadUrl, | ||
image: mockImageId, | ||
uploadUrlExpiresAt: Date.now() + 3600000, | ||
}, | ||
}, | ||
}); | ||
|
||
vi.mocked(mockAxios.put).mockResolvedValueOnce({}); | ||
|
||
const result = await uploader.uploadAsset(mockBlob); | ||
|
||
expect(mockAxios.post).toHaveBeenCalledWith( | ||
'/rest/images', | ||
{ | ||
initializeUploadRequest: { | ||
owner: `urn:li:person:${userId}`, | ||
}, | ||
}, | ||
{ | ||
headers: { | ||
[API_VERSION_HEADER]: [API_VERSION], | ||
}, | ||
params: { | ||
action: 'initializeUpload', | ||
}, | ||
} | ||
); | ||
|
||
expect(mockAxios.put).toHaveBeenCalledWith( | ||
mockUploadUrl, | ||
mockBlob, | ||
{ | ||
headers: { | ||
'Content-Type': 'application/octet-stream', | ||
}, | ||
} | ||
); | ||
|
||
expect(result).toBe(mockImageId); | ||
}); | ||
|
||
it('should handle create upload url error', async () => { | ||
const error = new AxiosError( | ||
'Network Error', | ||
'ERR_NETWORK', | ||
undefined, | ||
undefined, | ||
{ | ||
status: 500, | ||
data: { message: 'Server Error' }, | ||
} as any | ||
); | ||
|
||
vi.mocked(mockAxios.post).mockRejectedValueOnce(error); | ||
|
||
const mockBlob = new Blob(['test'], { type: 'image/jpeg' }); | ||
|
||
await expect(uploader.uploadAsset(mockBlob)) | ||
.rejects | ||
.toThrow('Failed create media upload url'); | ||
}); | ||
|
||
it('should handle upload media error', async () => { | ||
const mockUploadUrl = 'https://example.com/upload'; | ||
const mockImageId = 'image123'; | ||
const mockBlob = new Blob(['test'], { type: 'image/jpeg' }); | ||
|
||
vi.mocked(mockAxios.post).mockResolvedValueOnce({ | ||
data: { | ||
value: { | ||
uploadUrl: mockUploadUrl, | ||
image: mockImageId, | ||
uploadUrlExpiresAt: Date.now() + 3600000, | ||
}, | ||
}, | ||
}); | ||
|
||
const error = new Error('Upload failed'); | ||
vi.mocked(mockAxios.put).mockRejectedValueOnce(error); | ||
|
||
await expect(uploader.uploadAsset(mockBlob)) | ||
.rejects | ||
.toThrow('Failed to upload media: Error: Upload failed'); | ||
}); | ||
}); |
97 changes: 97 additions & 0 deletions
97
packages/client-linkedin/__tests__/LinkedinPostPublisher.test.ts
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,97 @@ | ||
import { describe, it, expect, vi } from 'vitest'; | ||
import { LinkedInPostPublisher } from '../src/repositories/LinkedinPostPublisher'; | ||
import { AxiosInstance, AxiosError } from 'axios'; | ||
import { API_VERSION, API_VERSION_HEADER } from '../src/interfaces'; | ||
|
||
describe('LinkedInPostPublisher', () => { | ||
const mockAxios = { | ||
post: vi.fn(), | ||
} as unknown as AxiosInstance; | ||
|
||
const userId = 'test-user-id'; | ||
const publisher = new LinkedInPostPublisher(mockAxios, userId); | ||
|
||
it('should publish text-only post successfully', async () => { | ||
const postText = 'Test post content'; | ||
|
||
await publisher.publishPost({ postText }); | ||
|
||
expect(mockAxios.post).toHaveBeenCalledWith( | ||
'/rest/posts', | ||
{ | ||
author: `urn:li:person:${userId}`, | ||
commentary: postText, | ||
visibility: 'PUBLIC', | ||
distribution: { | ||
feedDistribution: 'MAIN_FEED', | ||
targetEntities: [], | ||
thirdPartyDistributionChannels: [], | ||
}, | ||
lifecycleState: 'PUBLISHED', | ||
isReshareDisabledByAuthor: false, | ||
}, | ||
{ | ||
headers: { | ||
[API_VERSION_HEADER]: [API_VERSION], | ||
}, | ||
} | ||
); | ||
}); | ||
|
||
it('should publish post with media successfully', async () => { | ||
const postText = 'Test post with media'; | ||
const media = { id: 'media-id', title: 'media-title' }; | ||
|
||
await publisher.publishPost({ postText, media }); | ||
|
||
expect(mockAxios.post).toHaveBeenCalledWith( | ||
'/rest/posts', | ||
{ | ||
author: `urn:li:person:${userId}`, | ||
commentary: postText, | ||
visibility: 'PUBLIC', | ||
distribution: { | ||
feedDistribution: 'MAIN_FEED', | ||
targetEntities: [], | ||
thirdPartyDistributionChannels: [], | ||
}, | ||
lifecycleState: 'PUBLISHED', | ||
isReshareDisabledByAuthor: false, | ||
content: { media }, | ||
}, | ||
{ | ||
headers: { | ||
[API_VERSION_HEADER]: [API_VERSION], | ||
}, | ||
} | ||
); | ||
}); | ||
|
||
it('should handle axios error', async () => { | ||
const error = new AxiosError( | ||
'Network Error', | ||
'ERR_NETWORK', | ||
undefined, | ||
undefined, | ||
{ | ||
status: 500, | ||
data: { message: 'Internal Server Error' }, | ||
} as any | ||
); | ||
|
||
vi.mocked(mockAxios.post).mockRejectedValueOnce(error); | ||
|
||
await expect(publisher.publishPost({ postText: 'Test post' })) | ||
.rejects | ||
.toThrow('Failed to publish LinkedIn post'); | ||
}); | ||
|
||
it('should handle non-axios error', async () => { | ||
const error = new Error('Unexpected error'); | ||
vi.mocked(mockAxios.post).mockRejectedValueOnce(error); | ||
|
||
await expect(publisher.publishPost({ postText: 'Test post' })) | ||
.rejects | ||
.toThrow('Failed to publish LinkedIn post: Error: Unexpected error'); | ||
}); | ||
}); |
Oops, something went wrong.