Skip to content

Commit

Permalink
Add tests with missing fields
Browse files Browse the repository at this point in the history
  • Loading branch information
kazk committed Dec 21, 2020
1 parent fc2776b commit 01e3a3c
Showing 1 changed file with 137 additions and 81 deletions.
218 changes: 137 additions & 81 deletions examples/crd_derive_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,10 @@ use serde::{Deserialize, Serialize};
)]
#[kube(apiextensions = "v1")]
pub struct FooSpec {
// Required field
// Non-nullable without default is required.
//
// There shouldn't be any ambiguity here.
required_field: String,

// Nullable without default.
//
// By skipping to serialize, the field won't be present in the object.
// If serialized as `null`, the object will have the field set to `null`.
//
// Deserializing works as expected either way. `None` if it's missing or `null`.
#[serde(skip_serializing_if = "Option::is_none")]
nullable: Option<String>,
non_nullable: String,

// Non-nullable with default value.
//
Expand All @@ -45,9 +36,20 @@ pub struct FooSpec {
// The server prunes `null` for non-nullable field since 1.20 and the default is applied.
// To match the server's behavior exactly, we can use a custom deserializer.
#[serde(default = "default_value")]
with_default: String,
non_nullable_with_default: String,

// Nullable without default, skipping None.
//
// By skipping to serialize, the field won't be present in the object.
// If serialized as `null` (next field), the object will have the field set to `null`.
//
// Deserializing works as expected either way. `None` if it's missing or `null`.
#[serde(skip_serializing_if = "Option::is_none")]
nullable_skipped: Option<String>,
// Nullable without default, not skipping None.
nullable: Option<String>,

// Nullable with default value.
// Nullable with default, skipping None.
//
// By skipping to serialize when `None`, the server will set the the default value specified in the schema.
// If serialized as `null`, the server will conserve it and the defaulting does not happen (since 1.20).
Expand All @@ -56,14 +58,14 @@ pub struct FooSpec {
// This is consistent with how the server handles it since 1.20.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default = "default_nullable")]
nullable_with_default: Option<String>,
nullable_skipped_with_default: Option<String>,

// Nullable with default without skipping.
// Nullable with default, not skipping None.
//
// The default value won't be used unless missing, so this will set the value to `null`.
// If the resource is created with `kubectl` and if this field was missing, defaulting will happen.
#[serde(default = "default_nullable")]
nullable_with_default_no_skip: Option<String>,
nullable_with_default: Option<String>,
}

fn default_value() -> String {
Expand All @@ -82,10 +84,11 @@ fn main() {
#[cfg(test)]
mod tests {
use super::*;
use anyhow::{anyhow, Result};
use futures::{StreamExt, TryStreamExt};
use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition;
use kube::{
api::{Api, DeleteParams, ListParams, PostParams, WatchEvent},
api::{Api, DeleteParams, ListParams, PostParams, Resource, WatchEvent},
Client,
};

Expand Down Expand Up @@ -119,30 +122,35 @@ mod tests {
"properties": {
"spec": {
"properties": {
"nullable": {
"nullable": true,
"type": "string"
},
"nullable_with_default": {
"default": "default_nullable",
"nullable": true,
"type": "string"
},
"nullable_with_default_no_skip": {
"default": "default_nullable",
"nullable": true,
"type": "string"
},
"required_field": {
"type": "string"
},
"with_default": {
"default": "default_value",
"type": "string"
},
"non_nullable": {
"type": "string"
},
"non_nullable_with_default": {
"default": "default_value",
"type": "string"
},

"nullable_skipped": {
"nullable": true,
"type": "string"
},
"nullable": {
"nullable": true,
"type": "string"
},
"nullable_skipped_with_default": {
"default": "default_nullable",
"nullable": true,
"type": "string"
},
"nullable_with_default": {
"default": "default_nullable",
"nullable": true,
"type": "string"
},
},
"required": [
"required_field"
"non_nullable"
],
"type": "object"
}
Expand All @@ -160,8 +168,6 @@ mod tests {
}
});
let expected = serde_json::from_value(output).unwrap();
// println!("{}", serde_yaml::to_string(&crd).unwrap());
// println!("{}", serde_yaml::to_string(&expected).unwrap());
assert_eq!(crd, expected);
}

Expand All @@ -176,50 +182,106 @@ mod tests {
// Test creating Foo resource.
let namespace = std::env::var("NAMESPACE").unwrap_or("default".into());
let foos = Api::<Foo>::namespaced(client.clone(), &namespace);
let bar = Foo::new(
"bar",
// Create with defaults using typed Api first.
// `non_nullable` and `non_nullable_with_default` are set to empty strings.
// Nullables defaults to `None` and only sent if it's not configured to skip.
let bar = Foo::new("bar", FooSpec { ..FooSpec::default() });
let bar = foos.create(&PostParams::default(), &bar).await.unwrap();
assert_eq!(
bar.spec,
FooSpec {
required_field: "required".into(),
with_default: "".into(),
// Nonnullable without default is required.
non_nullable: String::default(),
// Defaulting didn't happen because an empty string was sent.
non_nullable_with_default: String::default(),
// `nullable_skipped` field does not exist in the object (see below).
nullable_skipped: None,
// `nullable` field exists in the object (see below).
nullable: None,
// Defaulting happened because serialization was skipped.
nullable_skipped_with_default: default_nullable(),
// Defaulting did not happen because `null` was sent.
// Deserialization does not apply the default either.
nullable_with_default: None,
nullable_with_default_no_skip: None,
},
}
);
let bar = foos.create(&PostParams::default(), &bar).await.unwrap();
// Defaulting happens if None is skipped.
assert_eq!(bar.spec.nullable_with_default, Some("default_nullable".into()));
// If `null` is sent, defaulting does not happen.
// Deserialization does not apply the default either.
assert_eq!(bar.spec.nullable_with_default_no_skip, None);
// `nullable` field should not exist on the object, but it's set to `None` when deserializing.
// TODO Any convenient way to check the raw response?
assert_eq!(bar.spec.nullable, None);

// TODO Any convenient way to send invalid/raw data?
// let baz = foos
// .create(
// &PostParams::default(),
// serde_json::json!({
// "apiVersion": "foos.clux.dev/v1",
// "kind": "Foo",
// "spec": {}
// }),
// )
// .await;
// // Missing a required field is an error.
// assert!(baz.is_err());
// Set up dynamic resource to test using raw values.
let resource = Resource::dynamic("Foo")
.group("clux.dev")
.version("v1")
.within(&namespace)
.into_resource();

// Test that skipped nullable field without default is not defined.
let val = client
.request::<serde_json::Value>(resource.get("bar").unwrap())
.await
.unwrap();
println!("{:?}", val["spec"]);
// `nullable_skipped` field does not exist, but `nullable` does.
let spec = val["spec"].as_object().unwrap();
assert!(!spec.contains_key("nullable_skipped"));
assert!(spec.contains_key("nullable"));

// Test defaulting of `non_nullable_with_default` field
let data = serde_json::to_vec(&serde_json::json!({
"apiVersion": "clux.dev/v1",
"kind": "Foo",
"metadata": {
"name": "baz"
},
"spec": {
"non_nullable": "a required field",
// `non_nullable_with_default` field is missing
}
}))
.unwrap();
let val = client
.request::<serde_json::Value>(resource.create(&PostParams::default(), data).unwrap())
.await
.unwrap();
println!("{:?}", val["spec"]);
// Defaulting happened for non-nullable field
assert_eq!(val["spec"]["non_nullable_with_default"], default_value());

// Missing required field (non-nullable without default) is an error
let data = serde_json::to_vec(&serde_json::json!({
"apiVersion": "clux.dev/v1",
"kind": "Foo",
"metadata": {
"name": "qux"
},
"spec": {}
}))
.unwrap();
let res = client
.request::<serde_json::Value>(resource.create(&PostParams::default(), data).unwrap())
.await;
assert!(res.is_err());
match res.err() {
Some(kube::Error::Api(err)) => {
assert_eq!(err.code, 422);
assert_eq!(err.reason, "Invalid");
assert_eq!(err.status, "Failure");
assert_eq!(
err.message,
"Foo.clux.dev \"qux\" is invalid: spec.non_nullable: Required value"
);
}
_ => assert!(false),
}

delete_crd(client.clone()).await.unwrap();
}

// Create CRD and wait for it to be ready.
async fn create_crd(client: Client) -> anyhow::Result<CustomResourceDefinition> {
async fn create_crd(client: Client) -> Result<CustomResourceDefinition> {
let api = Api::<CustomResourceDefinition>::all(client);
api.create(&PostParams::default(), &Foo::crd()).await?;

// Wait until ready
let timeout_secs = 5;
let timeout_secs = 15;
let lp = ListParams::default()
.fields("metadata.name=foos.clux.dev")
.timeout(timeout_secs);
Expand All @@ -241,20 +303,17 @@ mod tests {
}
}

Err(anyhow::anyhow!(format!(
"CR not ready after {} seconds",
timeout_secs
)))
Err(anyhow!(format!("CRD not ready after {} seconds", timeout_secs)))
}

// Delete the CRD if it exists and wait until it's deleted.
async fn delete_crd(client: Client) -> anyhow::Result<()> {
async fn delete_crd(client: Client) -> Result<()> {
let api = Api::<CustomResourceDefinition>::all(client);
if api.get("foos.clux.dev").await.is_ok() {
api.delete("foos.clux.dev", &DeleteParams::default()).await?;

// Wait until deleted
let timeout_secs = 5;
let timeout_secs = 15;
let lp = ListParams::default()
.fields("metadata.name=foos.clux.dev")
.timeout(timeout_secs);
Expand All @@ -264,10 +323,7 @@ mod tests {
return Ok(());
}
}
Err(anyhow::anyhow!(format!(
"CRD not deleted after {} seconds",
timeout_secs
)))
Err(anyhow!(format!("CRD not deleted after {} seconds", timeout_secs)))
} else {
Ok(())
}
Expand Down

0 comments on commit 01e3a3c

Please sign in to comment.