feat(tests): Add integration tests for blog scenario
- Implement basic operations tests for user, category, post, and comment management. - Create cross-node operations tests to verify distributed content creation and concurrent operations. - Introduce Jest configuration and setup files for testing environment. - Enhance BlogTestHelper with methods for user, category, post, and comment operations. - Ensure data consistency and network metrics verification across nodes.
This commit is contained in:
parent
e6636c8850
commit
8c8a19ab5f
@ -545,6 +545,11 @@ export abstract class BaseModel {
|
|||||||
private applyFieldDefaults(): void {
|
private applyFieldDefaults(): void {
|
||||||
const modelClass = this.constructor as typeof BaseModel;
|
const modelClass = this.constructor as typeof BaseModel;
|
||||||
|
|
||||||
|
// Ensure we have fields map
|
||||||
|
if (!modelClass.fields) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const [fieldName, fieldConfig] of modelClass.fields) {
|
for (const [fieldName, fieldConfig] of modelClass.fields) {
|
||||||
if (fieldConfig.default !== undefined) {
|
if (fieldConfig.default !== undefined) {
|
||||||
const privateKey = `_${fieldName}`;
|
const privateKey = `_${fieldName}`;
|
||||||
|
@ -3,30 +3,37 @@ import { BaseModel } from '../BaseModel';
|
|||||||
|
|
||||||
export function Field(config: FieldConfig) {
|
export function Field(config: FieldConfig) {
|
||||||
return function (target: any, propertyKey: string) {
|
return function (target: any, propertyKey: string) {
|
||||||
// When decorators are used in an ES module context, the `target` for a property decorator
|
// Validate field configuration
|
||||||
// can be undefined. We need to defer the Object.defineProperty call until we have
|
validateFieldConfig(config);
|
||||||
// a valid target. We can achieve this by replacing the original decorator with one
|
|
||||||
// that captures the config and applies it later.
|
|
||||||
|
|
||||||
// This is a workaround for the decorator context issue.
|
// Get the constructor function
|
||||||
const decorator = (instance: any) => {
|
const ctor = target.constructor as typeof BaseModel;
|
||||||
if (!Object.getOwnPropertyDescriptor(instance, propertyKey)) {
|
|
||||||
Object.defineProperty(instance, propertyKey, {
|
// Initialize fields map if it doesn't exist
|
||||||
get() {
|
if (!ctor.hasOwnProperty('fields')) {
|
||||||
const privateKey = `_${propertyKey}`;
|
const parentFields = ctor.fields ? new Map(ctor.fields) : new Map();
|
||||||
// Use the reliable getFieldValue method if available, otherwise fallback to private key
|
Object.defineProperty(ctor, 'fields', {
|
||||||
if (this.getFieldValue && typeof this.getFieldValue === 'function') {
|
value: parentFields,
|
||||||
return this.getFieldValue(propertyKey);
|
writable: true,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to direct private key access
|
// Store field configuration
|
||||||
|
ctor.fields.set(propertyKey, config);
|
||||||
|
|
||||||
|
// Define property on the prototype
|
||||||
|
Object.defineProperty(target, propertyKey, {
|
||||||
|
get() {
|
||||||
|
const privateKey = `_${propertyKey}`;
|
||||||
return this[privateKey];
|
return this[privateKey];
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
const ctor = this.constructor as typeof BaseModel;
|
|
||||||
const privateKey = `_${propertyKey}`;
|
const privateKey = `_${propertyKey}`;
|
||||||
|
const ctor = this.constructor as typeof BaseModel;
|
||||||
|
|
||||||
// One-time initialization of the fields map on the constructor
|
// Ensure fields map exists on the constructor
|
||||||
if (!ctor.hasOwnProperty('fields')) {
|
if (!ctor.hasOwnProperty('fields')) {
|
||||||
const parentFields = ctor.fields ? new Map(ctor.fields) : new Map();
|
const parentFields = ctor.fields ? new Map(ctor.fields) : new Map();
|
||||||
Object.defineProperty(ctor, 'fields', {
|
Object.defineProperty(ctor, 'fields', {
|
||||||
@ -46,7 +53,6 @@ export function Field(config: FieldConfig) {
|
|||||||
const transformedValue = config.transform ? config.transform(value) : value;
|
const transformedValue = config.transform ? config.transform(value) : value;
|
||||||
|
|
||||||
// Only validate non-required constraints during assignment
|
// Only validate non-required constraints during assignment
|
||||||
// Required field validation will happen during save()
|
|
||||||
const validationResult = validateFieldValueNonRequired(
|
const validationResult = validateFieldValueNonRequired(
|
||||||
transformedValue,
|
transformedValue,
|
||||||
config,
|
config,
|
||||||
@ -73,19 +79,6 @@ export function Field(config: FieldConfig) {
|
|||||||
enumerable: true,
|
enumerable: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// We need to apply this to the prototype. Since target is undefined,
|
|
||||||
// we can't do it directly. Instead, we can rely on the class constructor's
|
|
||||||
// prototype, which will be available when the class is instantiated.
|
|
||||||
// A common pattern is to add the decorator logic to a static array on the constructor
|
|
||||||
// and apply them in the base model constructor.
|
|
||||||
|
|
||||||
// Let's try a simpler approach for now by checking the target.
|
|
||||||
if (target) {
|
|
||||||
decorator(target);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,14 +91,29 @@ function createRelationshipProperty(
|
|||||||
propertyKey: string,
|
propertyKey: string,
|
||||||
config: RelationshipConfig,
|
config: RelationshipConfig,
|
||||||
): void {
|
): void {
|
||||||
// In an ES module context, `target` can be undefined when decorators are first evaluated.
|
// Get the constructor function
|
||||||
// We must ensure we only call Object.defineProperty on a valid object (the class prototype).
|
const ctor = target.constructor as typeof BaseModel;
|
||||||
if (target) {
|
|
||||||
|
// Initialize relationships map if it doesn't exist
|
||||||
|
if (!ctor.hasOwnProperty('relationships')) {
|
||||||
|
const parentRelationships = ctor.relationships ? new Map(ctor.relationships) : new Map();
|
||||||
|
Object.defineProperty(ctor, 'relationships', {
|
||||||
|
value: parentRelationships,
|
||||||
|
writable: true,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store relationship configuration
|
||||||
|
ctor.relationships.set(propertyKey, config);
|
||||||
|
|
||||||
|
// Define property on the prototype
|
||||||
Object.defineProperty(target, propertyKey, {
|
Object.defineProperty(target, propertyKey, {
|
||||||
get() {
|
get() {
|
||||||
const ctor = this.constructor as typeof BaseModel;
|
const ctor = this.constructor as typeof BaseModel;
|
||||||
|
|
||||||
// One-time initialization of the relationships map on the constructor
|
// Ensure relationships map exists on the constructor
|
||||||
if (!ctor.hasOwnProperty('relationships')) {
|
if (!ctor.hasOwnProperty('relationships')) {
|
||||||
const parentRelationships = ctor.relationships ? new Map(ctor.relationships) : new Map();
|
const parentRelationships = ctor.relationships ? new Map(ctor.relationships) : new Map();
|
||||||
Object.defineProperty(ctor, 'relationships', {
|
Object.defineProperty(ctor, 'relationships', {
|
||||||
@ -131,7 +146,7 @@ function createRelationshipProperty(
|
|||||||
set(value) {
|
set(value) {
|
||||||
const ctor = this.constructor as typeof BaseModel;
|
const ctor = this.constructor as typeof BaseModel;
|
||||||
|
|
||||||
// One-time initialization of the relationships map on the constructor
|
// Ensure relationships map exists on the constructor
|
||||||
if (!ctor.hasOwnProperty('relationships')) {
|
if (!ctor.hasOwnProperty('relationships')) {
|
||||||
const parentRelationships = ctor.relationships ? new Map(ctor.relationships) : new Map();
|
const parentRelationships = ctor.relationships ? new Map(ctor.relationships) : new Map();
|
||||||
Object.defineProperty(ctor, 'relationships', {
|
Object.defineProperty(ctor, 'relationships', {
|
||||||
@ -157,7 +172,6 @@ function createRelationshipProperty(
|
|||||||
configurable: true,
|
configurable: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Utility function to get relationship configuration
|
// Utility function to get relationship configuration
|
||||||
export function getRelationshipConfig(
|
export function getRelationshipConfig(
|
||||||
|
@ -0,0 +1,236 @@
|
|||||||
|
import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
|
||||||
|
import { blogTestHelper, TestUser, TestCategory, TestPost, TestComment } from './setup';
|
||||||
|
|
||||||
|
describe('Blog Basic Operations', () => {
|
||||||
|
let testUser: TestUser;
|
||||||
|
let testCategory: TestCategory;
|
||||||
|
let testPost: TestPost;
|
||||||
|
let testComment: TestComment;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
console.log('🔄 Waiting for all nodes to be ready...');
|
||||||
|
await blogTestHelper.waitForNodesReady();
|
||||||
|
console.log('✅ All nodes are ready for testing');
|
||||||
|
}, 60000); // 60 second timeout for setup
|
||||||
|
|
||||||
|
describe('User Management', () => {
|
||||||
|
test('should create a user successfully', async () => {
|
||||||
|
const testData = blogTestHelper.generateTestData();
|
||||||
|
|
||||||
|
testUser = await blogTestHelper.createUser(testData.user);
|
||||||
|
|
||||||
|
expect(testUser).toBeDefined();
|
||||||
|
expect(testUser.id).toBeDefined();
|
||||||
|
expect(testUser.username).toBe(testData.user.username);
|
||||||
|
expect(testUser.email).toBe(testData.user.email);
|
||||||
|
expect(testUser.displayName).toBe(testData.user.displayName);
|
||||||
|
|
||||||
|
console.log(`✅ Created user: ${testUser.username} (${testUser.id})`);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
test('should retrieve user by ID from all nodes', async () => {
|
||||||
|
expect(testUser?.id).toBeDefined();
|
||||||
|
|
||||||
|
// Test retrieval from each node
|
||||||
|
for (const node of blogTestHelper.getNodes()) {
|
||||||
|
const retrievedUser = await blogTestHelper.getUser(testUser.id!, node.id);
|
||||||
|
|
||||||
|
expect(retrievedUser).toBeDefined();
|
||||||
|
expect(retrievedUser!.id).toBe(testUser.id);
|
||||||
|
expect(retrievedUser!.username).toBe(testUser.username);
|
||||||
|
expect(retrievedUser!.email).toBe(testUser.email);
|
||||||
|
|
||||||
|
console.log(`✅ Retrieved user from ${node.id}: ${retrievedUser!.username}`);
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
test('should list users with pagination', async () => {
|
||||||
|
const result = await blogTestHelper.listUsers();
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.users).toBeInstanceOf(Array);
|
||||||
|
expect(result.users.length).toBeGreaterThan(0);
|
||||||
|
expect(result.page).toBeDefined();
|
||||||
|
expect(result.limit).toBeDefined();
|
||||||
|
|
||||||
|
// Verify our test user is in the list
|
||||||
|
const foundUser = result.users.find(u => u.id === testUser.id);
|
||||||
|
expect(foundUser).toBeDefined();
|
||||||
|
|
||||||
|
console.log(`✅ Listed ${result.users.length} users`);
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Category Management', () => {
|
||||||
|
test('should create a category successfully', async () => {
|
||||||
|
const testData = blogTestHelper.generateTestData();
|
||||||
|
|
||||||
|
testCategory = await blogTestHelper.createCategory(testData.category);
|
||||||
|
|
||||||
|
expect(testCategory).toBeDefined();
|
||||||
|
expect(testCategory.id).toBeDefined();
|
||||||
|
expect(testCategory.name).toBe(testData.category.name);
|
||||||
|
expect(testCategory.description).toBe(testData.category.description);
|
||||||
|
expect(testCategory.color).toBe(testData.category.color);
|
||||||
|
|
||||||
|
console.log(`✅ Created category: ${testCategory.name} (${testCategory.id})`);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
test('should retrieve category from all nodes', async () => {
|
||||||
|
expect(testCategory?.id).toBeDefined();
|
||||||
|
|
||||||
|
for (const node of blogTestHelper.getNodes()) {
|
||||||
|
const retrievedCategory = await blogTestHelper.getCategory(testCategory.id!, node.id);
|
||||||
|
|
||||||
|
expect(retrievedCategory).toBeDefined();
|
||||||
|
expect(retrievedCategory!.id).toBe(testCategory.id);
|
||||||
|
expect(retrievedCategory!.name).toBe(testCategory.name);
|
||||||
|
|
||||||
|
console.log(`✅ Retrieved category from ${node.id}: ${retrievedCategory!.name}`);
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
test('should list all categories', async () => {
|
||||||
|
const result = await blogTestHelper.listCategories();
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.categories).toBeInstanceOf(Array);
|
||||||
|
expect(result.categories.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify our test category is in the list
|
||||||
|
const foundCategory = result.categories.find(c => c.id === testCategory.id);
|
||||||
|
expect(foundCategory).toBeDefined();
|
||||||
|
|
||||||
|
console.log(`✅ Listed ${result.categories.length} categories`);
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Post Management', () => {
|
||||||
|
test('should create a post successfully', async () => {
|
||||||
|
expect(testUser?.id).toBeDefined();
|
||||||
|
expect(testCategory?.id).toBeDefined();
|
||||||
|
|
||||||
|
const testData = blogTestHelper.generateTestData();
|
||||||
|
const postData = testData.post(testUser.id!, testCategory.id!);
|
||||||
|
|
||||||
|
testPost = await blogTestHelper.createPost(postData);
|
||||||
|
|
||||||
|
expect(testPost).toBeDefined();
|
||||||
|
expect(testPost.id).toBeDefined();
|
||||||
|
expect(testPost.title).toBe(postData.title);
|
||||||
|
expect(testPost.content).toBe(postData.content);
|
||||||
|
expect(testPost.authorId).toBe(testUser.id);
|
||||||
|
expect(testPost.categoryId).toBe(testCategory.id);
|
||||||
|
expect(testPost.status).toBe('draft');
|
||||||
|
|
||||||
|
console.log(`✅ Created post: ${testPost.title} (${testPost.id})`);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
test('should retrieve post with relationships from all nodes', async () => {
|
||||||
|
expect(testPost?.id).toBeDefined();
|
||||||
|
|
||||||
|
for (const node of blogTestHelper.getNodes()) {
|
||||||
|
const retrievedPost = await blogTestHelper.getPost(testPost.id!, node.id);
|
||||||
|
|
||||||
|
expect(retrievedPost).toBeDefined();
|
||||||
|
expect(retrievedPost!.id).toBe(testPost.id);
|
||||||
|
expect(retrievedPost!.title).toBe(testPost.title);
|
||||||
|
expect(retrievedPost!.authorId).toBe(testUser.id);
|
||||||
|
expect(retrievedPost!.categoryId).toBe(testCategory.id);
|
||||||
|
|
||||||
|
console.log(`✅ Retrieved post from ${node.id}: ${retrievedPost!.title}`);
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
test('should publish post and update status', async () => {
|
||||||
|
expect(testPost?.id).toBeDefined();
|
||||||
|
|
||||||
|
const publishedPost = await blogTestHelper.publishPost(testPost.id!);
|
||||||
|
|
||||||
|
expect(publishedPost).toBeDefined();
|
||||||
|
expect(publishedPost.status).toBe('published');
|
||||||
|
expect(publishedPost.publishedAt).toBeDefined();
|
||||||
|
|
||||||
|
// Verify status change is replicated across nodes
|
||||||
|
await blogTestHelper.waitForDataReplication(async () => {
|
||||||
|
for (const node of blogTestHelper.getNodes()) {
|
||||||
|
const post = await blogTestHelper.getPost(testPost.id!, node.id);
|
||||||
|
if (!post || post.status !== 'published') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
console.log(`✅ Published post: ${publishedPost.title}`);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
test('should like post and increment count', async () => {
|
||||||
|
expect(testPost?.id).toBeDefined();
|
||||||
|
|
||||||
|
const result = await blogTestHelper.likePost(testPost.id!);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.likeCount).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
console.log(`✅ Liked post, count: ${result.likeCount}`);
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Comment Management', () => {
|
||||||
|
test('should create a comment successfully', async () => {
|
||||||
|
expect(testPost?.id).toBeDefined();
|
||||||
|
expect(testUser?.id).toBeDefined();
|
||||||
|
|
||||||
|
const testData = blogTestHelper.generateTestData();
|
||||||
|
const commentData = testData.comment(testPost.id!, testUser.id!);
|
||||||
|
|
||||||
|
testComment = await blogTestHelper.createComment(commentData);
|
||||||
|
|
||||||
|
expect(testComment).toBeDefined();
|
||||||
|
expect(testComment.id).toBeDefined();
|
||||||
|
expect(testComment.content).toBe(commentData.content);
|
||||||
|
expect(testComment.postId).toBe(testPost.id);
|
||||||
|
expect(testComment.authorId).toBe(testUser.id);
|
||||||
|
|
||||||
|
console.log(`✅ Created comment: ${testComment.id}`);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
test('should retrieve post comments from all nodes', async () => {
|
||||||
|
expect(testPost?.id).toBeDefined();
|
||||||
|
expect(testComment?.id).toBeDefined();
|
||||||
|
|
||||||
|
for (const node of blogTestHelper.getNodes()) {
|
||||||
|
const result = await blogTestHelper.getPostComments(testPost.id!, node.id);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.comments).toBeInstanceOf(Array);
|
||||||
|
|
||||||
|
// Find our test comment
|
||||||
|
const foundComment = result.comments.find(c => c.id === testComment.id);
|
||||||
|
expect(foundComment).toBeDefined();
|
||||||
|
expect(foundComment!.content).toBe(testComment.content);
|
||||||
|
|
||||||
|
console.log(`✅ Retrieved ${result.comments.length} comments from ${node.id}`);
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Data Metrics', () => {
|
||||||
|
test('should get data metrics from all nodes', async () => {
|
||||||
|
for (const node of blogTestHelper.getNodes()) {
|
||||||
|
const metrics = await blogTestHelper.getDataMetrics(node.id);
|
||||||
|
|
||||||
|
expect(metrics).toBeDefined();
|
||||||
|
expect(metrics.nodeId).toBe(node.id);
|
||||||
|
expect(metrics.counts).toBeDefined();
|
||||||
|
expect(metrics.counts.users).toBeGreaterThan(0);
|
||||||
|
expect(metrics.counts.categories).toBeGreaterThan(0);
|
||||||
|
expect(metrics.counts.posts).toBeGreaterThan(0);
|
||||||
|
expect(metrics.counts.comments).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
console.log(`✅ ${node.id} metrics:`, JSON.stringify(metrics.counts, null, 2));
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,285 @@
|
|||||||
|
import { describe, test, expect, beforeAll } from '@jest/globals';
|
||||||
|
import { blogTestHelper, TestUser, TestCategory, TestPost, TestComment } from './setup';
|
||||||
|
|
||||||
|
describe('Cross-Node Operations', () => {
|
||||||
|
let users: TestUser[] = [];
|
||||||
|
let categories: TestCategory[] = [];
|
||||||
|
let posts: TestPost[] = [];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
console.log('🔄 Waiting for all nodes to be ready...');
|
||||||
|
await blogTestHelper.waitForNodesReady();
|
||||||
|
console.log('✅ All nodes are ready for cross-node testing');
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
describe('Distributed Content Creation', () => {
|
||||||
|
test('should create users on different nodes', async () => {
|
||||||
|
const nodes = blogTestHelper.getNodes();
|
||||||
|
|
||||||
|
// Create one user on each node
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const testData = blogTestHelper.generateTestData();
|
||||||
|
const user = await blogTestHelper.createUser(testData.user, nodes[i].id);
|
||||||
|
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
users.push(user);
|
||||||
|
|
||||||
|
console.log(`✅ Created user ${user.username} on ${nodes[i].id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(users).toHaveLength(3);
|
||||||
|
}, 45000);
|
||||||
|
|
||||||
|
test('should verify users are replicated across all nodes', async () => {
|
||||||
|
// Wait for replication
|
||||||
|
await blogTestHelper.sleep(3000);
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
for (const node of blogTestHelper.getNodes()) {
|
||||||
|
const retrievedUser = await blogTestHelper.getUser(user.id!, node.id);
|
||||||
|
|
||||||
|
expect(retrievedUser).toBeDefined();
|
||||||
|
expect(retrievedUser!.id).toBe(user.id);
|
||||||
|
expect(retrievedUser!.username).toBe(user.username);
|
||||||
|
|
||||||
|
console.log(`✅ User ${user.username} found on ${node.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 45000);
|
||||||
|
|
||||||
|
test('should create categories on different nodes', async () => {
|
||||||
|
const nodes = blogTestHelper.getNodes();
|
||||||
|
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const testData = blogTestHelper.generateTestData();
|
||||||
|
const category = await blogTestHelper.createCategory(testData.category, nodes[i].id);
|
||||||
|
|
||||||
|
expect(category).toBeDefined();
|
||||||
|
expect(category.id).toBeDefined();
|
||||||
|
categories.push(category);
|
||||||
|
|
||||||
|
console.log(`✅ Created category ${category.name} on ${nodes[i].id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(categories).toHaveLength(3);
|
||||||
|
}, 45000);
|
||||||
|
|
||||||
|
test('should create posts with cross-node relationships', async () => {
|
||||||
|
const nodes = blogTestHelper.getNodes();
|
||||||
|
|
||||||
|
// Create posts where author and category are from different nodes
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const authorIndex = i;
|
||||||
|
const categoryIndex = (i + 1) % nodes.length; // Use next node's category
|
||||||
|
const nodeIndex = (i + 2) % nodes.length; // Create on third node
|
||||||
|
|
||||||
|
const testData = blogTestHelper.generateTestData();
|
||||||
|
const postData = testData.post(users[authorIndex].id!, categories[categoryIndex].id!);
|
||||||
|
|
||||||
|
const post = await blogTestHelper.createPost(postData, nodes[nodeIndex].id);
|
||||||
|
|
||||||
|
expect(post).toBeDefined();
|
||||||
|
expect(post.id).toBeDefined();
|
||||||
|
expect(post.authorId).toBe(users[authorIndex].id);
|
||||||
|
expect(post.categoryId).toBe(categories[categoryIndex].id);
|
||||||
|
posts.push(post);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`✅ Created post "${post.title}" on ${nodes[nodeIndex].id} ` +
|
||||||
|
`(author from node-${authorIndex + 1}, category from node-${categoryIndex + 1})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(posts).toHaveLength(3);
|
||||||
|
}, 45000);
|
||||||
|
|
||||||
|
test('should verify cross-node posts are accessible from all nodes', async () => {
|
||||||
|
// Wait for replication
|
||||||
|
await blogTestHelper.sleep(3000);
|
||||||
|
|
||||||
|
for (const post of posts) {
|
||||||
|
for (const node of blogTestHelper.getNodes()) {
|
||||||
|
const retrievedPost = await blogTestHelper.getPost(post.id!, node.id);
|
||||||
|
|
||||||
|
expect(retrievedPost).toBeDefined();
|
||||||
|
expect(retrievedPost!.id).toBe(post.id);
|
||||||
|
expect(retrievedPost!.title).toBe(post.title);
|
||||||
|
expect(retrievedPost!.authorId).toBe(post.authorId);
|
||||||
|
expect(retrievedPost!.categoryId).toBe(post.categoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ All cross-node posts are accessible from all nodes');
|
||||||
|
}, 45000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Concurrent Operations', () => {
|
||||||
|
test('should handle concurrent likes on same post from different nodes', async () => {
|
||||||
|
const post = posts[0];
|
||||||
|
const nodes = blogTestHelper.getNodes();
|
||||||
|
|
||||||
|
// Perform concurrent likes from all nodes
|
||||||
|
const likePromises = nodes.map(node =>
|
||||||
|
blogTestHelper.likePost(post.id!, node.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = await Promise.all(likePromises);
|
||||||
|
|
||||||
|
// All should succeed
|
||||||
|
results.forEach(result => {
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.likeCount).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for eventual consistency
|
||||||
|
await blogTestHelper.sleep(3000);
|
||||||
|
|
||||||
|
// Verify final like count is consistent across nodes
|
||||||
|
const finalCounts: number[] = [];
|
||||||
|
for (const node of nodes) {
|
||||||
|
const updatedPost = await blogTestHelper.getPost(post.id!, node.id);
|
||||||
|
expect(updatedPost).toBeDefined();
|
||||||
|
finalCounts.push(updatedPost!.likeCount || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All nodes should have the same final count
|
||||||
|
const uniqueCounts = [...new Set(finalCounts)];
|
||||||
|
expect(uniqueCounts).toHaveLength(1);
|
||||||
|
|
||||||
|
console.log(`✅ Concurrent likes handled, final count: ${finalCounts[0]}`);
|
||||||
|
}, 45000);
|
||||||
|
|
||||||
|
test('should handle simultaneous comment creation', async () => {
|
||||||
|
const post = posts[1];
|
||||||
|
const nodes = blogTestHelper.getNodes();
|
||||||
|
|
||||||
|
// Create comments simultaneously from different nodes
|
||||||
|
const commentPromises = nodes.map((node, index) => {
|
||||||
|
const testData = blogTestHelper.generateTestData();
|
||||||
|
const commentData = testData.comment(post.id!, users[index].id!);
|
||||||
|
return blogTestHelper.createComment(commentData, node.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const comments = await Promise.all(commentPromises);
|
||||||
|
|
||||||
|
// All comments should be created successfully
|
||||||
|
comments.forEach((comment, index) => {
|
||||||
|
expect(comment).toBeDefined();
|
||||||
|
expect(comment.id).toBeDefined();
|
||||||
|
expect(comment.postId).toBe(post.id);
|
||||||
|
expect(comment.authorId).toBe(users[index].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for replication
|
||||||
|
await blogTestHelper.sleep(3000);
|
||||||
|
|
||||||
|
// Verify all comments are visible from all nodes
|
||||||
|
for (const node of nodes) {
|
||||||
|
const result = await blogTestHelper.getPostComments(post.id!, node.id);
|
||||||
|
expect(result.comments.length).toBeGreaterThanOrEqual(3);
|
||||||
|
|
||||||
|
// Verify all our comments are present
|
||||||
|
for (const comment of comments) {
|
||||||
|
const found = result.comments.find(c => c.id === comment.id);
|
||||||
|
expect(found).toBeDefined();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Created ${comments.length} simultaneous comments`);
|
||||||
|
}, 45000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Load Distribution', () => {
|
||||||
|
test('should distribute read operations across nodes', async () => {
|
||||||
|
const readCounts = new Map<string, number>();
|
||||||
|
const totalReads = 30;
|
||||||
|
|
||||||
|
// Perform multiple reads and track which nodes are used
|
||||||
|
for (let i = 0; i < totalReads; i++) {
|
||||||
|
const randomPost = posts[Math.floor(Math.random() * posts.length)];
|
||||||
|
const node = blogTestHelper.getRandomNode();
|
||||||
|
|
||||||
|
const post = await blogTestHelper.getPost(randomPost.id!, node.id);
|
||||||
|
expect(post).toBeDefined();
|
||||||
|
|
||||||
|
readCounts.set(node.id, (readCounts.get(node.id) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify reads were distributed across nodes
|
||||||
|
const nodeIds = blogTestHelper.getNodes().map(n => n.id);
|
||||||
|
nodeIds.forEach(nodeId => {
|
||||||
|
const count = readCounts.get(nodeId) || 0;
|
||||||
|
console.log(`${nodeId}: ${count} reads`);
|
||||||
|
expect(count).toBeGreaterThan(0); // Each node should have at least one read
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Read operations distributed across all nodes');
|
||||||
|
}, 45000);
|
||||||
|
|
||||||
|
test('should verify consistent data across all read operations', async () => {
|
||||||
|
// Read the same post from all nodes multiple times
|
||||||
|
const post = posts[0];
|
||||||
|
const readResults: TestPost[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
for (const node of blogTestHelper.getNodes()) {
|
||||||
|
const result = await blogTestHelper.getPost(post.id!, node.id);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
readResults.push(result!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all reads return identical data
|
||||||
|
readResults.forEach(result => {
|
||||||
|
expect(result.id).toBe(post.id);
|
||||||
|
expect(result.title).toBe(post.title);
|
||||||
|
expect(result.content).toBe(post.content);
|
||||||
|
expect(result.authorId).toBe(post.authorId);
|
||||||
|
expect(result.categoryId).toBe(post.categoryId);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ ${readResults.length} read operations returned consistent data`);
|
||||||
|
}, 45000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Network Metrics', () => {
|
||||||
|
test('should show network connectivity between nodes', async () => {
|
||||||
|
for (const node of blogTestHelper.getNodes()) {
|
||||||
|
const metrics = await blogTestHelper.getNodeMetrics(node.id);
|
||||||
|
|
||||||
|
expect(metrics).toBeDefined();
|
||||||
|
expect(metrics.nodeId).toBe(node.id);
|
||||||
|
|
||||||
|
console.log(`📊 ${node.id} framework metrics:`, {
|
||||||
|
services: metrics.services,
|
||||||
|
environment: metrics.environment,
|
||||||
|
features: metrics.features
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
test('should verify data consistency across all nodes', async () => {
|
||||||
|
const allMetrics = [];
|
||||||
|
|
||||||
|
for (const node of blogTestHelper.getNodes()) {
|
||||||
|
const metrics = await blogTestHelper.getDataMetrics(node.id);
|
||||||
|
allMetrics.push(metrics);
|
||||||
|
|
||||||
|
console.log(`📊 ${node.id} data counts:`, metrics.counts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all nodes have the same data counts (eventual consistency)
|
||||||
|
const firstCounts = allMetrics[0].counts;
|
||||||
|
allMetrics.forEach((metrics, index) => {
|
||||||
|
expect(metrics.counts.users).toBe(firstCounts.users);
|
||||||
|
expect(metrics.counts.categories).toBe(firstCounts.categories);
|
||||||
|
expect(metrics.counts.posts).toBe(firstCounts.posts);
|
||||||
|
|
||||||
|
console.log(`✅ Node ${index + 1} data counts match reference`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Data consistency verified across all nodes');
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
});
|
19
tests/real-integration/blog-scenario/tests/jest.config.js
Normal file
19
tests/real-integration/blog-scenario/tests/jest.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
roots: ['<rootDir>'],
|
||||||
|
testMatch: ['**/*.test.ts'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.ts$': 'ts-jest',
|
||||||
|
},
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'**/*.ts',
|
||||||
|
'!**/*.d.ts',
|
||||||
|
],
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||||
|
testTimeout: 120000, // 2 minutes default timeout
|
||||||
|
maxWorkers: 1, // Run tests sequentially to avoid conflicts
|
||||||
|
verbose: true,
|
||||||
|
detectOpenHandles: true,
|
||||||
|
forceExit: true,
|
||||||
|
};
|
20
tests/real-integration/blog-scenario/tests/jest.setup.js
Normal file
20
tests/real-integration/blog-scenario/tests/jest.setup.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// Global test setup
|
||||||
|
console.log('🚀 Starting Blog Integration Tests');
|
||||||
|
console.log('📡 Target nodes: blog-node-1, blog-node-2, blog-node-3');
|
||||||
|
console.log('⏰ Test timeout: 120 seconds');
|
||||||
|
console.log('=====================================');
|
||||||
|
|
||||||
|
// Increase timeout for all tests
|
||||||
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
|
// Global error handler
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up console logs for better readability
|
||||||
|
const originalLog = console.log;
|
||||||
|
console.log = (...args) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
originalLog(`[${timestamp}]`, ...args);
|
||||||
|
};
|
23
tests/real-integration/blog-scenario/tests/package.json
Normal file
23
tests/real-integration/blog-scenario/tests/package.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "blog-integration-tests",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Integration tests for blog scenario",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest --config jest.config.js",
|
||||||
|
"test:basic": "jest --config jest.config.js basic-operations.test.ts",
|
||||||
|
"test:cross-node": "jest --config jest.config.js cross-node-operations.test.ts",
|
||||||
|
"test:watch": "jest --config jest.config.js --watch",
|
||||||
|
"test:coverage": "jest --config jest.config.js --coverage"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "^29.5.0",
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
306
tests/real-integration/blog-scenario/tests/setup.ts
Normal file
306
tests/real-integration/blog-scenario/tests/setup.ts
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
export interface TestNode {
|
||||||
|
id: string;
|
||||||
|
baseUrl: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestUser {
|
||||||
|
id?: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
displayName?: string;
|
||||||
|
avatar?: string;
|
||||||
|
roles?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestCategory {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestPost {
|
||||||
|
id?: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
excerpt?: string;
|
||||||
|
authorId: string;
|
||||||
|
categoryId?: string;
|
||||||
|
tags?: string[];
|
||||||
|
status?: 'draft' | 'published' | 'archived';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestComment {
|
||||||
|
id?: string;
|
||||||
|
content: string;
|
||||||
|
postId: string;
|
||||||
|
authorId: string;
|
||||||
|
parentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BlogTestHelper {
|
||||||
|
private nodes: TestNode[];
|
||||||
|
private timeout: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.nodes = [
|
||||||
|
{ id: 'blog-node-1', baseUrl: 'http://blog-node-1:3000', port: 3000 },
|
||||||
|
{ id: 'blog-node-2', baseUrl: 'http://blog-node-2:3000', port: 3000 },
|
||||||
|
{ id: 'blog-node-3', baseUrl: 'http://blog-node-3:3000', port: 3000 }
|
||||||
|
];
|
||||||
|
this.timeout = 30000; // 30 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodes(): TestNode[] {
|
||||||
|
return this.nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomNode(): TestNode {
|
||||||
|
return this.nodes[Math.floor(Math.random() * this.nodes.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForNodesReady(): Promise<void> {
|
||||||
|
const maxRetries = 30;
|
||||||
|
const retryDelay = 1000;
|
||||||
|
|
||||||
|
for (const node of this.nodes) {
|
||||||
|
let retries = 0;
|
||||||
|
let healthy = false;
|
||||||
|
|
||||||
|
while (retries < maxRetries && !healthy) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${node.baseUrl}/health`, {
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200 && response.data.status === 'healthy') {
|
||||||
|
console.log(`✅ Node ${node.id} is healthy`);
|
||||||
|
healthy = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
retries++;
|
||||||
|
console.log(`⏳ Waiting for ${node.id} to be ready (attempt ${retries}/${maxRetries})`);
|
||||||
|
await this.sleep(retryDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!healthy) {
|
||||||
|
throw new Error(`Node ${node.id} failed to become healthy after ${maxRetries} attempts`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional wait for inter-node connectivity
|
||||||
|
console.log('⏳ Waiting for nodes to establish connectivity...');
|
||||||
|
await this.sleep(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
// User operations
|
||||||
|
async createUser(user: TestUser, nodeId?: string): Promise<TestUser> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
const response = await axios.post(`${node.baseUrl}/api/users`, user, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(userId: string, nodeId?: string): Promise<TestUser | null> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${node.baseUrl}/api/users/${userId}`, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listUsers(nodeId?: string, params?: any): Promise<{ users: TestUser[], page: number, limit: number }> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
const response = await axios.get(`${node.baseUrl}/api/users`, {
|
||||||
|
params,
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category operations
|
||||||
|
async createCategory(category: TestCategory, nodeId?: string): Promise<TestCategory> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
const response = await axios.post(`${node.baseUrl}/api/categories`, category, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCategory(categoryId: string, nodeId?: string): Promise<TestCategory | null> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${node.baseUrl}/api/categories/${categoryId}`, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listCategories(nodeId?: string): Promise<{ categories: TestCategory[] }> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
const response = await axios.get(`${node.baseUrl}/api/categories`, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post operations
|
||||||
|
async createPost(post: TestPost, nodeId?: string): Promise<TestPost> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
const response = await axios.post(`${node.baseUrl}/api/posts`, post, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPost(postId: string, nodeId?: string): Promise<TestPost | null> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${node.baseUrl}/api/posts/${postId}`, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async publishPost(postId: string, nodeId?: string): Promise<TestPost> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
const response = await axios.post(`${node.baseUrl}/api/posts/${postId}/publish`, {}, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async likePost(postId: string, nodeId?: string): Promise<{ likeCount: number }> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
const response = await axios.post(`${node.baseUrl}/api/posts/${postId}/like`, {}, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment operations
|
||||||
|
async createComment(comment: TestComment, nodeId?: string): Promise<TestComment> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
const response = await axios.post(`${node.baseUrl}/api/comments`, comment, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPostComments(postId: string, nodeId?: string): Promise<{ comments: TestComment[] }> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
const response = await axios.get(`${node.baseUrl}/api/posts/${postId}/comments`, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics and health
|
||||||
|
async getNodeMetrics(nodeId: string): Promise<any> {
|
||||||
|
const node = this.getNodeById(nodeId);
|
||||||
|
const response = await axios.get(`${node.baseUrl}/api/metrics/framework`, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDataMetrics(nodeId?: string): Promise<any> {
|
||||||
|
const node = nodeId ? this.getNodeById(nodeId) : this.getRandomNode();
|
||||||
|
const response = await axios.get(`${node.baseUrl}/api/metrics/data`, {
|
||||||
|
timeout: this.timeout
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility methods
|
||||||
|
private getNodeById(nodeId: string): TestNode {
|
||||||
|
const node = this.nodes.find(n => n.id === nodeId);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error(`Node with id ${nodeId} not found`);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForDataReplication(
|
||||||
|
checkFunction: () => Promise<boolean>,
|
||||||
|
maxWaitMs: number = 10000,
|
||||||
|
intervalMs: number = 500
|
||||||
|
): Promise<void> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < maxWaitMs) {
|
||||||
|
if (await checkFunction()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.sleep(intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Data replication timeout after ${maxWaitMs}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateTestData() {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.random().toString(36).substring(7);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
username: `testuser_${random}`,
|
||||||
|
email: `test_${random}@example.com`,
|
||||||
|
displayName: `Test User ${random}`,
|
||||||
|
roles: ['user']
|
||||||
|
} as TestUser,
|
||||||
|
|
||||||
|
category: {
|
||||||
|
name: `Test Category ${random}`,
|
||||||
|
description: `Test category created at ${timestamp}`,
|
||||||
|
color: '#ff0000'
|
||||||
|
} as TestCategory,
|
||||||
|
|
||||||
|
post: (authorId: string, categoryId?: string) => ({
|
||||||
|
title: `Test Post ${random}`,
|
||||||
|
content: `This is test content created at ${timestamp}`,
|
||||||
|
excerpt: `Test excerpt ${random}`,
|
||||||
|
authorId,
|
||||||
|
categoryId,
|
||||||
|
tags: ['test', 'integration'],
|
||||||
|
status: 'draft' as const
|
||||||
|
} as TestPost),
|
||||||
|
|
||||||
|
comment: (postId: string, authorId: string) => ({
|
||||||
|
content: `Test comment created at ${timestamp}`,
|
||||||
|
postId,
|
||||||
|
authorId
|
||||||
|
} as TestComment)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const blogTestHelper = new BlogTestHelper();
|
Reference in New Issue
Block a user