Add tests.
This commit is contained in:
parent
ace0db0446
commit
ca428250fe
18 changed files with 2251 additions and 2 deletions
241
Jenkinsfile
vendored
Normal file
241
Jenkinsfile
vendored
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
environment {
|
||||||
|
APP_ENV = 'testing'
|
||||||
|
DB_CONNECTION = 'mysql'
|
||||||
|
DB_HOST = 'mysql'
|
||||||
|
DB_PORT = '3306'
|
||||||
|
DB_DATABASE = 'ffr_testing'
|
||||||
|
DB_USERNAME = 'ffr_user'
|
||||||
|
DB_PASSWORD = 'ffr_password'
|
||||||
|
CACHE_STORE = 'array'
|
||||||
|
SESSION_DRIVER = 'array'
|
||||||
|
QUEUE_CONNECTION = 'sync'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Setup Environment') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
sh '''
|
||||||
|
echo "Setting up environment for testing..."
|
||||||
|
cp .env.example .env.testing
|
||||||
|
echo "APP_ENV=testing" >> .env.testing
|
||||||
|
echo "DB_CONNECTION=${DB_CONNECTION}" >> .env.testing
|
||||||
|
echo "DB_HOST=${DB_HOST}" >> .env.testing
|
||||||
|
echo "DB_PORT=${DB_PORT}" >> .env.testing
|
||||||
|
echo "DB_DATABASE=${DB_DATABASE}" >> .env.testing
|
||||||
|
echo "DB_USERNAME=${DB_USERNAME}" >> .env.testing
|
||||||
|
echo "DB_PASSWORD=${DB_PASSWORD}" >> .env.testing
|
||||||
|
echo "CACHE_STORE=${CACHE_STORE}" >> .env.testing
|
||||||
|
echo "SESSION_DRIVER=${SESSION_DRIVER}" >> .env.testing
|
||||||
|
echo "QUEUE_CONNECTION=${QUEUE_CONNECTION}" >> .env.testing
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Install Dependencies') {
|
||||||
|
parallel {
|
||||||
|
stage('PHP Dependencies') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Installing PHP dependencies..."
|
||||||
|
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Node Dependencies') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Installing Node.js dependencies..."
|
||||||
|
npm ci
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Generate Application Key') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
php artisan key:generate --env=testing --force
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Database Setup') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Setting up test database..."
|
||||||
|
php artisan migrate:fresh --env=testing --force
|
||||||
|
php artisan config:clear --env=testing
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Code Quality Checks') {
|
||||||
|
parallel {
|
||||||
|
stage('PHP Syntax Check') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Checking PHP syntax..."
|
||||||
|
find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*" -exec php -l {} \\;
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('PHPStan Analysis') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
try {
|
||||||
|
sh '''
|
||||||
|
if [ -f "phpstan.neon" ]; then
|
||||||
|
echo "Running PHPStan static analysis..."
|
||||||
|
./vendor/bin/phpstan analyse --no-progress --error-format=table
|
||||||
|
else
|
||||||
|
echo "PHPStan configuration not found, skipping static analysis"
|
||||||
|
fi
|
||||||
|
'''
|
||||||
|
} catch (Exception e) {
|
||||||
|
unstable(message: "PHPStan found issues")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Security Audit') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
try {
|
||||||
|
sh '''
|
||||||
|
echo "Running security audit..."
|
||||||
|
composer audit
|
||||||
|
'''
|
||||||
|
} catch (Exception e) {
|
||||||
|
unstable(message: "Security vulnerabilities found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Unit Tests') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Running Unit Tests..."
|
||||||
|
php artisan test tests/Unit/ --env=testing --stop-on-failure
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
publishTestResults testResultsPattern: 'tests/Unit/results/*.xml'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Feature Tests') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Running Feature Tests..."
|
||||||
|
php artisan test tests/Feature/ --env=testing --stop-on-failure
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
publishTestResults testResultsPattern: 'tests/Feature/results/*.xml'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Full Regression Test Suite') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Running comprehensive regression test suite..."
|
||||||
|
chmod +x ./run-regression-tests.sh
|
||||||
|
./run-regression-tests.sh
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// Archive test results
|
||||||
|
archiveArtifacts artifacts: 'tests/reports/**/*', allowEmptyArchive: true
|
||||||
|
|
||||||
|
// Publish coverage reports if available
|
||||||
|
publishHTML([
|
||||||
|
allowMissing: false,
|
||||||
|
alwaysLinkToLastBuild: true,
|
||||||
|
keepAll: true,
|
||||||
|
reportDir: 'coverage',
|
||||||
|
reportFiles: 'index.html',
|
||||||
|
reportName: 'Coverage Report'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Performance Tests') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
try {
|
||||||
|
sh '''
|
||||||
|
echo "Running performance tests..."
|
||||||
|
|
||||||
|
# Test memory usage
|
||||||
|
php -d memory_limit=256M artisan test tests/Feature/DatabaseIntegrationTest.php --env=testing
|
||||||
|
|
||||||
|
# Test response times for API endpoints
|
||||||
|
time php artisan test tests/Feature/ApiEndpointRegressionTest.php --env=testing
|
||||||
|
'''
|
||||||
|
} catch (Exception e) {
|
||||||
|
unstable(message: "Performance tests indicated potential issues")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Build Assets') {
|
||||||
|
when {
|
||||||
|
anyOf {
|
||||||
|
branch 'main'
|
||||||
|
branch 'develop'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
echo "Building production assets..."
|
||||||
|
npm run build
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// Clean up
|
||||||
|
sh '''
|
||||||
|
echo "Cleaning up..."
|
||||||
|
rm -f .env.testing
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
success {
|
||||||
|
echo '✅ All regression tests passed successfully!'
|
||||||
|
// Notify success (customize as needed)
|
||||||
|
// slackSend channel: '#dev-team', color: 'good', message: "Regression tests passed for ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo '❌ Regression tests failed!'
|
||||||
|
// Notify failure (customize as needed)
|
||||||
|
// slackSend channel: '#dev-team', color: 'danger', message: "Regression tests failed for ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
|
||||||
|
}
|
||||||
|
unstable {
|
||||||
|
echo '⚠️ Tests completed with warnings'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Database\Factories\ArticlePublicationFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
|
@ -14,6 +16,9 @@
|
||||||
*/
|
*/
|
||||||
class ArticlePublication extends Model
|
class ArticlePublication extends Model
|
||||||
{
|
{
|
||||||
|
/** @use HasFactory<ArticlePublicationFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'article_id',
|
'article_id',
|
||||||
'platform_channel_id',
|
'platform_channel_id',
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Database\Factories\RouteFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
|
|
@ -18,6 +20,9 @@
|
||||||
*/
|
*/
|
||||||
class Route extends Pivot
|
class Route extends Pivot
|
||||||
{
|
{
|
||||||
|
/** @use HasFactory<RouteFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $table = 'routes';
|
protected $table = 'routes';
|
||||||
|
|
||||||
public $incrementing = false;
|
public $incrementing = false;
|
||||||
|
|
|
||||||
33
database/factories/ArticlePublicationFactory.php
Normal file
33
database/factories/ArticlePublicationFactory.php
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\ArticlePublication;
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class ArticlePublicationFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = ArticlePublication::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'article_id' => Article::factory(),
|
||||||
|
'platform_channel_id' => PlatformChannel::factory(),
|
||||||
|
'post_id' => $this->faker->uuid(),
|
||||||
|
'published_at' => $this->faker->dateTimeBetween('-1 month', 'now'),
|
||||||
|
'published_by' => $this->faker->userName(),
|
||||||
|
'created_at' => $this->faker->dateTimeBetween('-1 month', 'now'),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function recentlyPublished(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'published_at' => $this->faker->dateTimeBetween('-1 day', 'now'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
database/factories/KeywordFactory.php
Normal file
35
database/factories/KeywordFactory.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Keyword;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class KeywordFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Keyword::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'keyword' => $this->faker->word(),
|
||||||
|
'is_blocked' => $this->faker->boolean(30), // 30% chance of being blocked
|
||||||
|
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function blocked(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'is_blocked' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allowed(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'is_blocked' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
database/factories/RouteFactory.php
Normal file
38
database/factories/RouteFactory.php
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Route;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class RouteFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Route::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'feed_id' => Feed::factory(),
|
||||||
|
'platform_channel_id' => PlatformChannel::factory(),
|
||||||
|
'is_active' => $this->faker->boolean(80), // 80% chance of being active
|
||||||
|
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function active(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inactive(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'is_active' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
database/factories/SettingFactory.php
Normal file
35
database/factories/SettingFactory.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class SettingFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Setting::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'key' => $this->faker->unique()->slug(2, '_'),
|
||||||
|
'value' => $this->faker->word(),
|
||||||
|
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withKey(string $key): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'key' => $key,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withValue(string $value): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'value' => $value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,10 +19,12 @@
|
||||||
</source>
|
</source>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_KEY" value="base64:2fl+Ktvkdg+Fuz4Qp/A75G2RTiWVA/ZoKwE4qJowQPI="/>
|
||||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="CACHE_STORE" value="array"/>
|
<env name="CACHE_STORE" value="array"/>
|
||||||
<env name="DB_DATABASE" value="testing"/>
|
<env name="DB_CONNECTION" value="sqlite"/>
|
||||||
|
<env name="DB_DATABASE" value=":memory:"/>
|
||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
<env name="PULSE_ENABLED" value="false"/>
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
|
|
||||||
157
run-regression-tests.sh
Executable file
157
run-regression-tests.sh
Executable file
|
|
@ -0,0 +1,157 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# FFR Regression Test Suite Runner
|
||||||
|
# Comprehensive test runner for the FFR application
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🧪 Starting FFR Regression Test Suite"
|
||||||
|
echo "======================================"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_status() {
|
||||||
|
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if we're in the right directory
|
||||||
|
if [ ! -f "composer.json" ] || [ ! -f "phpunit.xml" ]; then
|
||||||
|
print_error "This script must be run from the project root directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set test environment
|
||||||
|
export APP_ENV=testing
|
||||||
|
|
||||||
|
print_status "Setting up test environment..."
|
||||||
|
|
||||||
|
# Clear configuration cache
|
||||||
|
php artisan config:clear --env=testing
|
||||||
|
|
||||||
|
# Ensure database is set up for testing
|
||||||
|
print_status "Preparing test database..."
|
||||||
|
php artisan migrate:fresh --env=testing --force
|
||||||
|
|
||||||
|
# Run database seeders for testing if they exist
|
||||||
|
if [ -f "database/seeders/TestSeeder.php" ]; then
|
||||||
|
php artisan db:seed --class=TestSeeder --env=testing
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_status "Running regression tests..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Track test results
|
||||||
|
TOTAL_TESTS=0
|
||||||
|
FAILED_TESTS=0
|
||||||
|
|
||||||
|
# Function to run test suite and track results
|
||||||
|
run_test_suite() {
|
||||||
|
local suite_name="$1"
|
||||||
|
local test_path="$2"
|
||||||
|
local description="$3"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_status "Running $suite_name..."
|
||||||
|
echo "Description: $description"
|
||||||
|
|
||||||
|
if php artisan test "$test_path" --env=testing; then
|
||||||
|
print_success "$suite_name passed"
|
||||||
|
else
|
||||||
|
print_error "$suite_name failed"
|
||||||
|
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run individual test suites
|
||||||
|
run_test_suite "API Endpoint Tests" "tests/Feature/ApiEndpointRegressionTest.php" "Tests all HTTP endpoints and routes"
|
||||||
|
|
||||||
|
run_test_suite "Database Integration Tests" "tests/Feature/DatabaseIntegrationTest.php" "Tests models, relationships, and database operations"
|
||||||
|
|
||||||
|
run_test_suite "Article Discovery Tests" "tests/Feature/ArticleDiscoveryCommandTest.php" "Tests article discovery command functionality"
|
||||||
|
|
||||||
|
run_test_suite "Article Publishing Tests" "tests/Feature/ArticlePublishingTest.php" "Tests article publishing workflow"
|
||||||
|
|
||||||
|
run_test_suite "Jobs and Events Tests" "tests/Feature/JobsAndEventsTest.php" "Tests queue jobs and event handling"
|
||||||
|
|
||||||
|
run_test_suite "Authentication & Authorization Tests" "tests/Feature/AuthenticationAndAuthorizationTest.php" "Tests security and access control"
|
||||||
|
|
||||||
|
run_test_suite "New Article Fetched Event Tests" "tests/Feature/NewArticleFetchedEventTest.php" "Tests new article event handling"
|
||||||
|
|
||||||
|
run_test_suite "Validate Article Listener Tests" "tests/Feature/ValidateArticleListenerTest.php" "Tests article validation logic"
|
||||||
|
|
||||||
|
# Run Unit Tests
|
||||||
|
echo ""
|
||||||
|
print_status "Running Unit Tests..."
|
||||||
|
|
||||||
|
run_test_suite "Article Fetcher Unit Tests" "tests/Unit/Services/ArticleFetcherTest.php" "Tests article fetching service"
|
||||||
|
|
||||||
|
run_test_suite "Validation Service Unit Tests" "tests/Unit/Services/ValidationServiceTest.php" "Tests article validation service"
|
||||||
|
|
||||||
|
run_test_suite "Dashboard Stats Service Unit Tests" "tests/Unit/Services/DashboardStatsServiceTest.php" "Tests dashboard statistics service"
|
||||||
|
|
||||||
|
# Run full test suite for coverage
|
||||||
|
echo ""
|
||||||
|
print_status "Running complete test suite for coverage..."
|
||||||
|
|
||||||
|
if php artisan test --coverage-text --min=70 --env=testing; then
|
||||||
|
print_success "Test coverage meets minimum requirements"
|
||||||
|
else
|
||||||
|
print_warning "Test coverage below 70% - consider adding more tests"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Performance Tests
|
||||||
|
echo ""
|
||||||
|
print_status "Running performance checks..."
|
||||||
|
|
||||||
|
# Test database query performance
|
||||||
|
php artisan test tests/Feature/DatabaseIntegrationTest.php --env=testing --stop-on-failure
|
||||||
|
|
||||||
|
# Memory usage test
|
||||||
|
if php -d memory_limit=128M artisan test --env=testing --stop-on-failure; then
|
||||||
|
print_success "Memory usage within acceptable limits"
|
||||||
|
else
|
||||||
|
print_warning "High memory usage detected during tests"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Final Results
|
||||||
|
echo ""
|
||||||
|
echo "======================================"
|
||||||
|
print_status "Test Suite Complete"
|
||||||
|
echo "======================================"
|
||||||
|
|
||||||
|
if [ $FAILED_TESTS -eq 0 ]; then
|
||||||
|
print_success "All $TOTAL_TESTS test suites passed! 🎉"
|
||||||
|
echo ""
|
||||||
|
echo "Regression test suite completed successfully."
|
||||||
|
echo "The application is ready for deployment."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
print_error "$FAILED_TESTS out of $TOTAL_TESTS test suites failed"
|
||||||
|
echo ""
|
||||||
|
echo "Please review the failing tests before proceeding."
|
||||||
|
echo "Run individual test suites with:"
|
||||||
|
echo " php artisan test tests/Feature/[TestFile].php"
|
||||||
|
echo " php artisan test tests/Unit/[TestFile].php"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
21
tests/CreatesApplication.php
Normal file
21
tests/CreatesApplication.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Console\Kernel;
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
|
||||||
|
trait CreatesApplication
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates the application.
|
||||||
|
*/
|
||||||
|
public function createApplication(): Application
|
||||||
|
{
|
||||||
|
$app = require __DIR__.'/../bootstrap/app.php';
|
||||||
|
|
||||||
|
$app->make(Kernel::class)->bootstrap();
|
||||||
|
|
||||||
|
return $app;
|
||||||
|
}
|
||||||
|
}
|
||||||
315
tests/Feature/ApiEndpointRegressionTest.php
Normal file
315
tests/Feature/ApiEndpointRegressionTest.php
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\PlatformAccount;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\Route;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ApiEndpointRegressionTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_homepage_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_onboarding_platform_page_loads(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/onboarding/platform');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_onboarding_feed_page_loads(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/onboarding/feed');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_onboarding_channel_page_loads(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/onboarding/channel');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_onboarding_complete_page_loads(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/onboarding/complete');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_articles_page_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/articles');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_articles_approve_endpoint_works(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'approval_status' => 'pending'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->post("/articles/{$article->id}/approve");
|
||||||
|
$response->assertRedirect();
|
||||||
|
|
||||||
|
$article->refresh();
|
||||||
|
$this->assertEquals('approved', $article->approval_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_articles_reject_endpoint_works(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'approval_status' => 'pending'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->post("/articles/{$article->id}/reject");
|
||||||
|
$response->assertRedirect();
|
||||||
|
|
||||||
|
$article->refresh();
|
||||||
|
$this->assertEquals('rejected', $article->approval_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_logs_page_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/logs');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_settings_page_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/settings');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_settings_update_endpoint_works(): void
|
||||||
|
{
|
||||||
|
Setting::factory()->create([
|
||||||
|
'key' => 'test_setting',
|
||||||
|
'value' => 'old_value'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->put('/settings', [
|
||||||
|
'test_setting' => 'new_value'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect();
|
||||||
|
$this->assertEquals('new_value', Setting::where('key', 'test_setting')->first()->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platforms_index_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/platforms');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platforms_create_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/platforms/create');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platforms_show_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$platform = PlatformAccount::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get("/platforms/{$platform->id}");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platforms_edit_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$platform = PlatformAccount::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get("/platforms/{$platform->id}/edit");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platforms_set_active_endpoint_works(): void
|
||||||
|
{
|
||||||
|
$platform = PlatformAccount::factory()->create(['is_active' => false]);
|
||||||
|
|
||||||
|
$response = $this->post("/platforms/{$platform->id}/set-active");
|
||||||
|
$response->assertRedirect();
|
||||||
|
|
||||||
|
$platform->refresh();
|
||||||
|
$this->assertTrue($platform->is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_channels_index_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/channels');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_channels_create_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/channels/create');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_channels_show_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get("/channels/{$channel->id}");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_channels_edit_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get("/channels/{$channel->id}/edit");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_channels_toggle_endpoint_works(): void
|
||||||
|
{
|
||||||
|
$channel = PlatformChannel::factory()->create(['is_active' => false]);
|
||||||
|
|
||||||
|
$response = $this->post("/channels/{$channel->id}/toggle");
|
||||||
|
$response->assertRedirect();
|
||||||
|
|
||||||
|
$channel->refresh();
|
||||||
|
$this->assertTrue($channel->is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_feeds_index_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/feeds');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_feeds_create_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/feeds/create');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_feeds_show_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get("/feeds/{$feed->id}");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_feeds_edit_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get("/feeds/{$feed->id}/edit");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_feeds_toggle_endpoint_works(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create(['is_active' => false]);
|
||||||
|
|
||||||
|
$response = $this->post("/feeds/{$feed->id}/toggle");
|
||||||
|
$response->assertRedirect();
|
||||||
|
|
||||||
|
$feed->refresh();
|
||||||
|
$this->assertTrue($feed->is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_routing_index_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/routing');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_routing_create_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/routing/create');
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_routing_edit_loads_successfully(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
Route::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get("/routing/{$feed->id}/{$channel->id}/edit");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_routing_toggle_endpoint_works(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
$route = Route::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'is_active' => false
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->post("/routing/{$feed->id}/{$channel->id}/toggle");
|
||||||
|
$response->assertRedirect();
|
||||||
|
|
||||||
|
$route->refresh();
|
||||||
|
$this->assertTrue($route->is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_all_get_routes_return_successful_status(): void
|
||||||
|
{
|
||||||
|
// Create necessary test data
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
$platform = PlatformAccount::factory()->create();
|
||||||
|
Route::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$routes = [
|
||||||
|
'/',
|
||||||
|
'/onboarding/platform',
|
||||||
|
'/onboarding/feed',
|
||||||
|
'/onboarding/channel',
|
||||||
|
'/onboarding/complete',
|
||||||
|
'/articles',
|
||||||
|
'/logs',
|
||||||
|
'/settings',
|
||||||
|
'/platforms',
|
||||||
|
'/platforms/create',
|
||||||
|
"/platforms/{$platform->id}",
|
||||||
|
"/platforms/{$platform->id}/edit",
|
||||||
|
'/channels',
|
||||||
|
'/channels/create',
|
||||||
|
"/channels/{$channel->id}",
|
||||||
|
"/channels/{$channel->id}/edit",
|
||||||
|
'/feeds',
|
||||||
|
'/feeds/create',
|
||||||
|
"/feeds/{$feed->id}",
|
||||||
|
"/feeds/{$feed->id}/edit",
|
||||||
|
'/routing',
|
||||||
|
'/routing/create',
|
||||||
|
"/routing/{$feed->id}/{$channel->id}/edit",
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
$response = $this->get($route);
|
||||||
|
$this->assertTrue(
|
||||||
|
$response->isSuccessful(),
|
||||||
|
"Route {$route} failed with status {$response->getStatusCode()}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
300
tests/Feature/AuthenticationAndAuthorizationTest.php
Normal file
300
tests/Feature/AuthenticationAndAuthorizationTest.php
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\PlatformAccount;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AuthenticationAndAuthorizationTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_guest_can_access_public_routes(): void
|
||||||
|
{
|
||||||
|
$publicRoutes = [
|
||||||
|
'/',
|
||||||
|
'/onboarding/platform',
|
||||||
|
'/onboarding/feed',
|
||||||
|
'/onboarding/channel',
|
||||||
|
'/onboarding/complete'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($publicRoutes as $route) {
|
||||||
|
$response = $this->get($route);
|
||||||
|
$this->assertTrue(
|
||||||
|
$response->isSuccessful(),
|
||||||
|
"Public route {$route} should be accessible to guests"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_application_routes_require_no_authentication_by_default(): void
|
||||||
|
{
|
||||||
|
// Test that main application routes are accessible
|
||||||
|
// This assumes the application doesn't have authentication middleware by default
|
||||||
|
|
||||||
|
$routes = [
|
||||||
|
'/articles',
|
||||||
|
'/logs',
|
||||||
|
'/settings',
|
||||||
|
'/platforms',
|
||||||
|
'/channels',
|
||||||
|
'/feeds',
|
||||||
|
'/routing'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
$response = $this->get($route);
|
||||||
|
$this->assertTrue(
|
||||||
|
$response->isSuccessful(),
|
||||||
|
"Route {$route} should be accessible"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_article_approval_permissions(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'approval_status' => 'pending'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Test approval endpoint
|
||||||
|
$response = $this->post("/articles/{$article->id}/approve");
|
||||||
|
$response->assertRedirect(); // Should redirect after successful approval
|
||||||
|
|
||||||
|
$article->refresh();
|
||||||
|
$this->assertEquals('approved', $article->approval_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_article_rejection_permissions(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'approval_status' => 'pending'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Test rejection endpoint
|
||||||
|
$response = $this->post("/articles/{$article->id}/reject");
|
||||||
|
$response->assertRedirect(); // Should redirect after successful rejection
|
||||||
|
|
||||||
|
$article->refresh();
|
||||||
|
$this->assertEquals('rejected', $article->approval_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platform_management_permissions(): void
|
||||||
|
{
|
||||||
|
$platform = PlatformAccount::factory()->create(['is_active' => false]);
|
||||||
|
|
||||||
|
// Test platform activation
|
||||||
|
$response = $this->post("/platforms/{$platform->id}/set-active");
|
||||||
|
$response->assertRedirect();
|
||||||
|
|
||||||
|
$platform->refresh();
|
||||||
|
$this->assertTrue($platform->is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_channel_management_permissions(): void
|
||||||
|
{
|
||||||
|
$channel = PlatformChannel::factory()->create(['is_active' => false]);
|
||||||
|
|
||||||
|
// Test channel toggle
|
||||||
|
$response = $this->post("/channels/{$channel->id}/toggle");
|
||||||
|
$response->assertRedirect();
|
||||||
|
|
||||||
|
$channel->refresh();
|
||||||
|
$this->assertTrue($channel->is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_feed_management_permissions(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create(['is_active' => false]);
|
||||||
|
|
||||||
|
// Test feed toggle
|
||||||
|
$response = $this->post("/feeds/{$feed->id}/toggle");
|
||||||
|
$response->assertRedirect();
|
||||||
|
|
||||||
|
$feed->refresh();
|
||||||
|
$this->assertTrue($feed->is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_settings_update_permissions(): void
|
||||||
|
{
|
||||||
|
Setting::factory()->create([
|
||||||
|
'key' => 'test_setting',
|
||||||
|
'value' => 'old_value'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Test settings update
|
||||||
|
$response = $this->put('/settings', [
|
||||||
|
'test_setting' => 'new_value'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect();
|
||||||
|
$this->assertEquals('new_value', Setting::where('key', 'test_setting')->first()->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_routing_management_permissions(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
// Test routing creation
|
||||||
|
$response = $this->post('/routing', [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Should either create successfully or have validation errors
|
||||||
|
$this->assertTrue(
|
||||||
|
$response->isRedirect() || $response->status() === 422,
|
||||||
|
'Routing creation should either succeed or fail with validation'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_crud_operations_on_resources(): void
|
||||||
|
{
|
||||||
|
// Test platform CRUD
|
||||||
|
$platform = PlatformAccount::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get("/platforms/{$platform->id}");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
|
||||||
|
$response = $this->get("/platforms/{$platform->id}/edit");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
|
||||||
|
// Test channel CRUD
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get("/channels/{$channel->id}");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
|
||||||
|
$response = $this->get("/channels/{$channel->id}/edit");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
|
||||||
|
// Test feed CRUD
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get("/feeds/{$feed->id}");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
|
||||||
|
$response = $this->get("/feeds/{$feed->id}/edit");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_resource_creation_pages_are_accessible(): void
|
||||||
|
{
|
||||||
|
$createRoutes = [
|
||||||
|
'/platforms/create',
|
||||||
|
'/channels/create',
|
||||||
|
'/feeds/create',
|
||||||
|
'/routing/create'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($createRoutes as $route) {
|
||||||
|
$response = $this->get($route);
|
||||||
|
$this->assertTrue(
|
||||||
|
$response->isSuccessful(),
|
||||||
|
"Create route {$route} should be accessible"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_nonexistent_resource_returns_404(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/platforms/99999');
|
||||||
|
$response->assertNotFound();
|
||||||
|
|
||||||
|
$response = $this->get('/channels/99999');
|
||||||
|
$response->assertNotFound();
|
||||||
|
|
||||||
|
$response = $this->get('/feeds/99999');
|
||||||
|
$response->assertNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_invalid_article_operations_handle_gracefully(): void
|
||||||
|
{
|
||||||
|
// Test operations on nonexistent articles
|
||||||
|
$response = $this->post('/articles/99999/approve');
|
||||||
|
$response->assertNotFound();
|
||||||
|
|
||||||
|
$response = $this->post('/articles/99999/reject');
|
||||||
|
$response->assertNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_invalid_platform_operations_handle_gracefully(): void
|
||||||
|
{
|
||||||
|
// Test operations on nonexistent platforms
|
||||||
|
$response = $this->post('/platforms/99999/set-active');
|
||||||
|
$response->assertNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_invalid_channel_operations_handle_gracefully(): void
|
||||||
|
{
|
||||||
|
// Test operations on nonexistent channels
|
||||||
|
$response = $this->post('/channels/99999/toggle');
|
||||||
|
$response->assertNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_invalid_feed_operations_handle_gracefully(): void
|
||||||
|
{
|
||||||
|
// Test operations on nonexistent feeds
|
||||||
|
$response = $this->post('/feeds/99999/toggle');
|
||||||
|
$response->assertNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_csrf_protection_on_post_requests(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
||||||
|
|
||||||
|
// Test that POST requests without CSRF token are rejected
|
||||||
|
$response = $this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class)
|
||||||
|
->post("/articles/{$article->id}/approve");
|
||||||
|
|
||||||
|
// Should work when CSRF middleware is disabled for testing
|
||||||
|
$response->assertRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_method_spoofing_works_for_put_delete(): void
|
||||||
|
{
|
||||||
|
// Test that method spoofing works for PUT/DELETE requests
|
||||||
|
Setting::factory()->create([
|
||||||
|
'key' => 'test_setting',
|
||||||
|
'value' => 'old_value'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->put('/settings', [
|
||||||
|
'test_setting' => 'new_value'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_route_model_binding_works_correctly(): void
|
||||||
|
{
|
||||||
|
// Test that route model binding resolves correctly
|
||||||
|
$platform = PlatformAccount::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
|
||||||
|
// These should all resolve the models correctly
|
||||||
|
$response = $this->get("/platforms/{$platform->id}");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
|
||||||
|
$response = $this->get("/channels/{$channel->id}");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
|
||||||
|
$response = $this->get("/feeds/{$feed->id}");
|
||||||
|
$response->assertSuccessful();
|
||||||
|
}
|
||||||
|
}
|
||||||
371
tests/Feature/DatabaseIntegrationTest.php
Normal file
371
tests/Feature/DatabaseIntegrationTest.php
Normal file
|
|
@ -0,0 +1,371 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\ArticlePublication;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\Keyword;
|
||||||
|
use App\Models\Language;
|
||||||
|
use App\Models\Log;
|
||||||
|
use App\Models\PlatformAccount;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\PlatformChannelPost;
|
||||||
|
use App\Models\PlatformInstance;
|
||||||
|
use App\Models\Route;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DatabaseIntegrationTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_user_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'name' => 'Test User',
|
||||||
|
'email' => 'test@example.com'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('users', [
|
||||||
|
'name' => 'Test User',
|
||||||
|
'email' => 'test@example.com'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals('Test User', $user->name);
|
||||||
|
$this->assertEquals('test@example.com', $user->email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_language_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create([
|
||||||
|
'name' => 'English',
|
||||||
|
'code' => 'en'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('languages', [
|
||||||
|
'name' => 'English',
|
||||||
|
'code' => 'en'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platform_instance_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create([
|
||||||
|
'name' => 'Test Instance',
|
||||||
|
'url' => 'https://test.lemmy.world'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('platform_instances', [
|
||||||
|
'name' => 'Test Instance',
|
||||||
|
'url' => 'https://test.lemmy.world'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platform_account_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$instance = PlatformInstance::factory()->create();
|
||||||
|
|
||||||
|
$account = PlatformAccount::factory()->create([
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'username' => 'testuser',
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('platform_accounts', [
|
||||||
|
'platform_instance_id' => $instance->id,
|
||||||
|
'username' => 'testuser',
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($instance->id, $account->platformInstance->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platform_channel_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
$account = PlatformAccount::factory()->create();
|
||||||
|
|
||||||
|
$channel = PlatformChannel::factory()->create([
|
||||||
|
'platform_account_id' => $account->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'name' => 'Test Channel',
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('platform_channels', [
|
||||||
|
'platform_account_id' => $account->id,
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'name' => 'Test Channel',
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($account->id, $channel->platformAccount->id);
|
||||||
|
$this->assertEquals($language->id, $channel->language->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_feed_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
|
||||||
|
$feed = Feed::factory()->create([
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'name' => 'Test Feed',
|
||||||
|
'url' => 'https://example.com/feed.rss',
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('feeds', [
|
||||||
|
'language_id' => $language->id,
|
||||||
|
'name' => 'Test Feed',
|
||||||
|
'url' => 'https://example.com/feed.rss',
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($language->id, $feed->language->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_article_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'title' => 'Test Article',
|
||||||
|
'url' => 'https://example.com/article',
|
||||||
|
'approval_status' => 'pending'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('articles', [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'title' => 'Test Article',
|
||||||
|
'url' => 'https://example.com/article',
|
||||||
|
'approval_status' => 'pending'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($feed->id, $article->feed->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_article_publication_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$article = Article::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
$publication = ArticlePublication::create([
|
||||||
|
'article_id' => $article->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'post_id' => 'test-post-123',
|
||||||
|
'published_at' => now(),
|
||||||
|
'published_by' => 'test-user'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('article_publications', [
|
||||||
|
'article_id' => $article->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'post_id' => 'test-post-123',
|
||||||
|
'published_by' => 'test-user'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($article->id, $publication->article->id);
|
||||||
|
$this->assertEquals($channel->id, $publication->platformChannel->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_route_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
$route = Route::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('routes', [
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($feed->id, $route->feed->id);
|
||||||
|
$this->assertEquals($channel->id, $route->platformChannel->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platform_channel_post_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
$post = PlatformChannelPost::create([
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'post_id' => 'external-post-123',
|
||||||
|
'title' => 'Test Post',
|
||||||
|
'content' => 'Test content',
|
||||||
|
'url' => 'https://example.com/post',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('platform_channel_posts', [
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'post_id' => 'external-post-123',
|
||||||
|
'title' => 'Test Post'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($channel->id, $post->platformChannel->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_keyword_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$keyword = Keyword::factory()->create([
|
||||||
|
'keyword' => 'test keyword',
|
||||||
|
'is_blocked' => false
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('keywords', [
|
||||||
|
'keyword' => 'test keyword',
|
||||||
|
'is_blocked' => false
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_log_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$log = Log::create([
|
||||||
|
'level' => 'info',
|
||||||
|
'message' => 'Test log message',
|
||||||
|
'context' => json_encode(['key' => 'value']),
|
||||||
|
'logged_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('logs', [
|
||||||
|
'level' => 'info',
|
||||||
|
'message' => 'Test log message'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_setting_model_creates_successfully(): void
|
||||||
|
{
|
||||||
|
$setting = Setting::factory()->create([
|
||||||
|
'key' => 'test_setting',
|
||||||
|
'value' => 'test_value'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('settings', [
|
||||||
|
'key' => 'test_setting',
|
||||||
|
'value' => 'test_value'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_feed_articles_relationship(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$articles = Article::factory()->count(3)->create(['feed_id' => $feed->id]);
|
||||||
|
|
||||||
|
$this->assertCount(3, $feed->articles);
|
||||||
|
|
||||||
|
foreach ($articles as $article) {
|
||||||
|
$this->assertTrue($feed->articles->contains($article));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_article_publications_relationship(): void
|
||||||
|
{
|
||||||
|
$article = Article::factory()->create();
|
||||||
|
$publications = ArticlePublication::factory()->count(2)->create(['article_id' => $article->id]);
|
||||||
|
|
||||||
|
$this->assertCount(2, $article->publications);
|
||||||
|
|
||||||
|
foreach ($publications as $publication) {
|
||||||
|
$this->assertTrue($article->publications->contains($publication));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platform_account_channels_relationship(): void
|
||||||
|
{
|
||||||
|
$account = PlatformAccount::factory()->create();
|
||||||
|
$channels = PlatformChannel::factory()->count(2)->create(['platform_account_id' => $account->id]);
|
||||||
|
|
||||||
|
$this->assertCount(2, $account->channels);
|
||||||
|
|
||||||
|
foreach ($channels as $channel) {
|
||||||
|
$this->assertTrue($account->channels->contains($channel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platform_channel_routes_relationship(): void
|
||||||
|
{
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
$routes = Route::factory()->count(2)->create(['platform_channel_id' => $channel->id]);
|
||||||
|
|
||||||
|
$this->assertCount(2, $channel->routes);
|
||||||
|
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
$this->assertTrue($channel->routes->contains($route));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_language_platform_instances_relationship(): void
|
||||||
|
{
|
||||||
|
$language = Language::factory()->create();
|
||||||
|
$instances = PlatformInstance::factory()->count(2)->create();
|
||||||
|
|
||||||
|
// Attach language to instances via pivot table
|
||||||
|
foreach ($instances as $instance) {
|
||||||
|
$language->platformInstances()->attach($instance->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertCount(2, $language->platformInstances);
|
||||||
|
|
||||||
|
foreach ($instances as $instance) {
|
||||||
|
$this->assertTrue($language->platformInstances->contains($instance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_model_soft_deletes_work_correctly(): void
|
||||||
|
{
|
||||||
|
// Test models that might use soft deletes
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$feedId = $feed->id;
|
||||||
|
|
||||||
|
$feed->delete();
|
||||||
|
|
||||||
|
// Should not find with normal query if soft deleted
|
||||||
|
$this->assertNull(Feed::find($feedId));
|
||||||
|
|
||||||
|
// Should find with withTrashed if model uses soft deletes
|
||||||
|
if (method_exists($feed, 'withTrashed')) {
|
||||||
|
$this->assertNotNull(Feed::withTrashed()->find($feedId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_database_constraints_are_enforced(): void
|
||||||
|
{
|
||||||
|
// Test foreign key constraints
|
||||||
|
$this->expectException(\Illuminate\Database\QueryException::class);
|
||||||
|
|
||||||
|
// Try to create article with non-existent feed_id
|
||||||
|
Article::factory()->create(['feed_id' => 99999]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_all_factories_work_correctly(): void
|
||||||
|
{
|
||||||
|
// Test that all model factories can create valid records
|
||||||
|
$models = [
|
||||||
|
User::factory()->make(),
|
||||||
|
Language::factory()->make(),
|
||||||
|
PlatformInstance::factory()->make(),
|
||||||
|
PlatformAccount::factory()->make(),
|
||||||
|
PlatformChannel::factory()->make(),
|
||||||
|
Feed::factory()->make(),
|
||||||
|
Article::factory()->make(),
|
||||||
|
Route::factory()->make(),
|
||||||
|
Keyword::factory()->make(),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($models as $model) {
|
||||||
|
$this->assertNotNull($model);
|
||||||
|
$this->assertInstanceOf(\Illuminate\Database\Eloquent\Model::class, $model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
301
tests/Feature/JobsAndEventsTest.php
Normal file
301
tests/Feature/JobsAndEventsTest.php
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Events\ArticleApproved;
|
||||||
|
use App\Events\ArticleReadyToPublish;
|
||||||
|
use App\Events\ExceptionLogged;
|
||||||
|
use App\Events\ExceptionOccurred;
|
||||||
|
use App\Events\NewArticleFetched;
|
||||||
|
use App\Jobs\ArticleDiscoveryJob;
|
||||||
|
use App\Jobs\ArticleDiscoveryForFeedJob;
|
||||||
|
use App\Jobs\PublishToLemmyJob;
|
||||||
|
use App\Jobs\SyncChannelPostsJob;
|
||||||
|
use App\Listeners\LogExceptionToDatabase;
|
||||||
|
use App\Listeners\PublishApprovedArticle;
|
||||||
|
use App\Listeners\PublishArticle;
|
||||||
|
use App\Listeners\ValidateArticleListener;
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\Log;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class JobsAndEventsTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_article_discovery_job_processes_successfully(): void
|
||||||
|
{
|
||||||
|
Queue::fake();
|
||||||
|
|
||||||
|
$feed = Feed::factory()->create(['is_active' => true]);
|
||||||
|
|
||||||
|
$job = new ArticleDiscoveryJob();
|
||||||
|
$job->handle();
|
||||||
|
|
||||||
|
// Should dispatch individual feed jobs
|
||||||
|
Queue::assertPushed(ArticleDiscoveryForFeedJob::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_article_discovery_for_feed_job_processes_feed(): void
|
||||||
|
{
|
||||||
|
Event::fake();
|
||||||
|
|
||||||
|
$feed = Feed::factory()->create([
|
||||||
|
'url' => 'https://example.com/feed',
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$job = new ArticleDiscoveryForFeedJob($feed);
|
||||||
|
|
||||||
|
// Mock the external dependency behavior
|
||||||
|
$this->app->bind(\App\Services\Article\ArticleFetcher::class, function () {
|
||||||
|
$mock = \Mockery::mock(\App\Services\Article\ArticleFetcher::class);
|
||||||
|
$mock->shouldReceive('fetchArticles')->andReturn([
|
||||||
|
['title' => 'Test Article', 'url' => 'https://example.com/article1'],
|
||||||
|
['title' => 'Another Article', 'url' => 'https://example.com/article2']
|
||||||
|
]);
|
||||||
|
return $mock;
|
||||||
|
});
|
||||||
|
|
||||||
|
$job->handle();
|
||||||
|
|
||||||
|
// Should create articles and fire events
|
||||||
|
$this->assertCount(2, Article::all());
|
||||||
|
Event::assertDispatched(NewArticleFetched::class, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_sync_channel_posts_job_processes_successfully(): void
|
||||||
|
{
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
$job = new SyncChannelPostsJob($channel);
|
||||||
|
|
||||||
|
// Mock the external dependency
|
||||||
|
$this->app->bind(\App\Modules\Lemmy\Services\LemmyApiService::class, function () {
|
||||||
|
$mock = \Mockery::mock(\App\Modules\Lemmy\Services\LemmyApiService::class);
|
||||||
|
$mock->shouldReceive('getChannelPosts')->andReturn([]);
|
||||||
|
return $mock;
|
||||||
|
});
|
||||||
|
|
||||||
|
$result = $job->handle();
|
||||||
|
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_publish_to_lemmy_job_has_correct_configuration(): void
|
||||||
|
{
|
||||||
|
$article = Article::factory()->create();
|
||||||
|
|
||||||
|
$job = new PublishToLemmyJob($article);
|
||||||
|
|
||||||
|
$this->assertEquals('lemmy-posts', $job->queue);
|
||||||
|
$this->assertInstanceOf(PublishToLemmyJob::class, $job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_new_article_fetched_event_is_dispatched(): void
|
||||||
|
{
|
||||||
|
Event::fake();
|
||||||
|
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
||||||
|
|
||||||
|
event(new NewArticleFetched($article));
|
||||||
|
|
||||||
|
Event::assertDispatched(NewArticleFetched::class, function (NewArticleFetched $event) use ($article) {
|
||||||
|
return $event->article->id === $article->id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_article_approved_event_is_dispatched(): void
|
||||||
|
{
|
||||||
|
Event::fake();
|
||||||
|
|
||||||
|
$article = Article::factory()->create();
|
||||||
|
|
||||||
|
event(new ArticleApproved($article));
|
||||||
|
|
||||||
|
Event::assertDispatched(ArticleApproved::class, function (ArticleApproved $event) use ($article) {
|
||||||
|
return $event->article->id === $article->id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_article_ready_to_publish_event_is_dispatched(): void
|
||||||
|
{
|
||||||
|
Event::fake();
|
||||||
|
|
||||||
|
$article = Article::factory()->create();
|
||||||
|
|
||||||
|
event(new ArticleReadyToPublish($article));
|
||||||
|
|
||||||
|
Event::assertDispatched(ArticleReadyToPublish::class, function (ArticleReadyToPublish $event) use ($article) {
|
||||||
|
return $event->article->id === $article->id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_exception_occurred_event_is_dispatched(): void
|
||||||
|
{
|
||||||
|
Event::fake();
|
||||||
|
|
||||||
|
$exception = new \Exception('Test exception');
|
||||||
|
|
||||||
|
event(new ExceptionOccurred($exception, ['context' => 'test']));
|
||||||
|
|
||||||
|
Event::assertDispatched(ExceptionOccurred::class, function (ExceptionOccurred $event) {
|
||||||
|
return $event->exception->getMessage() === 'Test exception';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_exception_logged_event_is_dispatched(): void
|
||||||
|
{
|
||||||
|
Event::fake();
|
||||||
|
|
||||||
|
$logData = [
|
||||||
|
'level' => 'error',
|
||||||
|
'message' => 'Test error',
|
||||||
|
'context' => ['key' => 'value']
|
||||||
|
];
|
||||||
|
|
||||||
|
event(new ExceptionLogged($logData));
|
||||||
|
|
||||||
|
Event::assertDispatched(ExceptionLogged::class, function (ExceptionLogged $event) use ($logData) {
|
||||||
|
return $event->logData['message'] === 'Test error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_validate_article_listener_processes_new_article(): void
|
||||||
|
{
|
||||||
|
Event::fake([ArticleReadyToPublish::class]);
|
||||||
|
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'is_valid' => null,
|
||||||
|
'validated_at' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$listener = new ValidateArticleListener();
|
||||||
|
$event = new NewArticleFetched($article);
|
||||||
|
|
||||||
|
$listener->handle($event);
|
||||||
|
|
||||||
|
$article->refresh();
|
||||||
|
$this->assertNotNull($article->validated_at);
|
||||||
|
$this->assertNotNull($article->is_valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_publish_approved_article_listener_queues_job(): void
|
||||||
|
{
|
||||||
|
Queue::fake();
|
||||||
|
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'approval_status' => 'approved',
|
||||||
|
'is_valid' => true,
|
||||||
|
'validated_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$listener = new PublishApprovedArticle();
|
||||||
|
$event = new ArticleApproved($article);
|
||||||
|
|
||||||
|
$listener->handle($event);
|
||||||
|
|
||||||
|
Event::assertDispatched(ArticleReadyToPublish::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_publish_article_listener_queues_publish_job(): void
|
||||||
|
{
|
||||||
|
Queue::fake();
|
||||||
|
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'is_valid' => true,
|
||||||
|
'validated_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$listener = new PublishArticle();
|
||||||
|
$event = new ArticleReadyToPublish($article);
|
||||||
|
|
||||||
|
$listener->handle($event);
|
||||||
|
|
||||||
|
Queue::assertPushed(PublishToLemmyJob::class, function (PublishToLemmyJob $job) use ($article) {
|
||||||
|
return $job->article->id === $article->id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_log_exception_to_database_listener_creates_log(): void
|
||||||
|
{
|
||||||
|
$logData = [
|
||||||
|
'level' => 'error',
|
||||||
|
'message' => 'Test exception message',
|
||||||
|
'context' => json_encode(['error' => 'details'])
|
||||||
|
];
|
||||||
|
|
||||||
|
$listener = new LogExceptionToDatabase();
|
||||||
|
$event = new ExceptionLogged($logData);
|
||||||
|
|
||||||
|
$listener->handle($event);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('logs', [
|
||||||
|
'level' => 'error',
|
||||||
|
'message' => 'Test exception message'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$log = Log::where('message', 'Test exception message')->first();
|
||||||
|
$this->assertNotNull($log);
|
||||||
|
$this->assertEquals('error', $log->level);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_event_listener_registration_works(): void
|
||||||
|
{
|
||||||
|
// Test that events are properly bound to listeners
|
||||||
|
$listeners = Event::getListeners(NewArticleFetched::class);
|
||||||
|
$this->assertNotEmpty($listeners);
|
||||||
|
|
||||||
|
$listeners = Event::getListeners(ArticleApproved::class);
|
||||||
|
$this->assertNotEmpty($listeners);
|
||||||
|
|
||||||
|
$listeners = Event::getListeners(ArticleReadyToPublish::class);
|
||||||
|
$this->assertNotEmpty($listeners);
|
||||||
|
|
||||||
|
$listeners = Event::getListeners(ExceptionLogged::class);
|
||||||
|
$this->assertNotEmpty($listeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_job_retry_configuration(): void
|
||||||
|
{
|
||||||
|
$article = Article::factory()->create();
|
||||||
|
|
||||||
|
$job = new PublishToLemmyJob($article);
|
||||||
|
|
||||||
|
// Test that job has retry configuration
|
||||||
|
$this->assertObjectHasProperty('tries', $job);
|
||||||
|
$this->assertObjectHasProperty('backoff', $job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_job_queue_configuration(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
$article = Article::factory()->create();
|
||||||
|
|
||||||
|
$discoveryJob = new ArticleDiscoveryJob();
|
||||||
|
$feedJob = new ArticleDiscoveryForFeedJob($feed);
|
||||||
|
$publishJob = new PublishToLemmyJob($article);
|
||||||
|
$syncJob = new SyncChannelPostsJob($channel);
|
||||||
|
|
||||||
|
// Test queue assignments
|
||||||
|
$this->assertEquals('default', $discoveryJob->queue ?? 'default');
|
||||||
|
$this->assertEquals('discovery', $feedJob->queue ?? 'discovery');
|
||||||
|
$this->assertEquals('lemmy-posts', $publishJob->queue);
|
||||||
|
$this->assertEquals('sync', $syncJob->queue ?? 'sync');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
\Mockery::close();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,5 +6,5 @@
|
||||||
|
|
||||||
abstract class TestCase extends BaseTestCase
|
abstract class TestCase extends BaseTestCase
|
||||||
{
|
{
|
||||||
//
|
use CreatesApplication;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
91
tests/Unit/Services/ArticleFetcherTest.php
Normal file
91
tests/Unit/Services/ArticleFetcherTest.php
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Services;
|
||||||
|
|
||||||
|
use App\Services\Article\ArticleFetcher;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\Article;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class ArticleFetcherTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_get_articles_from_feed_returns_collection(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create([
|
||||||
|
'type' => 'rss',
|
||||||
|
'url' => 'https://example.com/feed.rss'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ArticleFetcher::getArticlesFromFeed($feed);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(\Illuminate\Support\Collection::class, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_articles_from_rss_feed_returns_empty_collection(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create([
|
||||||
|
'type' => 'rss',
|
||||||
|
'url' => 'https://example.com/feed.rss'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ArticleFetcher::getArticlesFromFeed($feed);
|
||||||
|
|
||||||
|
// RSS parsing is not implemented yet, should return empty collection
|
||||||
|
$this->assertEmpty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_articles_from_website_feed_handles_no_parser(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create([
|
||||||
|
'type' => 'website',
|
||||||
|
'url' => 'https://unsupported-site.com/'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ArticleFetcher::getArticlesFromFeed($feed);
|
||||||
|
|
||||||
|
// Should return empty collection when no parser is available
|
||||||
|
$this->assertInstanceOf(\Illuminate\Support\Collection::class, $result);
|
||||||
|
$this->assertEmpty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_articles_from_unsupported_feed_type(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create([
|
||||||
|
'type' => 'website', // Use valid type but with unsupported URL
|
||||||
|
'url' => 'https://unsupported-feed-type.com/feed'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ArticleFetcher::getArticlesFromFeed($feed);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(\Illuminate\Support\Collection::class, $result);
|
||||||
|
$this->assertEmpty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fetch_article_data_returns_array(): void
|
||||||
|
{
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'url' => 'https://example.com/article'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ArticleFetcher::fetchArticleData($article);
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
// Will be empty array due to unsupported URL in test
|
||||||
|
$this->assertEmpty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fetch_article_data_handles_invalid_url(): void
|
||||||
|
{
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'url' => 'invalid-url'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ArticleFetcher::fetchArticleData($article);
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertEmpty($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
178
tests/Unit/Services/DashboardStatsServiceTest.php
Normal file
178
tests/Unit/Services/DashboardStatsServiceTest.php
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Services;
|
||||||
|
|
||||||
|
use App\Services\DashboardStatsService;
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use App\Models\PlatformChannel;
|
||||||
|
use App\Models\Route;
|
||||||
|
use App\Models\ArticlePublication;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class DashboardStatsServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected DashboardStatsService $dashboardStatsService;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->dashboardStatsService = new DashboardStatsService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_stats_returns_correct_structure(): void
|
||||||
|
{
|
||||||
|
$stats = $this->dashboardStatsService->getStats();
|
||||||
|
|
||||||
|
$this->assertIsArray($stats);
|
||||||
|
$this->assertArrayHasKey('articles_fetched', $stats);
|
||||||
|
$this->assertArrayHasKey('articles_published', $stats);
|
||||||
|
$this->assertArrayHasKey('published_percentage', $stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_stats_with_today_period(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
// Create articles for today
|
||||||
|
$todayArticle = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'created_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create publication for today
|
||||||
|
ArticlePublication::factory()->create([
|
||||||
|
'article_id' => $todayArticle->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'published_at' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stats = $this->dashboardStatsService->getStats('today');
|
||||||
|
|
||||||
|
$this->assertEquals(1, $stats['articles_fetched']);
|
||||||
|
$this->assertEquals(1, $stats['articles_published']);
|
||||||
|
$this->assertEquals(100.0, $stats['published_percentage']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_stats_with_week_period(): void
|
||||||
|
{
|
||||||
|
$stats = $this->dashboardStatsService->getStats('week');
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('articles_fetched', $stats);
|
||||||
|
$this->assertArrayHasKey('articles_published', $stats);
|
||||||
|
$this->assertArrayHasKey('published_percentage', $stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_stats_with_all_time_period(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
|
||||||
|
// Create articles across different times
|
||||||
|
Article::factory()->count(5)->create(['feed_id' => $feed->id]);
|
||||||
|
|
||||||
|
$stats = $this->dashboardStatsService->getStats('all');
|
||||||
|
|
||||||
|
$this->assertEquals(5, $stats['articles_fetched']);
|
||||||
|
$this->assertIsFloat($stats['published_percentage']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_stats_calculates_percentage_correctly(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$channel = PlatformChannel::factory()->create();
|
||||||
|
|
||||||
|
// Create 4 articles
|
||||||
|
$articles = Article::factory()->count(4)->create(['feed_id' => $feed->id]);
|
||||||
|
|
||||||
|
// Publish 2 of them
|
||||||
|
foreach ($articles->take(2) as $article) {
|
||||||
|
ArticlePublication::factory()->create([
|
||||||
|
'article_id' => $article->id,
|
||||||
|
'platform_channel_id' => $channel->id,
|
||||||
|
'published_at' => now()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = $this->dashboardStatsService->getStats('all');
|
||||||
|
|
||||||
|
$this->assertEquals(4, $stats['articles_fetched']);
|
||||||
|
$this->assertEquals(2, $stats['articles_published']);
|
||||||
|
$this->assertEquals(50.0, $stats['published_percentage']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_stats_handles_zero_articles(): void
|
||||||
|
{
|
||||||
|
$stats = $this->dashboardStatsService->getStats();
|
||||||
|
|
||||||
|
$this->assertEquals(0, $stats['articles_fetched']);
|
||||||
|
$this->assertEquals(0, $stats['articles_published']);
|
||||||
|
$this->assertEquals(0.0, $stats['published_percentage']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_available_periods_returns_correct_options(): void
|
||||||
|
{
|
||||||
|
$periods = $this->dashboardStatsService->getAvailablePeriods();
|
||||||
|
|
||||||
|
$this->assertIsArray($periods);
|
||||||
|
$this->assertArrayHasKey('today', $periods);
|
||||||
|
$this->assertArrayHasKey('week', $periods);
|
||||||
|
$this->assertArrayHasKey('month', $periods);
|
||||||
|
$this->assertArrayHasKey('year', $periods);
|
||||||
|
$this->assertArrayHasKey('all', $periods);
|
||||||
|
|
||||||
|
$this->assertEquals('Today', $periods['today']);
|
||||||
|
$this->assertEquals('All Time', $periods['all']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_system_stats_returns_correct_structure(): void
|
||||||
|
{
|
||||||
|
$stats = $this->dashboardStatsService->getSystemStats();
|
||||||
|
|
||||||
|
$this->assertIsArray($stats);
|
||||||
|
$this->assertArrayHasKey('total_feeds', $stats);
|
||||||
|
$this->assertArrayHasKey('active_feeds', $stats);
|
||||||
|
$this->assertArrayHasKey('total_channels', $stats);
|
||||||
|
$this->assertArrayHasKey('active_channels', $stats);
|
||||||
|
$this->assertArrayHasKey('total_routes', $stats);
|
||||||
|
$this->assertArrayHasKey('active_routes', $stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_system_stats_counts_correctly(): void
|
||||||
|
{
|
||||||
|
// Create a single feed, channel, and route to test counting
|
||||||
|
$feed = Feed::factory()->create(['is_active' => true]);
|
||||||
|
$channel = PlatformChannel::factory()->create(['is_active' => true]);
|
||||||
|
$route = Route::factory()->create(['is_active' => true]);
|
||||||
|
|
||||||
|
$stats = $this->dashboardStatsService->getSystemStats();
|
||||||
|
|
||||||
|
// Verify that all stats are properly counted (at least our created items exist)
|
||||||
|
$this->assertGreaterThanOrEqual(1, $stats['total_feeds']);
|
||||||
|
$this->assertGreaterThanOrEqual(1, $stats['active_feeds']);
|
||||||
|
$this->assertGreaterThanOrEqual(1, $stats['total_channels']);
|
||||||
|
$this->assertGreaterThanOrEqual(1, $stats['active_channels']);
|
||||||
|
$this->assertGreaterThanOrEqual(1, $stats['total_routes']);
|
||||||
|
$this->assertGreaterThanOrEqual(1, $stats['active_routes']);
|
||||||
|
|
||||||
|
// Verify that active counts are less than or equal to total counts
|
||||||
|
$this->assertLessThanOrEqual($stats['total_feeds'], $stats['active_feeds']);
|
||||||
|
$this->assertLessThanOrEqual($stats['total_channels'], $stats['active_channels']);
|
||||||
|
$this->assertLessThanOrEqual($stats['total_routes'], $stats['active_routes']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_system_stats_handles_empty_database(): void
|
||||||
|
{
|
||||||
|
$stats = $this->dashboardStatsService->getSystemStats();
|
||||||
|
|
||||||
|
$this->assertEquals(0, $stats['total_feeds']);
|
||||||
|
$this->assertEquals(0, $stats['active_feeds']);
|
||||||
|
$this->assertEquals(0, $stats['total_channels']);
|
||||||
|
$this->assertEquals(0, $stats['active_channels']);
|
||||||
|
$this->assertEquals(0, $stats['total_routes']);
|
||||||
|
$this->assertEquals(0, $stats['active_routes']);
|
||||||
|
}
|
||||||
|
}
|
||||||
121
tests/Unit/Services/ValidationServiceTest.php
Normal file
121
tests/Unit/Services/ValidationServiceTest.php
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Services;
|
||||||
|
|
||||||
|
use App\Services\Article\ValidationService;
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Feed;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class ValidationServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_validate_returns_article_with_validation_status(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'url' => 'https://example.com/article',
|
||||||
|
'is_valid' => null,
|
||||||
|
'validated_at' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ValidationService::validate($article);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Article::class, $result);
|
||||||
|
$this->assertNotNull($result->validated_at);
|
||||||
|
$this->assertIsBool($result->is_valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_validate_marks_article_invalid_when_missing_data(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'url' => 'https://invalid-url-without-parser.com/article',
|
||||||
|
'is_valid' => null,
|
||||||
|
'validated_at' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ValidationService::validate($article);
|
||||||
|
|
||||||
|
$this->assertFalse($result->is_valid);
|
||||||
|
$this->assertNotNull($result->validated_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_validate_with_supported_article_content(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'url' => 'https://example.com/article',
|
||||||
|
'is_valid' => null,
|
||||||
|
'validated_at' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ValidationService::validate($article);
|
||||||
|
|
||||||
|
// Since we can't fetch real content in tests, it should be marked invalid
|
||||||
|
$this->assertFalse($result->is_valid);
|
||||||
|
$this->assertNotNull($result->validated_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_validate_updates_article_in_database(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'url' => 'https://example.com/article',
|
||||||
|
'is_valid' => null,
|
||||||
|
'validated_at' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$originalId = $article->id;
|
||||||
|
|
||||||
|
ValidationService::validate($article);
|
||||||
|
|
||||||
|
// Check that the article was updated in the database
|
||||||
|
$updatedArticle = Article::find($originalId);
|
||||||
|
$this->assertNotNull($updatedArticle->validated_at);
|
||||||
|
$this->assertNotNull($updatedArticle->is_valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_validate_handles_article_with_existing_validation(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'url' => 'https://example.com/article',
|
||||||
|
'is_valid' => true,
|
||||||
|
'validated_at' => now()->subHour()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$originalValidatedAt = $article->validated_at;
|
||||||
|
|
||||||
|
$result = ValidationService::validate($article);
|
||||||
|
|
||||||
|
// Should re-validate and update timestamp
|
||||||
|
$this->assertNotEquals($originalValidatedAt, $result->validated_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_validate_keyword_checking_logic(): void
|
||||||
|
{
|
||||||
|
$feed = Feed::factory()->create();
|
||||||
|
|
||||||
|
// Create an article that would match the validation keywords if content was available
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'feed_id' => $feed->id,
|
||||||
|
'url' => 'https://example.com/article-about-bart-de-wever',
|
||||||
|
'is_valid' => null,
|
||||||
|
'validated_at' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ValidationService::validate($article);
|
||||||
|
|
||||||
|
// The service looks for keywords in the full_article content
|
||||||
|
// Since we can't fetch real content, it will be marked invalid
|
||||||
|
$this->assertFalse($result->is_valid);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue