From ef69415fe175ff6988fd8f47cc3d410e239984fa Mon Sep 17 00:00:00 2001 From: tpluscode Date: Thu, 25 Jul 2019 13:40:24 +0200 Subject: [PATCH] feat: setting up request messages adds the possibility to set request headers and body fix #32 fix #42 --- .../app/hypermedia/testing/dsl/Core.xtext | 30 +++ .../app/hypermedia/testing/dsl/Hydra.xtext | 5 +- .../CoreValueConverterService.xtend | 5 + .../conversion/MultilineBlockConverter.xtend | 25 +++ .../dsl/generator/HydraGenerator.xtend | 24 ++- .../dsl/tests/generator/hydra/InvokeTest.snap | 174 ++++++++++++++++++ .../tests/generator/hydra/InvokeTest.xtend | 112 +++++++++++ .../dsl/tests/hydra/InvokeParsingTest.xtend | 117 ++++++++++++ .../dsl/tests/hydra/StatusParsingTest.xtend | 2 +- 9 files changed, 488 insertions(+), 6 deletions(-) create mode 100644 app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/conversion/MultilineBlockConverter.xtend create mode 100644 app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/generator/hydra/InvokeTest.snap create mode 100644 app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/generator/hydra/InvokeTest.xtend create mode 100644 app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/hydra/InvokeParsingTest.xtend diff --git a/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/Core.xtext b/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/Core.xtext index f0e5de4..f453c08 100644 --- a/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/Core.xtext +++ b/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/Core.xtext @@ -80,7 +80,37 @@ FollowStatement: 'Follow' variable=VARIABLE ; +RequestBlock: + {RequestBlock} '{' + (headers+=RequestHeader)* + (body=RequestBody)? + '}' +; + +ResponseBlock: + {ResponseBlock} '{' + (children+=ResponseStep)* + '}' +; + +RequestBody: + contents=MULTILINE_BLOCK | + reference=RequestFileBody +; + +RequestFileBody: + '<<<' path=STRING +; + +RequestHeader: + fieldName=FIELD_NAME value=STRING +; + terminal VARIABLE: '[' ( ALPHA )* ']'; terminal FIELD_NAME: ALPHA ('-' | ALPHA)*; terminal ALPHA: ('a'..'z' | 'A'..'Z'); + +terminal MULTILINE_BLOCK: + '```' -> '```' +; diff --git a/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/Hydra.xtext b/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/Hydra.xtext index 597bf52..6996daa 100644 --- a/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/Hydra.xtext +++ b/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/Hydra.xtext @@ -24,7 +24,6 @@ RelaxedOperationBlock: '}' ; -// TODO: Can we use only one step for this and restrict Expect Operation from being used on top level?? OperationBlock: modifier=Modifier 'Operation' name=STRING '{' (invocations+=InvocationBlock)* @@ -32,7 +31,5 @@ OperationBlock: ; InvocationBlock: - {InvocationBlock} 'Invoke' '{' - (children+=ResponseStep)* - '}' + 'Invoke' (request=RequestBlock '=>')? response=ResponseBlock ; \ No newline at end of file diff --git a/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/conversion/CoreValueConverterService.xtend b/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/conversion/CoreValueConverterService.xtend index b14edbb..63fc56e 100644 --- a/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/conversion/CoreValueConverterService.xtend +++ b/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/conversion/CoreValueConverterService.xtend @@ -9,5 +9,10 @@ class CoreValueConverterService extends DefaultTerminalConverters { def IValueConverter getVariableConverter() { return new VariableReferenceConverter() } + + @ValueConverter(rule = "MULTILINE_BLOCK") + def IValueConverter getMultilineBlockConverter() { + return new MultilineBlockConverter() + } } diff --git a/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/conversion/MultilineBlockConverter.xtend b/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/conversion/MultilineBlockConverter.xtend new file mode 100644 index 0000000..30f500e --- /dev/null +++ b/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/conversion/MultilineBlockConverter.xtend @@ -0,0 +1,25 @@ +package app.hypermedia.testing.dsl.conversion + +import org.eclipse.xtext.conversion.IValueConverter +import org.eclipse.xtext.conversion.ValueConverterException +import org.eclipse.xtext.nodemodel.INode + +class MultilineBlockConverter implements IValueConverter { + final String INVALID_VALUE_ERROR = "Code block must be between triple backticks" + + override toString(String value) { + return value + } + + override toValue(String string, INode node) throws ValueConverterException { + if (string === null) { + return null + } + + if(string.length < 7 || !string.startsWith("```") || !string.endsWith("```")) { + throw new ValueConverterException(INVALID_VALUE_ERROR, node, null) + } + + return string.substring(3, string.length - 3).trim() + } +} \ No newline at end of file diff --git a/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/generator/HydraGenerator.xtend b/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/generator/HydraGenerator.xtend index d9d44b3..86f6184 100644 --- a/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/generator/HydraGenerator.xtend +++ b/app.hypermedia.testing.dsl/src/main/java/app/hypermedia/testing/dsl/generator/HydraGenerator.xtend @@ -8,6 +8,7 @@ import app.hypermedia.testing.dsl.hydra.InvocationBlock import app.hypermedia.testing.dsl.hydra.RelaxedOperationBlock import app.hypermedia.testing.dsl.Modifier import java.util.HashMap +import org.json.JSONObject /** * Generates code from your model files on save. @@ -35,6 +36,27 @@ class HydraGenerator extends CoreGenerator { def dispatch step(InvocationBlock it) { val map = new HashMap - return buildBlock('Invocation', children, map) + if(request !== null) { + if(request.headers.length > 0) { + val headers = new JSONObject + request.headers.forEach[header | + headers.put(header.fieldName, header.value) + ] + + map.put('headers', headers) + } + + if(request.body !== null) { + if (request.body.reference !== null) { + val body = new JSONObject() + body.put('path', request.body.reference.path) + map.put('body', body) + } else { + map.put('body', request.body.contents) + } + } + } + + return buildBlock('Invocation', response.children, map) } } diff --git a/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/generator/hydra/InvokeTest.snap b/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/generator/hydra/InvokeTest.snap new file mode 100644 index 0000000..2d03fe8 --- /dev/null +++ b/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/generator/hydra/InvokeTest.snap @@ -0,0 +1,174 @@ +app.hypermedia.testing.dsl.tests.generator.hydra.InvokeTest.classOperation_invokeTwiceWithRequests_generatesSteps=[ + { + "map": { + "steps": { + "myArrayList": [ + { + "map": { + "children": { + "myArrayList": [ + { + "map": { + "children": { + "myArrayList": [ + { + "map": { + "body": { + "map": { + "path": "/some/path/data.csv" + }, + "empty": false + }, + "children": { + "myArrayList": [ + { + "map": { + "code": 201, + "type": "ResponseStatus" + }, + "empty": false + } + ], + "empty": false + }, + "headers": { + "map": { + "Content-Type": "text/csv" + }, + "empty": false + }, + "type": "Invocation" + }, + "empty": false + }, + { + "map": { + "body": "{\n \"hello\": \"world\",\n \"foo\": \"bar\"\n }", + "children": { + "myArrayList": [ + { + "map": { + "code": 409, + "type": "ResponseStatus" + }, + "empty": false + } + ], + "empty": false + }, + "headers": { + "map": { + "Content-Type": "application/json" + }, + "empty": false + }, + "type": "Invocation" + }, + "empty": false + } + ], + "empty": false + }, + "operationId": "CreateUser", + "strict": false, + "type": "Operation" + }, + "empty": false + } + ], + "empty": false + }, + "classId": "People", + "type": "Class" + }, + "empty": false + } + ], + "empty": false + } + }, + "empty": false + } +] + + +app.hypermedia.testing.dsl.tests.generator.hydra.InvokeTest.topLevelOperation_invokeTwiceWithRequests_generatesSteps=[ + { + "map": { + "steps": { + "myArrayList": [ + { + "map": { + "children": { + "myArrayList": [ + { + "map": { + "body": { + "map": { + "path": "/some/path/data.csv" + }, + "empty": false + }, + "children": { + "myArrayList": [ + { + "map": { + "code": 201, + "type": "ResponseStatus" + }, + "empty": false + } + ], + "empty": false + }, + "headers": { + "map": { + "Content-Type": "text/csv" + }, + "empty": false + }, + "type": "Invocation" + }, + "empty": false + }, + { + "map": { + "body": "{\n \"hello\": \"world\",\n \"foo\": \"bar\"\n }", + "children": { + "myArrayList": [ + { + "map": { + "code": 409, + "type": "ResponseStatus" + }, + "empty": false + } + ], + "empty": false + }, + "headers": { + "map": { + "Content-Type": "application/json" + }, + "empty": false + }, + "type": "Invocation" + }, + "empty": false + } + ], + "empty": false + }, + "operationId": "CreateUser", + "strict": false, + "type": "Operation" + }, + "empty": false + } + ], + "empty": false + } + }, + "empty": false + } +] \ No newline at end of file diff --git a/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/generator/hydra/InvokeTest.xtend b/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/generator/hydra/InvokeTest.xtend new file mode 100644 index 0000000..4d041e9 --- /dev/null +++ b/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/generator/hydra/InvokeTest.xtend @@ -0,0 +1,112 @@ +package app.hypermedia.testing.dsl.tests.generator.hydra + +import org.eclipse.xtext.generator.InMemoryFileSystemAccess +import org.junit.jupiter.api.^extension.ExtendWith +import org.eclipse.xtext.testing.extensions.InjectionExtension +import org.eclipse.xtext.testing.InjectWith +import com.google.inject.Inject +import org.eclipse.xtext.testing.util.ParseHelper +import app.hypermedia.testing.dsl.core.Model +import org.junit.jupiter.api.Test +import static io.github.jsonSnapshot.SnapshotMatcher.* +import org.eclipse.xtext.generator.IGenerator2 +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.AfterAll +import org.json.JSONObject +import app.hypermedia.testing.dsl.tests.HydraInjectorProvider +import org.eclipse.xtext.generator.GeneratorContext + +@ExtendWith(InjectionExtension) +@InjectWith(HydraInjectorProvider) +class InvokeTest { + @Inject IGenerator2 generator + @Inject extension ParseHelper + + @BeforeAll + static def beforeAll() { + start() + } + + @AfterAll + static def afterAll() { + validateSnapshots(); + } + + @Test + def topLevelOperation_invokeTwiceWithRequests_generatesSteps() { + // given + val model = ''' + With Operation "CreateUser" { + Invoke { + Content-Type "text/csv" + + <<< "/some/path/data.csv" + } => { + Expect Status 201 + } + + Invoke { + Content-Type "application/json" + + ``` + { + "hello": "world", + "foo": "bar" + } + ``` + } => { + Expect Status 409 + } + } + '''.parse + + // when + val fsa = new InMemoryFileSystemAccess() + generator.doGenerate(model.eResource, fsa, new GeneratorContext()) + println(fsa.textFiles) + + // then + val file = new JSONObject(fsa.textFiles.values.get(0).toString) + expect(file).toMatchSnapshot() + } + + @Test + def classOperation_invokeTwiceWithRequests_generatesSteps() { + // given + val model = ''' + With Class "People" { + With Operation "CreateUser" { + Invoke { + Content-Type "text/csv" + + <<< "/some/path/data.csv" + } => { + Expect Status 201 + } + + Invoke { + Content-Type "application/json" + + ``` + { + "hello": "world", + "foo": "bar" + } + ``` + } => { + Expect Status 409 + } + } + } + '''.parse + + // when + val fsa = new InMemoryFileSystemAccess() + generator.doGenerate(model.eResource, fsa, new GeneratorContext()) + println(fsa.textFiles) + + // then + val file = new JSONObject(fsa.textFiles.values.get(0).toString) + expect(file).toMatchSnapshot() + } +} \ No newline at end of file diff --git a/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/hydra/InvokeParsingTest.xtend b/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/hydra/InvokeParsingTest.xtend new file mode 100644 index 0000000..2c3e573 --- /dev/null +++ b/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/hydra/InvokeParsingTest.xtend @@ -0,0 +1,117 @@ +/* + * generated by Xtext 2.18.0 + */ +package app.hypermedia.testing.dsl.tests.hydra + +import app.hypermedia.testing.dsl.core.Model +import com.google.inject.Inject +import org.eclipse.xtext.testing.InjectWith +import org.eclipse.xtext.testing.extensions.InjectionExtension +import org.eclipse.xtext.testing.util.ParseHelper +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.^extension.ExtendWith +import app.hypermedia.testing.dsl.tests.HydraInjectorProvider +import static org.assertj.core.api.Assertions.* +import app.hypermedia.testing.dsl.tests.TestHelpers +import app.hypermedia.testing.dsl.hydra.RelaxedOperationBlock +import app.hypermedia.testing.dsl.core.StatusStatement + +@ExtendWith(InjectionExtension) +@InjectWith(HydraInjectorProvider) +class InvokeParsingTest { + @Inject extension + ParseHelper parseHelper + + @Test + def void invokeWithHeaders_parsesHeadersSuccessfully() { + // when + val result = parseHelper.parse(''' + With Operation "CreateUser" { + Invoke { + Content-Type "text/turtle" + ETag 'W/""' + } => { + Expect Status 405 + } + } + ''') + + // then + TestHelpers.assertModelParsedSuccessfully(result) + + val operationBlock = result.steps.get(0) as RelaxedOperationBlock + val invocation = operationBlock.invocations.get(0) + assertThat(invocation.request.headers).hasSize(2) + assertThat(invocation.request.headers.get(0).fieldName).isEqualTo('Content-Type') + assertThat(invocation.request.headers.get(1).fieldName).isEqualTo('ETag') + assertThat(invocation.request.headers.get(0).value).isEqualTo('text/turtle') + assertThat(invocation.request.headers.get(1).value).isEqualTo('W/""') + } + + @Test + def void invokeWithInlineBody_parsesBodyContentsSuccessfully() { + // when + val result = parseHelper.parse(''' + With Operation "CreateUser" { + Invoke { + ``` + <> a api:Person . + ``` + } => { + Expect Status 405 + } + } + ''') + + // then + TestHelpers.assertModelParsedSuccessfully(result) + + val operationBlock = result.steps.get(0) as RelaxedOperationBlock + val invocation = operationBlock.invocations.get(0) + assertThat(invocation.request.body.contents).contains('<> a api:Person .') + } + + @Test + def void invokeWithBodyFromFile_parsesPathSuccessfully() { + // when + val result = parseHelper.parse(''' + With Operation "CreateUser" { + Invoke { + <<< "../../sample-bodies/data.csv" + } => { + Expect Status 405 + } + } + ''') + + // then + TestHelpers.assertModelParsedSuccessfully(result) + + val operationBlock = result.steps.get(0) as RelaxedOperationBlock + val invocation = operationBlock.invocations.get(0) + assertThat(invocation.request.body.reference.path).isEqualTo('../../sample-bodies/data.csv') + } + + @Test + def void invokeWithBodyFromFile_parsesResponeBlock() { + // when + val result = parseHelper.parse(''' + With Operation "CreateUser" { + Invoke { + <<< "../../sample-bodies/data.csv" + } => { + Expect Status 405 + } + } + ''') + + // then + TestHelpers.assertModelParsedSuccessfully(result) + + val operationBlock = result.steps.get(0) as RelaxedOperationBlock + val invocation = operationBlock.invocations.get(0) + assertThat(invocation.response.children).hasSize(1) + val statusStatement = invocation.response.children.get(0) as StatusStatement + assertThat(statusStatement.status).isEqualTo(405) + } +} diff --git a/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/hydra/StatusParsingTest.xtend b/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/hydra/StatusParsingTest.xtend index 06b4174..0e9bc52 100644 --- a/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/hydra/StatusParsingTest.xtend +++ b/app.hypermedia.testing.dsl/src/test/java/app/hypermedia/testing/dsl/tests/hydra/StatusParsingTest.xtend @@ -43,7 +43,7 @@ class StatusParsingTest { val operationBlock = result.steps.get(0) as RelaxedOperationBlock val invocationBlock = operationBlock.invocations.get(0) as InvocationBlock - val statusBlock = invocationBlock.children.get(0) as StatusStatement + val statusBlock = invocationBlock.response.children.get(0) as StatusStatement assertThat(statusBlock.status).isEqualTo(status) }