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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ jobs:
echo "REDIS_PORT=6379" >> .env
echo "RATE_LIMIT_MAX_REQUESTS=10" >> .env
echo "RATE_LIMIT_DECAY=600" >> .env


- name: Run Database Migrations
run: php bin/migrate.php

- name: Run PHPUnit Tests
run: vendor/bin/phpunit
40 changes: 40 additions & 0 deletions bin/migrate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Psr\Container\ContainerInterface;

/** @var ContainerInterface $container */
$container = require __DIR__ . '/../config/container.php';
$pdo = $container->get(PDO::class);

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$sqlPath = __DIR__ . '/../schema.sql';

if (!file_exists($sqlPath)) {
echo "\033[31m[ERROR] The migration file was not found in: {$sqlPath}\033[0m\n";
exit(1);
}

echo "\033[33mRunning migration in PostgreSQL...\033[0m\n";

try {
$sql = file_get_contents($sqlPath);

// Execute the complete SQL within an explicit transaction
$pdo->beginTransaction();
$pdo->exec($sql);
$pdo->commit();

echo "\033[32m[SUCCESS] All tables, indexes, triggers, and initial records were successfully created!\033[0m\n";
} catch (\PDOException $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo "\033[31m[MIGRATION ERROR]\033[0m\n";
echo "Message: " . $e->getMessage() . "\n";
exit(1);
}
13 changes: 11 additions & 2 deletions schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS states (
CREATE TABLE IF NOT EXISTS feedbacks (
feedback_id SERIAL PRIMARY KEY,
message TEXT NOT NULL,
state_id INT NOT NULL,
state_id INT NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Expand Down Expand Up @@ -95,4 +95,13 @@ VALUES
('read'),
('archived'),
('deleted')
ON CONFLICT (state_id) DO NOTHING;
ON CONFLICT (name) DO NOTHING;

INSERT INTO users (user_id, username, password_hash)
VALUES (1, 'system', '$2y$10$e.g.placeholder.hash.system.user')
ON CONFLICT (user_id) DO NOTHING;

-- 5. INDEX

CREATE INDEX IF NOT EXISTS idx_feedbacks_state_id ON feedbacks(state_id);
CREATE INDEX IF NOT EXISTS idx_feedback_records_feedback_id ON feedback_records(feedback_id);
52 changes: 52 additions & 0 deletions tests/Integration/FeedbackMigrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Tests\Integration;

use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use PDO;

class FeedbackMigrationTest extends TestCase
{
private PDO $pdo;

protected function setUp(): void
{
parent::setUp();
/** @var ContainerInterface $container */
$container = require __DIR__ . '/../../config/container.php';
$this->pdo = $container->get(PDO::class);
}

public function test_trigger_records_state_change_automatically(): void
{
// 1. Insert anonymous feedback
$stmt = $this->pdo->prepare("INSERT INTO feedbacks (message) VALUES (:msg) RETURNING feedback_id");
$stmt->execute(['msg' => 'TDD test feedback']);
$feedbackId = (int) $stmt->fetchColumn();

$this->assertGreaterThan(0, $feedbackId);

// 2. Simulate an authenticated user in the app session in Postgres
$this->pdo->exec("SET LOCAL app.current_user_id = '1'");

// 3. Change the state from 1 (unread) to 2 (read)
$updateStmt = $this->pdo->prepare("UPDATE feedbacks SET state_id = 2 WHERE feedback_id = :id");
$updateStmt->execute(['id' => $feedbackId]);

// 4. Verify that the TRIGGER created the audit log in feedback_records
$auditStmt = $this->pdo->prepare("SELECT * FROM feedback_records WHERE feedback_id = :id");
$auditStmt->execute(['id' => $feedbackId]);
$record = $auditStmt->fetch(PDO::FETCH_ASSOC);

$this->assertNotEmpty($record);
$this->assertEquals(1, $record['user_id']);
$this->assertEquals(1, $record['old_state_id']);
$this->assertEquals(2, $record['new_state_id']);

// Clean
$this->pdo->prepare("DELETE FROM feedbacks WHERE feedback_id = :id")->execute(['id' => $feedbackId]);
}
}
Loading