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 {
|
||||
const modelClass = this.constructor as typeof BaseModel;
|
||||
|
||||
// Ensure we have fields map
|
||||
if (!modelClass.fields) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [fieldName, fieldConfig] of modelClass.fields) {
|
||||
if (fieldConfig.default !== undefined) {
|
||||
const privateKey = `_${fieldName}`;
|
||||
|
@ -3,89 +3,82 @@ import { BaseModel } from '../BaseModel';
|
||||
|
||||
export function Field(config: FieldConfig) {
|
||||
return function (target: any, propertyKey: string) {
|
||||
// When decorators are used in an ES module context, the `target` for a property decorator
|
||||
// can be undefined. We need to defer the Object.defineProperty call until we have
|
||||
// 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.
|
||||
const decorator = (instance: any) => {
|
||||
if (!Object.getOwnPropertyDescriptor(instance, propertyKey)) {
|
||||
Object.defineProperty(instance, propertyKey, {
|
||||
get() {
|
||||
const privateKey = `_${propertyKey}`;
|
||||
// Use the reliable getFieldValue method if available, otherwise fallback to private key
|
||||
if (this.getFieldValue && typeof this.getFieldValue === 'function') {
|
||||
return this.getFieldValue(propertyKey);
|
||||
}
|
||||
|
||||
// Fallback to direct private key access
|
||||
return this[privateKey];
|
||||
},
|
||||
set(value) {
|
||||
const ctor = this.constructor as typeof BaseModel;
|
||||
const privateKey = `_${propertyKey}`;
|
||||
|
||||
// One-time initialization of the fields map on the constructor
|
||||
if (!ctor.hasOwnProperty('fields')) {
|
||||
const parentFields = ctor.fields ? new Map(ctor.fields) : new Map();
|
||||
Object.defineProperty(ctor, 'fields', {
|
||||
value: parentFields,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Store field configuration if it's not already there
|
||||
if (!ctor.fields.has(propertyKey)) {
|
||||
ctor.fields.set(propertyKey, config);
|
||||
}
|
||||
|
||||
// Apply transformation first
|
||||
const transformedValue = config.transform ? config.transform(value) : value;
|
||||
|
||||
// Only validate non-required constraints during assignment
|
||||
// Required field validation will happen during save()
|
||||
const validationResult = validateFieldValueNonRequired(
|
||||
transformedValue,
|
||||
config,
|
||||
propertyKey,
|
||||
);
|
||||
if (!validationResult.valid) {
|
||||
throw new ValidationError(validationResult.errors);
|
||||
}
|
||||
|
||||
// Check if value actually changed
|
||||
const oldValue = this[privateKey];
|
||||
if (oldValue !== transformedValue) {
|
||||
// Set the value and mark as dirty
|
||||
this[privateKey] = transformedValue;
|
||||
if (this._isDirty !== undefined) {
|
||||
this._isDirty = true;
|
||||
}
|
||||
// Track field modification
|
||||
if (this.markFieldAsModified && typeof this.markFieldAsModified === 'function') {
|
||||
this.markFieldAsModified(propertyKey);
|
||||
}
|
||||
}
|
||||
},
|
||||
enumerable: 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);
|
||||
// Validate field configuration
|
||||
validateFieldConfig(config);
|
||||
|
||||
// Get the constructor function
|
||||
const ctor = target.constructor as typeof BaseModel;
|
||||
|
||||
// Initialize fields map if it doesn't exist
|
||||
if (!ctor.hasOwnProperty('fields')) {
|
||||
const parentFields = ctor.fields ? new Map(ctor.fields) : new Map();
|
||||
Object.defineProperty(ctor, 'fields', {
|
||||
value: parentFields,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Store field configuration
|
||||
ctor.fields.set(propertyKey, config);
|
||||
|
||||
// Define property on the prototype
|
||||
Object.defineProperty(target, propertyKey, {
|
||||
get() {
|
||||
const privateKey = `_${propertyKey}`;
|
||||
return this[privateKey];
|
||||
},
|
||||
set(value) {
|
||||
const privateKey = `_${propertyKey}`;
|
||||
const ctor = this.constructor as typeof BaseModel;
|
||||
|
||||
// Ensure fields map exists on the constructor
|
||||
if (!ctor.hasOwnProperty('fields')) {
|
||||
const parentFields = ctor.fields ? new Map(ctor.fields) : new Map();
|
||||
Object.defineProperty(ctor, 'fields', {
|
||||
value: parentFields,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Store field configuration if it's not already there
|
||||
if (!ctor.fields.has(propertyKey)) {
|
||||
ctor.fields.set(propertyKey, config);
|
||||
}
|
||||
|
||||
// Apply transformation first
|
||||
const transformedValue = config.transform ? config.transform(value) : value;
|
||||
|
||||
// Only validate non-required constraints during assignment
|
||||
const validationResult = validateFieldValueNonRequired(
|
||||
transformedValue,
|
||||
config,
|
||||
propertyKey,
|
||||
);
|
||||
if (!validationResult.valid) {
|
||||
throw new ValidationError(validationResult.errors);
|
||||
}
|
||||
|
||||
// Check if value actually changed
|
||||
const oldValue = this[privateKey];
|
||||
if (oldValue !== transformedValue) {
|
||||
// Set the value and mark as dirty
|
||||
this[privateKey] = transformedValue;
|
||||
if (this._isDirty !== undefined) {
|
||||
this._isDirty = true;
|
||||
}
|
||||
// Track field modification
|
||||
if (this.markFieldAsModified && typeof this.markFieldAsModified === 'function') {
|
||||
this.markFieldAsModified(propertyKey);
|
||||
}
|
||||
}
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -91,72 +91,86 @@ function createRelationshipProperty(
|
||||
propertyKey: string,
|
||||
config: RelationshipConfig,
|
||||
): void {
|
||||
// In an ES module context, `target` can be undefined when decorators are first evaluated.
|
||||
// We must ensure we only call Object.defineProperty on a valid object (the class prototype).
|
||||
if (target) {
|
||||
Object.defineProperty(target, propertyKey, {
|
||||
get() {
|
||||
const ctor = this.constructor as typeof BaseModel;
|
||||
|
||||
// One-time initialization of the relationships map on the constructor
|
||||
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 if it's not already there
|
||||
if (!ctor.relationships.has(propertyKey)) {
|
||||
ctor.relationships.set(propertyKey, config);
|
||||
}
|
||||
|
||||
// Check if relationship is already loaded
|
||||
if (this._loadedRelations && this._loadedRelations.has(propertyKey)) {
|
||||
return this._loadedRelations.get(propertyKey);
|
||||
}
|
||||
|
||||
if (config.lazy) {
|
||||
// Return a promise for lazy loading
|
||||
return this.loadRelation(propertyKey);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Relationship '${propertyKey}' not loaded. Use .load(['${propertyKey}']) first.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
const ctor = this.constructor as typeof BaseModel;
|
||||
|
||||
// One-time initialization of the relationships map on the constructor
|
||||
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 if it's not already there
|
||||
if (!ctor.relationships.has(propertyKey)) {
|
||||
ctor.relationships.set(propertyKey, config);
|
||||
}
|
||||
|
||||
// Allow manual setting of relationship values
|
||||
if (!this._loadedRelations) {
|
||||
this._loadedRelations = new Map();
|
||||
}
|
||||
this._loadedRelations.set(propertyKey, value);
|
||||
},
|
||||
enumerable: true,
|
||||
// Get the constructor function
|
||||
const ctor = target.constructor as typeof BaseModel;
|
||||
|
||||
// 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, {
|
||||
get() {
|
||||
const ctor = this.constructor as typeof BaseModel;
|
||||
|
||||
// Ensure relationships map exists on the constructor
|
||||
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 if it's not already there
|
||||
if (!ctor.relationships.has(propertyKey)) {
|
||||
ctor.relationships.set(propertyKey, config);
|
||||
}
|
||||
|
||||
// Check if relationship is already loaded
|
||||
if (this._loadedRelations && this._loadedRelations.has(propertyKey)) {
|
||||
return this._loadedRelations.get(propertyKey);
|
||||
}
|
||||
|
||||
if (config.lazy) {
|
||||
// Return a promise for lazy loading
|
||||
return this.loadRelation(propertyKey);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Relationship '${propertyKey}' not loaded. Use .load(['${propertyKey}']) first.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
const ctor = this.constructor as typeof BaseModel;
|
||||
|
||||
// Ensure relationships map exists on the constructor
|
||||
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 if it's not already there
|
||||
if (!ctor.relationships.has(propertyKey)) {
|
||||
ctor.relationships.set(propertyKey, config);
|
||||
}
|
||||
|
||||
// Allow manual setting of relationship values
|
||||
if (!this._loadedRelations) {
|
||||
this._loadedRelations = new Map();
|
||||
}
|
||||
this._loadedRelations.set(propertyKey, value);
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Utility function to get relationship configuration
|
||||
|
@ -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();
|
Loading…
x
Reference in New Issue
Block a user