faster spreadsheet exporter based on opensout and other export improvements (#5238)

This commit is contained in:
Kevin Papst
2024-12-22 18:36:47 +01:00
committed by GitHub
parent c7f0508707
commit 136104d4b0
80 changed files with 2023 additions and 1699 deletions

View File

@@ -7,41 +7,62 @@
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Renderer;
namespace App\Tests\Export\Base;
use App\Export\Renderer\CsvRenderer;
use App\Entity\User;
use App\Export\Base\CsvRenderer;
use App\Export\Base\SpreadsheetRenderer;
use App\Tests\Export\Renderer\AbstractRendererTestCase;
use App\Tests\Export\Renderer\MetaFieldColumnSubscriber;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @covers \App\Export\Base\CsvRenderer
* @covers \App\Export\Base\AbstractSpreadsheetRenderer
* @covers \App\Export\Base\SpreadsheetRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Renderer\CsvRenderer
* @covers \App\Export\Package\SpoutSpreadsheet
* @group integration
*/
class CsvRendererTest extends AbstractRendererTestCase
{
protected function getAbstractRenderer(): CsvRenderer
{
$security = $this->createMock(Security::class);
$security->expects($this->any())->method('getUser')->willReturn(new User());
$security->expects($this->any())->method('isGranted')->willReturn(true);
$translator = $this->createMock(TranslatorInterface::class);
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());
return new CsvRenderer(new SpreadsheetRenderer($translator, $dispatcher, $security));
}
public function testConfiguration(): void
{
$sut = $this->getAbstractRenderer(CsvRenderer::class);
$sut = $this->getAbstractRenderer();
self::assertEquals('csv', $sut->getId());
self::assertEquals('csv', $sut->getTitle());
}
public static function getTestModel()
public static function getTestModel(): array
{
return [
['400', '2437.12', ' EUR 1,947.99 ', 7, 6, 1, 2, 2]
['400', '2437.12', '1947.99', 7, 6, 1, 2, 2]
];
}
/**
* @dataProvider getTestModel
*/
public function testRender($totalDuration, $totalRate, $expectedRate, $expectedRows, $expectedDescriptions, $expectedUser1, $expectedUser2, $expectedUser3): void
public function testRender(string $totalDuration, string $totalRate, string $expectedRate, int $expectedRows, int $expectedDescriptions, int $expectedUser1, int $expectedUser2, int $expectedUser3): void
{
$sut = $this->getAbstractRenderer(CsvRenderer::class);
$sut = $this->getAbstractRenderer();
/** @var BinaryFileResponse $response */
$response = $this->render($sut);
@@ -53,17 +74,19 @@ class CsvRendererTest extends AbstractRendererTestCase
self::assertTrue(file_exists($file->getRealPath()));
$content = file_get_contents($file->getRealPath());
self::assertIsString($content);
self::assertStringContainsString('"' . $expectedRate . '"', $content);
self::assertStringContainsString($expectedRate, $content);
self::assertEquals($expectedRows, substr_count($content, PHP_EOL));
self::assertEquals($expectedDescriptions, substr_count($content, 'activity description'));
self::assertEquals($expectedUser1, substr_count($content, ',"kevin",'));
self::assertEquals($expectedUser3, substr_count($content, ',"hello-world",'));
self::assertEquals($expectedUser2, substr_count($content, ',"foo-bar",'));
self::assertEquals($expectedDescriptions, substr_count($content, '"activity description"'));
self::assertEquals($expectedUser1, substr_count($content, ',kevin,'));
self::assertEquals($expectedUser3, substr_count($content, ',hello-world,'));
self::assertEquals($expectedUser2, substr_count($content, ',foo-bar,'));
ob_start();
$response->sendContent();
$content2 = ob_get_clean();
self::assertIsString($content2);
self::assertEquals($content, $content2);
self::assertFalse(file_exists($file->getRealPath()));
@@ -71,6 +94,7 @@ class CsvRendererTest extends AbstractRendererTestCase
$all = [];
$rows = str_getcsv($content2, PHP_EOL);
foreach ($rows as $row) {
self::assertIsString($row);
$all[] = str_getcsv($row);
}
@@ -78,70 +102,70 @@ class CsvRendererTest extends AbstractRendererTestCase
'2019-06-16',
'12:00',
'12:06',
'400',
'0.11',
'EUR',
'0',
'',
'kevin',
'0',
'0',
'84',
'kevin',
'',
'Customer Name',
'project name',
'activity description',
'',
'1',
'foo, bar',
'timesheet',
'work',
'A-0123456789',
'',
'',
'foo,bar',
'',
' EUR 84.00 ',
'DE-9876543210',
'ORDER-123',
'meta-bar',
'meta-bar2',
'customer-bar',
'',
'project-foo2',
'activity-bar',
'timesheet',
'work',
'A-0123456789',
'DE-9876543210',
'ORDER-123',
];
$expected2 = [
'2019-06-16',
'12:00',
'12:06',
'400',
'0.11',
'EUR',
'0',
'',
'nivek',
'0',
'0',
'-100.92',
'nivek',
'',
'Customer Name',
'project name',
'activity description',
'',
'1',
'',
'timesheet',
'work',
'A-0123456789',
'',
'',
'',
' EUR -100.92',
'DE-9876543210',
'ORDER-123',
'',
'',
'customer-bar',
'',
'project-foo2',
'activity-bar',
'timesheet',
'work',
'A-0123456789',
'DE-9876543210',
'ORDER-123',
];
self::assertEquals(7, \count($all));
self::assertEquals($expected, $all[5]);
self::assertEquals($expected2, $all[6]);
self::assertEquals(\count($expected), \count($all[0]));
self::assertEquals('foo', $all[4][15]);
self::assertEquals('foo', $all[4][16]);
}
}

