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;
|
||||
|
||||
use Database\Factories\ArticlePublicationFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
|
|
@ -14,6 +16,9 @@
|
|||
*/
|
||||
class ArticlePublication extends Model
|
||||
{
|
||||
/** @use HasFactory<ArticlePublicationFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'article_id',
|
||||
'platform_channel_id',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\RouteFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
|
|
@ -18,6 +20,9 @@
|
|||
*/
|
||||
class Route extends Pivot
|
||||
{
|
||||
/** @use HasFactory<RouteFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'routes';
|
||||
|
||||
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>
|
||||
<php>
|
||||
<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="BCRYPT_ROUNDS" value="4"/>
|
||||
<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="PULSE_ENABLED" value="false"/>
|
||||
<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
|
||||
{
|
||||
//
|
||||
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