Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: CI Suite

on:
push:
branches: [ main, master, "feature/*" ]
pull_request:
branches: [ main, master ]

jobs:
phpunit:
name: PHPUnit Tests (PHP ${{ matrix.php-version }})
runs-on: ubuntu-latest

strategy:
matrix:
php-version: ['8.4']

services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_DB: feedback
POSTGRES_USER: feedback_user
POSTGRES_PASSWORD: password123
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: pdo, pdo_pgsql, mbstring, ctype, iconv
coverage: none

- name: Get Composer Cache Directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Cache Composer Dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-

- name: Install Dependencies
run: composer install --prefer-dist --no-progress --no-interaction

- name: Create .env file for Testing
run: |
echo "APP_NAME=\"Anonymous Feedback System\"" > .env
echo "APP_ENV=testing" >> .env
echo "APP_DEBUG=true" >> .env
echo "APP_URL=http://localhost:8000" >> .env
echo "DB_DRIVER=pgsql" >> .env
echo "DB_HOST=127.0.0.1" >> .env
echo "DB_PORT=5432" >> .env
echo "DB_DATABASE=feedback" >> .env
echo "DB_USERNAME=feedback_user" >> .env
echo "DB_PASSWORD=password123" >> .env
echo "DB_CHARSET=utf8" >> .env
echo "DB_SCHEMA=public" >> .env
echo "REDIS_SCHEME=tcp" >> .env
echo "REDIS_HOST=127.0.0.1" >> .env
echo "REDIS_PORT=6379" >> .env
echo "RATE_LIMIT_MAX_REQUESTS=10" >> .env
echo "RATE_LIMIT_DECAY=600" >> .env

- name: Run PHPUnit Tests
run: vendor/bin/phpunit
7 changes: 6 additions & 1 deletion config/container.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use Illuminate\Database\Capsule\Manager as Capsule;
use Predis\Client as RedisClient;

use Akisolu\AnonymousFeedback\Services\RateLimiter;

$config = require __DIR__ . '/config.php';

$builder = new ContainerBuilder();
Expand All @@ -32,7 +34,10 @@
},
RedisClient::class => function (ContainerInterface $c) {
return new RedisClient($c->get('config')['redis']);
}
},
RateLimiter::class => function (ContainerInterface $c) {
return new RateLimiter($c->get(RedisClient::class));
},
]);

return $builder->build();
38 changes: 38 additions & 0 deletions src/Services/RateLimiter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);

namespace Akisolu\AnonymousFeedback\Services;

use Predis\Client as RedisClient;

class RateLimiter
{
public function __construct(
private RedisClient $redis
) {}

public function tooManyAttempts(string $key, int $maxAttempts): bool {
$attempts = $this->attempts($key);
return $attempts >= $maxAttempts;
}

public function hit(string $key, int $decaySeconds = 600): int {
$hits = (int) $this->redis->incr($key);
if ($hits === 1) {
$this->redis->expire($key, $decaySeconds);
}
return $hits;
}

public function attempts(string $key): int {
$value = $this->redis->get($key);
if (is_null($value)){
return 0;
}
return (int) $value;
}

public function resetAttempts(string $key): void {
$this->redis->del([$key]);
}
}
12 changes: 12 additions & 0 deletions tests/Integration/DatabaseConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,16 @@ public function testRedisConnectionAndOperationsAreSuccessful(): void

$redis->del([$testKey]);
}

public function testRateLimiterCanBeResolvedFromContainer(): void
{
/** @var \Akisolu\AnonymousFeedback\Services\RateLimiter $rateLimiter */
$rateLimiter = $this->container->get(\Akisolu\AnonymousFeedback\Services\RateLimiter::class);

$this->assertInstanceOf(\Akisolu\AnonymousFeedback\Services\RateLimiter::class, $rateLimiter);

// Verificación rápida de estado
$testKey = 'container_test_key_' . uniqid();
$this->assertFalse($rateLimiter->tooManyAttempts($testKey, 5));
}
}
52 changes: 52 additions & 0 deletions tests/Unit/RateLimiterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use Akisolu\AnonymousFeedback\Services\RateLimiter;
use Predis\Client as RedisClient;
use Psr\Container\ContainerInterface;

class RateLimiterTest extends TestCase {
private ContainerInterface $container;
private RedisClient $redis;
private RateLimiter $rateLimiter;
private string $testKey;

public function setUp(): void {
$this->container = require __DIR__ . '/../../config/container.php';
$this->redis = $this->container->get(RedisClient::class);
$this->rateLimiter = new RateLimiter($this->redis);
$this->testKey = 'test_rate_limit:' . uniqid();
}

public function tearDown(): void {
$this->redis->del($this->testKey);
}

public function test_allows_requests_under_limit() {
$this->rateLimiter->tooManyAttempts($this->testKey, 10);
for ($i = 0; $i < 5; $i++) {
$this->rateLimiter->hit($this->testKey, 600);
}
$this->assertEquals(5, $this->rateLimiter->attempts($this->testKey));
$this->assertFalse($this->rateLimiter->tooManyAttempts($this->testKey, 10));
}

public function test_blocks_requests_exceeding_limit() {
for ($i = 1; $i < 10; $i++){
$this->rateLimiter->hit($this->testKey, 600);
}
$this->assertFalse($this->rateLimiter->tooManyAttempts($this->testKey, 10));
$this->rateLimiter->hit($this->testKey, 600);
$this->assertTrue($this->rateLimiter->tooManyAttempts($this->testKey, 10));
$this->assertEquals(10, $this->rateLimiter->attempts($this->testKey));
}

public function test_can_reset_attempts() {
$this->rateLimiter->hit($this->testKey);
$this->rateLimiter->hit($this->testKey);
$this->rateLimiter->hit($this->testKey);
$this->rateLimiter->resetAttempts($this->testKey);
$this->assertEquals(0, $this->rateLimiter->attempts($this->testKey));
}
}
Loading