View File

@@ -0,0 +1,102 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Base;
use App\Entity\ExportableItem;
use App\Export\Base\SpreadsheetRenderer;
use App\Export\Package\SpreadsheetPackage;
use App\Repository\Query\TimesheetQuery;
use App\Tests\Export\Renderer\AbstractRendererTestCase;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @covers \App\Export\Base\SpreadsheetRenderer
* @group integration
*/
class SpreadsheetRendererTest extends AbstractRendererTestCase
{
public function testWriteSpreadsheetCreatesSpreadsheetWithCorrectHeaders(): void
{
$translator = $this->createMock(TranslatorInterface::class);
$translator->method('trans')->willReturnArgument(0);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$security = $this->createMock(Security::class);
$spreadsheetPackage = $this->createMock(SpreadsheetPackage::class);
$spreadsheetPackage->expects(self::once())->method('setHeader')->with([
'date', 'begin', 'end', 'duration', 'currency', 'rate', 'internalRate', 'hourlyRate', 'fixedRate', 'username', 'account_number', 'customer', 'project', 'activity', 'description', 'billable', 'tags', 'type', 'category', 'number', 'project_number', 'vat_id', 'orderNumber'
]);
$renderer = new SpreadsheetRenderer($translator, $dispatcher, $security);
$renderer->writeSpreadsheet($spreadsheetPackage, [], new TimesheetQuery());
}
public function testWriteSpreadsheetAddsRowsForExportItems(): void
{
$translator = $this->createMock(TranslatorInterface::class);
$translator->method('trans')->willReturnArgument(0);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$security = $this->createMock(Security::class);
$spreadsheetPackage = $this->createMock(SpreadsheetPackage::class);
$spreadsheetPackage->expects(self::exactly(2))->method('addRow');
$spreadsheetPackage->expects(self::once())->method('save');
$exportItem = $this->createMock(ExportableItem::class);
$exportItem->method('getBegin')->willReturn(new \DateTime());
$exportItem->method('getEnd')->willReturn(new \DateTime());
$exportItem->method('getDuration')->willReturn(3600);
$exportItem->method('getRate')->willReturn(100.0);
$exportItem->method('getInternalRate')->willReturn(80.0);
$exportItem->method('getHourlyRate')->willReturn(50.0);
$exportItem->method('getFixedRate')->willReturn(200.0);
$exportItem->method('getUser')->willReturn(null);
$exportItem->method('getProject')->willReturn(null);
$exportItem->method('getActivity')->willReturn(null);
$exportItem->method('getDescription')->willReturn('Test description');
$exportItem->method('isBillable')->willReturn(true);
$exportItem->method('getTagsAsArray')->willReturn(['tag1', 'tag2']);
$exportItem->method('getType')->willReturn('type');
$exportItem->method('getCategory')->willReturn('category');
$renderer = new SpreadsheetRenderer($translator, $dispatcher, $security);
$renderer->writeSpreadsheet($spreadsheetPackage, [$exportItem], new TimesheetQuery());
}
public function testWriteSpreadsheetAddsTotalRowWhenMoreThanOneRow(): void
{
$translator = $this->createMock(TranslatorInterface::class);
$translator->method('trans')->willReturnArgument(0);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$security = $this->createMock(Security::class);
$spreadsheetPackage = $this->createMock(SpreadsheetPackage::class);
$spreadsheetPackage->expects(self::exactly(3))->method('addRow');
$exportItem = $this->createMock(ExportableItem::class);
$exportItem->method('getBegin')->willReturn(new \DateTime());
$exportItem->method('getEnd')->willReturn(new \DateTime());
$exportItem->method('getDuration')->willReturn(3600);
$exportItem->method('getRate')->willReturn(100.0);
$exportItem->method('getInternalRate')->willReturn(80.0);
$exportItem->method('getHourlyRate')->willReturn(50.0);
$exportItem->method('getFixedRate')->willReturn(200.0);
$exportItem->method('getUser')->willReturn(null);
$exportItem->method('getProject')->willReturn(null);
$exportItem->method('getActivity')->willReturn(null);
$exportItem->method('getDescription')->willReturn('Test description');
$exportItem->method('isBillable')->willReturn(true);
$exportItem->method('getTagsAsArray')->willReturn(['tag1', 'tag2']);
$exportItem->method('getType')->willReturn('type');
$exportItem->method('getCategory')->willReturn('category');
$renderer = new SpreadsheetRenderer($translator, $dispatcher, $security);
$renderer->writeSpreadsheet($spreadsheetPackage, [$exportItem, $exportItem], new TimesheetQuery());
}
}

