Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b03199f024 | |||
| 77267619ad |
+61
-22
@@ -1,7 +1,6 @@
|
||||
name: backend-ci-cd
|
||||
|
||||
# CI/CD: только push git-тега (ручное тегирование на ветке prod|test|stage).
|
||||
# Push в ветки и feature-ветки pipeline не запускают.
|
||||
# CI/CD: только push git-тега. Pre-deploy tests на test|stage; prod — без тестов (ручной аппрув релиза).
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -14,25 +13,6 @@ env:
|
||||
IMAGE_DEPLOY: git.sova.local/sova/backend
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
extensions: pdo_pgsql, redis, intl, zip, gd
|
||||
- name: Prepare CI environment
|
||||
run: |
|
||||
cp .env.ci .env.local
|
||||
mkdir -p config/jwt var
|
||||
openssl genrsa -out config/jwt/private.pem 2048
|
||||
openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem
|
||||
- run: composer install --prefer-dist --no-interaction
|
||||
- run: composer phpunit || true
|
||||
- run: composer audit || true
|
||||
|
||||
parse-tag:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
@@ -48,8 +28,67 @@ jobs:
|
||||
echo "env=$(echo "$TAG" | sed -E 's/backend-v([0-9.]+)-([a-z]+)/\2/')" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$(echo "$TAG" | sed -E 's/backend-v([0-9.]+).*/\1/')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
test:
|
||||
needs: [parse-tag]
|
||||
if: needs.parse-tag.outputs.env != 'prod'
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
env:
|
||||
POSTGRES_USER: ci
|
||||
POSTGRES_PASSWORD: ci
|
||||
POSTGRES_DB: ci
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U ci -d ci"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
extensions: pdo_pgsql, redis, intl, zip, gd
|
||||
coverage: xdebug
|
||||
- name: Prepare CI environment
|
||||
run: |
|
||||
cp .env.ci .env.local
|
||||
mkdir -p config/jwt var/coverage
|
||||
openssl genrsa -out config/jwt/private.pem 2048
|
||||
openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem
|
||||
- run: composer install --prefer-dist --no-interaction
|
||||
- name: Unit tests + coverage
|
||||
run: composer phpunit:coverage
|
||||
- name: Integration tests (test contour only)
|
||||
if: needs.parse-tag.outputs.env == 'test'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
php bin/console doctrine:database:create --if-not-exists --env=test || true
|
||||
php bin/console doctrine:migrations:migrate --no-interaction --env=test || true
|
||||
composer phpunit:integration
|
||||
- name: Upload coverage
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: backend-coverage-${{ needs.parse-tag.outputs.full_tag }}
|
||||
path: var/coverage/
|
||||
retention-days: 14
|
||||
- run: composer audit || true
|
||||
|
||||
test-prod-skip:
|
||||
needs: [parse-tag]
|
||||
if: needs.parse-tag.outputs.env == 'prod'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Prod tag — automated tests skipped (no tests on production contour)."
|
||||
|
||||
build-and-push:
|
||||
needs: [test, parse-tag]
|
||||
needs: [parse-tag, test, test-prod-skip]
|
||||
if: always() && (needs.test.result == 'success' || needs.test.result == 'skipped') && (needs.test-prod-skip.result == 'success' || needs.test-prod-skip.result == 'skipped')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -101,6 +101,9 @@
|
||||
"@php bin/console cache:clear"
|
||||
],
|
||||
"phpunit": "phpunit",
|
||||
"phpunit:unit": "phpunit --testsuite=unit",
|
||||
"phpunit:integration": "phpunit --testsuite=integration",
|
||||
"phpunit:coverage": "phpunit --testsuite=unit --coverage-clover var/coverage/clover.xml --coverage-text",
|
||||
"generate-swagger": "php ./vendor/bin/openapi --output ./public/swagger.json ./src",
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
|
||||
@@ -31,6 +31,10 @@ services:
|
||||
- '../src/Entity/'
|
||||
- '../src/Kernel.php'
|
||||
|
||||
App\Log\TestTraceProcessor:
|
||||
tags:
|
||||
- { name: monolog.processor }
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
Psr\Log\LoggerInterface: '@logger'
|
||||
|
||||
+18
-2
@@ -18,8 +18,13 @@
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Project Test Suite">
|
||||
<directory>tests</directory>
|
||||
<testsuite name="unit">
|
||||
<directory>tests/Unit</directory>
|
||||
<directory>tests/Service</directory>
|
||||
</testsuite>
|
||||
<testsuite name="integration">
|
||||
<directory>tests/Controller</directory>
|
||||
<directory>tests/Integration</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
@@ -27,8 +32,19 @@
|
||||
<include>
|
||||
<directory>src</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<directory>src/Entity</directory>
|
||||
</exclude>
|
||||
</source>
|
||||
|
||||
<coverage>
|
||||
<report>
|
||||
<clover outputFile="var/coverage/clover.xml"/>
|
||||
<html outputDirectory="var/coverage/html" lowUpperBound="50" highLowerBound="80"/>
|
||||
<text outputFile="php://stdout" showOnlySummary="true"/>
|
||||
</report>
|
||||
</coverage>
|
||||
|
||||
<extensions>
|
||||
</extensions>
|
||||
</phpunit>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Log;
|
||||
|
||||
use Monolog\LogRecord;
|
||||
use Monolog\Processor\ProcessorInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* Marks logs from E2E / autotest HTTP clients for Loki (extra.is_test, extra.test_trace_id).
|
||||
*/
|
||||
final class TestTraceProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RequestStack $requestStack,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(LogRecord $record): LogRecord
|
||||
{
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
if ($request === null || !$request->headers->has('X-Is-Auto-Test')) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
return $record->with(extra: array_merge($record->extra, [
|
||||
'is_test' => true,
|
||||
'test_trace_id' => $request->headers->get('X-Test-Trace-Id', 'unknown'),
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Integration\Repository;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* Integration tests for specialist filters (PostgreSQL in CI on *-test tags).
|
||||
*/
|
||||
final class SpecialistRepositoryTest extends KernelTestCase
|
||||
{
|
||||
public function testPlaceholderUntilMigrationsInCi(): void
|
||||
{
|
||||
self::markTestSkipped('Month 2: enable after doctrine migrations in Gitea runner PostgreSQL service.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Log;
|
||||
|
||||
use App\Log\TestTraceProcessor;
|
||||
use Monolog\Level;
|
||||
use Monolog\LogRecord;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
final class TestTraceProcessorTest extends TestCase
|
||||
{
|
||||
public function testAddsExtraWhenAutoTestHeaderPresent(): void
|
||||
{
|
||||
$request = Request::create('/api/test', 'GET');
|
||||
$request->headers->set('X-Is-Auto-Test', 'true');
|
||||
$request->headers->set('X-Test-Trace-Id', 'run-42');
|
||||
|
||||
$stack = new RequestStack();
|
||||
$stack->push($request);
|
||||
|
||||
$processor = new TestTraceProcessor($stack);
|
||||
$record = new LogRecord(new \DateTimeImmutable(), 'app', Level::Info, 'msg');
|
||||
|
||||
$out = $processor($record);
|
||||
|
||||
self::assertTrue($out->extra['is_test']);
|
||||
self::assertSame('run-42', $out->extra['test_trace_id']);
|
||||
}
|
||||
|
||||
public function testLeavesRecordUntouchedWithoutHeader(): void
|
||||
{
|
||||
$stack = new RequestStack();
|
||||
$processor = new TestTraceProcessor($stack);
|
||||
$record = new LogRecord(new \DateTimeImmutable(), 'app', Level::Info, 'msg', extra: ['foo' => 1]);
|
||||
|
||||
$out = $processor($record);
|
||||
|
||||
self::assertSame(['foo' => 1], $out->extra);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Repository;
|
||||
|
||||
use App\Entity\PriceList;
|
||||
use App\Repository\PriceListRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Unit-level checks for PriceListRepository filter DQL (no DB execution).
|
||||
*/
|
||||
final class PriceListRepositoryFilterTest extends TestCase
|
||||
{
|
||||
private PriceListRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$em = $this->createMock(EntityManagerInterface::class);
|
||||
$em->method('getClassMetadata')
|
||||
->with(PriceList::class)
|
||||
->willReturn(new ClassMetadata(PriceList::class));
|
||||
|
||||
$registry = $this->createMock(ManagerRegistry::class);
|
||||
$registry->method('getManagerForClass')
|
||||
->with(PriceList::class)
|
||||
->willReturn($em);
|
||||
|
||||
$this->repository = new PriceListRepository($registry);
|
||||
}
|
||||
|
||||
public function testFilialFilterInDql(): void
|
||||
{
|
||||
$qb = $this->repository->createFilteredQueryBuilder(['filial' => 12]);
|
||||
self::assertStringContainsString('filial', $qb->getDQL());
|
||||
self::assertStringContainsString(':filial', $qb->getDQL());
|
||||
}
|
||||
|
||||
public function testSearchFilterInDql(): void
|
||||
{
|
||||
$qb = $this->repository->createFilteredQueryBuilder(['search' => 'therapy']);
|
||||
self::assertStringContainsString('schname', $qb->getDQL());
|
||||
self::assertStringContainsString(':search', $qb->getDQL());
|
||||
}
|
||||
|
||||
public function testKodoperArrayFilterInDql(): void
|
||||
{
|
||||
$qb = $this->repository->createFilteredQueryBuilder(['kodoper' => ['A01', 'B02']]);
|
||||
self::assertStringContainsString('kodoper', $qb->getDQL());
|
||||
self::assertStringContainsString(':codes', $qb->getDQL());
|
||||
}
|
||||
|
||||
public function testIgnoresEmptyFilters(): void
|
||||
{
|
||||
$qb = $this->repository->createFilteredQueryBuilder(['search' => '', 'filial' => '']);
|
||||
self::assertStringNotContainsString(':search', $qb->getDQL());
|
||||
self::assertStringNotContainsString(':filial', $qb->getDQL());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user