Skip to content

Commit

Permalink
Merge pull request #25 from blockydevs/feat/linkedin-client-posts-aut…
Browse files Browse the repository at this point in the history
…omation-tests

feat: add tests
  • Loading branch information
KacperKoza343 authored Jan 29, 2025
2 parents 873d20a + 3876920 commit 88500e6
Show file tree
Hide file tree
Showing 9 changed files with 725 additions and 16 deletions.
133 changes: 133 additions & 0 deletions packages/client-linkedin/__tests__/LinkedInPostScheduler.test.ts
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 packages/client-linkedin/__tests__/LinkedinFileUploader.test.ts
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 packages/client-linkedin/__tests__/LinkedinPostPublisher.test.ts
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');
});
});
Loading

0 comments on commit 88500e6

Please sign in to comment.