Http 204 response results with exception in rest resolution
[ccsdk/cds.git] / ms / blueprintsprocessor / modules / commons / rest-lib / src / test / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / rest / service / RestClientServiceTest.kt
1 /*
2  * Copyright © 2017-2018 AT&T Intellectual Property.
3  * Copyright (C) 2019 Nordix Foundation
4  * Modifications Copyright © 2019 Huawei.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18
19 package org.onap.ccsdk.cds.blueprintsprocessor.rest.service
20
21 import com.fasterxml.jackson.databind.JsonNode
22 import com.fasterxml.jackson.databind.ObjectMapper
23 import kotlinx.coroutines.CoroutineStart
24 import kotlinx.coroutines.async
25 import kotlinx.coroutines.runBlocking
26 import org.junit.After
27 import org.junit.Before
28 import org.junit.Test
29 import org.junit.runner.RunWith
30 import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintPropertiesService
31 import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintPropertyConfiguration
32 import org.onap.ccsdk.cds.blueprintsprocessor.rest.BluePrintRestLibConfiguration
33 import org.springframework.beans.factory.annotation.Autowired
34 import org.springframework.boot.autoconfigure.EnableAutoConfiguration
35 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
36 import org.springframework.boot.test.context.SpringBootTest
37 import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory
38 import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory
39 import org.springframework.boot.web.server.WebServer
40 import org.springframework.context.annotation.Bean
41 import org.springframework.http.HttpMethod
42 import org.springframework.http.HttpStatus
43 import org.springframework.http.ResponseEntity
44 import org.springframework.http.server.reactive.HttpHandler
45 import org.springframework.security.config.web.server.ServerHttpSecurity
46 import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
47 import org.springframework.security.core.userdetails.User
48 import org.springframework.security.core.userdetails.UserDetails
49 import org.springframework.security.web.server.SecurityWebFilterChain
50 import org.springframework.test.context.ContextConfiguration
51 import org.springframework.test.context.TestPropertySource
52 import org.springframework.test.context.junit4.SpringRunner
53 import org.springframework.web.bind.annotation.DeleteMapping
54 import org.springframework.web.bind.annotation.GetMapping
55 import org.springframework.web.bind.annotation.PatchMapping
56 import org.springframework.web.bind.annotation.PathVariable
57 import org.springframework.web.bind.annotation.PostMapping
58 import org.springframework.web.bind.annotation.PutMapping
59 import org.springframework.web.bind.annotation.RequestHeader
60 import org.springframework.web.bind.annotation.RequestMapping
61 import org.springframework.web.bind.annotation.RequestParam
62 import org.springframework.web.bind.annotation.RestController
63 import kotlin.test.assertEquals
64 import kotlin.test.assertNotNull
65
66 @RunWith(SpringRunner::class)
67 @EnableAutoConfiguration(exclude = [DataSourceAutoConfiguration::class])
68 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
69 @ContextConfiguration(
70     classes = [
71         BluePrintRestLibConfiguration::class, SampleController::class,
72         SecurityConfiguration::class,
73         BluePrintPropertyConfiguration::class, BluePrintPropertiesService::class
74     ]
75 )
76 @TestPropertySource(
77     properties =
78         [
79             "server.port=8443",
80             "server.ssl.enabled=true",
81             "server.ssl.key-store=classpath:keystore.p12",
82             "server.ssl.key-store-password=changeit",
83             "server.ssl.keyStoreType=PKCS12",
84             "server.ssl.keyAlias=tomcat",
85             "blueprintsprocessor.restclient.sample.type=basic-auth",
86             "blueprintsprocessor.restclient.sample.url=http://127.0.0.1:9081",
87             "blueprintsprocessor.restclient.sample.username=admin",
88             "blueprintsprocessor.restclient.sample.password=jans",
89             "blueprintsprocessor.restclient.test.type=ssl-basic-auth",
90             "blueprintsprocessor.restclient.test.url=https://localhost:8443",
91             "blueprintsprocessor.restclient.test.username=admin",
92             "blueprintsprocessor.restclient.test.password=jans",
93             "blueprintsprocessor.restclient.test.keyStoreInstance=PKCS12",
94             "blueprintsprocessor.restclient.test.sslTrust=src/test/resources/keystore.p12",
95             "blueprintsprocessor.restclient.test.sslTrustPassword=changeit"
96         ]
97 )
98 class RestClientServiceTest {
99
100     @Autowired
101     lateinit var bluePrintRestLibPropertyService: BluePrintRestLibPropertyService
102
103     @Autowired
104     lateinit var httpHandler: HttpHandler
105
106     lateinit var http: WebServer
107
108     fun localPort() = http.port
109
110     @Before
111     fun start() {
112         // Second Http server required for non-SSL requests to be processed along with the https server.
113         val factory: ReactiveWebServerFactory = NettyReactiveWebServerFactory(9081)
114         this.http = factory.getWebServer(this.httpHandler)
115         this.http.start()
116     }
117
118     @After
119     fun stop() {
120         this.http.stop()
121     }
122
123     @Test
124     fun testGetQueryParam() {
125         val restClientService = bluePrintRestLibPropertyService
126             .blueprintWebClientService("sample")
127         val response = restClientService.exchangeResource(
128             HttpMethod.GET.name, "/sample/query?id=3", ""
129         )
130         assertEquals(
131             "query with id:3", response.body,
132             "failed to get query param response"
133         )
134     }
135
136     @Test
137     fun testPatch() {
138         val restClientService = bluePrintRestLibPropertyService
139             .blueprintWebClientService("sample")
140         val response = restClientService.exchangeResource(
141             HttpMethod.PATCH.name, "/sample/name", ""
142         )
143         assertEquals(
144             "Patch request successful", response.body,
145             "failed to get patch response"
146         )
147     }
148
149     @Test
150     fun testBaseAuth() {
151         val restClientService = bluePrintRestLibPropertyService
152             .blueprintWebClientService("sample")
153         val headers = mutableMapOf<String, String>()
154         headers["X-Transaction-Id"] = "1234"
155         val response = restClientService.exchangeResource(
156             HttpMethod.GET.name,
157             "/sample/name", ""
158         )
159         assertNotNull(response.body, "failed to get response")
160     }
161
162     @Test
163     fun testSimpleBasicAuth() {
164         val json: String = "{\n" +
165             "  \"type\" : \"basic-auth\",\n" +
166             "  \"url\" : \"http://localhost:9081\",\n" +
167             "  \"username\" : \"admin\",\n" +
168             "  \"password\" : \"jans\"\n" +
169             "}"
170         val mapper = ObjectMapper()
171         val actualObj: JsonNode = mapper.readTree(json)
172         val restClientService = bluePrintRestLibPropertyService
173             .blueprintWebClientService(actualObj)
174         lateinit var res: String
175         runBlocking {
176             val get = async(start = CoroutineStart.LAZY) {
177                 restClientService.exchangeNB(
178                     HttpMethod.GET.name,
179                     "/sample/basic", ""
180                 ).body
181             }
182             get.start()
183             res = get.await()
184         }
185         assertNotNull(res, "failed to get response")
186         assertEquals(res, "Basic request arrived successfully")
187     }
188
189     @Test
190     fun testSampleAaiReq() {
191         val restClientService = bluePrintRestLibPropertyService
192             .blueprintWebClientService("test")
193         val headers = mutableMapOf<String, String>()
194         headers["X-TransactionId"] = "9999"
195         headers["X-FromAppId"] = "AAI"
196         val post1 = "{\n" +
197             "  \"customer\": {\n" +
198             "    \"global-customer-id\": \"ONSDEMOBJHKCustomer\",\n" +
199             "    \"subscriber-name\": \"ONSDEMOBJHKCustomer\",\n" +
200             "    \"subscriber-type\": \"CUST\",\n" +
201             "    \"resource-version\": \"1552985011163\"\n" +
202             "  }\n" +
203             "}"
204         lateinit var res1: Customer
205         lateinit var res2: Customer
206         lateinit var res3: String
207         lateinit var res4: String
208         lateinit var res5: String
209         lateinit var res6: String
210         runBlocking {
211             val get1 = async(start = CoroutineStart.LAZY) {
212                 restClientService.exchangeNB(
213                     HttpMethod.GET.name,
214                     "/sample/aai/v22/business/customers", "", headers,
215                     Customer::class.java
216                 ).body
217             }
218
219             val get2 = async(start = CoroutineStart.LAZY) {
220                 restClientService.exchangeNB(
221                     HttpMethod.GET.name,
222                     "/sample/aai/v22/business/customers", "", headers,
223                     Customer::class.java
224                 ).body
225             }
226
227             val post = async(start = CoroutineStart.LAZY) {
228                 restClientService.exchangeNB(
229                     HttpMethod.POST.name,
230                     "/sample/aai/v22/business/customers", post1, headers,
231                     String::class.java
232                 ).body
233             }
234
235             val put = async(start = CoroutineStart.LAZY) {
236                 restClientService.exchangeNB(
237                     HttpMethod.PUT.name,
238                     "/sample/aai/v22/business/customers", post1, headers,
239                     String::class.java
240                 ).body
241             }
242
243             val patch = async(start = CoroutineStart.LAZY) {
244                 restClientService.exchangeNB(
245                     HttpMethod.PATCH.name,
246                     "/sample/aai/v22/business/customers", post1, headers,
247                     String::class.java
248                 ).body
249             }
250
251             val delete = async(start = CoroutineStart.LAZY) {
252                 restClientService.exchangeNB(
253                     HttpMethod.DELETE.name,
254                     "/sample/aai/v22/business/customers", "", headers,
255                     String::class.java
256                 ).body
257             }
258
259             get1.start()
260             get2.start()
261             post.start()
262             put.start()
263             patch.start()
264             delete.start()
265             res1 = get1.await()
266             res2 = get2.await()
267             res3 = post.await()
268             res4 = put.await()
269             res5 = patch.await()
270             res6 = delete.await()
271         }
272         assertNotNull(res1, "failed to get response")
273         assertNotNull(res2, "failed to get response")
274         assertEquals(res1.id, "ONSDEMOBJHKCustomer")
275         assertEquals(res1.name, "ONSDEMOBJHKCustomer")
276         assertEquals(res1.type, "CUST")
277         assertEquals(res1.resource, "1552985011163")
278         assertEquals(res2.id, "ONSDEMOBJHKCustomer")
279         assertEquals(res2.name, "ONSDEMOBJHKCustomer")
280         assertEquals(res2.type, "CUST")
281         assertEquals(res2.resource, "1552985011163")
282         assertEquals(res3, "The message is successfully posted")
283         assertEquals(res4, "The put request is success")
284         assertEquals(res5, "The patch request is success")
285         assertEquals(res6, "The message is successfully deleted")
286     }
287     @Test
288     fun testDeleteWith204StatusAndResponseAsString() {
289         val restClientService = bluePrintRestLibPropertyService
290             .blueprintWebClientService("sample")
291         val headers = mutableMapOf<String, String>()
292         headers["X-Transaction-Id"] = "1234"
293         val response = restClientService.exchangeResource(
294             HttpMethod.DELETE.name,
295             "/sample/name", ""
296         )
297         assertEquals(response.status, 204)
298         assertEquals(response.body, "")
299     }
300
301     @Test
302     fun testDeleteWith204StatusAndResponseAsCustomerWithDefaultConstructor() {
303         val restClientService = bluePrintRestLibPropertyService
304             .blueprintWebClientService("sample")
305         val headers = mutableMapOf<String, String>()
306         headers["X-Transaction-Id"] = "1234"
307         runBlocking {
308             val response = restClientService.exchangeNB(
309                 HttpMethod.DELETE.name,
310                 "/sample/customersWithDefaultConstructor", "", headers, CustomerWithDefaultConstructor::class.java
311             )
312             assertEquals(response.status, 204)
313             assertEquals(response.body, CustomerWithDefaultConstructor())
314         }
315     }
316
317     // @Test
318     fun testDeleteWith204StatusAndResponseAsCustomerWithoutDefaultConstructor() {
319         val restClientService = bluePrintRestLibPropertyService
320             .blueprintWebClientService("sample")
321         val headers = mutableMapOf<String, String>()
322         headers["X-Transaction-Id"] = "1234"
323         runBlocking {
324             val response = restClientService.exchangeNB(
325                 HttpMethod.DELETE.name,
326                 "/sample/customersWithoutDefaultConstructor", "", headers, CustomerWithoutDefaultConstructor::class.java
327             )
328             assertEquals(response.status, 204)
329             assertEquals(response.body, CustomerWithoutDefaultConstructor(""))
330         }
331     }
332 }
333
334 /**
335  * Sample controller code for testing both http and https requests.
336  */
337 @RestController
338 @RequestMapping("/sample")
339 open class SampleController {
340
341     @GetMapping("/name")
342     fun getName(): String = "Sample Controller"
343     @DeleteMapping("/name")
344     fun deleteNameWith204StatusAndEmptyBody(): ResponseEntity<Unit> {
345         return ResponseEntity(HttpStatus.NO_CONTENT)
346     }
347
348     @GetMapping("/query")
349     fun getQuery(@RequestParam("id") id: String): String =
350         "query with id:$id"
351
352     @GetMapping("/path/{id}/get")
353     fun getPathParam(@PathVariable("id") id: String): String =
354         "path param id:$id"
355
356     @PatchMapping("/name")
357     fun patchName(): String = "Patch request successful"
358
359     @GetMapping("/basic")
360     fun getBasic(): String = "Basic request arrived successfully"
361
362     @GetMapping("/aai/v22/business/customers")
363     fun getAaiCustomers(
364         @RequestHeader(name = "X-TransactionId", required = true)
365         transId: String,
366         @RequestHeader(name = "X-FromAppId", required = true)
367         appId: String
368     ): String {
369         if (transId != "9999" || appId != "AAI") {
370             return ""
371         }
372         return "{\n" +
373             "  \"id\": \"ONSDEMOBJHKCustomer\",\n" +
374             "  \"name\": \"ONSDEMOBJHKCustomer\",\n" +
375             "  \"type\": \"CUST\",\n" +
376             "  \"resource\": \"1552985011163\"\n" +
377             "}"
378     }
379
380     @PostMapping("/aai/v22/business/customers")
381     fun postAaiCustomers(
382         @RequestHeader(name = "X-TransactionId", required = true)
383         transId: String,
384         @RequestHeader(name = "X-FromAppId", required = true)
385         appId: String
386     ): String {
387         if (transId != "9999" || appId != "AAI") {
388             return ""
389         }
390         return "The message is successfully posted"
391     }
392
393     @PutMapping("/aai/v22/business/customers")
394     fun putAaiCustomers(
395         @RequestHeader(name = "X-TransactionId", required = true)
396         transId: String,
397         @RequestHeader(name = "X-FromAppId", required = true)
398         appId: String
399     ): String {
400         if (transId != "9999" || appId != "AAI") {
401             return ""
402         }
403         return "The put request is success"
404     }
405
406     @PatchMapping("/aai/v22/business/customers")
407     fun patchAaiCustomers(
408         @RequestHeader(name = "X-TransactionId", required = true)
409         transId: String,
410         @RequestHeader(name = "X-FromAppId", required = true)
411         appId: String
412     ): String {
413         if (transId != "9999" || appId != "AAI") {
414             return ""
415         }
416         return "The patch request is success"
417     }
418
419     @DeleteMapping("/aai/v22/business/customers")
420     fun deleteAaiCustomers(
421         @RequestHeader(name = "X-TransactionId", required = true)
422         transId: String,
423         @RequestHeader(name = "X-FromAppId", required = true)
424         appId: String
425     ): String {
426         if (transId != "9999" || appId != "AAI") {
427             return ""
428         }
429         return "The message is successfully deleted"
430     }
431     @DeleteMapping("/customersWithDefaultConstructor")
432     fun deleteCustomersWith204StatusAndResponseAsCustomerWithDefaultConstructor():
433         ResponseEntity<CustomerWithDefaultConstructor> {
434             return ResponseEntity(CustomerWithDefaultConstructor(), HttpStatus.NO_CONTENT)
435         }
436
437     @DeleteMapping("/customersWithoutDefaultConstructor")
438     fun deleteCustomersWith204StatusAndResponseAsCustomerWithoutDefaultConstructor():
439         ResponseEntity<CustomerWithoutDefaultConstructor> {
440             return ResponseEntity(CustomerWithoutDefaultConstructor(""), HttpStatus.NO_CONTENT)
441         }
442 }
443
444 /**
445  * Security configuration required for basic authentication with username and
446  * password for any request in the server.
447  */
448 open class SecurityConfiguration {
449
450     @Bean
451     open fun userDetailsService(): MapReactiveUserDetailsService {
452         val user: UserDetails = User.withDefaultPasswordEncoder()
453             .username("admin")
454             .password("jans")
455             .roles("USER")
456             .build()
457         return MapReactiveUserDetailsService(user)
458     }
459
460     @Bean
461     open fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
462         return http
463             .csrf().disable()
464             .authorizeExchange().anyExchange().authenticated()
465             .and().httpBasic()
466             .and().build()
467     }
468 }
469
470 /**
471  * Data class required for response
472  */
473 data class Customer(
474     val id: String,
475     val name: String,
476     val type: String,
477     val resource: String
478 )
479
480 data class CustomerWithoutDefaultConstructor(
481     val name: String
482 )
483
484 data class CustomerWithDefaultConstructor(
485     val name: String
486 ) {
487     constructor() : this("")
488 }