View File

@@ -7,23 +7,44 @@
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Renderer;
namespace App\Tests\Export\Base;
use App\Export\Renderer\XlsxRenderer;
use App\Entity\User;
use App\Export\Base\SpreadsheetRenderer;
use App\Export\Base\XlsxRenderer;
use App\Tests\Export\Renderer\AbstractRendererTestCase;
use App\Tests\Export\Renderer\MetaFieldColumnSubscriber;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @covers \App\Export\Base\XlsxRenderer
* @covers \App\Export\Base\AbstractSpreadsheetRenderer
* @covers \App\Export\Base\SpreadsheetRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Renderer\XlsxRenderer
* @covers \App\Export\Package\SpoutSpreadsheet
* @group integration
*/
class XlsxRendererTest extends AbstractRendererTestCase
{
protected function getAbstractRenderer(): XlsxRenderer
{
$security = $this->createMock(Security::class);
$security->expects($this->any())->method('getUser')->willReturn(new User());
$security->expects($this->any())->method('isGranted')->willReturn(true);
$translator = $this->createMock(TranslatorInterface::class);
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());
return new XlsxRenderer(new SpreadsheetRenderer($translator, $dispatcher, $security));
}
public function testConfiguration(): void
{
$sut = $this->getAbstractRenderer(XlsxRenderer::class);
$sut = $this->getAbstractRenderer();
self::assertEquals('xlsx', $sut->getId());
self::assertEquals('xlsx', $sut->getTitle());
@@ -31,7 +52,7 @@ class XlsxRendererTest extends AbstractRendererTestCase
public function testRender(): void
{
$sut = $this->getAbstractRenderer(XlsxRenderer::class);
$sut = $this->getAbstractRenderer();
/** @var BinaryFileResponse $response */
$response = $this->render($sut);

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\ArrayFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\ArrayFormatter
*/
class ArrayFormatterTest extends TestCase
{
public function testFormatValueReturnsCommaSeparatedStringForArray(): void
{
$formatter = new ArrayFormatter();
$result = $formatter->formatValue(['one', 'two', 'three']);
self::assertEquals('one, two, three', $result);
}
public function testFormatValueThrowsExceptionForNonArray(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only arrays are supported');
$formatter = new ArrayFormatter();
$formatter->formatValue('not an array');
}
public function testFormatValueReturnsEmptyStringForEmptyArray(): void
{
$formatter = new ArrayFormatter();
$result = $formatter->formatValue([]);
self::assertEquals('', $result);
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\BooleanFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\BooleanFormatter
*/
class BooleanFormatterTest extends TestCase
{
public function testFormatValueReturnsFalseForNull(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue(null);
self::assertFalse($result);
}
public function testFormatValueReturnsTrueForTrue(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue(true);
self::assertTrue($result);
}
public function testFormatValueReturnsFalseForFalse(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue(false);
self::assertFalse($result);
}
public function testFormatValueReturnsTrueForNonZeroNumber(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue(1);
self::assertTrue($result);
}
public function testFormatValueReturnsFalseForZero(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue(0);
self::assertFalse($result);
}
public function testFormatValueReturnsTrueForNonEmptyString(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue('non-empty');
self::assertTrue($result);
}
public function testFormatValueReturnsFalseForEmptyString(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue('');
self::assertFalse($result);
}
public function testFormatValueThrowsExceptionForNonScalar(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only scalar values are supported');
$formatter = new BooleanFormatter();
$formatter->formatValue([]);
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\DateFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\DateFormatter
*/
class DateFormatterTest extends TestCase
{
public function testFormatValueReturnsFormattedDateForDateTime(): void
{
$formatter = new DateFormatter();
$date = new \DateTime('2023-10-01');
$result = $formatter->formatValue($date);
self::assertEquals('2023-10-01', $result);
}
public function testFormatValueReturnsNullForNullValue(): void
{
$formatter = new DateFormatter();
$result = $formatter->formatValue(null);
self::assertNull($result);
}
public function testFormatValueThrowsExceptionForNonDateTime(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only DateTimeInterface can be formatted');
$formatter = new DateFormatter();
$formatter->formatValue('not a date');
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\DefaultFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\DefaultFormatter
*/
class DefaultFormatterTest extends TestCase
{
public function testFormatValueReturnsSameValueForScalar(): void
{
$formatter = new DefaultFormatter();
$result = $formatter->formatValue('string');
self::assertEquals('string', $result);
$result = $formatter->formatValue(123);
self::assertEquals(123, $result);
$result = $formatter->formatValue(45.67);
self::assertEquals(45.67, $result);
$result = $formatter->formatValue(true);
self::assertTrue($result);
$result = $formatter->formatValue(null);
self::assertNull($result);
}
public function testFormatValueThrowsExceptionForNonScalar(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only scalar values are supported');
$formatter = new DefaultFormatter();
$formatter->formatValue([]);
}
public function testFormatValueThrowsExceptionForObject(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only scalar values are supported');
$formatter = new DefaultFormatter();
$formatter->formatValue(new \stdClass());
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\DurationFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\DurationFormatter
*/
class DurationFormatterTest extends TestCase
{
public function testFormatValueReturnsFormattedDurationForNumericValue(): void
{
$formatter = new DurationFormatter();
$result = $formatter->formatValue(7200);
self::assertEquals(2.00, $result);
}
public function testFormatValueReturnsZeroForNonNumericValue(): void
{
$formatter = new DurationFormatter();
$result = $formatter->formatValue('not a number');
self::assertEquals(0.0, $result);
}
public function testFormatValueReturnsFormattedDurationForFloatValue(): void
{
$formatter = new DurationFormatter();
$result = $formatter->formatValue(4500.5);
self::assertEquals(1.25, $result);
}
public function testFormatValueReturnsZeroForNullValue(): void
{
$formatter = new DurationFormatter();
$result = $formatter->formatValue(null);
self::assertEquals(0.0, $result);
}
public function testFormatValueReturnsFormattedDurationForNegativeValue(): void
{
$formatter = new DurationFormatter();
$result = $formatter->formatValue(-3600);
self::assertEquals(-1.00, $result);
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\RateFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\RateFormatter
*/
class RateFormatterTest extends TestCase
{
public function testFormatValueReturnsFormattedFloatForNumericValue(): void
{
$formatter = new RateFormatter();
$result = $formatter->formatValue(1234.5678);
self::assertEquals(1234.57, $result);
$result = $formatter->formatValue('1234.5678');
self::assertEquals(1234.57, $result);
}
public function testFormatValueThrowsForNonNumericValue(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only numeric values be formatted');
$formatter = new RateFormatter();
$result = $formatter->formatValue('not a number');
self::assertEquals(0.0, $result);
}
public function testFormatValueReturnsZeroForNullValue(): void
{
$formatter = new RateFormatter();
$result = $formatter->formatValue(null);
self::assertEquals(0.0, $result);
}
public function testFormatValueReturnsFormattedFloatForNegativeNumericValue(): void
{
$formatter = new RateFormatter();
$result = $formatter->formatValue(-1234.5678);
self::assertEquals(-1234.57, $result);
}
public function testFormatValueReturnsFormattedFloatForFloatWithTwoDecimalPlaces(): void
{
$formatter = new RateFormatter();
$result = $formatter->formatValue(1234.56);
self::assertEquals(1234.56, $result);
}
public function testFormatValueThrowsExceptionForNonNumericNonNullValue(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only numeric values be formatted');
$formatter = new RateFormatter();
$formatter->formatValue(new \stdClass());
}
public function testFormatValueThrowsExceptionFoArrayValue(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only numeric values be formatted');
$formatter = new RateFormatter();
$formatter->formatValue([]);
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\TextFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\TextFormatter
*/
class TextFormatterTest extends TestCase
{
public function testFormatValueReturnsSanitizedStringWhenSanitizeDdeIsTrue(): void
{
$formatter = new TextFormatter(true);
$result = $formatter->formatValue('=cmd|\' /C calc\'!A0');
self::assertEquals('\' =cmd|\' /C calc\'!A0', $result);
}
public function testFormatValueReturnsOriginalStringWhenSanitizeDdeIsFalse(): void
{
$formatter = new TextFormatter(false);
$result = $formatter->formatValue('=cmd|\' /C calc\'!A0');
self::assertEquals('=cmd|\' /C calc\'!A0', $result);
}
public function testFormatValueReturnsOriginalValueForNonStringWhenSanitizeDdeIsTrue(): void
{
$formatter = new TextFormatter(true);
$result = $formatter->formatValue(123);
self::assertEquals(123, $result);
$result = $formatter->formatValue(45.67);
self::assertEquals(45.67, $result);
$result = $formatter->formatValue(true);
self::assertTrue($result);
}
public function testFormatValueReturnsOriginalValueForNonStringWhenSanitizeDdeIsFalse(): void
{
$formatter = new TextFormatter(false);
$result = $formatter->formatValue(123);
self::assertEquals(123, $result);
$result = $formatter->formatValue(45.67);
self::assertEquals(45.67, $result);
$result = $formatter->formatValue(true);
self::assertTrue($result);
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\TimeFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\TimeFormatter
*/
class TimeFormatterTest extends TestCase
{
public function testFormatValueReturnsFormattedTimeForDateTimeInterface(): void
{
$formatter = new TimeFormatter();
$dateTime = new \DateTime('2023-10-10 14:30:00');
$result = $formatter->formatValue($dateTime);
self::assertEquals('14:30', $result);
}
public function testFormatValueReturnsNullForNullValue(): void
{
$formatter = new TimeFormatter();
$result = $formatter->formatValue(null);
self::assertNull($result);
}
public function testFormatValueThrowsExceptionForNonDateTimeInterface(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only DateTimeInterface can be formatted');
$formatter = new TimeFormatter();
$formatter->formatValue('not a DateTime');
}
}

View File

@@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package;
use App\Entity\ExportableItem;
use App\Export\Package\CellFormatter\CellFormatterInterface;
use App\Export\Package\Column;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\Column
*/
class ColumnTest extends TestCase
{
public function testGetNameReturnsColumnName(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
self::assertEquals('testName', $column->getName());
}
public function withHeaderSetsHeader(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
$column->withHeader('testHeader');
self::assertEquals('testHeader', $column->getHeader());
}
public function withExtractorSetsExtractor(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
$extractor = function (ExportableItem $item) {
return $item->getId();
};
$column->withExtractor($extractor);
$exportableItem = $this->createMock(ExportableItem::class);
$exportableItem->method('getId')->willReturn(123);
self::assertEquals(123, $column->extract($exportableItem));
}
public function testExtractThrowsExceptionWhenExtractorIsNull(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Missing extractor on column: testName');
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
$exportableItem = $this->createMock(ExportableItem::class);
$column->extract($exportableItem);
}
public function testGetValueReturnsFormattedValue(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$formatter->method('formatValue')->willReturn('formattedValue');
$column = new Column('testName', $formatter);
$extractor = function (ExportableItem $item) {
return 'rawValue';
};
$column->withExtractor($extractor);
$exportableItem = $this->createMock(ExportableItem::class);
self::assertEquals('formattedValue', $column->getValue($exportableItem));
}
public function testGetHeaderReturnsHeaderWhenSet(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
$column->withHeader('testHeader');
self::assertEquals('testHeader', $column->getHeader());
}
public function testGetHeaderReturnsNameWhenHeaderIsNull(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
self::assertEquals('testName', $column->getHeader());
}
}

View File

@@ -0,0 +1,121 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package;
use App\Export\Package\PhpOfficeSpreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\PhpOfficeSpreadsheet
*/
class PhpOfficeSpreadsheetTest extends TestCase
{
private string $filename;
private int $counter = 1;
protected function setUp(): void
{
$this->filename = realpath(__DIR__ . '/../../_data/') . '/test' . $this->counter++ . '.xlsx';
}
protected function tearDown(): void
{
if (file_exists($this->filename)) {
unlink($this->filename);
}
}
public function testopenSetsFilename(): void
{
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$reflection = new \ReflectionClass($spreadsheetPackage);
$property = $reflection->getProperty('filename');
$property->setAccessible(true);
self::assertEquals($this->filename, $property->getValue($spreadsheetPackage));
}
public function testsaveThrowsExceptionWhenFilenameIsNull(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Need to call open() first before save()');
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->save();
}
public function testsaveWritesFile(): void
{
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->setHeader(['Foo', 'Bar']);
$spreadsheetPackage->addRow(['Data1', 'Data2']);
$spreadsheetPackage->addRow(['Data3', 'Data4']);
$spreadsheetPackage->save();
self::assertGreaterThan(0, filesize($this->filename));
}
public function testsaveThrowsExceptionWhenSpreadsheetIsNull(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Cannot re-use spreadsheet after calling save()');
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->save();
$spreadsheetPackage->save();
}
public function testsetHeaderSetsHeaderRow(): void
{
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->setHeader(['Column1', 'Column2']);
$reflection = new \ReflectionClass($spreadsheetPackage);
$property = $reflection->getProperty('worksheet');
$property->setAccessible(true);
/** @var Worksheet $worksheet */
$worksheet = $property->getValue($spreadsheetPackage);
self::assertNotNull($worksheet);
self::assertEquals('Column1', $worksheet->getCell('A1')->getValue());
self::assertEquals('Column2', $worksheet->getCell('B1')->getValue());
}
public function testaddRowAddsDataRow(): void
{
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->addRow(['Data1', 'Data2']);
$reflection = new \ReflectionClass($spreadsheetPackage);
$property = $reflection->getProperty('worksheet');
$property->setAccessible(true);
/** @var Worksheet $worksheet */
$worksheet = $property->getValue($spreadsheetPackage);
self::assertEquals('Data1', $worksheet->getCell('A1')->getValue());
self::assertEquals('Data2', $worksheet->getCell('B1')->getValue());
}
public function testaddRowThrowsExceptionWhenSpreadsheetIsNull(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Cannot re-use spreadsheet after calling save()');
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->save();
$spreadsheetPackage->addRow(['Data1', 'Data2']);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package;
use App\Export\Package\SpoutSpreadsheet;
use OpenSpout\Writer\CSV\Writer;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\SpoutSpreadsheet
*/
class SpoutSpreadsheetTest extends TestCase
{
private string $filename;
private int $counter = 1;
protected function setUp(): void
{
$this->filename = realpath(__DIR__ . '/../../_data/') . '/test' . $this->counter++ . '.xlsx';
}
protected function tearDown(): void
{
if (file_exists($this->filename)) {
unlink($this->filename);
}
}
public function testsaveWritesFile(): void
{
$spreadsheetPackage = new SpoutSpreadsheet(new Writer());
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->setHeader(['Foo', 'Bar']);
$spreadsheetPackage->addRow(['Data1', 'Data2']);
$spreadsheetPackage->addRow(['Data3', 'Data4']);
$spreadsheetPackage->save();
self::assertGreaterThan(0, filesize($this->filename));
}
}

View File

@@ -9,7 +9,6 @@
namespace App\Tests\Export\Renderer;
use App\Configuration\LocaleService;
use App\Entity\Activity;
use App\Entity\ActivityMeta;
use App\Entity\Customer;
@@ -26,41 +25,14 @@ use App\Event\CustomerMetaDisplayEvent;
use App\Event\ProjectMetaDisplayEvent;
use App\Event\TimesheetMetaDisplayEvent;
use App\Export\ExportRendererInterface;
use App\Export\TimesheetExportInterface;
use App\Repository\Query\TimesheetQuery;
use App\Twig\LocaleFormatExtensions;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;
abstract class AbstractRendererTestCase extends KernelTestCase
{
/**
* @param class-string $classname
*/
protected function getAbstractRenderer(string $classname): ExportRendererInterface|TimesheetExportInterface
{
$languages = [
'en' => LocaleService::DEFAULT_SETTINGS
];
$security = $this->createMock(Security::class);
$security->expects($this->any())->method('getUser')->willReturn(new User());
$security->expects($this->any())->method('isGranted')->willReturn(true);
$translator = $this->createMock(TranslatorInterface::class);
$dateExtension = new LocaleFormatExtensions(new LocaleService($languages));
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());
return new $classname($translator, $dateExtension, $dispatcher, $security);
}
protected function render(ExportRendererInterface $renderer): Response
{
$customer = new Customer('Customer Name');

View File

@@ -10,7 +10,7 @@
namespace App\Tests\Export\Renderer;
use App\Activity\ActivityStatisticService;
use App\Export\Renderer\HtmlRenderer;
use App\Export\Base\HtmlRenderer;
use App\Export\Renderer\HtmlRendererFactory;
use App\Project\ProjectStatisticService;
use PHPUnit\Framework\TestCase;
@@ -35,5 +35,6 @@ class HtmlRendererFactoryTest extends TestCase
self::assertInstanceOf(HtmlRenderer::class, $renderer);
self::assertEquals('foo', $renderer->getId());
self::assertEquals('print', $renderer->getTitle());
}
}

View File

@@ -11,7 +11,7 @@ namespace App\Tests\Export\Renderer;
use App\Activity\ActivityStatisticService;
use App\Entity\User;
use App\Export\Renderer\HtmlRenderer;
use App\Export\Base\HtmlRenderer;
use App\Project\ProjectStatisticService;
use Symfony\Bridge\Twig\AppVariable;
use Symfony\Component\EventDispatcher\EventDispatcher;
@@ -24,7 +24,6 @@ use Twig\Environment;
/**
* @covers \App\Export\Base\HtmlRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Renderer\HtmlRenderer
* @group integration
*/
class HtmlRendererTest extends AbstractRendererTestCase

View File

@@ -9,7 +9,7 @@
namespace App\Tests\Export\Renderer;
use App\Export\Renderer\PDFRenderer;
use App\Export\Base\PDFRenderer;
use App\Export\Renderer\PdfRendererFactory;
use App\Pdf\HtmlToPdfConverter;
use App\Project\ProjectStatisticService;

View File

@@ -9,7 +9,7 @@
namespace App\Tests\Export\Renderer;
use App\Export\Renderer\PDFRenderer;
use App\Export\Base\PDFRenderer;
use App\Pdf\HtmlToPdfConverter;
use App\Pdf\MPdfConverter;
use App\Project\ProjectStatisticService;
@@ -21,7 +21,6 @@ use Twig\Environment;
/**
* @covers \App\Export\Base\PDFRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Renderer\PDFRenderer
* @group integration
*/
class PdfRendererTest extends AbstractRendererTestCase

View File

@@ -10,8 +10,8 @@
namespace App\Tests\Export;
use App\Activity\ActivityStatisticService;
use App\Export\Base\HtmlRenderer;
use App\Export\ExportRepositoryInterface;
use App\Export\Renderer\HtmlRenderer;
use App\Export\ServiceExport;
use App\Export\Timesheet\HtmlRenderer as HtmlExporter;
use App\Project\ProjectStatisticService;

View File

@@ -9,7 +9,6 @@
namespace App\Tests\Export\Timesheet;
use App\Configuration\LocaleService;
use App\Entity\Activity;
use App\Entity\ActivityMeta;
use App\Entity\Customer;
@@ -27,42 +26,13 @@ use App\Event\ProjectMetaDisplayEvent;
use App\Event\TimesheetMetaDisplayEvent;
use App\Export\TimesheetExportInterface;
use App\Repository\Query\TimesheetQuery;
use App\Twig\LocaleFormatExtensions;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;
abstract class AbstractRendererTestCase extends KernelTestCase
{
/**
* @param class-string $classname
*/
protected function getAbstractRenderer(string $classname): TimesheetExportInterface
{
$languages = [
'en' => LocaleService::DEFAULT_SETTINGS
];
$user = new User();
$user->setUserIdentifier('ssdf');
$security = $this->createMock(Security::class);
$security->expects($this->any())->method('getUser')->willReturn($user);
$security->expects($this->any())->method('isGranted')->willReturn(true);
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
$dateExtension = new LocaleFormatExtensions(new LocaleService($languages));
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MetaFieldColumnSubscriber());
return new $classname($translator, $dateExtension, $dispatcher, $security);
}
protected function render(TimesheetExportInterface $renderer): Response
{
$customer = new Customer('Customer Name');

View File

@@ -1,114 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Timesheet;
use App\Export\Timesheet\CsvRenderer;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* @covers \App\Export\Base\CsvRenderer
* @covers \App\Export\Base\AbstractSpreadsheetRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Timesheet\CsvRenderer
* @group integration
*/
class CsvRendererTest extends AbstractRendererTestCase
{
public function testConfiguration(): void
{
$sut = $this->getAbstractRenderer(CsvRenderer::class);
self::assertEquals('csv', $sut->getId());
self::assertEquals('csv', $sut->getTitle());
}
public static function getTestModel()
{
return [
['400', '2437.12', ' EUR 1,947.99 ', 6, 5, 1, 2, 2]
];
}
/**
* @dataProvider getTestModel
*/
public function testRender($totalDuration, $totalRate, $expectedRate, $expectedRows, $expectedDescriptions, $expectedUser1, $expectedUser2, $expectedUser3): void
{
$sut = $this->getAbstractRenderer(CsvRenderer::class);
/** @var BinaryFileResponse $response */
$response = $this->render($sut);
$file = $response->getFile();
$prefix = date('Ymd');
self::assertEquals('text/csv', $response->headers->get('Content-Type'));
self::assertEquals('attachment; filename=' . $prefix . '-Customer_Name-project_name.csv', $response->headers->get('Content-Disposition'));
self::assertTrue(file_exists($file->getRealPath()));
$content = file_get_contents($file->getRealPath());
self::assertStringContainsString('"' . $expectedRate . '"', $content);
self::assertEquals($expectedRows, substr_count($content, PHP_EOL));
self::assertEquals($expectedDescriptions, substr_count($content, 'activity description'));
self::assertEquals($expectedUser1, substr_count($content, ',"kevin",'));
self::assertEquals($expectedUser3, substr_count($content, ',"hello-world",'));
self::assertEquals($expectedUser2, substr_count($content, ',"foo-bar",'));
ob_start();
$response->sendContent();
$content2 = ob_get_clean();
self::assertEquals($content, $content2);
self::assertFalse(file_exists($file->getRealPath()));
$all = [];
$rows = str_getcsv($content2, PHP_EOL);
foreach ($rows as $row) {
$all[] = str_getcsv($row);
}
$expected = [
'2019-06-16',
'12:00',
'12:06',
'400',
'0',
'',
'kevin',
'kevin',
'',
'Customer Name',
'project name',
'activity description',
'',
'',
'',
'foo,bar',
'',
' EUR 84.00 ',
'meta-bar',
'meta-bar2',
'customer-bar',
'',
'project-foo2',
'activity-bar',
'timesheet',
'work',
'A-0123456789',
'DE-9876543210',
'ORDER-123',
];
self::assertEquals(6, \count($all));
self::assertEquals($expected, $all[5]);
self::assertEquals(\count($expected), \count($all[0]));
self::assertEquals('foo', $all[4][15]);
}
}

View File

@@ -9,7 +9,7 @@
namespace App\Tests\Export\Timesheet;
use App\Export\Timesheet\PDFRenderer;
use App\Export\Base\PDFRenderer;
use App\Pdf\HtmlToPdfConverter;
use App\Pdf\MPdfConverter;
use App\Project\ProjectStatisticService;
@@ -21,7 +21,6 @@ use Twig\Environment;
/**
* @covers \App\Export\Base\PDFRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Timesheet\PDFRenderer
* @group integration
*/
class PdfRendererTest extends AbstractRendererTestCase

View File

@@ -1,53 +0,0 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Timesheet;
use App\Export\Timesheet\XlsxRenderer;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* @covers \App\Export\Base\XlsxRenderer
* @covers \App\Export\Base\AbstractSpreadsheetRenderer
* @covers \App\Export\Base\RendererTrait
* @covers \App\Export\Timesheet\XlsxRenderer
* @group integration
*/
class XlsxRendererTest extends AbstractRendererTestCase
{
public function testConfiguration(): void
{
$sut = $this->getAbstractRenderer(XlsxRenderer::class);
self::assertEquals('xlsx', $sut->getId());
self::assertEquals('xlsx', $sut->getTitle());
}
public function testRender(): void
{
$sut = $this->getAbstractRenderer(XlsxRenderer::class);
/** @var BinaryFileResponse $response */
$response = $this->render($sut);
$file = $response->getFile();
$prefix = date('Ymd');
self::assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type'));
self::assertEquals('attachment; filename=' . $prefix . '-Customer_Name-project_name.xlsx', $response->headers->get('Content-Disposition'));
self::assertTrue(file_exists($file->getRealPath()));
ob_start();
$response->sendContent();
$content2 = ob_get_clean();
self::assertNotEmpty($content2);
self::assertFalse(file_exists($file->getRealPath()));
}
}