Skip to content
Draft

WIP #83253

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* Copyright (c) 2026 Airbyte, Inc., all rights reserved. */
package io.airbyte.cdk.command

import com.fasterxml.jackson.databind.JsonNode
import io.airbyte.cdk.util.Jsons
import java.nio.file.Files
import java.nio.file.Path
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.TestFactory

/**
* Snapshots the JSON schema that [ValidatedJsonUtils.generateAirbyteJsonSchema] produces for a set
* of specification classes, and compares it against a checked-in golden file.
*
* Schema generation is a pure function of the class, so unlike the per-connector spec tests under
* `src/test-integration` this needs no Micronaut context, no connector process, and no credentials.
* That makes it cheap enough to run on every build, which is the point: it is the safety net for
* changes to the schema generator itself.
*
* Goldens live in `src/test/resources/spec-schema-snapshots/<SimpleName>.json`. Key order is
* significant and is preserved verbatim, because the platform UI renders properties in the order
* the schema declares them.
*
* To accept an intended change, re-run with `-Dairbyte.specSnapshot.update=true` and commit the
* diff. Snapshots deliberately do not self-update on a normal run β€” a golden that rewrites itself
* cannot serve as a baseline.
*/
abstract class SpecSchemaSnapshotTest(
private val specClasses: List<Class<*>>,
) {
constructor(vararg specClasses: Class<*>) : this(specClasses.toList())

@TestFactory
fun specSchemaSnapshots(): List<DynamicTest> =
specClasses.map { klazz ->
DynamicTest.dynamicTest(klazz.simpleName) { assertSnapshotMatches(klazz) }
}

private fun assertSnapshotMatches(klazz: Class<*>) {
val schema: JsonNode = ValidatedJsonUtils.generateAirbyteJsonSchema(klazz)
val actual: String = Jsons.writerWithDefaultPrettyPrinter().writeValueAsString(schema) + "\n"
val goldenPath: Path = snapshotDir.resolve("${klazz.simpleName}.json")

if (updateSnapshots || !Files.exists(goldenPath)) {
Files.createDirectories(goldenPath.parent)
Files.writeString(goldenPath, actual)
if (!updateSnapshots) {
Assertions.fail<Unit>(
"No snapshot existed for ${klazz.name}; wrote one to $goldenPath. " +
"Review it and commit it.",
)
}
return
}

val expected: String = Files.readString(goldenPath)
if (expected == actual) {
return
}
// Leave the generated schema on disk so the diff is a normal file comparison.
val actualPath: Path = goldenPath.resolveSibling("${klazz.simpleName}.json.actual")
Files.writeString(actualPath, actual)
Assertions.fail<Unit>(
"Generated JSON schema for ${klazz.name} no longer matches $goldenPath.\n" +
"Wrote the generated schema to $actualPath; run `diff $goldenPath $actualPath`.\n" +
"If the change is intended, re-run with -Dairbyte.specSnapshot.update=true and " +
"commit the diff.",
)
}

companion object {
val snapshotDir: Path = Path.of("src/test/resources/spec-schema-snapshots")

val updateSnapshots: Boolean =
System.getProperty("airbyte.specSnapshot.update").toBoolean() ||
System.getenv("AIRBYTE_SPEC_SNAPSHOT_UPDATE").toBoolean()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* Copyright (c) 2026 Airbyte, Inc., all rights reserved. */
package io.airbyte.cdk.fakesource

import io.airbyte.cdk.command.SpecSchemaSnapshotTest

/**
* Guards the schema generated for the CDK's own reference source specification. Between this and
* the JDBC toolkit snapshots, the shared annotation surface β€” titles, descriptions, defaults, raw
* JSON injection, unique-item arrays, and `oneOf` from sealed interfaces β€” is covered without
* needing any connector to be built.
*/
class FakeSourceSpecSchemaSnapshotTest :
SpecSchemaSnapshotTest(FakeSourceConfigurationSpecification::class.java)
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
{
"$schema" : "http://json-schema.org/draft-07/schema#",
"title" : "Test Source Spec",
"type" : "object",
"additionalProperties" : true,
"properties" : {
"host" : {
"type" : "string",
"default" : "localhost",
"description" : "Hostname of the database.",
"title" : "Host",
"order" : 1
},
"port" : {
"type" : "integer",
"default" : 9092,
"description" : "Port of the database.",
"title" : "Port",
"order" : 2,
"minimum" : 0,
"maximum" : 65536
},
"database" : {
"type" : "string",
"description" : "Name of the database.",
"title" : "Database",
"order" : 3
},
"schemas" : {
"type" : "array",
"items" : {
"type" : "string"
},
"description" : "The list of schemas to sync from. Defaults to PUBLIC.",
"title" : "Schemas",
"order" : 4,
"minItems" : 1,
"uniqueItems" : true
},
"tunnel_method" : {
"oneOf" : [ {
"title" : "No Tunnel",
"type" : "object",
"additionalProperties" : true,
"description" : "No ssh tunnel needed to connect to database",
"properties" : {
"tunnel_method" : {
"type" : "string",
"enum" : [ "NO_TUNNEL" ],
"default" : "NO_TUNNEL"
}
},
"required" : [ "tunnel_method" ]
}, {
"title" : "SSH Key Authentication",
"type" : "object",
"additionalProperties" : true,
"description" : "Connect through a jump server tunnel host using username and ssh key",
"properties" : {
"tunnel_method" : {
"type" : "string",
"enum" : [ "SSH_KEY_AUTH" ],
"default" : "SSH_KEY_AUTH"
},
"tunnel_host" : {
"type" : "string",
"description" : "Hostname of the jump server host that allows inbound ssh tunnel.",
"title" : "SSH Tunnel Jump Server Host",
"order" : 1
},
"tunnel_port" : {
"type" : "integer",
"default" : 22,
"description" : "Port on the proxy/jump server that accepts inbound ssh connections.",
"title" : "SSH Connection Port",
"order" : 2,
"minimum" : 0,
"maximum" : 65536
},
"tunnel_user" : {
"type" : "string",
"description" : "OS-level username for logging into the jump server host",
"title" : "SSH Login Username",
"order" : 3
},
"ssh_key" : {
"type" : "string",
"description" : "OS-level user account ssh key credentials in RSA PEM format ( created with ssh-keygen -t rsa -m PEM -f myuser_rsa )",
"title" : "SSH Private Key",
"order" : 4,
"multiline" : true,
"airbyte_secret" : true
}
},
"required" : [ "tunnel_method", "tunnel_host", "tunnel_port", "tunnel_user", "ssh_key" ]
}, {
"title" : "Password Authentication",
"type" : "object",
"additionalProperties" : true,
"description" : "Connect through a jump server tunnel host using username and password authentication",
"properties" : {
"tunnel_method" : {
"type" : "string",
"enum" : [ "SSH_PASSWORD_AUTH" ],
"default" : "SSH_PASSWORD_AUTH"
},
"tunnel_host" : {
"type" : "string",
"description" : "Hostname of the jump server host that allows inbound ssh tunnel.",
"title" : "SSH Tunnel Jump Server Host",
"order" : 1
},
"tunnel_port" : {
"type" : "integer",
"default" : 22,
"description" : "Port on the proxy/jump server that accepts inbound ssh connections.",
"title" : "SSH Connection Port",
"order" : 2,
"minimum" : 0,
"maximum" : 65536
},
"tunnel_user" : {
"type" : "string",
"description" : "OS-level username for logging into the jump server host",
"title" : "SSH Login Username",
"order" : 3
},
"tunnel_user_password" : {
"type" : "string",
"description" : "OS-level password for logging into the jump server host",
"title" : "Password",
"order" : 4,
"airbyte_secret" : true
}
},
"required" : [ "tunnel_method", "tunnel_host", "tunnel_port", "tunnel_user", "tunnel_user_password" ]
} ],
"description" : "Whether to initiate an SSH tunnel before connecting to the database, and if so, which kind of authentication to use.",
"title" : "SSH Tunnel Method",
"order" : 5,
"type" : "object"
},
"cursor" : {
"oneOf" : [ {
"title" : "Scan Changes with User Defined Cursor",
"type" : "object",
"additionalProperties" : true,
"description" : "Configures how data is extracted from the database.",
"properties" : {
"cursor_method" : {
"type" : "string",
"enum" : [ "user_defined" ],
"default" : "user_defined"
}
},
"required" : [ "cursor_method" ]
}, {
"title" : "Read Changes using Change Data Capture (CDC)",
"type" : "object",
"additionalProperties" : true,
"description" : "Configures how data is extracted from the database.",
"properties" : {
"cursor_method" : {
"type" : "string",
"enum" : [ "cdc" ],
"default" : "cdc"
}
},
"required" : [ "cursor_method" ]
} ],
"description" : "Configures how data is extracted from the database.",
"title" : "Update Method",
"order" : 6,
"display_type" : "radio",
"type" : "object"
},
"resumable_preferred" : {
"type" : "boolean",
"default" : true,
"order" : 7,
"display_type" : "check"
},
"timeout" : {
"type" : "string",
"default" : "PT0S",
"order" : 8
}
},
"required" : [ "host", "port", "database" ]
}
Loading
Loading