fix(repository-api): reject lossy schemas and revision ranges
This commit is contained in:
@@ -278,6 +278,16 @@ fn collect_schema_references(
|
|||||||
fn render_schema(value: &Value, context: &str) -> Result<String, GenerationError> {
|
fn render_schema(value: &Value, context: &str) -> Result<String, GenerationError> {
|
||||||
let schema = object(value, context)?;
|
let schema = object(value, context)?;
|
||||||
if let Some(reference) = schema.get("$ref") {
|
if let Some(reference) = schema.get("$ref") {
|
||||||
|
const ALLOWED_REFERENCE_SIBLINGS: &[&str] =
|
||||||
|
&["$ref", "$comment", "deprecated", "description", "title"];
|
||||||
|
if let Some(keyword) = schema
|
||||||
|
.keys()
|
||||||
|
.find(|keyword| !ALLOWED_REFERENCE_SIBLINGS.contains(&keyword.as_str()))
|
||||||
|
{
|
||||||
|
return Err(GenerationError::invalid(format!(
|
||||||
|
"{context} contains unsupported `$ref` sibling `{keyword}`"
|
||||||
|
)));
|
||||||
|
}
|
||||||
let reference = reference.as_str().ok_or_else(|| {
|
let reference = reference.as_str().ok_or_else(|| {
|
||||||
GenerationError::invalid(format!("{context} contains a non-string $ref"))
|
GenerationError::invalid(format!("{context} contains a non-string $ref"))
|
||||||
})?;
|
})?;
|
||||||
@@ -568,6 +578,15 @@ mod tests {
|
|||||||
assert!(error.to_string().contains("ambiguous anyOf"));
|
assert!(error.to_string().contains("ambiguous anyOf"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generator_rejects_lossy_reference_siblings() {
|
||||||
|
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
||||||
|
document["components"]["schemas"]["RepositorySummary"]["properties"]["source"]["maxLength"] =
|
||||||
|
serde_json::json!(1);
|
||||||
|
let error = generate_repository_typescript(&document.to_string()).unwrap_err();
|
||||||
|
assert!(error.to_string().contains("unsupported `$ref` sibling"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generator_is_bounded_to_the_repository_operation_contract() {
|
fn generator_is_bounded_to_the_repository_operation_contract() {
|
||||||
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
||||||
|
|||||||
@@ -131,6 +131,14 @@ function repositoryArray(value: unknown, path: string): unknown[] {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function repositorySourceRevision(value: unknown, path: string): number {
|
||||||
|
const parsed = integer(value, path);
|
||||||
|
if (parsed < 0) {
|
||||||
|
throw new Error(`${path} must be between 0 and Number.MAX_SAFE_INTEGER`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
function optionalNullableString(
|
function optionalNullableString(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
path: string,
|
path: string,
|
||||||
@@ -279,7 +287,10 @@ function repositorySummary(value: unknown, path: string): RepositorySummary {
|
|||||||
kind: repositoryString(item.kind, `${path}.kind`),
|
kind: repositoryString(item.kind, `${path}.kind`),
|
||||||
provider: repositoryString(item.provider, `${path}.provider`),
|
provider: repositoryString(item.provider, `${path}.provider`),
|
||||||
source: repositorySource(item.source, `${path}.source`),
|
source: repositorySource(item.source, `${path}.source`),
|
||||||
source_revision: integer(item.source_revision, `${path}.source_revision`),
|
source_revision: repositorySourceRevision(
|
||||||
|
item.source_revision,
|
||||||
|
`${path}.source_revision`,
|
||||||
|
),
|
||||||
source_fingerprint: repositoryString(
|
source_fingerprint: repositoryString(
|
||||||
item.source_fingerprint,
|
item.source_fingerprint,
|
||||||
`${path}.source_fingerprint`,
|
`${path}.source_fingerprint`,
|
||||||
@@ -368,7 +379,10 @@ function workspaceRepositoryRecord(
|
|||||||
provider: nullableString(item.provider, `${path}.provider`),
|
provider: nullableString(item.provider, `${path}.provider`),
|
||||||
source: repositorySource(item.source, `${path}.source`),
|
source: repositorySource(item.source, `${path}.source`),
|
||||||
default_ref: nullableString(item.default_ref, `${path}.default_ref`),
|
default_ref: nullableString(item.default_ref, `${path}.default_ref`),
|
||||||
source_revision: integer(item.source_revision, `${path}.source_revision`),
|
source_revision: repositorySourceRevision(
|
||||||
|
item.source_revision,
|
||||||
|
`${path}.source_revision`,
|
||||||
|
),
|
||||||
source_fingerprint: repositoryString(
|
source_fingerprint: repositoryString(
|
||||||
item.source_fingerprint,
|
item.source_fingerprint,
|
||||||
`${path}.source_fingerprint`,
|
`${path}.source_fingerprint`,
|
||||||
|
|||||||
@@ -74,6 +74,24 @@ Deno.test("stale repository aliases fail closed at the JSON boundary", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("repository source revisions enforce the OpenAPI integer range", () => {
|
||||||
|
const negative = structuredClone(repositoryList) as Record<string, unknown>;
|
||||||
|
const negativeItems = negative.items as Array<Record<string, unknown>>;
|
||||||
|
negativeItems[0].source_revision = -1;
|
||||||
|
assertThrows(
|
||||||
|
() => parseRepositoryListResponse(negative),
|
||||||
|
"must be between 0 and Number.MAX_SAFE_INTEGER",
|
||||||
|
);
|
||||||
|
|
||||||
|
const unsafe = structuredClone(repositoryList) as Record<string, unknown>;
|
||||||
|
const unsafeItems = unsafe.items as Array<Record<string, unknown>>;
|
||||||
|
unsafeItems[0].source_revision = Number.MAX_SAFE_INTEGER + 1;
|
||||||
|
assertThrows(
|
||||||
|
() => parseRepositoryListResponse(unsafe),
|
||||||
|
"must be a safe integer",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test("repository create/error responses use the generated strict contract", () => {
|
Deno.test("repository create/error responses use the generated strict contract", () => {
|
||||||
const created = parseCreateWorkspaceRepositoryResponse({
|
const created = parseCreateWorkspaceRepositoryResponse({
|
||||||
workspace_id: repositoryList.workspace_id,
|
workspace_id: repositoryList.workspace_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user