how to test equality of data class with OffsetDateTime attribute? - spring

I have an attribute in a DTO and Entity defined like this:
val startDate: OffsetDateTime,
The dto has a toEntity method:
data class SomeDTO(
val id: Long? = null,
val startDate: OffsetDateTime,
) {
fun toEntity(): SomeEntity {
return SomeEntity(
id = id,
startDate = startDate,
)
}
}
And a controller
#RestController
#RequestMapping("/some/api")
class SomeController(
private val someService: SomeService,
) {
#PostMapping("/new")
#ResponseStatus(HttpStatus.CREATED)
suspend fun create(#RequestBody dto: SomeDTO): SomeEntity {
return someService.save(dto.toEntity())
}
}
And I have a failing test:
#Test
fun `create Ok`() {
val expectedId = 123L
val zoneId = ZoneId.of("Europe/Berlin")
val dto = SomeDTO(
id = null,
startDate = LocalDate.of(2021, 4, 23)
.atStartOfDay(zoneId).toOffsetDateTime(),
)
val expectedToStore = dto.toEntity()
val stored = expectedToStore.copy(id = expectedId)
coEvery { someService.save(any()) } returns stored
client
.post()
.uri("/some/api/new")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(dto)
.exchange()
.expectStatus().isCreated
.expectBody()
.jsonPath("$.id").isEqualTo(expectedId)
coVerify {
someService.save(expectedToStore)
}
}
The test fails for the coVerify because the startDate does not match:
Verification failed: ...
... arguments are not matching:
[0]: argument: SomeEntity(id=null, startDate=2021-04-22T22:00Z),
matcher: eq(SomeEntity(id=null, startDate=2021-04-23T00:00+02:00)),
result: -
Semantically, the startDates match, but the timezone is different. I wonder how I can enforce coVerify to either use a proper semantic comparison for type OffsetDateTime or how I can enforce the internal format of OffsetDateTime=? Or what other approach should we use to verify the expectedToStore value is passed to someService.save(...) ?
I could use withArgs but it is cumbersome:
coVerify {
someService.save(withArg {
assertThat(it.startDate).isEqualTo(expectedToStore.startDate)
// other manual asserts
})
}

tl;dr
Add this to your application.properties:
spring.jackson.deserialization.adjust-dates-to-context-time-zone=false
This way, the offset will be deserialized as retrieved and not altered.
I created a (slightly modified) reproduction repository on GitHub. Inside the Controller the value of dto.startDate is already 2021-04-22T22:00Z, thus at UTC.
By default, the serialization library used "Jackson" aligns all offsets during deserialization to the same configured offset.
The default offset used is +00:00 or Z, which resembles UTC.
You can enable / disable this behaviour over the property spring.jackson.deserialization.adjust-dates-to-context-time-zone={true false} and set the timezone with spring.jackson.time-zone=<timezone>
Alternatively, you can force to align the offset with an other timezone during deserialization:
spring.jackson.time-zone=Europe/Berlin
This way, the offset will be the aligned with the timezone Europe/Berlin.

Related

The properties of the file for records generated by jooq are always set to Optional

