Compare commits
2 Commits
prod
...
issues/27-test
| Author | SHA1 | Date | |
|---|---|---|---|
| b03199f024 | |||
| 77267619ad |
+61
-22
@@ -1,7 +1,6 @@
|
|||||||
name: backend-ci-cd
|
name: backend-ci-cd
|
||||||
|
|
||||||
# CI/CD: только push git-тега (ручное тегирование на ветке prod|test|stage).
|
# CI/CD: только push git-тега. Pre-deploy tests на test|stage; prod — без тестов (ручной аппрув релиза).
|
||||||
# Push в ветки и feature-ветки pipeline не запускают.
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -14,25 +13,6 @@ env:
|
|||||||
IMAGE_DEPLOY: git.sova.local/sova/backend
|
IMAGE_DEPLOY: git.sova.local/sova/backend
|
||||||
|
|
||||||
jobs:
|
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:
|
parse-tag:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
@@ -48,8 +28,67 @@ jobs:
|
|||||||
echo "env=$(echo "$TAG" | sed -E 's/backend-v([0-9.]+)-([a-z]+)/\2/')" >> "$GITHUB_OUTPUT"
|
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"
|
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:
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -101,6 +101,9 @@
|
|||||||
"@php bin/console cache:clear"
|
"@php bin/console cache:clear"
|
||||||
],
|
],
|
||||||
"phpunit": "phpunit",
|
"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",
|
"generate-swagger": "php ./vendor/bin/openapi --output ./public/swagger.json ./src",
|
||||||
"auto-scripts": {
|
"auto-scripts": {
|
||||||
"cache:clear": "symfony-cmd",
|
"cache:clear": "symfony-cmd",
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ services:
|
|||||||
- '../src/Entity/'
|
- '../src/Entity/'
|
||||||
- '../src/Kernel.php'
|
- '../src/Kernel.php'
|
||||||
|
|
||||||
|
App\Log\TestTraceProcessor:
|
||||||
|
tags:
|
||||||
|
- { name: monolog.processor }
|
||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
Psr\Log\LoggerInterface: '@logger'
|
Psr\Log\LoggerInterface: '@logger'
|
||||||
|
|||||||
+18
-2
@@ -18,8 +18,13 @@
|
|||||||
</php>
|
</php>
|
||||||
|
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="Project Test Suite">
|
<testsuite name="unit">
|
||||||
<directory>tests</directory>
|
<directory>tests/Unit</directory>
|
||||||
|
<directory>tests/Service</directory>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="integration">
|
||||||
|
<directory>tests/Controller</directory>
|
||||||
|
<directory>tests/Integration</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
@@ -27,8 +32,19 @@
|
|||||||
<include>
|
<include>
|
||||||
<directory>src</directory>
|
<directory>src</directory>
|
||||||
</include>
|
</include>
|
||||||
|
<exclude>
|
||||||
|
<directory>src/Entity</directory>
|
||||||
|
</exclude>
|
||||||
</source>
|
</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>
|
||||||
</extensions>
|
</extensions>
|
||||||
</phpunit>
|
</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