Files
qbit-document-generation/foundation/src/test/java/com/kingsrook/qbits/documentgeneration/PostgresGenerationQueueTest.java
T
ee782b7d6e
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Failing after 7s
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Failing after 5s
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
chore: preserve unfinished document generation source and status
Checkpoint implementation at user-requested stop; document known failures and incomplete release qualification.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-Authored-By: GPT-6 Codex <noreply@openai.com>
2026-11-04 10:45:09 -06:00

191 lines
8.8 KiB
Java

/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.documentgeneration;
import java.sql.Connection;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/***************************************************************************
** Run the unchanged native authorization/transaction/concurrency suite on a
** real PostgreSQL 17 engine, plus PostgreSQL-specific durability and drift cases.
***************************************************************************/
class PostgresGenerationQueueTest extends GenerationQueueJdbcTest
{
private static PostgresGenerationDatabase postgres;
/***********************************************************************
** One exact disposable engine per suite; all test cases use new databases.
***********************************************************************/
@BeforeAll
static void startPostgres()
{
postgres = new PostgresGenerationDatabase();
}
/***********************************************************************
** Remove only the fixture-owned engine after every inherited assertion.
***********************************************************************/
@AfterAll
static void stopPostgres()
{
if(postgres != null)
{
postgres.close();
}
}
/***********************************************************************
** Native actions receive only the DML identity, never migration authority.
***********************************************************************/
@Override
protected RDBMSBackendMetaData database(String name, String defaultUrl) throws Exception
{
return (postgres.database(name));
}
/***********************************************************************
** Assertion and fault-injection connections remain separate from QQQ.
***********************************************************************/
@Override
protected Connection connection() throws Exception
{
return (postgres.connection());
}
/***********************************************************************
** Execute fixed fixture DDL, with no compatibility/emulation database mode.
***********************************************************************/
@Override
protected void sql(String source) throws Exception
{
postgres.sql(source);
}
/***********************************************************************
** Exercise the shipped native schema without the H2 timestamp rewrite.
***********************************************************************/
@Override
protected String schema(DocumentGenerationQBitConfig configuration)
{
return (DocumentGenerationSchema.postgresScript(configuration));
}
/***************************************************************************
** Cold server restart preserves committed results and untouched work.
***************************************************************************/
@Test
void serverRestartPreservesQueueAndIndependentAudit() throws Exception
{
int id = enqueue("restart");
var first = queue.advance(id);
assertThat(first.state()).isEqualTo("GENERATED");
postgres.restart();
assertThat(queue.status(id, -1, 10).items().getFirst()).isEqualTo(first);
assertThat(queue.advance(id).state()).isEqualTo("GENERATED");
assertThat(queue.advance(id)).isNull();
try(var connection = connection(); var statement = connection.createStatement();
var rows = statement.executeQuery("SELECT COUNT(*) FROM document_generation_event WHERE outcome='GENERATION_RETAINED'"))
{
rows.next();
assertThat(rows.getInt(1)).isEqualTo(2);
}
}
/***************************************************************************
** Exact text comparison keeps case and trailing-space keys distinct, and a retry is idempotent.
***************************************************************************/
@Test
void caseAndTrailingSpaceRemainDistinctIdempotencyKeys() throws Exception
{
int upper = enqueue("Case");
int lower = enqueue("case");
int spaced = enqueue("Case ");
assertThat(List.of(upper, lower, spaced)).doesNotHaveDuplicates();
assertThat(enqueue("Case")).isEqualTo(upper);
}
/***************************************************************************
** Unlogged tables, nondeterministic collations and missing idempotency keys fail closed before any write.
***************************************************************************/
@Test
void realSchemaDriftFailsClosed() throws Exception
{
sql("ALTER TABLE document_generation_stock SET UNLOGGED");
assertThatThrownBy(() -> enqueue("unlogged")).isInstanceOf(QException.class).hasMessageContaining("logged");
assertThatThrownBy(() -> new DocumentDefinitionService(config).createType("must not persist")).isInstanceOf(QException.class).hasMessageContaining("logged");
sql("ALTER TABLE document_generation_stock SET LOGGED");
sql("CREATE COLLATION fixture_ci (provider = icu, locale = 'und-u-ks-level2', deterministic = false)");
sql("ALTER TABLE document_generation_queue ALTER COLUMN request_key TYPE VARCHAR(130) COLLATE fixture_ci");
assertThatThrownBy(() -> enqueue("collation")).isInstanceOf(QException.class).hasMessageContaining("deterministic");
sql("ALTER TABLE document_generation_queue ALTER COLUMN request_key TYPE VARCHAR(130) COLLATE \"default\"");
sql("DROP INDEX uq_document_generation_queue_1");
assertThatThrownBy(() -> enqueue("index")).isInstanceOf(QException.class).hasMessageContaining("idempotency");
}
/***************************************************************************
** Role-level settings are observed from the actual server, never assumed from fixture setup.
***************************************************************************/
@Test
void weakCommitAndUnboundedLockSettingsAreRejectedBeforeWriting() throws Exception
{
try
{
sql("ALTER ROLE generation_app SET synchronous_commit='off'");
ConnectionManager.closeConnectionProviders();
assertThatThrownBy(() -> enqueue("weak-commit")).isInstanceOf(QException.class).hasMessageContaining("synchronous PostgreSQL");
sql("ALTER ROLE generation_app SET synchronous_commit='on'");
sql("ALTER ROLE generation_app RESET lock_timeout");
ConnectionManager.closeConnectionProviders();
assertThatThrownBy(() -> enqueue("unbounded-lock")).isInstanceOf(QException.class).hasMessageContaining("lock_timeout");
sql("ALTER ROLE generation_app SET lock_timeout='60s'");
ConnectionManager.closeConnectionProviders();
assertThatThrownBy(() -> enqueue("long-lock")).isInstanceOf(QException.class).hasMessageContaining("lock_timeout");
}
finally
{
sql("ALTER ROLE generation_app SET synchronous_commit='on'");
sql("ALTER ROLE generation_app SET lock_timeout='5s'");
ConnectionManager.closeConnectionProviders();
}
assertThat(queue.status(enqueue("restored"), -1, 10).items()).hasSize(2);
}
/***************************************************************************
** The native identity must be the postgres module on a real PostgreSQL17 server, not a vendor label.
***************************************************************************/
@Test
void vendorLabelWithoutPostgresModuleIsRejected() throws Exception
{
jdbcBackend.setBackendType("rdbms");
assertThatThrownBy(() -> enqueue("wrong-module")).isInstanceOf(QException.class).hasMessageContaining("not qualified");
}
}