Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Repo info
Activity
  • 12:54

    spring-buildmaster on gh-pages

    Sync docs from master to gh-pag… (compare)

  • 09:55

    spring-buildmaster on gh-pages

    Sync docs from master to gh-pag… (compare)

  • 06:55

    spring-buildmaster on gh-pages

    Sync docs from master to gh-pag… (compare)

  • 03:55

    spring-buildmaster on gh-pages

    Sync docs from master to gh-pag… (compare)

  • 02:33

    spring-buildmaster on gh-pages

    Sync docs from 2.1.x to gh-pages (compare)

  • 00:56

    spring-buildmaster on gh-pages

    Sync docs from master to gh-pag… (compare)

  • 00:32

    spring-buildmaster on gh-pages

    Sync docs from master to gh-pag… (compare)

  • Dec 13 21:56

    spring-buildmaster on gh-pages

    Sync docs from master to gh-pag… (compare)

  • Dec 13 18:59

    spring-buildmaster on gh-pages

    Sync docs from master to gh-pag… (compare)

  • Dec 13 17:33
    marcingrzejszczak closed #1288
  • Dec 13 17:33
    marcingrzejszczak commented #1288
  • Dec 13 16:03
    Schlese closed #1289
  • Dec 13 16:03
    Schlese commented #1289
  • Dec 13 15:58
    spring-issuemaster unlabeled #1288
  • Dec 13 15:58
    spring-issuemaster labeled #1288
  • Dec 13 15:56
    izacristina commented #1288
  • Dec 13 15:55

    spring-buildmaster on gh-pages

    Sync docs from master to gh-pag… (compare)

  • Dec 13 15:54
    izacristina commented #1288
  • Dec 13 15:18
    marcingrzejszczak labeled #1288
  • Dec 13 15:18
    marcingrzejszczak unlabeled #1288
Marcin Grzejszczak
@marcingrzejszczak
so put a fixed value of the request and response, you don't need to reference it
it's a contract test not an end to end test
Arthur Kazemi
@bidadh
yes I guess I'm missing something here which causes this
I'll explain the request and response and you help defining the proper contract test for it
Marcin Grzejszczak
@marcingrzejszczak
i have a better idea
you can explain it and you will prepare the contract and I'll review it
Arthur Kazemi
@bidadh

let's assume:

/checkStatus?name=Name1&name=Name2&name=Name3&name=Name4

json response is:

[
{"name": "Name1", "status": "OK"},
{"name": "Name2", "status": "NONE"},
{"name": "Name3", "status": "NOK"},
{"name": "Name4", "status": "OK"}
]
aha ok.
let me ask a wider question.
Arthur Kazemi
@bidadh
I guess I'm missing something more basic here.
assuming this requirement which I.hope it's clear enough :D
what is the contract and what's part of the functionality ? if I make it clear then maybe the problem is solved!
Arthur Kazemi
@bidadh
to me contract here is:
1- accepts list for name
2- returns an array including one and only one object per name
3- object includes name and status
4- status is on of the provided values
5- and of course status is 200
is this a fair assumption as contract or I'm completely wrong ?
Marcin Grzejszczak
@marcingrzejszczak
I'd suggest you watch one of my talks - maybe this one https://www.youtube.com/watch?v=ZyHG-VOzPZg and read the documentation (https://cloud.spring.io/spring-cloud-static/spring-cloud-contract/2.2.0.RELEASE/reference/html/index.html) where we explain all the terms related to contract testing
Arthur Kazemi
@bidadh

@marcingrzejszczak could you please take a look into this:

package contracts.goods

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  request {
    method GET()
    urlPath($(c('/check'), p('/check'))) {
      queryParameters {
        ['name1', 'name2', 'name3'].collect {
          parameter 'name': $(c(it), p(it))
        }
      }
    }
    headers {
      accept(applicationJson())
    }
  }

  response {
    status OK()
    body([
        [
            name  : fromRequest().query('name', 0),
            status: anyOf('YES', 'NO', 'NOT_FOUND')
        ],
        [
            name  : fromRequest().query('name', 1),
            status: anyOf('YES', 'NO', 'NOT_FOUND')
        ],
        [
            name  : fromRequest().query('name', 2),
            status: anyOf('YES', 'NO', 'NOT_FOUND')
        ],
    ])
  }
}

