Merge "Nodes query rewritten to generic rest client"
[vid.git] / vid-app-common / src / test / java / org / onap / vid / aai / AaiClientTest.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * VID
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * Modifications Copyright (C) 2018 Nokia. All rights reserved.
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *      http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  */
21
22 package org.onap.vid.aai;
23
24 import com.fasterxml.jackson.databind.ObjectMapper;
25 import com.google.common.collect.ImmutableList;
26 import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
27 import org.apache.commons.lang3.builder.ToStringStyle;
28 import org.apache.commons.lang3.exception.ExceptionUtils;
29 import org.apache.commons.lang3.reflect.FieldUtils;
30 import org.apache.commons.lang3.tuple.Pair;
31 import org.mockito.Mockito;
32 import org.onap.portalsdk.core.logging.logic.EELFLoggerDelegate;
33 import org.onap.portalsdk.core.util.SystemProperties;
34 import org.onap.vid.aai.model.AaiGetTenatns.GetTenantsResponse;
35 import org.onap.vid.aai.model.AaiNodeQueryResponse;
36 import org.onap.vid.aai.model.PortDetailsTranslator;
37 import org.onap.vid.aai.model.ResourceType;
38 import org.onap.vid.aai.util.AAIRestInterface;
39 import org.onap.vid.aai.util.HttpsAuthClient;
40 import org.onap.vid.aai.util.ServletRequestHelper;
41 import org.onap.vid.aai.util.SystemPropertyHelper;
42 import org.onap.vid.controllers.LocalWebConfig;
43 import org.onap.vid.exceptions.GenericUncheckedException;
44 import org.onap.vid.model.Subscriber;
45 import org.onap.vid.model.SubscriberList;
46 import org.onap.vid.model.probes.ExternalComponentStatus;
47 import org.onap.vid.model.probes.HttpRequestMetadata;
48 import org.onap.vid.model.probes.StatusMetadata;
49 import org.onap.vid.testUtils.TestUtils;
50 import org.springframework.http.HttpMethod;
51 import org.springframework.test.context.ContextConfiguration;
52 import org.springframework.test.context.web.WebAppConfiguration;
53 import org.testng.Assert;
54 import org.testng.annotations.BeforeMethod;
55 import org.testng.annotations.DataProvider;
56 import org.testng.annotations.Test;
57 import sun.security.provider.certpath.SunCertPathBuilderException;
58 import sun.security.validator.ValidatorException;
59
60 import javax.crypto.BadPaddingException;
61 import javax.net.ssl.SSLHandshakeException;
62 import javax.servlet.ServletContext;
63 import javax.ws.rs.ProcessingException;
64 import javax.ws.rs.client.Client;
65 import javax.ws.rs.core.Response;
66 import java.io.FileNotFoundException;
67 import java.io.IOException;
68 import java.security.cert.CertificateException;
69 import java.util.ArrayList;
70 import java.util.function.BiConsumer;
71 import java.util.function.Function;
72 import java.util.stream.Collectors;
73 import java.util.stream.Stream;
74
75 import static org.hamcrest.CoreMatchers.*;
76 import static org.hamcrest.MatcherAssert.assertThat;
77 import static org.hamcrest.Matchers.equalToIgnoringCase;
78 import static org.mockito.Matchers.any;
79 import static org.mockito.Matchers.*;
80 import static org.mockito.Mockito.doReturn;
81 import static org.mockito.Mockito.mock;
82 import static org.mockito.Mockito.verify;
83 import static org.mockito.Mockito.when;
84 import static org.testng.Assert.*;
85
86 @ContextConfiguration(classes = {LocalWebConfig.class, SystemProperties.class})
87 @WebAppConfiguration
88 public class AaiClientTest {
89
90     private AaiClient aaiClientMock;
91     private ServletContext servletContext;
92
93     @BeforeMethod
94     public void initMocks() {
95         aaiClientMock = mock(AaiClient.class);
96         aaiClientMock.logger = mock(EELFLoggerDelegate.class);
97         servletContext = mock(ServletContext.class);
98
99         when(servletContext.getRealPath(any(String.class))).thenReturn("");
100
101         when(aaiClientMock.doAaiGet(any(String.class), any(Boolean.class))).thenReturn(null);
102     }
103
104     @DataProvider
105     public static Object[][] logicalLinkData() {
106         return new Object[][]{
107                 {"", "network/logical-links/logical-link/"},
108                 {"link", "network/logical-links/logical-link/link"}
109         };
110     }
111
112     @Test(dataProvider = "logicalLinkData")
113     public void getLogicalLink_Link_Is_Empty(String link, String expectedUrl) {
114
115         when(aaiClientMock.getLogicalLink(any(String.class))).thenCallRealMethod();
116         aaiClientMock.getLogicalLink(link);
117         Mockito.verify(aaiClientMock).doAaiGet(argThat(equalToIgnoringCase(expectedUrl)), any(Boolean.class));
118     }
119
120     @DataProvider
121     public static Object[][] subscribersResults() {
122         return new Object[][]{
123                 {new SubscriberList(new ArrayList<Subscriber>() {{
124                     add(new Subscriber());
125                     add(new Subscriber());
126                 }}), true},
127                 {new SubscriberList(new ArrayList<Subscriber>() {{
128                     add(new Subscriber());
129                 }}), true},
130                 {new SubscriberList(new ArrayList<Subscriber>()), false}
131         };
132     }
133
134     @Test(dataProvider = "subscribersResults")
135     public void testProbeAaiGetAllSubscribers_returnsTwoToZeroSubscribers_ResultsAsExpected(SubscriberList subscribers, boolean isAvailable) {
136         ExternalComponentStatus expectedStatus = new ExternalComponentStatus(ExternalComponentStatus.Component.AAI, isAvailable, new HttpRequestMetadata(
137                 HttpMethod.GET,
138                 200,
139                 "url",
140                 "rawData",
141                 isAvailable ? "OK" : "No subscriber received",
142                 0
143         ));
144         Mockito.when(aaiClientMock.getAllSubscribers(true)).thenReturn(
145                 new AaiResponseWithRequestInfo<>(
146                         HttpMethod.GET, "url", new AaiResponse<>(subscribers, null, 200),
147                         "rawData"));
148         Mockito.when(aaiClientMock.probeAaiGetAllSubscribers()).thenCallRealMethod();
149         ExternalComponentStatus result = aaiClientMock.probeAaiGetAllSubscribers();
150         assertThat(statusDataReflected(result), is(statusDataReflected(expectedStatus)));
151         assertThat(requestMetadataReflected(result.getMetadata()), is(requestMetadataReflected(expectedStatus.getMetadata())));
152     }
153
154     //serialize fields except of fields we cannot know ahead of time
155     private static String requestMetadataReflected(StatusMetadata metadata) {
156         return new ReflectionToStringBuilder(metadata, ToStringStyle.SHORT_PREFIX_STYLE)
157                 .setExcludeFieldNames("duration")
158                 .toString();
159     }
160
161     private static String statusDataReflected(ExternalComponentStatus status) {
162         return new ReflectionToStringBuilder(status, ToStringStyle.SHORT_PREFIX_STYLE)
163                 .setExcludeFieldNames("metadata")
164                 .toString();
165     }
166
167     @DataProvider
168     public static Object[][] rawData() {
169         return new Object[][]{
170                 {"errorMessage",}, {""}, {null}
171         };
172     }
173
174     @Test(dataProvider = "rawData")
175     public void testProbeAaiGetFullSubscribersWithNullResponse_returnsNotAvailableWithErrorRawData(String rawData) {
176         Mockito.when(aaiClientMock.getAllSubscribers(true)).thenReturn(
177                 new AaiResponseWithRequestInfo<>(HttpMethod.GET, "url", null,
178                         rawData));
179         ExternalComponentStatus result = callProbeAaiGetAllSubscribersAndAssertNotAvailable();
180         assertThat(result.getMetadata(), instanceOf(HttpRequestMetadata.class));
181         assertEquals(((HttpRequestMetadata) result.getMetadata()).getRawData(), rawData);
182     }
183
184     @DataProvider
185     public static Object[][] exceptions() {
186         return new Object[][]{
187                 {"NullPointerException", "errorMessage",
188                         new ExceptionWithRequestInfo(HttpMethod.GET, "url",
189                                 "errorMessage", null, new NullPointerException())},
190                 {"RuntimeException", null,
191                         new ExceptionWithRequestInfo(HttpMethod.GET, "url",
192                                 null, null, new RuntimeException())},
193                 {"RuntimeException", null,
194                         new RuntimeException()},
195         };
196     }
197
198     @Test(dataProvider = "exceptions")
199     public void testProbeAaiGetFullSubscribersWithNullResponse_returnsNotAvailableWithErrorRawData(String description, String expectedRawData, Exception exception) {
200         Mockito.when(aaiClientMock.getAllSubscribers(true)).thenThrow(exception);
201         ExternalComponentStatus result = callProbeAaiGetAllSubscribersAndAssertNotAvailable();
202         if (exception instanceof ExceptionWithRequestInfo) {
203             assertThat(result.getMetadata(), instanceOf(HttpRequestMetadata.class));
204             assertEquals(((HttpRequestMetadata) result.getMetadata()).getRawData(), expectedRawData);
205         }
206         assertThat(result.getMetadata().getDescription(), containsString(description));
207     }
208
209     private ExternalComponentStatus callProbeAaiGetAllSubscribersAndAssertNotAvailable() {
210         Mockito.when(aaiClientMock.probeAaiGetAllSubscribers()).thenCallRealMethod();
211         ExternalComponentStatus result = aaiClientMock.probeAaiGetAllSubscribers();
212         assertFalse(result.isAvailable());
213         return result;
214     }
215
216
217     @Test
218     public void getTenants_Arguments_Are_Null_Or_Empty() {
219
220         when(aaiClientMock.getTenants(any(String.class), any(String.class))).thenCallRealMethod();
221
222         AaiResponse response = aaiClientMock.getTenants("", "");
223
224         assertEquals(response.getErrorMessage(), "{\"statusText\":\" Failed to retrieve LCP Region & Tenants from A&AI, Subscriber ID or Service Type is missing.\"}");
225
226
227         response = aaiClientMock.getTenants(null, null);
228
229         assertEquals(response.getErrorMessage(), "{\"statusText\":\" Failed to retrieve LCP Region & Tenants from A&AI, Subscriber ID or Service Type is missing.\"}");
230     }
231
232     @Test
233     public void getTenants_Arguments_Are_Valid_But_Tenants_Not_Exist() {
234
235         when(aaiClientMock.getTenants(any(String.class), any(String.class))).thenCallRealMethod();
236
237         Response generalEmptyResponse = mock(Response.class);
238         when(aaiClientMock.doAaiGet(any(String.class), any(Boolean.class))).thenReturn(generalEmptyResponse);
239
240         AaiResponse response = aaiClientMock.getTenants("subscriberId", "serviceType");
241
242         assertEquals(response.getErrorMessage(), "{\"statusText\":\" A&AI has no LCP Region & Tenants associated to subscriber 'subscriberId' and service type 'serviceType'\"}");
243
244     }
245
246     @Test
247     public void getTenants_Arguments_Are_Valid_Get_The_Tenanats() {
248
249         when(aaiClientMock.getTenants(any(String.class), any(String.class))).thenCallRealMethod();
250
251
252         Response generalEmptyResponse = mock(Response.class);
253
254         when(generalEmptyResponse.readEntity(String.class)).thenReturn(tenantResponseRaw);
255         when(generalEmptyResponse.getStatus()).thenReturn(200);
256         when(generalEmptyResponse.getStatusInfo()).thenReturn(new Response.StatusType() {
257             @Override
258             public int getStatusCode() {
259                 return 200;
260             }
261
262             @Override
263             public Response.Status.Family getFamily() {
264                 return Response.Status.Family.SUCCESSFUL;
265             }
266
267             @Override
268             public String getReasonPhrase() {
269                 return null;
270             }
271         });
272
273
274         when(aaiClientMock.doAaiGet(any(String.class), any(Boolean.class))).thenReturn(generalEmptyResponse);
275
276         AaiResponse<GetTenantsResponse[]> response = aaiClientMock.getTenants("subscriberId", "serviceType");
277
278         Assert.assertTrue(response.t.length > 0);
279     }
280
281     final String tenantResponseRaw = "" +
282             "{" +
283             "\"service-type\": \"VIRTUAL USP\"," +
284             "\"resource-version\": \"1494001841964\"," +
285             "\"relationship-list\": {" +
286             "\"relationship\": [{" +
287             "\"related-to\": \"tenant\"," +
288             "\"related-link\": \"/aai/v11/cloud-infrastructure/cloud-regions/cloud-region/att-aic/AAIAIC25/tenants/tenant/092eb9e8e4b7412e8787dd091bc58e86\"," +
289             "\"relationship-data\": [{" +
290             "\"relationship-key\": \"cloud-region.cloud-owner\"," +
291             "\"relationship-value\": \"att-aic\"" +
292             "}," +
293             "{" +
294             "\"relationship-key\": \"cloud-region.cloud-region-id\"," +
295             "\"relationship-value\": \"AAIAIC25\"" +
296             "}," +
297             "{" +
298             "\"relationship-key\": \"tenant.tenant-id\"," +
299             "\"relationship-value\": \"092eb9e8e4b7412e8787dd091bc58e86\"" +
300             "}" +
301             "]," +
302             "\"related-to-property\": [{" +
303             "\"property-key\": \"tenant.tenant-name\"," +
304             "\"property-value\": \"USP-SIP-IC-24335-T-01\"" +
305             "}]" +
306             "}]" +
307             "}" +
308             "}";
309
310     @DataProvider
311     public static Object[][] resourceTypesProvider() {
312         return new Object[][]{
313                 {"service-instance", ResourceType.SERVICE_INSTANCE},
314                 {"generic-vnf", ResourceType.GENERIC_VNF},
315                 {"vf-module", ResourceType.VF_MODULE}
316         };
317     }
318
319     @Test(dataProvider = "resourceTypesProvider")
320     public void aaiNodeQueryResponseDeserializationTest(String resourceType, ResourceType expectedResourceType) throws IOException {
321         String link = "/aai/v12/business/customers/customer/a9a77d5a-123e-4ca2-9eb9-0b015d2ee0fb/service-subscriptions/service-subscription/Nimbus/service-instances/service-instance/7131d483-b450-406f-8e30-0c650645fc67";
322         String json =
323                 "{\"result-data\": [{" +
324                         "\"resource-type\": \"" + resourceType + "\"," +
325                         "\"resource-link\": \"" + link + "\"" +
326                         "}]}";
327
328         AaiNodeQueryResponse nodeQueryResponse = new ObjectMapper().readValue(json, AaiNodeQueryResponse.class);
329         assertThat(nodeQueryResponse.resultData.get(0).resourceLink, equalTo(link));
330         assertThat(nodeQueryResponse.resultData.get(0).resourceType, is(expectedResourceType));
331     }
332
333     @Test
334     public void aaiNodeQueryEmptyResponseDeserializationTest() throws IOException {
335         String json = "{}";
336         AaiNodeQueryResponse nodeQueryResponse = new ObjectMapper().readValue(json, AaiNodeQueryResponse.class);
337         assertNull(nodeQueryResponse.resultData);
338     }
339
340     @DataProvider
341     public static Object[][] nameAndResourceTypeProvider() {
342         return new Object[][]{
343                 {"SRIOV_SVC", ResourceType.SERVICE_INSTANCE, "search/nodes-query?search-node-type=service-instance&filter=service-instance-name:EQUALS:SRIOV_SVC"},
344                 {"b1707vidnf", ResourceType.GENERIC_VNF, "search/nodes-query?search-node-type=generic-vnf&filter=vnf-name:EQUALS:b1707vidnf"},
345                 {"connectivity_test", ResourceType.VF_MODULE, "search/nodes-query?search-node-type=vf-module&filter=vf-module-name:EQUALS:connectivity_test"},
346                 {"MjVg1234", ResourceType.VOLUME_GROUP, "search/nodes-query?search-node-type=volume-group&filter=volume-group-name:EQUALS:MjVg1234"}
347         };
348     }
349
350     @Test(dataProvider = "nameAndResourceTypeProvider")
351     public void whenSearchNodeTypeByName_callRightAaiPath(String name, ResourceType type, String expectedUrl) {
352         when(aaiClientMock.searchNodeTypeByName(any(String.class), any(ResourceType.class))).thenCallRealMethod();
353         aaiClientMock.searchNodeTypeByName(name, type);
354         Mockito.verify(aaiClientMock).doAaiGet(eq(expectedUrl), eq(false));
355     }
356
357     @DataProvider
358     public static Object[][] aaiClientInternalExceptions() {
359         return Stream.<Pair<Class<? extends Throwable>, UncheckedBiConsumer<HttpsAuthClient, Client>>>of(
360
361                 // Exception out of httpsAuthClientMock
362                 Pair.of(CertificateException.class, (httpsAuthClientMock, javaxClientMock) -> {
363                     final CertificateException e0 = new CertificateException("No X509TrustManager implementation available");
364                     SSLHandshakeException e = new SSLHandshakeException(e0.toString());
365                     e.initCause(e0);
366
367                     when(httpsAuthClientMock.getClient(any())).thenThrow(e);
368                 }),
369
370                 Pair.of(StringIndexOutOfBoundsException.class, mockExceptionOnClientProvider(new StringIndexOutOfBoundsException(4))),
371
372                 Pair.of(NullPointerException.class, mockExceptionOnClientProvider(new NullPointerException("null"))),
373
374                 Pair.of(FileNotFoundException.class, mockExceptionOnClientProvider(new FileNotFoundException("vid/WEB-INF/cert/aai-client-cert.p12"))),
375
376                 Pair.of(BadPaddingException.class, mockExceptionOnClientProvider(
377                         new IOException("keystore password was incorrect", new BadPaddingException("Given final block not properly padded")))
378                 ),
379                 Pair.of(GenericUncheckedException.class, mockExceptionOnClientProvider(new GenericUncheckedException("basa"))),
380
381                 Pair.of(NullPointerException.class, (httpsAuthClientMock, javaxClientMock) ->
382                         when(httpsAuthClientMock.getClient(any())).thenReturn(null)),
383
384
385                 // Exception out of javax's Client
386                 Pair.of(SSLHandshakeException.class, (httpsAuthClientMock, javaxClientMock) -> {
387                     when(javaxClientMock.target(anyString())).thenThrow(
388                             new ProcessingException(new SSLHandshakeException("Received fatal alert: certificate_expired"))
389                     );
390                 }),
391
392                 Pair.of(SunCertPathBuilderException.class, (httpsAuthClientMock, javaxClientMock) -> {
393                     SunCertPathBuilderException e0 = new SunCertPathBuilderException("unable to find valid certification path to requested target");
394                     when(javaxClientMock.target(anyString())).thenThrow(
395                             new ProcessingException(new ValidatorException("PKIX path building failed: " + e0.toString(), e0))
396                     );
397                 }),
398
399                 Pair.of(GenericUncheckedException.class, (httpsAuthClientMock, javaxClientMock) ->
400                         when(javaxClientMock.target(anyString())).thenThrow(new GenericUncheckedException("basa")))
401
402         ).flatMap(l -> Stream.of(
403                 // double each case to propagateExceptions = true/false, to verify that "don't propagate" really still work
404                 ImmutableList.of(l.getLeft(), l.getRight(), true).toArray(),
405                 ImmutableList.of(l.getLeft(), l.getRight(), false).toArray()
406         )).collect(Collectors.toList()).toArray(new Object[][]{});
407     }
408
409     private static UncheckedBiConsumer<HttpsAuthClient, Client> mockExceptionOnClientProvider(Exception e) {
410         return (httpsAuthClientMock, javaxClientMock) ->
411                 when(httpsAuthClientMock.getClient(any())).thenThrow(e);
412     }
413
414     @Test(dataProvider = "aaiClientInternalExceptions")
415     public void propagateExceptions_internalsThrowException_ExceptionRethrown(Class<? extends Throwable> expectedType, BiConsumer<HttpsAuthClient, Client> setupMocks, boolean propagateExceptions) throws Exception {
416         /*
417         Call chain is like:
418             this test -> AaiClient -> AAIRestInterface -> HttpsAuthClient -> javax's Client
419
420         In this test, *AaiClient* and *AAIRestInterface* are under test (actual
421         implementation is used), while HttpsAuthClient and the javax's Client are
422         mocked to return pseudo-responses or - better- throw exceptions.
423          */
424
425         // prepare mocks
426         HttpsAuthClient httpsAuthClientMock = mock(HttpsAuthClient.class);
427         TestUtils.JavaxRsClientMocks mocks = new TestUtils.JavaxRsClientMocks();
428         Client javaxClientMock = mocks.getFakeClient();
429         Response responseMock = mocks.getFakeResponse();
430
431         // prepare real AAIRestInterface and AaiClient, and wire mocks
432         AAIRestInterface aaiRestInterface = new AAIRestInterface(httpsAuthClientMock, new ServletRequestHelper(), new SystemPropertyHelper());
433         final AaiClient aaiClient = new AaiClient(aaiRestInterface, null);
434         when(httpsAuthClientMock.getClient(any())).thenReturn(javaxClientMock);
435
436         // define atomic method under test, including reset of "aaiRestInterface.client"
437         final Function<Boolean, Response> doAaiGet = (propagateExceptions1) -> {
438             try {
439                 FieldUtils.writeField(aaiRestInterface, "client", null, true);
440                 return aaiClient.doAaiGet("uri", false, propagateExceptions1).getResponse();
441             } catch (IllegalAccessException e) {
442                 throw new RuntimeException(e);
443             }
444         };
445
446         // verify setup again
447         assertThat("mocks setup should make doAaiGet return our responseMock", doAaiGet.apply(true), is(sameInstance(responseMock)));
448
449
450         /// TEST:
451         setupMocks.accept(httpsAuthClientMock, javaxClientMock);
452
453         try {
454             final Response response = doAaiGet.apply(propagateExceptions);
455         } catch (Exception e) {
456             if (propagateExceptions) {
457                 assertThat("root cause incorrect for " + ExceptionUtils.getStackTrace(e), ExceptionUtils.getRootCause(e), instanceOf(expectedType));
458                 return; // ok, done
459             } else {
460                 // Verify that "don't propagate" really still work
461                 Assert.fail("calling doAaiGet when propagateExceptions is false must result with no exception", e);
462             }
463         }
464
465         // If no exception caught
466         // We're asserting that the legacy behaviour is still in place. Hopefully
467         // one day we will remove the non-propagateExceptions case
468         assertFalse(propagateExceptions, "calling doAaiGet when propagateExceptions is 'true' must result with an exception (in this test)");
469     }
470
471     @Test
472     public void shouldProperlyReadResponseOnceWhenSubscribersAreNotPresent() {
473         AAIRestInterface restInterface = mock(AAIRestInterface.class);
474         PortDetailsTranslator portDetailsTranslator = mock(PortDetailsTranslator.class);
475         Response response = mock(Response.class);
476         when(response.getStatus()).thenReturn(404);
477         when(response.readEntity(String.class)).thenReturn("sampleEntity");
478         when(response.getStatusInfo()).thenReturn(Response.Status.NOT_FOUND);
479         ResponseWithRequestInfo responseWithRequestInfo = new ResponseWithRequestInfo(response, "test", HttpMethod.GET);
480         when(restInterface.RestGet(eq("VidAaiController"), any(String.class),
481                 eq("business/customers?subscriber-type=INFRA&depth=0"), eq(false), eq(true))).thenReturn(responseWithRequestInfo);
482         AaiClient aaiClient = new AaiClient(restInterface, portDetailsTranslator);
483
484
485         aaiClient.getAllSubscribers(true);
486
487         verify(response).readEntity(String.class);
488     }
489
490     @FunctionalInterface
491     public interface UncheckedBiConsumer<T, U> extends BiConsumer<T, U> {
492         @Override
493         default void accept(T t, U u) {
494             try {
495                 acceptThrows(t, u);
496             } catch (Exception e) {
497                 throw new RuntimeException(e);
498             }
499         }
500
501         void acceptThrows(T t, U u) throws Exception;
502     }
503 }