invoicemgmt-1.0.0/tests/src/Unit/InvoiceNumberGeneratorTest.php
tests/src/Unit/InvoiceNumberGeneratorTest.php
<?php
declare(strict_types=1);
namespace Drupal\Tests\invoicemgmt\Unit;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\Select;
use Drupal\Core\Database\StatementInterface;
use Drupal\invoicemgmt\InvoiceNumberGenerator;
use Drupal\Tests\UnitTestCase;
use PHPUnit\Framework\MockObject\MockObject;
/**
* Unit tests for InvoiceNumberGenerator service.
*
* @coversDefaultClass \Drupal\invoicemgmt\InvoiceNumberGenerator
* @group invoicemgmt
*/
class InvoiceNumberGeneratorTest extends UnitTestCase {
/**
* The config factory mock.
*/
protected ConfigFactoryInterface|MockObject $configFactory;
/**
* The database connection mock.
*/
protected Connection|MockObject $database;
/**
* The config mock.
*/
protected ImmutableConfig|MockObject $config;
/**
* The invoice number generator service under test.
*/
protected InvoiceNumberGenerator $invoiceNumberGenerator;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->configFactory = $this->createMock(ConfigFactoryInterface::class);
$this->database = $this->createMock(Connection::class);
$this->config = $this->createMock(ImmutableConfig::class);
$this->configFactory->method('get')
->with('invoicemgmt.settings')
->willReturn($this->config);
$this->invoiceNumberGenerator = new InvoiceNumberGenerator(
$this->configFactory,
$this->database
);
}
/**
* Tests invoice number generation with default prefix.
*
* @covers ::generateInvoiceNumber
* @covers ::getNextUniqueCode
*/
public function testGenerateInvoiceNumberWithDefaultPrefix(): void {
// Mock config to return null (default prefix).
$this->config->method('get')
->with('invoice_number_prefix')
->willReturn(null);
// Mock database query to return no existing invoices.
$select = $this->createMock(Select::class);
$statement = $this->createMock(StatementInterface::class);
$this->database->method('select')
->with('node__field_invoice_number', 'fin')
->willReturn($select);
$select->method('fields')
->with('fin', ['field_invoice_number_value'])
->willReturn($select);
$select->method('condition')
->with('field_invoice_number_value', $this->stringContains('INV-'), 'LIKE')
->willReturn($select);
$select->method('orderBy')
->with('field_invoice_number_value', 'DESC')
->willReturn($select);
$select->method('range')
->with(0, 1)
->willReturn($select);
$select->method('execute')
->willReturn($statement);
$statement->method('fetchField')
->willReturn(FALSE);
$result = $this->invoiceNumberGenerator->generateInvoiceNumber();
// Expected format: INV-YYYYMM-001
$expected_pattern = '/^INV-\d{6}-001$/';
$this->assertMatchesRegularExpression($expected_pattern, $result);
}
/**
* Tests invoice number generation with custom prefix.
*
* @covers ::generateInvoiceNumber
* @covers ::getNextUniqueCode
*/
public function testGenerateInvoiceNumberWithCustomPrefix(): void {
// Mock config to return custom prefix.
$this->config->method('get')
->with('invoice_number_prefix')
->willReturn('CUSTOM');
// Mock database query to return no existing invoices.
$select = $this->createMock(Select::class);
$statement = $this->createMock(StatementInterface::class);
$this->database->method('select')
->with('node__field_invoice_number', 'fin')
->willReturn($select);
$select->method('fields')
->with('fin', ['field_invoice_number_value'])
->willReturn($select);
$select->method('condition')
->with('field_invoice_number_value', $this->stringContains('CUSTOM-'), 'LIKE')
->willReturn($select);
$select->method('orderBy')
->with('field_invoice_number_value', 'DESC')
->willReturn($select);
$select->method('range')
->with(0, 1)
->willReturn($select);
$select->method('execute')
->willReturn($statement);
$statement->method('fetchField')
->willReturn(FALSE);
$result = $this->invoiceNumberGenerator->generateInvoiceNumber();
// Expected format: CUSTOM-YYYYMM-001
$expected_pattern = '/^CUSTOM-\d{6}-001$/';
$this->assertMatchesRegularExpression($expected_pattern, $result);
}
/**
* Tests invoice number generation with existing invoices.
*
* @covers ::generateInvoiceNumber
* @covers ::getNextUniqueCode
*/
public function testGenerateInvoiceNumberWithExistingInvoices(): void {
// Mock config to return default prefix.
$this->config->method('get')
->with('invoice_number_prefix')
->willReturn('INV');
// Mock database query to return existing invoice.
$select = $this->createMock(Select::class);
$statement = $this->createMock(StatementInterface::class);
$this->database->method('select')
->with('node__field_invoice_number', 'fin')
->willReturn($select);
$select->method('fields')
->with('fin', ['field_invoice_number_value'])
->willReturn($select);
$select->method('condition')
->with('field_invoice_number_value', $this->stringContains('INV-'), 'LIKE')
->willReturn($select);
$select->method('orderBy')
->with('field_invoice_number_value', 'DESC')
->willReturn($select);
$select->method('range')
->with(0, 1)
->willReturn($select);
$select->method('execute')
->willReturn($statement);
$current_month = date('Ym');
$existing_invoice = "INV-{$current_month}-005";
$statement->method('fetchField')
->willReturn($existing_invoice);
$result = $this->invoiceNumberGenerator->generateInvoiceNumber();
// Expected format: INV-YYYYMM-006 (incremented from 005)
$expected = "INV-{$current_month}-006";
$this->assertEquals($expected, $result);
}
/**
* Tests getNextUniqueCode with malformed existing invoice number.
*
* @covers ::getNextUniqueCode
*/
public function testGetNextUniqueCodeWithMalformedInvoiceNumber(): void {
// Mock config to return default prefix.
$this->config->method('get')
->with('invoice_number_prefix')
->willReturn('INV');
// Mock database query to return malformed invoice.
$select = $this->createMock(Select::class);
$statement = $this->createMock(StatementInterface::class);
$this->database->method('select')
->with('node__field_invoice_number', 'fin')
->willReturn($select);
$select->method('fields')
->with('fin', ['field_invoice_number_value'])
->willReturn($select);
$select->method('condition')
->with('field_invoice_number_value', $this->stringContains('INV-'), 'LIKE')
->willReturn($select);
$select->method('orderBy')
->with('field_invoice_number_value', 'DESC')
->willReturn($select);
$select->method('range')
->with(0, 1)
->willReturn($select);
$select->method('execute')
->willReturn($statement);
// Return malformed invoice number (only 2 parts instead of 3).
$statement->method('fetchField')
->willReturn('INV-202501');
$result = $this->invoiceNumberGenerator->generateInvoiceNumber();
// Should default to 001 when malformed number is found.
$current_month = date('Ym');
$expected = "INV-{$current_month}-001";
$this->assertEquals($expected, $result);
}
/**
* Tests getNextUniqueCode with non-numeric suffix.
*
* @covers ::getNextUniqueCode
*/
public function testGetNextUniqueCodeWithNonNumericSuffix(): void {
// Mock config to return default prefix.
$this->config->method('get')
->with('invoice_number_prefix')
->willReturn('INV');
// Mock database query to return invoice with non-numeric suffix.
$select = $this->createMock(Select::class);
$statement = $this->createMock(StatementInterface::class);
$this->database->method('select')
->with('node__field_invoice_number', 'fin')
->willReturn($select);
$select->method('fields')
->with('fin', ['field_invoice_number_value'])
->willReturn($select);
$select->method('condition')
->with('field_invoice_number_value', $this->stringContains('INV-'), 'LIKE')
->willReturn($select);
$select->method('orderBy')
->with('field_invoice_number_value', 'DESC')
->willReturn($select);
$select->method('range')
->with(0, 1)
->willReturn($select);
$select->method('execute')
->willReturn($statement);
$current_month = date('Ym');
// Return invoice with non-numeric suffix.
$statement->method('fetchField')
->willReturn("INV-{$current_month}-ABC");
$result = $this->invoiceNumberGenerator->generateInvoiceNumber();
// Should default to 001 when non-numeric suffix is found.
$expected = "INV-{$current_month}-001";
$this->assertEquals($expected, $result);
}
/**
* Tests constructor dependency injection.
*
* @covers ::__construct
*/
public function testConstructor(): void {
$generator = new InvoiceNumberGenerator($this->configFactory, $this->database);
$this->assertInstanceOf(InvoiceNumberGenerator::class, $generator);
}
/**
* Tests invoice number format consistency.
*
* @covers ::generateInvoiceNumber
*/
public function testInvoiceNumberFormat(): void {
// Mock config to return test prefix.
$this->config->method('get')
->with('invoice_number_prefix')
->willReturn('TEST');
// Mock database query to return no existing invoices.
$select = $this->createMock(Select::class);
$statement = $this->createMock(StatementInterface::class);
$this->database->method('select')->willReturn($select);
$select->method('fields')->willReturn($select);
$select->method('condition')->willReturn($select);
$select->method('orderBy')->willReturn($select);
$select->method('range')->willReturn($select);
$select->method('execute')->willReturn($statement);
$statement->method('fetchField')->willReturn(FALSE);
$result = $this->invoiceNumberGenerator->generateInvoiceNumber();
// Verify format: PREFIX-YYYYMM-NNN
$parts = explode('-', $result);
$this->assertCount(3, $parts);
$this->assertEquals('TEST', $parts[0]);
$this->assertMatchesRegularExpression('/^\d{6}$/', $parts[1]);
$this->assertEquals('001', $parts[2]);
}
}