lthn.io/app/Core/Tests/Feature/EmailShieldTest.php
Claude 41a90cbff8
feat: lthn.io API serving live chain data
Fixed: basePath self→static binding, namespace detection, event wiring,
SQLite cache, file cache driver. All Mod Boot classes converted to
$listens pattern for lifecycle event discovery.

Working endpoints:
- /v1/explorer/info — live chain height, difficulty, aliases
- /v1/explorer/stats — formatted chain statistics
- /v1/names/directory — alias directory grouped by type
- /v1/names/available/{name} — name availability check
- /v1/names/lookup/{name} — name details

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 17:17:42 +01:00

419 lines
14 KiB
PHP

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
use Carbon\Carbon;
use Core\Mail\EmailShield;
use Core\Mail\EmailShieldStat;
use Core\Mail\EmailValidationResult;
use Core\Mail\Rules\ValidatedEmail;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
uses(RefreshDatabase::class);
beforeEach(function () {
// Clear the cache before each test to ensure clean state
Cache::forget('email_shield:disposable_domains');
// Create a fresh instance after clearing cache
$this->emailShield = new EmailShield;
});
// EmailValidationResult Value Object Tests
describe('EmailValidationResult', function () {
it('creates a valid result', function () {
$result = EmailValidationResult::valid('example.com');
expect($result->isValid)->toBeTrue()
->and($result->isDisposable)->toBeFalse()
->and($result->domain)->toBe('example.com')
->and($result->passes())->toBeTrue()
->and($result->fails())->toBeFalse()
->and($result->getMessage())->toBe('Valid email address');
});
it('creates an invalid result', function () {
$result = EmailValidationResult::invalid('Invalid format', 'bad-domain');
expect($result->isValid)->toBeFalse()
->and($result->isDisposable)->toBeFalse()
->and($result->domain)->toBe('bad-domain')
->and($result->reason)->toBe('Invalid format')
->and($result->passes())->toBeFalse()
->and($result->fails())->toBeTrue()
->and($result->getMessage())->toBe('Invalid format');
});
it('creates a disposable result', function () {
$result = EmailValidationResult::disposable('tempmail.com');
expect($result->isValid)->toBeFalse()
->and($result->isDisposable)->toBeTrue()
->and($result->domain)->toBe('tempmail.com')
->and($result->passes())->toBeFalse()
->and($result->fails())->toBeTrue()
->and($result->getMessage())->toBe('Disposable email addresses are not allowed');
});
});
// EmailShield Service Tests
describe('EmailShield Service', function () {
it('validates a valid email address', function () {
$result = $this->emailShield->validate('user@example.com');
expect($result->isValid)->toBeTrue()
->and($result->isDisposable)->toBeFalse()
->and($result->domain)->toBe('example.com')
->and($result->passes())->toBeTrue();
});
it('rejects invalid email format', function () {
$result = $this->emailShield->validate('not-an-email');
expect($result->isValid)->toBeFalse()
->and($result->reason)->toBe('Invalid email format')
->and($result->passes())->toBeFalse();
});
it('rejects email without @ symbol', function () {
$result = $this->emailShield->validate('userexample.com');
expect($result->isValid)->toBeFalse()
->and($result->passes())->toBeFalse();
});
it('rejects disposable email domains', function () {
$result = $this->emailShield->validate('user@tempmail.com');
expect($result->isValid)->toBeFalse()
->and($result->isDisposable)->toBeTrue()
->and($result->domain)->toBe('tempmail.com')
->and($result->passes())->toBeFalse();
});
it('checks multiple disposable domains', function () {
$disposableDomains = [
'tempmail.com',
'guerrillamail.com',
'10minutemail.com',
'mailinator.com',
'throwaway.email',
];
foreach ($disposableDomains as $domain) {
$result = $this->emailShield->validate("user@{$domain}");
expect($result->isDisposable)->toBeTrue()
->and($result->passes())->toBeFalse();
}
});
it('is case-insensitive for domains', function () {
$result1 = $this->emailShield->validate('user@TEMPMAIL.COM');
$result2 = $this->emailShield->validate('user@TempMail.com');
$result3 = $this->emailShield->validate('user@tempmail.com');
expect($result1->isDisposable)->toBeTrue()
->and($result2->isDisposable)->toBeTrue()
->and($result3->isDisposable)->toBeTrue();
});
it('loads disposable domains from file', function () {
$count = $this->emailShield->getDisposableDomainsCount();
expect($count)->toBeGreaterThan(500);
});
it('caches disposable domains', function () {
// First call - loads from file
$shield1 = new EmailShield;
$count1 = $shield1->getDisposableDomainsCount();
// Second call - should load from cache
$shield2 = new EmailShield;
$count2 = $shield2->getDisposableDomainsCount();
expect($count1)->toBe($count2)
->and($count1)->toBeGreaterThan(0);
});
it('can refresh cache', function () {
$shield = new EmailShield;
$count1 = $shield->getDisposableDomainsCount();
Cache::put('email_shield:disposable_domains', ['fake.com' => true], 86400);
$shield->refreshCache();
$count2 = $shield->getDisposableDomainsCount();
expect($count2)->toBe($count1)
->and($count2)->toBeGreaterThan(1);
});
});
// Statistics Tracking Tests
describe('EmailShield Statistics', function () {
it('records valid email statistics', function () {
$this->emailShield->validate('user@example.com');
$stat = EmailShieldStat::whereDate('date', today())->first();
expect($stat)->not->toBeNull()
->and($stat->valid_count)->toBe(1)
->and($stat->invalid_count)->toBe(0)
->and($stat->disposable_count)->toBe(0);
});
it('records invalid email statistics', function () {
$this->emailShield->validate('not-an-email');
$stat = EmailShieldStat::whereDate('date', today())->first();
expect($stat)->not->toBeNull()
->and($stat->valid_count)->toBe(0)
->and($stat->invalid_count)->toBe(1)
->and($stat->disposable_count)->toBe(0);
});
it('records disposable email statistics', function () {
$this->emailShield->validate('user@tempmail.com');
$stat = EmailShieldStat::whereDate('date', today())->first();
expect($stat)->not->toBeNull()
->and($stat->valid_count)->toBe(0)
->and($stat->invalid_count)->toBe(0)
->and($stat->disposable_count)->toBe(1);
});
it('increments counters for multiple validations', function () {
$this->emailShield->validate('user@example.com'); // valid
$this->emailShield->validate('user@tempmail.com'); // disposable
$this->emailShield->validate('not-an-email'); // invalid
$this->emailShield->validate('another@example.com'); // valid
$stat = EmailShieldStat::whereDate('date', today())->first();
expect($stat)->not->toBeNull()
->and($stat->valid_count)->toBe(2)
->and($stat->invalid_count)->toBe(1)
->and($stat->disposable_count)->toBe(1);
});
});
// EmailShieldStat Model Tests
describe('EmailShieldStat Model', function () {
it('can increment valid count', function () {
EmailShieldStat::incrementValid();
EmailShieldStat::incrementValid();
$stat = EmailShieldStat::whereDate('date', today())->first();
expect($stat->valid_count)->toBe(2);
});
it('can increment invalid count', function () {
EmailShieldStat::incrementInvalid();
EmailShieldStat::incrementInvalid();
EmailShieldStat::incrementInvalid();
$stat = EmailShieldStat::whereDate('date', today())->first();
expect($stat->invalid_count)->toBe(3);
});
it('can increment disposable count', function () {
EmailShieldStat::incrementDisposable();
$stat = EmailShieldStat::whereDate('date', today())->first();
expect($stat->disposable_count)->toBe(1);
});
it('creates new record if date does not exist', function () {
expect(EmailShieldStat::count())->toBe(0);
EmailShieldStat::incrementValid();
expect(EmailShieldStat::count())->toBe(1);
});
it('updates existing record for the same date', function () {
EmailShieldStat::incrementValid();
$stat1 = EmailShieldStat::whereDate('date', today())->first();
EmailShieldStat::incrementValid();
$stat2 = EmailShieldStat::whereDate('date', today())->first();
expect(EmailShieldStat::count())->toBe(1)
->and($stat1->id)->toBe($stat2->id)
->and($stat2->valid_count)->toBe(2);
});
it('scopes to date range', function () {
// Use unique dates far in the future to avoid conflicts with other tests
EmailShieldStat::create(['date' => '2040-06-01', 'valid_count' => 10]);
EmailShieldStat::create(['date' => '2040-06-02', 'valid_count' => 20]);
EmailShieldStat::create(['date' => '2040-06-03', 'valid_count' => 30]);
$stats = EmailShieldStat::forDateRange(
Carbon::parse('2040-06-02'),
Carbon::parse('2040-06-03')
)->get();
expect($stats)->toHaveCount(2)
->and($stats->pluck('valid_count')->toArray())->toContain(20, 30);
});
it('gets stats for date range', function () {
// Use unique dates far in the future to avoid conflicts
EmailShieldStat::create([
'date' => '2040-07-01',
'valid_count' => 10,
'invalid_count' => 5,
'disposable_count' => 3,
]);
EmailShieldStat::create([
'date' => '2040-07-02',
'valid_count' => 20,
'invalid_count' => 8,
'disposable_count' => 2,
]);
$stats = EmailShieldStat::getStatsForRange(
Carbon::parse('2040-07-01'),
Carbon::parse('2040-07-02')
);
expect($stats['total_valid'])->toBe(30)
->and($stats['total_invalid'])->toBe(13)
->and($stats['total_disposable'])->toBe(5)
->and($stats['total_checked'])->toBe(48);
});
});
// ValidatedEmail Validation Rule Tests
describe('ValidatedEmail Validation Rule', function () {
it('passes for valid email', function () {
$validator = Validator::make(
['email' => 'user@example.com'],
['email' => [new ValidatedEmail]]
);
expect($validator->passes())->toBeTrue();
});
it('fails for invalid email format', function () {
$validator = Validator::make(
['email' => 'not-an-email'],
['email' => [new ValidatedEmail]]
);
expect($validator->fails())->toBeTrue()
->and($validator->errors()->first('email'))->toContain('Invalid email format');
});
it('blocks disposable email by default', function () {
$validator = Validator::make(
['email' => 'user@tempmail.com'],
['email' => [new ValidatedEmail]]
);
expect($validator->fails())->toBeTrue()
->and($validator->errors()->first('email'))->toContain('Disposable email addresses are not allowed');
});
it('can allow disposable emails when configured', function () {
$validator = Validator::make(
['email' => 'user@tempmail.com'],
['email' => [new ValidatedEmail(blockDisposable: false)]]
);
expect($validator->passes())->toBeTrue();
});
it('fails for non-string values', function () {
$validator = Validator::make(
['email' => 12345],
['email' => [new ValidatedEmail]]
);
expect($validator->fails())->toBeTrue();
});
it('can be combined with other validation rules', function () {
$validator = Validator::make(
['email' => 'user@example.com'],
['email' => ['required', 'email', new ValidatedEmail]]
);
expect($validator->passes())->toBeTrue();
});
it('fails when required and empty', function () {
$validator = Validator::make(
['email' => ''],
['email' => ['required', new ValidatedEmail]]
);
expect($validator->fails())->toBeTrue();
});
});
// Integration Tests
describe('EmailShield Integration', function () {
it('works with real-world valid emails', function () {
$validEmails = [
'john.doe@example.com',
'jane@company.co.uk',
'support@host.uk.com',
'admin@example.org',
'test.user+tag@domain.net',
];
foreach ($validEmails as $email) {
$result = $this->emailShield->validate($email);
expect($result->passes())->toBeTrue()
->and($result->isDisposable)->toBeFalse();
}
});
it('blocks common disposable email services', function () {
$disposableEmails = [
'test@tempmail.com',
'user@guerrillamail.com',
'spam@10minutemail.com',
'throwaway@mailinator.com',
'temp@trashmail.com',
];
foreach ($disposableEmails as $email) {
$result = $this->emailShield->validate($email);
expect($result->passes())->toBeFalse()
->and($result->isDisposable)->toBeTrue();
}
});
it('can retrieve statistics via service', function () {
$this->emailShield->validate('user@example.com');
$this->emailShield->validate('user@tempmail.com');
$stats = $this->emailShield->getStats(Carbon::today(), Carbon::today());
expect($stats['total_valid'])->toBe(1)
->and($stats['total_disposable'])->toBe(1)
->and($stats['total_checked'])->toBe(2);
});
});