Initial commit

This commit is contained in:
root
2026-01-26 08:34:48 +01:00
commit 387314581f
216 changed files with 81204 additions and 0 deletions
+534
View File
@@ -0,0 +1,534 @@
/**
* Property-Based Tests for Bot Posting Module
*
* Feature: bot-system
* - Property 38: Post Content Validation
*
* Tests that generated posts are validated against platform requirements
* (length, format) before publishing.
*
* **Validates: Requirements 11.5**
*/
import { describe, it, expect } from 'vitest';
import * as fc from 'fast-check';
import {
validatePostContent,
sanitizePostContent,
POST_MAX_LENGTH,
POST_MIN_LENGTH,
MAX_URLS_PER_POST,
} from './posting';
// ============================================
// GENERATORS
// ============================================
/**
* Generator for valid post content (within length limits).
*/
const validPostContentArb = fc.string({
minLength: POST_MIN_LENGTH,
maxLength: POST_MAX_LENGTH,
}).filter(s => s.trim().length >= POST_MIN_LENGTH);
/**
* Generator for post content that exceeds maximum length.
*/
const tooLongPostContentArb = fc.string({
minLength: POST_MAX_LENGTH + 1,
maxLength: POST_MAX_LENGTH + 500,
});
/**
* Generator for post content that is too short (empty or whitespace).
*/
const tooShortPostContentArb = fc.oneof(
fc.constant(''),
fc.constant(' '),
fc.constant('\n\n'),
fc.constant('\t\t')
);
/**
* Generator for post content with forbidden patterns.
*/
const forbiddenContentArb = fc.oneof(
fc.constant('This is spam content'),
fc.constant('Check out this scam'),
fc.constant('Phishing attempt here'),
fc.string({ minLength: 10, maxLength: 100 }).map(s => `${s} spam ${s}`),
fc.string({ minLength: 10, maxLength: 100 }).map(s => `${s} scam ${s}`),
fc.string({ minLength: 10, maxLength: 100 }).map(s => `${s} phishing ${s}`)
);
/**
* Generator for URLs.
*/
const urlArb = fc.webUrl();
/**
* Generator for post content with too many URLs.
*/
const tooManyUrlsContentArb = fc.array(urlArb, {
minLength: MAX_URLS_PER_POST + 1,
maxLength: MAX_URLS_PER_POST + 5,
}).map(urls => urls.join(' '));
/**
* Generator for post content with acceptable number of URLs.
*/
const acceptableUrlsContentArb = fc.tuple(
fc.string({ minLength: 10, maxLength: 100 }),
fc.array(urlArb, { minLength: 0, maxLength: MAX_URLS_PER_POST })
).map(([text, urls]) => `${text} ${urls.join(' ')}`);
/**
* Generator for content with null bytes and special characters.
*/
const unsafeContentArb = fc.string({ minLength: 10, maxLength: 100 }).map(s =>
`${s}\0null byte\0${s}`
);
/**
* Generator for content with excessive whitespace.
*/
const excessiveWhitespaceArb = fc.string({ minLength: 10, maxLength: 100 }).map(s =>
`${s}\n\n\n\n\n${s}\n\n\n\n`
);
// ============================================
// PROPERTY TESTS
// ============================================
describe('Feature: bot-system, Property 38: Post Content Validation', () => {
/**
* Property 38: Post Content Validation
*
* *For any* generated post, the content SHALL be validated against platform
* requirements (length, format) before publishing.
*
* **Validates: Requirements 11.5**
*/
it('validates that content within length limits is valid (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
validPostContentArb,
async (content) => {
const result = validatePostContent(content);
// Valid content should pass validation
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
}
),
{ numRuns: 100 }
);
});
it('rejects content exceeding maximum length (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
tooLongPostContentArb,
async (content) => {
// Only test if trimmed content exceeds max length
if (content.trim().length <= POST_MAX_LENGTH) {
return; // Skip this test case
}
const result = validatePostContent(content);
// Content exceeding max length should fail validation
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
// Should have specific error about length
const hasLengthError = result.errors.some(err =>
err.includes('must not exceed') || err.includes('characters')
);
expect(hasLengthError).toBe(true);
}
),
{ numRuns: 100 }
);
});
it('rejects content below minimum length (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
tooShortPostContentArb,
async (content) => {
const result = validatePostContent(content);
// Content below min length should fail validation
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
// Should have error about content being required, minimum length, or empty/whitespace
const hasRelevantError = result.errors.some(err =>
err.includes('required') || err.includes('at least') ||
err.includes('character') || err.includes('empty') || err.includes('whitespace')
);
expect(hasRelevantError).toBe(true);
}
),
{ numRuns: 100 }
);
});
it('rejects content with forbidden patterns (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
forbiddenContentArb,
async (content) => {
const result = validatePostContent(content);
// Content with forbidden patterns should fail validation
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
// Should have error about forbidden content
const hasForbiddenError = result.errors.some(err =>
err.includes('forbidden')
);
expect(hasForbiddenError).toBe(true);
}
),
{ numRuns: 100 }
);
});
it('rejects content with too many URLs (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
tooManyUrlsContentArb,
async (content) => {
// Only test if content is within length limits
if (content.length > POST_MAX_LENGTH) {
return; // Skip this test case
}
const result = validatePostContent(content);
// Content with too many URLs should fail validation
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
// Should have error about URL count
const hasUrlError = result.errors.some(err =>
err.includes('URL') || err.includes('urls')
);
expect(hasUrlError).toBe(true);
}
),
{ numRuns: 100 }
);
});
it('accepts content with acceptable number of URLs (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
acceptableUrlsContentArb,
async (content) => {
// Only test if content is within length limits
if (content.length > POST_MAX_LENGTH || content.trim().length < POST_MIN_LENGTH) {
return; // Skip this test case
}
const result = validatePostContent(content);
// Content with acceptable URLs should pass validation
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
}
),
{ numRuns: 100 }
);
});
it('validation is deterministic for same input (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 0, maxLength: POST_MAX_LENGTH + 100 }),
async (content) => {
const result1 = validatePostContent(content);
const result2 = validatePostContent(content);
// Same input should produce same validation result
expect(result1.valid).toBe(result2.valid);
expect(result1.errors).toEqual(result2.errors);
}
),
{ numRuns: 100 }
);
});
it('validation handles null and undefined gracefully (Requirement 11.5)', async () => {
const nullResult = validatePostContent(null as any);
const undefinedResult = validatePostContent(undefined as any);
// Both should fail validation with appropriate error
expect(nullResult.valid).toBe(false);
expect(nullResult.errors.length).toBeGreaterThan(0);
expect(undefinedResult.valid).toBe(false);
expect(undefinedResult.errors.length).toBeGreaterThan(0);
});
it('validation handles non-string types gracefully (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
fc.oneof(
fc.integer(),
fc.boolean(),
fc.object(),
fc.array(fc.string())
),
async (nonString) => {
const result = validatePostContent(nonString as any);
// Non-string content should fail validation
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
}
),
{ numRuns: 100 }
);
});
it('validation trims whitespace before checking length (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
validPostContentArb,
fc.string({ minLength: 0, maxLength: 50 }).filter(s => /^\s+$/.test(s) || s === ''),
async (validContent, whitespace) => {
const contentWithWhitespace = `${whitespace}${validContent}${whitespace}`;
const result = validatePostContent(contentWithWhitespace);
// Should validate based on trimmed length
const trimmedLength = contentWithWhitespace.trim().length;
if (trimmedLength >= POST_MIN_LENGTH && trimmedLength <= POST_MAX_LENGTH) {
expect(result.valid).toBe(true);
} else {
expect(result.valid).toBe(false);
}
}
),
{ numRuns: 100 }
);
});
it('validation provides specific error messages (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
fc.oneof(
tooLongPostContentArb.filter(s => s.trim().length > POST_MAX_LENGTH), // Only truly too long
tooShortPostContentArb,
forbiddenContentArb.filter(s => s.trim().length >= POST_MIN_LENGTH && s.trim().length <= POST_MAX_LENGTH) // Valid length but forbidden
),
async (invalidContent) => {
const result = validatePostContent(invalidContent);
// Invalid content should have specific error messages
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
// Each error should be a non-empty string
result.errors.forEach(error => {
expect(typeof error).toBe('string');
expect(error.length).toBeGreaterThan(0);
});
}
),
{ numRuns: 100 }
);
});
it('sanitization removes null bytes (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
unsafeContentArb,
async (content) => {
const sanitized = sanitizePostContent(content);
// Sanitized content should not contain null bytes
expect(sanitized).not.toContain('\0');
}
),
{ numRuns: 100 }
);
});
it('sanitization normalizes line breaks (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 10, maxLength: 100 }).map(s =>
`${s}\r\n${s}\r\n${s}`
),
async (content) => {
const sanitized = sanitizePostContent(content);
// Sanitized content should not contain \r\n
expect(sanitized).not.toContain('\r\n');
// Should only have \n for line breaks
if (sanitized.includes('\n')) {
expect(sanitized.split('\n').length).toBeGreaterThan(1);
}
}
),
{ numRuns: 100 }
);
});
it('sanitization removes excessive whitespace (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
excessiveWhitespaceArb,
async (content) => {
const sanitized = sanitizePostContent(content);
// Sanitized content should not have more than 2 consecutive newlines
expect(sanitized).not.toMatch(/\n{3,}/);
}
),
{ numRuns: 100 }
);
});
it('sanitization trims leading and trailing whitespace (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 10, maxLength: 100 }),
fc.string({ minLength: 1, maxLength: 20 }).filter(s => /^\s+$/.test(s)),
async (content, whitespace) => {
const contentWithWhitespace = `${whitespace}${content}${whitespace}`;
const sanitized = sanitizePostContent(contentWithWhitespace);
// Sanitized content should be trimmed
expect(sanitized).toBe(sanitized.trim());
expect(sanitized).not.toMatch(/^\s/);
expect(sanitized).not.toMatch(/\s$/);
}
),
{ numRuns: 100 }
);
});
it('sanitization is idempotent (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 0, maxLength: 200 }),
async (content) => {
const sanitized1 = sanitizePostContent(content);
const sanitized2 = sanitizePostContent(sanitized1);
// Sanitizing twice should produce the same result
expect(sanitized1).toBe(sanitized2);
}
),
{ numRuns: 100 }
);
});
it('sanitization preserves valid content structure (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
validPostContentArb,
async (content) => {
const sanitized = sanitizePostContent(content);
// Sanitized valid content should still be valid
// (assuming it doesn't have null bytes or excessive whitespace)
if (!content.includes('\0') && !content.match(/\n{3,}/)) {
expect(sanitized.trim()).toBe(content.trim());
}
}
),
{ numRuns: 100 }
);
});
it('validation constants are properly defined (Requirement 11.5)', async () => {
// Verify constants are reasonable
expect(POST_MAX_LENGTH).toBe(400);
expect(POST_MIN_LENGTH).toBe(1);
expect(MAX_URLS_PER_POST).toBe(5);
// Verify relationships
expect(POST_MAX_LENGTH).toBeGreaterThan(POST_MIN_LENGTH);
expect(MAX_URLS_PER_POST).toBeGreaterThan(0);
});
it('validation rejects content at boundary conditions (Requirement 11.5)', async () => {
// Test exact boundary: POST_MAX_LENGTH
const exactMaxContent = 'a'.repeat(POST_MAX_LENGTH);
const exactMaxResult = validatePostContent(exactMaxContent);
expect(exactMaxResult.valid).toBe(true);
// Test one over boundary: POST_MAX_LENGTH + 1
const overMaxContent = 'a'.repeat(POST_MAX_LENGTH + 1);
const overMaxResult = validatePostContent(overMaxContent);
expect(overMaxResult.valid).toBe(false);
// Test exact minimum: POST_MIN_LENGTH
const exactMinContent = 'a'.repeat(POST_MIN_LENGTH);
const exactMinResult = validatePostContent(exactMinContent);
expect(exactMinResult.valid).toBe(true);
// Test below minimum: empty string
const belowMinContent = '';
const belowMinResult = validatePostContent(belowMinContent);
expect(belowMinResult.valid).toBe(false);
});
it('validation handles mixed invalid conditions (Requirement 11.5)', async () => {
await fc.assert(
fc.asyncProperty(
fc.tuple(
fc.boolean(), // too long?
fc.boolean(), // has forbidden content?
fc.boolean() // too many URLs?
),
async ([tooLong, forbidden, tooManyUrls]) => {
let content = 'Test content ';
if (tooLong) {
content = 'a'.repeat(POST_MAX_LENGTH + 10);
}
if (forbidden) {
content += ' spam ';
}
if (tooManyUrls) {
const urls = Array(MAX_URLS_PER_POST + 2).fill('https://example.com');
content += urls.join(' ');
}
const result = validatePostContent(content);
// If any condition is invalid, validation should fail
if (tooLong || forbidden || tooManyUrls) {
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
}
}
),
{ numRuns: 100 }
);
});
it('validation error count matches number of violations (Requirement 11.5)', async () => {
// Content that is too long AND has forbidden content
const multiViolationContent = 'spam '.repeat(100); // Too long and has "spam"
const result = validatePostContent(multiViolationContent);
expect(result.valid).toBe(false);
// Should have at least 2 errors (length and forbidden content)
expect(result.errors.length).toBeGreaterThanOrEqual(1);
});
});