From 7ebf3ee57fc16def329729f5dd6a7120c191b173 Mon Sep 17 00:00:00 2001 From: David Griffin Date: Wed, 4 Dec 2024 10:47:58 -0800 Subject: [PATCH] Implement tests as GitHub workflow (#23) Co-authored-by: David Griffin Co-authored-by: Darren Cunningham --- .github/workflows/test.yml | 41 +- .gitignore | 11 +- seed/categories.json | 14 + seed/customers.json | 13 + seed/orders.fql | 39 +- seed/products.fql | 30 +- setup.sh | 10 - src/main/java/fauna/sample/AppConfig.java | 8 + .../products/ProductsController.java | 5 +- test/http-client.env.json | 5 + test/requests.http | 423 ++++++++++++++++++ test/setup.sh | 24 + test/validate.sh | 27 ++ 13 files changed, 580 insertions(+), 70 deletions(-) create mode 100644 seed/categories.json create mode 100644 seed/customers.json delete mode 100755 setup.sh create mode 100644 test/http-client.env.json create mode 100644 test/requests.http create mode 100755 test/setup.sh create mode 100755 test/validate.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78db6f3..64d1d25 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,22 +4,31 @@ on: pull_request: jobs: - build: + validate: runs-on: ubuntu-latest - services: - fauna: - image: fauna/faunadb - ports: - - 8443:8443 - steps: - - name: Check out repository - uses: actions/checkout@v4 + services: + fauna: + image: fauna/faunadb + ports: + - 8443:8443 + steps: + - name: Check out repository + uses: actions/checkout@v4 - - name: Set up JDK - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: corretto + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: corretto + - name: Install fauna-shell + run: + npm install -g fauna-shell + - name: Setup Test Database + run: ./test/setup.sh - - name: Run sample app - run: ./gradlew build + - name: Test sample app + run: | + ./gradlew build + FAUNA_ENDPOINT=http://localhost:8443 FAUNA_SECRET=`cat .fauna_key` ./gradlew bootRun > bootrun.log 2>&1 & + ./test/validate.sh + cat bootrun.log \ No newline at end of file diff --git a/.gitignore b/.gitignore index ac4068c..82058ba 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,7 @@ build/ .env ### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ +.idea/ *.iws *.iml *.ipr @@ -40,4 +37,8 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +### Fauna ### +.fauna* + diff --git a/seed/categories.json b/seed/categories.json new file mode 100644 index 0000000..68c8183 --- /dev/null +++ b/seed/categories.json @@ -0,0 +1,14 @@ +[ + { + "name": "electronics", + "description": "bargain electronics" + }, + { + "name": "books", + "description": "bargain books" + }, + { + "name": "movies", + "description": "bargain movies" + } +] diff --git a/seed/customers.json b/seed/customers.json new file mode 100644 index 0000000..0c70300 --- /dev/null +++ b/seed/customers.json @@ -0,0 +1,13 @@ +[ + { + "name": "Valued Customer", + "email": "fake@fauna.com", + "address": { + "street": "Herengracht", + "city": "Amsterdam", + "state": "North Holland", + "postalCode": "1015BT", + "country": "Netherlands" + } + } +] diff --git a/seed/orders.fql b/seed/orders.fql index 0b1fd0f..3a4bab8 100644 --- a/seed/orders.fql +++ b/seed/orders.fql @@ -1,23 +1,16 @@ -{ - "query": " - let customer = Customer.byEmail('fake@fauna.com').first()!\n - let orders = ['cart', 'processing', 'shipped', 'delivered'].map(status => {\n - let order: Any = Order.byCustomer(customer).firstWhere(o => o.status == status)\n - if (order == null) {\n - let newOrder: Any = Order.create({\n - customer: customer,\n - status: status,\n - createdAt: Time.now(),\n - payment: {}\n - })\n - let product: Any = Product.byName('Drone').first()!\n - let orderItem: Any = OrderItem.create({ order: newOrder, product: product, quantity: 1 })\n - orderItem\n - newOrder\n - } else {\n - order\n - }\n - })\n - orders - " -} +let customer = Customer.byEmail('fake@fauna.com').first()! +['cart', 'processing', 'shipped', 'delivered'].map(status => { + let order: Any = Order.byCustomer(customer).firstWhere(o => o.status == status) + if (order == null) { + let newOrder: Any = Order.create({ + customer: customer, + status: status, + createdAt: Time.now(), payment: {} + }) + + let product: Any = Product.byName('Drone').first()! + let orderItem: Any = OrderItem.create( {order: newOrder, product: product, quantity: 1 }) + orderItem + newOrder + } else { order } +}) diff --git a/seed/products.fql b/seed/products.fql index d177df3..1014949 100644 --- a/seed/products.fql +++ b/seed/products.fql @@ -1,4 +1,4 @@ -{"query":"[ +[ { 'name': 'iPhone', 'price': 10000, @@ -62,17 +62,17 @@ 'stock': 10, 'category': 'movies' } -].map(p => {\n - let existing: Any = Product.byName(p.name).first()\n - if (existing != null) {\n - existing!.update({ stock: p.stock })\n - } else {\n - Product.create({\n - name: p.name,\n - price: p.price,\n - description: p.description,\n - stock: p.stock,\n - category: Category.byName(p.category).first()!\n - })\n - }\n -}\n)"} +].map(p => { + let existing: Any = Product.byName(p.name).first() + if (existing != null) { + existing!.update({ stock: p.stock }) + } else { + Product.create({ + name: p.name, + price: p.price, + description: p.description, + stock: p.stock, + category: Category.byName(p.category).first()! + }) + } +}) diff --git a/setup.sh b/setup.sh deleted file mode 100755 index 81c0021..0000000 --- a/setup.sh +++ /dev/null @@ -1,10 +0,0 @@ - -EP="${FAUNA_ENDPOINT:-https://db.fauna.com}" - -cat seed/categories.fql | curl -X POST -u "$FAUNA_SECRET": "$EP/query/1" -H 'Content-Type: application/json' -d@- -echo -e "\n\n" -cat seed/customers.fql | curl -X POST -u "$FAUNA_SECRET": "$EP/query/1" -H 'Content-Type: application/json' -d@- -echo -e "\n\n" -cat seed/products.fql | curl -X POST -u "$FAUNA_SECRET": "$EP/query/1" -H 'Content-Type: application/json' -d@- -echo -e "\n\n" -cat seed/orders.fql | curl -X POST -u "$FAUNA_SECRET": "$EP/query/1" -H 'Content-Type: application/json' -d@- diff --git a/src/main/java/fauna/sample/AppConfig.java b/src/main/java/fauna/sample/AppConfig.java index 0f86f33..cc2ea53 100644 --- a/src/main/java/fauna/sample/AppConfig.java +++ b/src/main/java/fauna/sample/AppConfig.java @@ -4,11 +4,13 @@ import com.fauna.client.FaunaClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; @Configuration class AppConfig { @Bean + @Profile("!local") FaunaClient faunaClient() { // Creates a `FaunaClient` with its default configuration. Note that by // default it looks for `FAUNA_ENDPOINT` and `FAUNA_SECRET` environment @@ -16,4 +18,10 @@ FaunaClient faunaClient() { // the endpoint is not set, it uses `https://db.fauna.com`. return Fauna.client(); } + + @Bean + @Profile("local") + FaunaClient localClient() { + return Fauna.local(); + } } \ No newline at end of file diff --git a/src/main/java/fauna/sample/controllers/products/ProductsController.java b/src/main/java/fauna/sample/controllers/products/ProductsController.java index 23ae2f4..de3715f 100644 --- a/src/main/java/fauna/sample/controllers/products/ProductsController.java +++ b/src/main/java/fauna/sample/controllers/products/ProductsController.java @@ -1,6 +1,7 @@ package fauna.sample.controllers.products; import com.fauna.client.FaunaClient; +import com.fauna.query.QueryOptions; import com.fauna.query.builder.Query; import com.fauna.response.QuerySuccess; import com.fauna.types.Page; @@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.time.Duration; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -90,7 +92,8 @@ Future> paginate( // as a parameter as well as an optional return type. In this case, we are // using the Product.class to specify that the query will return a single // item representing a Product. - return CompletableFuture.completedFuture(client.paginate(query, Product.class).next()); + return CompletableFuture.completedFuture(client.paginate(query, Product.class, + QueryOptions.builder().timeout(Duration.ofSeconds(30)).build()).next()); } @Async diff --git a/test/http-client.env.json b/test/http-client.env.json new file mode 100644 index 0000000..f81a90c --- /dev/null +++ b/test/http-client.env.json @@ -0,0 +1,5 @@ +{ + "dev": { + "host": "http://localhost:8080" + } +} \ No newline at end of file diff --git a/test/requests.http b/test/requests.http new file mode 100644 index 0000000..ed84c8b --- /dev/null +++ b/test/requests.http @@ -0,0 +1,423 @@ +### List Products +GET {{host}}/products?pageSize=1 +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response contains products", function() { + let jsonData = response.body; + client.assert(jsonData["data"][0]["name"] != "", "Must have a name"); + client.assert(jsonData["data"][0]["description"] != "", "Must have a description"); + client.global.set("productId", jsonData["data"][0]["id"]); + client.global.set("afterToken", encodeURIComponent(jsonData["after"])); + }); +%} + +### Paginate Products +GET {{host}}/products?pageSize=1&afterToken={{afterToken}} +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response contains products", function() { + let jsonData = response.body; + client.assert(jsonData["data"][0]["name"] != "", "Must have a name"); + client.assert(jsonData["data"][0]["description"] != "", "Must have a description"); + client.global.set("afterToken", encodeURIComponent(jsonData["after"])); + }); +%} + +### List Product Categories +GET {{host}}/products/categories +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response contains products", function() { + let jsonData = response.body; + let count = jsonData.length; + client.assert(count == 3, "Expected 3 categories, got " + count); + }); +%} + +### Search Products +GET {{host}}/products/search?minPrice=1000&maxPrice=10000 +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response contains products", function() { + let jsonData = response.body; + client.assert(jsonData != null, "Must have a product"); + client.assert(jsonData["data"][0]["name"] != "", "Must have a name"); + client.assert(jsonData["data"][0]["description"] != "", "Must have a description"); + }); +%} + +### Create Product +POST {{host}}/products +Accept: application/json +Content-Type: application/json + +{ + "name": "Coolest toy", + "description": "All the cool kids have one.", + "category": "electronics", + "price": 9999, + "quantity": 99 +} + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response is a product", function() { + let jsonData = response.body; + client.assert(jsonData["name"] != "", "Must have a name"); + client.assert(jsonData["description"] != "", "Must have a description"); + client.global.set("productId", jsonData["id"]); + }); +%} + +### Update Product +POST {{host}}/products/{{productId}} +Accept: application/json +Content-Type: application/json + +{ + "name": "Coolest toy", + "description": "All the cool kids have one.", + "category": "electronics", + "price": 9999, + "quantity": 1 +} + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response is a product", function() { + let jsonData = response.body; + client.assert(jsonData["name"] != "", "Must have a name"); + client.assert(jsonData["description"] != "", "Must have a description"); + }); +%} + +### Delete Product +DELETE {{host}}/products/{{productId}} +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 204", function() { + client.assert(response.status === 204, "Response status is not 200"); + }); +%} + +### Create Customer +POST {{host}}/customers +Accept: application/json +Content-Type: application/json + +{ + "name": "Natasha Romanoff", + "email": "natasha.romanoff@avengers.com", + "address": { + "street": "Avengers Tower", + "city": "New York", + "state": "NY", + "postalCode": "10001", + "country": "USA" + } +} + +> {% + client.test("Response status is 201", function() { + client.assert(response.status === 201, "Response status is not 201"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response is a Customer", function() { + let jsonData = response.body; + client.assert(jsonData["name"] != "", "Must have a name"); + client.assert(jsonData["email"] != "", "Must have an email"); + client.global.set("customerId", jsonData["id"]); + }); +%} + +### Get Customer +GET {{host}}/customers/{{customerId}} +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response is a Customer", function() { + let jsonData = response.body; + client.assert(jsonData["name"] != "", "Must have a name"); + client.assert(jsonData["email"] != "", "Must have an email"); + }); +%} + +### Update Customer + +POST {{host}}/customers/{{customerId}} +Accept: application/json +Content-Type: application/json + +{ + "name": "Natasha Romanoff", + "email": "natasha.romanoff@avengers.com", + "address": { + "street": "Avengers Compound", + "city": "Schroon Lake", + "state": "NY", + "postalCode": "12866", + "country": "USA" + } +} + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200, got " + response.status); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response is a Customer", function() { + let jsonData = response.body; + client.assert(jsonData["name"] != "", "Must have a name"); + client.assert(jsonData["email"] != "", "Must have an email"); + client.global.set("customerId", jsonData["id"]); + }); +%} + +### Get Customer Orders +GET {{host}}/customers/{{customerId}}/orders +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + // client.test("Response contains orders", function() { + // let jsonData = response.body; + // client.assert(jsonData.length > 0, "Must have an order"); + // }); +%} + +### Create Cart for Customer +POST {{host}}/customers/{{customerId}}/cart +Accept: application/json +Content-Type: application/json + +{} + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response is a Cart", function() { + let jsonData = response.body; + client.assert(jsonData["customerId"] != "", "Must have a customerId"); + client.global.set("cartId", jsonData["id"]); + }); +%} + + +### Get Order +GET {{host}}/orders/{{cartId}} +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response is an Order", function() { + let jsonData = response.body; + client.assert(jsonData["customerId"] != "", "Must have a customerId"); + }) +%} + +### Add Item to Cart +POST {{host}}/customers/{{customerId}}/cart/item +Accept: application/json +Content-Type: application/json + +{ + "productName": "Raspberry Pi", + "quantity": 1 +} + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response is an Order", function() { + let jsonData = response.body; + client.assert(jsonData["customerId"] != "", "Must have a customerId"); + }) +%} + + +### Get Cart +GET {{host}}/customers/{{customerId}}/cart +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + client.assert(response.contentType.mimeType == "application/json", "Response is not JSON"); + }); + + client.test("Response is an Order", function() { + let jsonData = response.body; + client.assert(jsonData["items"].length == 1, "Must have an item in the cart"); + }) +%} + +### Update Order +POST {{host}}/orders/{{cartId}} +Accept: application/json +Content-Type: application/json + +{ + "status": "processing", + "payment": { + "type": "card", + "number": "4111111111111111", + "expMonth": "12", + "expYear": "2025", + "cvc": "123" + } +} + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + }); + + client.test("Response is an Order", function() { + let jsonData = response.body; + client.assert(jsonData["customerId"] != "", "Must have a customerId"); + }); +%} + +### List Orders +GET {{host}}/orders/list +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 200", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + + client.test("Response is JSON", function() { + }); +%} + +### Delete Customer +DELETE {{host}}/customers/{{customerId}} +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 204", function() { + client.assert(response.status === 204, "Response status is not 204"); + }); +%} + +### Reset +POST {{host}}/reset +Accept: application/json +Content-Type: application/json + +> {% + client.test("Response status is 204", function() { + client.assert(response.status === 204, "Response status is not 204"); + }); +%} \ No newline at end of file diff --git a/test/setup.sh b/test/setup.sh new file mode 100755 index 0000000..bfd81de --- /dev/null +++ b/test/setup.sh @@ -0,0 +1,24 @@ +#! /bin/sh + +DB_NAME="ECommerceJava" +LOCAL_ENDPOINT="http://localhost:8443/" +SECRET="secret" + +touch .fauna-project + +fauna endpoint add local -y --set-default --url "$LOCAL_ENDPOINT" --secret "$SECRET" +echo "Added local endpoint" + +fauna create-database "$DB_NAME" +echo "Created database $DB_NAME" + +fauna environment add --name local --endpoint local --database $DB_NAME -y +fauna environment select local +fauna eval "Key.create({ role: 'server' }).secret" | xargs > .fauna_key + +fauna schema push -y --active --dir=schema + +fauna import --collection Category --path seed/categories.json +fauna import --collection Customer --path seed/customers.json +fauna eval --file seed/products.fql +fauna eval --file seed/orders.fql diff --git a/test/validate.sh b/test/validate.sh new file mode 100755 index 0000000..f0c2b43 --- /dev/null +++ b/test/validate.sh @@ -0,0 +1,27 @@ +#! /bin/sh + +ENDPOINT="http://localhost:8080" +ACCEPT="Accept: application/json" +CONTENT="Content-Type: application/json" + +curl --silent -H "$ACCEPT" -H "$CONTENT" --retry-all-errors \ + --connect-timeout 5 --max-time 10 --retry 5 --retry-delay 10 --retry-max-time 60 \ + "$ENDPOINT/products?pageSize=1" > page_one.json +cat one.json + +AFTER=`jq '.after | .token' page_one.json | xargs` + +curl --silent -H "$ACCEPT" -H "$CONTENT" --silent \ + "$ENDPOINT/products?pageSize=1&afterToken=$AFTER" > page_two.json + +jq '.data | .[] | .name' page_two.json + +curl --silent -H "$ACCEPT" -H "$CONTENT" "$ENDPOINT/products/search?minPrice=1000&maxPrice=10000" > search.json + +jq '.data | .[] | .name' search.json + +NEW_PRODUCT="{ \"name\": \"Coolest toy\", \"description\": \"All the cool kids have one.\", + \"category\": \"electronics\", \"price\": 9999, \"stock\": 99 }" + +# curl --silent -H "$ACCEPT" -H "$CONTENT" -X POST "$ENDPOINT/products" -d "$NEW_PRODUCT" +