and here is the dummy API:

  @GetMapping(value = "/check", produces = MediaType.APPLICATION_JSON_VALUE)
  public Stream<Map<String, String>> hasDG(@RequestParam("name") List<String> objects) {
    List<String> values = Arrays.asList("YES1", "NO1", "NOT_FOUND");
    Supplier<String> statusSupplier = () -> values.get(new Random().nextInt(values.size()));
    return objects.stream()
        .map(name -> {
          Map<String, String> result = new HashMap<>();
          result.put("name", name);
          result.put("status", statusSupplier.get());
          return result;
        });
  }
Marcin Grzejszczak
@marcingrzejszczak
ok let me see
Marcin Grzejszczak
@marcingrzejszczak
Contract.make {
  request {
    method GET()
    urlPath('/check') {
      queryParameters {
        ['name1', 'name2', 'name3'].each {
          parameter('name': it)
        }
      }
    }
    headers {
      accept(applicationJson())
    }
  }

  response {
    status OK()
    body([
        [
            name  : fromRequest().query('name', 0),
            status: anyOf('YES', 'NO', 'NOT_FOUND')
        ],
        [
            name  : fromRequest().query('name', 1),
            status: anyOf('YES', 'NO', 'NOT_FOUND')
        ],
        [
            name  : fromRequest().query('name', 2),
            status: anyOf('YES', 'NO', 'NOT_FOUND')
        ],
    ])
  }
}

I would put this code

 List<String> values = Arrays.asList("YES1", "NO1", "NOT_FOUND");
    Supplier<String> statusSupplier = () -> values.get(new Random().nextInt(values.size()));
    return objects.stream()
        .map(name -> {
          Map<String, String> result = new HashMap<>();
          result.put("name", name);
          result.put("status", statusSupplier.get());
          return result;
        });

in a service and would have a dummy implementation contain that code

Arthur Kazemi
@bidadh
@marcingrzejszczak great. so the contract overall looks fine. I might need to have $(consumer(..)) and $(producer(...)) though
thanks for the review
Marcin Grzejszczak
@marcingrzejszczak
np
Jesper Josefsson
@jesjos
Folks, I have a typical CRUD setup where /documents returns all documents in the database. I want to say things like "all returned documents should have a numeric id" and "all returned documents should have a filename that is a string". How would I go about doing that?
Arthur Kazemi
@bidadh
@jesjos you could use anyNumber and anyNonEmptyString in the response section if that helps
Jesper Josefsson
@jesjos
@bidadh what confuses me is that the docs never describe a case where the body has a array inthe root of the reponse
the body method won't accept an array argument
Arthur Kazemi
@bidadh
it can. here is an example of an array in the response:
response {
    status OK()
    body([
        [
            name  : fromRequest().query('name', 0),
            status: anyOf('YES', 'NO', 'NOT_FOUND')
        ],
        [
            name  : fromRequest().query('name', 1),
            status: anyOf('YES', 'NO', 'NOT_FOUND')
        ],
        [
            name  : fromRequest().query('name', 2),
            status: anyOf('YES', 'NO', 'NOT_FOUND')
        ],
    ])
  }
yours might be something like:
response {
    status OK()
    body([
        [
            id  : anyNumber(),
            filename: anyNonEmptyString()
        ],
        [
            id  : anyNumber(),
            filename: anyNonEmptyString()
        ],
        [
            id  : anyNumber(),
            filename: anyNonEmptyString()
        ],
    ])
  }
Arthur Kazemi
@bidadh

is it a good thing to add a contract test for default 400 responses? for example missing a query parameter causes 400 response with body like:

{
  "timestamp": "2019-12-12T10:33:48.519+0000",
  "status": 400,
  "error": "Bad Request",
  "message": "Required List parameter 'name' is not present",
  "path": "/check"
}

the response body is something we might need to use in the consumer.

if I add a test, and initialise the test with webAppContextSetup rather than standaloneSetup I have the proper status code which is 400 but body is empty

Marcin Grzejszczak
@marcingrzejszczak
If the query param is necessary them i would test it
Arthur Kazemi
@bidadh

makes sense. I have an issue with the body though

for 400 and 401 responses, body is always blank!

here is the Base class configuration:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@TestPropertySource(properties = {"spring.security.user.name=user", "spring.security.user.password=pass"})

any ideas ?