I have created the User table and generateJooq generates UserRecord.kt, but always the properties of the UserRecord class are optional.
I would like to know how to avoid sharing optional properties.
By the way, the schema of the User table is Not Null.
DB schema
CREATE TABLE user
(
id varchar(256) UNIQUE NOT NULL,
email varchar(256) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
build.gradle.kts
:
jooq {
configurations {
create("main") {
jooqConfiguration.apply {
jdbc.apply {
url = System.getenv("DB_URL")
user = System.getenv("DB_USER")
password = System.getenv("DB_PASS")
}
generator.apply {
name = "org.jooq.codegen.KotlinGenerator"
database.apply {
name = "org.jooq.meta.mysql.MySQLDatabase"
inputSchema = System.getenv("DB_NAME")
excludes = "flyway_schema_history"
}
generate.apply {
isDeprecated = false
isTables = true
isComments = true
}
target.apply {
packageName = "com.example.infra.jooq"
directory = "${buildDir}/generated/source/jooq/main"
}
}
}
}
}
}
Generated UserRecord.kt
:
/**
* This class is generated by jOOQ.
*/
#Suppress("UNCHECKED_CAST")
open class UserRecord() : UpdatableRecordImpl<UserRecord>(User.USER), Record2<String?, String?> {
var id: String?
set(value): Unit = set(0, value)
get(): String? = get(0) as String?
var email: String?
set(value): Unit = set(1, value)
get(): String? = get(1) as String?
:
}
Technologies
JOOQ: 3.16.4
Gradle
Spring Boot: 3.0.1
This feature will ship with jOOQ 3.18, see:
https://github.com/jOOQ/jOOQ/issues/10212
You can configure:
<kotlinNotNullPojoAttributes>true</kotlinNotNullPojoAttributes>
<kotlinNotNullRecordAttributes>true</kotlinNotNullRecordAttributes>
<kotlinNotNullInterfaceAttributes>true</kotlinNotNullInterfaceAttributes>
Of course, beware that as documented in the above issue, and throughout jOOQ's github issues, Stack Overflow, etc., the guarantee of non-nullability in this case will be a "lie." When jOOQ creates a new UserRecord for you, then those valuse will still be null:
// Contains nulls!
val user: UserRecord = ctx.newRecord(USER)

Jackson + KotlinModule: Conflicting/ambiguous property name definitions (implicit name 'isFoo')

Stumbling through a strange behaviour in Jackson when used with KotlinModule. Trying to deserialize a JSON object with isXxx-Boolean and xxx-none-Boolean property. Any solution how to deal with this?
data class FooObject(
#JsonProperty("isFoo")
val isFoo: Boolean,
#JsonProperty("foo")
val foo: String,
)
#Test
fun `deserialization should work` (){
val serialized = """
{
"isFoo": true,
"foo": "bar"
}
""".trimIndent()
val objectMapper: ObjectMapper = Jackson2ObjectMapperBuilder()
.modules(KotlinModule())
.build()
val deserialized = objectMapper.readValue(serialized, FooObject::class.java)
assertNotNull(deserialized)
}
throws
Results in
java.lang.IllegalStateException: Conflicting/ambiguous property name definitions (implicit name 'isFoo'): found multiple explicit names: [isFoo, foo], but also implicit accessor: [method org.dnltsk.Test$FooObject#getFoo()][visible=true,ignore=false,explicitName=false], [method org.dnltsk.Test$FooObject#isFoo()][visible=true,ignore=false,explicitName=false]
By removing the #JsonProperty-annotations the exception turns to
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Duplicate creator property "isFoo" (index 0 vs 1) for type `org.dnltsk.Test$FooObject`
at [Source: (String)"{
"isFoo": true,
"foo": "bar"
}"; line: 1, column: 1]
Add the following annotation to the top of your data class:
#JsonAutoDetect(
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
)

NestJS GraphQL custom argument type

I'm trying to use LocalDate type from js-joda as parameter on GraphQL query like this:
#Query(() => DataResponse)
async getData(#Args() filter: DataFilter): Promise<DataResponse> { ... }
And here is filter type definition:
#ArgsType()
export class DataFilter {
#Field({ nullable: true })
#IsOptional()
date?: LocalDate;
#Field()
#Min(1)
page: number;
#Field()
#Min(1)
pageSize: number;
}
I've also registered LocalDate as scalar type and added it to application providers.
#Scalar('LocalDate', (type) => LocalDate)
export class LocalDateScalar implements CustomScalar<string, LocalDate> {
description = 'A date string, such as 2018-07-01, serialized in ISO8601 format';
parseValue(value: string): LocalDate {
return LocalDate.parse(value);
}
serialize(value: LocalDate): string {
return value.toString();
}
parseLiteral(ast: ValueNode): LocalDate {
if (ast.kind === Kind.STRING) {
return LocalDate.parse(ast.value);
}
return null;
}
}
This is the error I'm getting
[Nest] 9973 - 02/16/2022, 5:33:41 PM ERROR [ExceptionsHandler] year
must not be null NullPointerException: year must not be null
at requireNonNull (/Users/usr/my-app/node_modules/#js-joda/core/src/assert.js:33:15)
at new LocalDate (/Users/usr/my-app/node_modules/#js-joda/core/src/LocalDate.js:284:9)
at TransformOperationExecutor.transform (/Users/usr/my-app/node_modules/src/TransformOperationExecutor.ts:160:22)
at TransformOperationExecutor.transform (/Users/usr/my-app/node_modules/src/TransformOperationExecutor.ts:333:33)
at ClassTransformer.plainToInstance (/Users/usr/my-app/node_modules/src/ClassTransformer.ts:77:21)
at Object.plainToClass (/Users/usr/my-app/node_modules/src/index.ts:71:27)
at ValidationPipe.transform (/Users/usr/my-app/node_modules/#nestjs/common/pipes/validation.pipe.js:51:39)
at /Users/usr/my-app/node_modules/#nestjs/core/pipes/pipes-consumer.js:17:33
at processTicksAndRejections (node:internal/process/task_queues:96:5)
I'm not sure why is this exactly happening but from what I've managed to debug, is that LocalDateScalar defined above is transforming the value from string to LocalDate correctly, but the problem is that class-transformer is also trying to transform the value, and since it's already transformed it recognizes it as object, which is automatically being call through parameterless constructor and it's causing this error.
This is the line from class-transformer that's calling the constructor
newValue = new (targetType as any)();
Is there maybe a way to tell class-transformers which types to ignore? I'm aware of the #Exclude attribute, but then property is completely excluded, I just need to exclude property being transformed via plainToClass method of class-transformer. Or this whole situation should be handled differently?
Any suggestion will be well appreciated.
Not sure if this is the right solution but I had a similar scalar <string, Big> working with the following decorators:
#Field(() => AmountScalar) // your actual scalar class
#Type(() => String) // the "serialized" type of the scalar
#Transform(({ value }) => {
return Big(value) // custom parse function
})
amount: Big // the "parsed" type of the scalar
The two custom parse functions in the scalar can also contain some validation steps (like moment.isValid() in your case) since it will be called before class-validator.

passing json in json to spring controller

I am trying to pass json object to spring controller and I manage to do that, but value of one property is in json and I think that I have problem because of it. But there is no other way to pass that data. Code is below,
data class:
#Entity
data class Section(
#Id
#GeneratedValue
val id: Long = 0L,
val name: String = "",
var text: String,
#ManyToOne
var notebook: Notebook
)
Controller code:
#PutMapping("/sections/{id}")
fun updateSection(#RequestBody section: Section, #PathVariable id: Long): Section =
sectionRepository.findById(id).map {
it.text = section.text
it.notebook = section.notebook
sectionRepository.save(it)
}.orElseThrow { SectionNotFoundException(id) }
javascript sending post to api:
function updateApi(data) {
axios.put(MAIN_URL + 'sections/' + data.id, {
data
})
.then(showChangesSaved())
.catch(ShowErrorSync());
}
function saveSection() {
var data = JSON.parse(window.sessionStorage.getItem("curr-section"));
data.text = JSON.stringify(element.editor).toString();
updateApi(data);
}
I get error like this:
2020-11-18 15:06:24.052 WARN 16172 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Instantiation of [simple type, class org.dn.model.Section] value failed for JSON property text due to missing (therefore NULL) value for creator parameter text which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class org.dn.model.Section] value failed for JSON property text due to missing (therefore NULL) value for creator parameter text which is a non-nullable type
at [Source: (PushbackInputStream); line: 1, column: 375] (through reference chain: org.dn.model.Section["text"])]
so text in element.editor is JSON formatted string and I need to pass it as it is to controller. Is there any way to do that? I tried searching, but I can't find json in json help...
Whole project is available on github
What does your json looks like? If I check out your project and run the following two tests:
one with Section as an object as request body
one with Section as json
Both will succeed. So the problem might lie in your JSON:
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class HttpRequestTest {
#LocalServerPort
private val port = 0
#Autowired
private val restTemplate: TestRestTemplate? = null
#Test
fun sectionAsObject() {
val section = Section(0L, "2L", "text", Notebook(1L, "1", "2"))
assertThat(restTemplate!!.put("http://localhost:$port/sections/123", section
)).isNotNull
}
#Test
fun sectionAsJson() {
val sectionAsJson = """
{
"id": 0,
"name": "aName",
"text": "aText",
"noteBook": {
"id": 0,
"name": "aName",
"desc": "2"
}
}
""".trimIndent()
assertThat(restTemplate!!.put("http://localhost:$port/sections/123", sectionAsJson
)).isNotNull
}
}
BTW: it is not a pretty good habit to expose your database ids, which is considered to be a security risk as it exposes your database layer. Instead, you might want to use a functional unique key ;)

Spring Boot + Mongo - com.mongodb.BasicDocument, you can't add a second 'id' criteria

Any ideas why I get this error when making a query:
org.springframework.data.mongodb.InvalidMongoDbApiUsageException: Due to limitations of the com.mongodb.BasicDocument, you can't add a second 'id' criteria. Query already contains '{ "id" : "123"}'
I'm using Spring Boot and Mongo:
fun subGenreNames(subGenreIds: List<String>?): List<String> {
val results = mutableListOf<String>()
var query = Query()
subGenreIds!!.forEach{
query.addCriteria(Criteria.where("id").`is`(it))
var subGenreName = mongoTemplate.findById(it, SubGenre::class.java)
results.add(subGenreName!!.name)
}
return results
}
I have the class SubGenre set with:
#Document(collection = "subgenres")
data class SubGenre(
#Field("id")
val id: String,
val name: String
)
Thanks
Based on your code, you need to use either
query.addCriteria(Criteria.where("id").`is`(it))
var subGenreName = mongoTemplate.find(query, SubGenre::class.java)
or
var subGenreName = mongoTemplate.findById(it, SubGenre::class.java)
but not both.

Resources