Marcin Grzejszczak
@marcingrzejszczak
Can you show the whole base class
Arthur Kazemi
@bidadh
sure.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@TestPropertySource(properties = {"spring.security.user.name=user", "spring.security.user.password=pass"})
public class NamesBase {
  public static final String NAME1 = "name1";
  public static final String NAME2 = "name2";
  public static final String NAME3 = "name3";

  @MockBean
  private CheckService checkService;

  @Autowired
  private WebApplicationContext context;

  @Autowired
  private FilterChainProxy springSecurityFilterChain;

  protected String basicAuth() {
    String auth = "user" + ":" + "pass";
    return  "Basic " + new String(Base64.encodeBase64(auth.getBytes(StandardCharsets.UTF_8) ));
  }

  @Before
  public void setup() {
    DefaultMockMvcBuilder mvcBuilder = MockMvcBuilders
        .webAppContextSetup(this.context)
        .addFilter(this.springSecurityFilterChain);

    RestAssuredMockMvc.standaloneSetup(mvcBuilder);

    List<String> names = Arrays.asList(NAME1, NAME2, NAME3);
    Map<String, DGStatus> statusMap = new HashMap<>();
    statusMap.put(NAME1, DGStatus.YES);
    statusMap.put(NAME2, DGStatus.NO);
    statusMap.put(NAME3, DGStatus.NOT_FOUND);

    List<Result> response = ...

    doReturn(response).when(checkService).check(names);
  }
}
Actually, it's a bit strange. I'm having same issue in the sample controller test. seems test is missing a configuration
the only way it return full json body is when I run the application . otherwise it's empty
Arthur Kazemi
@bidadh
Maybe this isn't about spring-cloud-contract but anyways, seems in the tests BasicErrorController doesn't get involved in the error message response! but in the real app this controller produces the error
Marcin Grzejszczak
@marcingrzejszczak
Arthur Kazemi
@bidadh
yes, I looked into the samples but couldn't find any that verifies the bad request body and messages. that's fine I'll try in another way.

I have another issue with execute

assuming the contract includes this header

header(authorization(), execute('basicAuth()'))

or

header(authorization(), $(c(anyNonEmptyString()), p(execute('basicAuth()'))))

producer test is passed but consumer fails because if finds the following stub:

{
  "id" : "0592b223-a1cf-4aad-a056-a61b3e0bc004",
  "request" : {
    "urlPath" : "/check",
    "method" : "GET",
    "headers" : {
      "Accept" : {
        "matches" : "application/json.*"
      },
      "Authorization" : {
        "equalTo" : "basicAuth()"
      }
    }
  },
  "response" : {
    "status" : 400,
    "transformers" : [ "response-template" ]
  },
  "uuid" : "0592b223-a1cf-4aad-a056-a61b3e0bc004"
}
Marcin Grzejszczak
@marcingrzejszczak
can you show the full contract please
Arthur Kazemi
@bidadh
package contracts.goods

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  def name1 = "Name1"
  def name2 = "Name2"
  def name3 = "Name3"
  def names = [name1, name2, name3]

  request {
    method GET()
    urlPath('/check') {
      queryParameters {
        names.forEach({
            parameter 'name': it
          }
        )
      }
    }
    headers {
      accept(applicationJson())
      header(authorization(), $(c(anyNonEmptyString()), p(execute('basicAuth()'))))
    }
  }

  response {
    status OK()
    body([
        [
            name: name1,
            status: 'YES'
        ],
        [
            name: name2,
            status: 'NO'
        ],
        [
            name: name3,
            status: 'NOT_FOUND'
        ],
    ])
    headers {
      contentType(applicationJson())
    }
  }
}
Arthur Kazemi
@bidadh
here is the full contract
Marcin Grzejszczak
@marcingrzejszczak
hmm that should work
:tm:
are you using the latest sc-contract version?
Arthur Kazemi
@bidadh

I think so

here are the version numbers in the pom:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
    </properties>

and maven plugin in the producer:

            <plugin>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-contract-maven-plugin</artifactId>
                <version>2.2.0.RELEASE</version>
                <extensions>true</extensions>
                <configuration>
                    <packageWithBaseClasses>com.ideabaker.samples.contract.contractproducer.contract</packageWithBaseClasses>
                </configuration>
            </plugin>
Marcin Grzejszczak
@marcingrzejszczak
:|
sounds like a bug
can you file it?
Arthur Kazemi
@bidadh
I will