diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..6c5d8e791 --- /dev/null +++ b/.gitignore @@ -0,0 +1,116 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +### Go template +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof +### Java template +/target/ +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +### OSX template +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea + +## File-based project format: +*.iws +*.iml + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +# Created by .ignore support plugin (hsz.mobi) + +# Maven builds +*/target +*/*/target + +# AWS ECS install scripts generates an SSH key file +weave-ecs-demo-key.pem + +# Load test generates pyc files +*.pyc + +# Ignore Vagrant cache files +*.vagrant/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..267cc9bf9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: java +sudo: required +services: + - docker +jdk: + - oraclejdk8 +install: true + +env: + - GROUP=weaveworksdemos COMMIT=$TRAVIS_COMMIT TAG=$TRAVIS_TAG; + +script: + - set -e + - ./scripts/build.sh; + - ./test/test.sh unit.py + - ./test/test.sh component.py +# - ./test/test.sh container.py --tag $TAG + +after_success: + - set -e; + - ./test/test.sh coveralls.py + - if [ -z "$DOCKER_PASS" ] ; then + echo "This is a build triggered by an external PR. Skipping docker push."; + exit 0; + fi; + - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS; + - ./scripts/push.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..bf46d5e41 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM java:openjdk-8-alpine + +WORKDIR /usr/src/app +COPY ./target/*.jar ./app.jar + +ENTRYPOINT ["java","-Djava.security.egd=file:/dev/urandom","-jar","./app.jar", "--port=80"] diff --git a/README.md b/README.md index 83d369d18..4cd96da1c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ # cart -Carts service for microservices-demo application +A microservices-demo service that provides user account information. + +This build is built, tested and released by travis. + +# Test +`./test/test.sh < python testing file >`. For example: `./test/test.sh unit.py` + +# Build +`GROUP=weaveworksdemos COMMIT=test ./scripts/build.sh` + +# Push +`GROUP=weaveworksdemos COMMIT=test ./scripts/push.sh` diff --git a/docker/cart/Dockerfile b/docker/cart/Dockerfile new file mode 100644 index 000000000..d157724b6 --- /dev/null +++ b/docker/cart/Dockerfile @@ -0,0 +1,6 @@ +FROM java:openjdk-8-alpine + +WORKDIR /usr/src/app +COPY *.jar ./app.jar + +ENTRYPOINT ["java","-Djava.security.egd=file:/dev/urandom","-jar","./app.jar", "--port=80"] diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..1c0b28a44 --- /dev/null +++ b/pom.xml @@ -0,0 +1,131 @@ + + + 4.0.0 + + works.weave.microservices-demo + carts + jar + + carts + Carts service for microservices-demo application + + + org.springframework.boot + spring-boot-starter-parent + 1.4.0.RELEASE + + + + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-data-rest + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + org.springframework.data + spring-data-rest-hal-browser + + + org.springframework.boot + spring-boot-starter-test + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + 1.50.5 + test + + + com.openpojo + openpojo + 0.8.4 + test + + + + + carts + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + 2.19.1 + + + **/Unit*.java + + + **/IT*.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.18.1 + + + **/IT*.java + + + **/Unit*.java + + + + + + integration-test + verify + + + + + + org.jacoco + jacoco-maven-plugin + 0.7.6.201602180812 + + + prepare-agent + + prepare-agent + + + + + + org.eluder.coveralls + coveralls-maven-plugin + 4.2.0 + + + + + + + spring-releases + https://repo.spring.io/libs-release + + + + + spring-releases + https://repo.spring.io/libs-release + + + diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 000000000..acb657dda --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -ev + +SCRIPT_DIR=$(dirname "$0") + +if [[ -z "$GROUP" ]] ; then + echo "Cannot find GROUP env var" + exit 1 +fi + +if [[ -z "$COMMIT" ]] ; then + echo "Cannot find COMMIT env var" + exit 1 +fi + +if [[ "$(uname)" == "Darwin" ]]; then + DOCKER_CMD=docker +else + DOCKER_CMD="sudo docker" +fi +CODE_DIR=$(cd $SCRIPT_DIR/..; pwd) +echo $CODE_DIR +$DOCKER_CMD run --rm -v $HOME/.m2:/root/.m2 -v $CODE_DIR:/usr/src/mymaven -w /usr/src/mymaven maven:3.2-jdk-8 mvn -DskipTests package + +cp $CODE_DIR/target/*.jar $CODE_DIR/docker/cart + +for m in ./docker/*/; do + REPO=${GROUP}/$(basename $m) + $DOCKER_CMD build -t ${REPO}:${COMMIT} $CODE_DIR/$m; +done; diff --git a/scripts/push.sh b/scripts/push.sh new file mode 100755 index 000000000..e993474f5 --- /dev/null +++ b/scripts/push.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +set -ev + +if [[ -z "$GROUP" ]] ; then + echo "Cannot find GROUP env var" + exit 1 +fi + +if [[ -z "$COMMIT" ]] ; then + echo "Cannot find COMMIT env var" + exit 1 +fi + +push() { + DOCKER_PUSH=1; + while [ $DOCKER_PUSH -gt 0 ] ; do + echo "Pushing $1"; + docker push $1; + DOCKER_PUSH=$(echo $?); + if [[ "$DOCKER_PUSH" -gt 0 ]] ; then + echo "Docker push failed with exit code $DOCKER_PUSH"; + fi; + done; +} + +tag_and_push_all() { + if [[ -z "$1" ]] ; then + echo "Please pass the tag" + exit 1 + else + TAG=$1 + fi + for m in ./docker/*/; do + REPO=${GROUP}/$(basename $m) + if [[ "$COMMIT" != "$TAG" ]]; then + docker tag ${REPO}:${COMMIT} ${REPO}:${TAG} + fi + push "$REPO:$TAG"; + done; +} + +# Always push commit +tag_and_push_all $COMMIT + +# Push snapshot when in master +if [ "$TRAVIS_BRANCH" == "master" ]; then + tag_and_push_all snapshot +fi; + +# Push tag and latest when tagged +if [ -n "$TRAVIS_TAG" ]; then + tag_and_push_all ${TRAVIS_TAG} + tag_and_push_all latest +fi; diff --git a/src/main/java/works/weave/socks/cart/CartApplication.java b/src/main/java/works/weave/socks/cart/CartApplication.java new file mode 100644 index 000000000..3cd695f88 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/CartApplication.java @@ -0,0 +1,12 @@ +package works.weave.socks.cart; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CartApplication { + + public static void main(String[] args) { + SpringApplication.run(CartApplication.class, args); + } +} diff --git a/src/main/java/works/weave/socks/cart/action/FirstResultOrDefault.java b/src/main/java/works/weave/socks/cart/action/FirstResultOrDefault.java new file mode 100644 index 000000000..73ee1e81f --- /dev/null +++ b/src/main/java/works/weave/socks/cart/action/FirstResultOrDefault.java @@ -0,0 +1,19 @@ +package works.weave.socks.cart.action; + +import java.util.Collection; +import java.util.function.Supplier; + +public class FirstResultOrDefault implements Supplier { + private final Collection collection; + private final Supplier nonePresent; + + public FirstResultOrDefault(final Collection collection, final Supplier nonePresent) { + this.collection = collection; + this.nonePresent = nonePresent; + } + + @Override + public T get() { + return collection.stream().findFirst().orElseGet(nonePresent); + } +} diff --git a/src/main/java/works/weave/socks/cart/cart/CartContentsResource.java b/src/main/java/works/weave/socks/cart/cart/CartContentsResource.java new file mode 100644 index 000000000..05a98c231 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/cart/CartContentsResource.java @@ -0,0 +1,47 @@ +package works.weave.socks.cart.cart; + +import org.slf4j.Logger; +import works.weave.socks.cart.entities.Cart; +import works.weave.socks.cart.entities.Item; + +import java.util.List; +import java.util.function.Supplier; + +import static org.slf4j.LoggerFactory.getLogger; + +public class CartContentsResource implements Contents { + private final Logger LOG = getLogger(getClass()); + + private final CartDAO cartRepository; + private final Supplier> parent; + + public CartContentsResource(CartDAO cartRepository, Supplier> parent) { + this.cartRepository = cartRepository; + this.parent = parent; + } + + @Override + public Supplier> contents() { + return () -> parentCart().contents(); + } + + @Override + public Runnable add(Supplier item) { + return () -> { + LOG.debug("Adding for user: " + parent.get().value().get().toString() + ", " + item.get()); + cartRepository.save(parentCart().add(item.get())); + }; + } + + @Override + public Runnable delete(Supplier item) { + return () -> { + LOG.debug("Deleting for user: " + parent.get().value().get().toString() + ", " + item.get()); + cartRepository.save(parentCart().remove(item.get())); + }; + } + + private Cart parentCart() { + return parent.get().value().get(); + } +} diff --git a/src/main/java/works/weave/socks/cart/cart/CartDAO.java b/src/main/java/works/weave/socks/cart/cart/CartDAO.java new file mode 100644 index 000000000..9d921e8e7 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/cart/CartDAO.java @@ -0,0 +1,40 @@ +package works.weave.socks.cart.cart; + +import works.weave.socks.cart.entities.Cart; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public interface CartDAO { + void delete(Cart cart); + + Cart save(Cart cart); + + List findByCustomerId(String customerId); + + class Fake implements CartDAO { + private Map cartStore = new HashMap<>(); + + @Override + public void delete(Cart cart) { + cartStore.remove(cart.customerId); + } + + @Override + public Cart save(Cart cart) { + return cartStore.put(cart.customerId, cart); + } + + @Override + public List findByCustomerId(String customerId) { + Cart cart = cartStore.get(customerId); + if (cart != null) { + return Collections.singletonList(cart); + } else { + return Collections.emptyList(); + } + } + } +} diff --git a/src/main/java/works/weave/socks/cart/cart/CartResource.java b/src/main/java/works/weave/socks/cart/cart/CartResource.java new file mode 100644 index 000000000..3f2895262 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/cart/CartResource.java @@ -0,0 +1,46 @@ +package works.weave.socks.cart.cart; + +import works.weave.socks.cart.action.FirstResultOrDefault; +import works.weave.socks.cart.entities.Cart; + +import java.util.function.Supplier; + +public class CartResource implements Resource, HasContents { + private final CartDAO cartRepository; + private final String customerId; + + public CartResource(CartDAO cartRepository, String customerId) { + this.cartRepository = cartRepository; + this.customerId = customerId; + } + + @Override + public Runnable destroy() { + return () -> cartRepository.delete(value().get()); + } + + @Override + public Supplier create() { + return () -> cartRepository.save(new Cart(customerId)); + } + + @Override + public Supplier value() { + return new FirstResultOrDefault<>( + cartRepository.findByCustomerId(customerId), + () -> { + create().get(); + return value().get(); + }); + } + + @Override + public Runnable merge(Cart toMerge) { + return () -> toMerge.contents().forEach(item -> contents().get().add(() -> item).run()); + } + + @Override + public Supplier contents() { + return () -> new CartContentsResource(cartRepository, () -> this); + } +} diff --git a/src/main/java/works/weave/socks/cart/cart/Contents.java b/src/main/java/works/weave/socks/cart/cart/Contents.java new file mode 100644 index 000000000..5c68848cd --- /dev/null +++ b/src/main/java/works/weave/socks/cart/cart/Contents.java @@ -0,0 +1,14 @@ +package works.weave.socks.cart.cart; + +import works.weave.socks.cart.entities.Item; + +import java.util.List; +import java.util.function.Supplier; + +public interface Contents { + Supplier> contents(); + + Runnable add(Supplier item); + + Runnable delete(Supplier item); +} diff --git a/src/main/java/works/weave/socks/cart/cart/HasContents.java b/src/main/java/works/weave/socks/cart/cart/HasContents.java new file mode 100644 index 000000000..62052fbe8 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/cart/HasContents.java @@ -0,0 +1,7 @@ +package works.weave.socks.cart.cart; + +import java.util.function.Supplier; + +public interface HasContents { + Supplier contents(); +} diff --git a/src/main/java/works/weave/socks/cart/cart/Resource.java b/src/main/java/works/weave/socks/cart/cart/Resource.java new file mode 100644 index 000000000..1c6b269a5 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/cart/Resource.java @@ -0,0 +1,47 @@ +package works.weave.socks.cart.cart; + +import works.weave.socks.cart.entities.Cart; + +import java.util.function.Supplier; + +public interface Resource { + Runnable destroy(); + + Supplier create(); + + Supplier value(); + + Runnable merge(T toMerge); + + class CartFake implements Resource { + private final String customerId; + private Cart cart = null; + + public CartFake(String customerId) { + this.customerId = customerId; + } + + @Override + public Runnable destroy() { + return () -> cart = null; + } + + @Override + public Supplier create() { + return () -> cart = new Cart(customerId); + } + + @Override + public Supplier value() { + if (cart == null) { + create().get(); + } + return () -> cart; + } + + @Override + public Runnable merge(Cart toMerge) { + return null; + } + } +} diff --git a/src/main/java/works/weave/socks/cart/configuration/BeanConfiguration.java b/src/main/java/works/weave/socks/cart/configuration/BeanConfiguration.java new file mode 100644 index 000000000..f9caa3305 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/configuration/BeanConfiguration.java @@ -0,0 +1,58 @@ +package works.weave.socks.cart.configuration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import works.weave.socks.cart.cart.CartDAO; +import works.weave.socks.cart.entities.Cart; +import works.weave.socks.cart.entities.Item; +import works.weave.socks.cart.item.ItemDAO; +import works.weave.socks.cart.repositories.CartRepository; +import works.weave.socks.cart.repositories.ItemRepository; + +import java.util.List; + +@Configuration +public class BeanConfiguration { + @Bean + @Autowired + public CartDAO getCartDao(CartRepository cartRepository) { + return new CartDAO() { + @Override + public void delete(Cart cart) { + cartRepository.delete(cart); + } + + @Override + public Cart save(Cart cart) { + return cartRepository.save(cart); + } + + @Override + public List findByCustomerId(String customerId) { + return cartRepository.findByCustomerId(customerId); + } + }; + } + + @Bean + @Autowired + public ItemDAO getItemDao(ItemRepository itemRepository) { + return new ItemDAO() { + @Override + public Item save(Item item) { + return itemRepository.save(item); + } + + @Override + public void destroy(Item item) { + itemRepository.delete(item); + } + + @Override + public Item findOne(String id) { + return itemRepository.findOne(id); + } + }; + } +} diff --git a/src/main/java/works/weave/socks/cart/configuration/ValidationConfiguration.java b/src/main/java/works/weave/socks/cart/configuration/ValidationConfiguration.java new file mode 100644 index 000000000..248a4fde4 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/configuration/ValidationConfiguration.java @@ -0,0 +1,19 @@ +package works.weave.socks.cart.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +@Configuration +public class ValidationConfiguration { + @Bean + public ValidatingMongoEventListener validatingMongoEventListener() { + return new ValidatingMongoEventListener(validator()); + } + + @Bean + public LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } +} diff --git a/src/main/java/works/weave/socks/cart/controllers/CartsController.java b/src/main/java/works/weave/socks/cart/controllers/CartsController.java new file mode 100644 index 000000000..d1326ef5b --- /dev/null +++ b/src/main/java/works/weave/socks/cart/controllers/CartsController.java @@ -0,0 +1,42 @@ +package works.weave.socks.cart.controllers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import works.weave.socks.cart.cart.CartDAO; +import works.weave.socks.cart.cart.CartResource; +import works.weave.socks.cart.entities.Cart; + +@RestController +@RequestMapping(path = "/carts") +public class CartsController { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private CartDAO cartDAO; + + @ResponseStatus(HttpStatus.OK) + @RequestMapping(value = "/{customerId}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) + public Cart get(@PathVariable String customerId) { + return new CartResource(cartDAO, customerId).value().get(); + } + + @ResponseStatus(HttpStatus.ACCEPTED) + @RequestMapping(value = "/{customerId}", method = RequestMethod.DELETE) + public void delete(@PathVariable String customerId) { + new CartResource(cartDAO, customerId).destroy().run(); + } + + @ResponseStatus(HttpStatus.ACCEPTED) + @RequestMapping(value = "/{customerId}/merge", method = RequestMethod.GET) + public void mergeCarts(@PathVariable String customerId, @RequestParam(value = "sessionId") String sessionId) { + logger.debug("Merge carts request received for ids: " + customerId + " and " + sessionId); + CartResource sessionCart = new CartResource(cartDAO, sessionId); + CartResource customerCart = new CartResource(cartDAO, customerId); + customerCart.merge(sessionCart.value().get()).run(); + delete(sessionId); + } +} diff --git a/src/main/java/works/weave/socks/cart/controllers/ItemsController.java b/src/main/java/works/weave/socks/cart/controllers/ItemsController.java new file mode 100644 index 000000000..c596aee4e --- /dev/null +++ b/src/main/java/works/weave/socks/cart/controllers/ItemsController.java @@ -0,0 +1,83 @@ +package works.weave.socks.cart.controllers; + +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import works.weave.socks.cart.cart.CartDAO; +import works.weave.socks.cart.cart.CartResource; +import works.weave.socks.cart.entities.Item; +import works.weave.socks.cart.item.FoundItem; +import works.weave.socks.cart.item.ItemDAO; +import works.weave.socks.cart.item.ItemResource; + +import java.util.List; +import java.util.function.Supplier; + +import static org.slf4j.LoggerFactory.getLogger; + +@RestController +@RequestMapping(value = "/carts/{customerId:.*}/items") +public class ItemsController { + private final Logger LOG = getLogger(getClass()); + + @Autowired + private ItemDAO itemDAO; + @Autowired + private CartsController cartsController; + @Autowired + private CartDAO cartDAO; + + @ResponseStatus(HttpStatus.OK) + @RequestMapping(value = "/{itemId:.*}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) + public Item get(@PathVariable String customerId, @PathVariable String itemId) { + return new FoundItem(() -> getItems(customerId), () -> new Item(itemId)).get(); + } + + @ResponseStatus(HttpStatus.OK) + @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) + public List getItems(@PathVariable String customerId) { + return cartsController.get(customerId).contents(); + } + + @ResponseStatus(HttpStatus.CREATED) + @RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST) + public Item addToCart(@PathVariable String customerId, @RequestBody Item item) { + // If the item does not exist in the cart, create new one in the repository. + FoundItem foundItem = new FoundItem(() -> cartsController.get(customerId).contents(), () -> item); + if (!foundItem.hasItem()) { + Supplier newItem = new ItemResource(itemDAO, () -> item).create(); + LOG.debug("Did not find item. Creating item for user: " + customerId + ", " + newItem.get()); + new CartResource(cartDAO, customerId).contents().get().add(newItem).run(); + return item; + } else { + Item newItem = new Item(foundItem.get(), foundItem.get().quantity() + 1); + LOG.debug("Found item in cart. Incrementing for user: " + customerId + ", " + newItem); + updateItem(customerId, newItem); + return newItem; + } + } + + @ResponseStatus(HttpStatus.ACCEPTED) + @RequestMapping(value = "/{itemId:.*}", method = RequestMethod.DELETE) + public void removeItem(@PathVariable String customerId, @PathVariable String itemId) { + FoundItem foundItem = new FoundItem(() -> getItems(customerId), () -> new Item(itemId)); + Item item = foundItem.get(); + + LOG.debug("Removing item from cart: " + item); + new CartResource(cartDAO, customerId).contents().get().delete(() -> item).run(); + + LOG.debug("Removing item from repository: " + item); + new ItemResource(itemDAO, () -> item).destroy().run(); + } + + @ResponseStatus(HttpStatus.ACCEPTED) + @RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.PATCH) + public void updateItem(@PathVariable String customerId, @RequestBody Item item) { + // Merge old and new items + ItemResource itemResource = new ItemResource(itemDAO, () -> get(customerId, item.itemId())); + LOG.debug("Merging item in cart for user: " + customerId + ", " + item); + itemResource.merge(item).run(); + } +} diff --git a/src/main/java/works/weave/socks/cart/entities/Cart.java b/src/main/java/works/weave/socks/cart/entities/Cart.java new file mode 100644 index 000000000..a4a713d89 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/entities/Cart.java @@ -0,0 +1,70 @@ +package works.weave.socks.cart.entities; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.mapping.Document; + +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; + +@Document +public class Cart { + @NotNull + public String customerId; // Public instead of getters/setters. + @Id + private String id; + @DBRef + private List items = new ArrayList<>(); + + public Cart(String customerId) { + this.customerId = customerId; + } + + public Cart() { + this(null); + } + + public List contents() { + return items; + } + + public Cart add(Item item) { + items.add(item); + return this; + } + + public Cart remove(Item item) { + items.remove(item); + return this; + } + + @Override + public String toString() { + return "Cart{" + + "id='" + id + '\'' + + ", customerId='" + customerId + '\'' + + ", items=" + items + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Cart cart = (Cart) o; + + if (customerId != null ? !customerId.equals(cart.customerId) : cart.customerId != null) return false; + if (id != null ? !id.equals(cart.id) : cart.id != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = customerId != null ? customerId.hashCode() : 0; + result = 31 * result + (id != null ? id.hashCode() : 0); + return result; + } +} diff --git a/src/main/java/works/weave/socks/cart/entities/Item.java b/src/main/java/works/weave/socks/cart/entities/Item.java new file mode 100644 index 000000000..5171eaee4 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/entities/Item.java @@ -0,0 +1,106 @@ +package works.weave.socks.cart.entities; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import javax.validation.constraints.NotNull; + +@Document +public class Item { + @Id + private String id; + + @NotNull(message = "Item Id must not be null") + private String itemId; + private int quantity; + private float unitPrice; + + public Item(String id, String itemId, int quantity, float unitPrice) { + this.id = id; + this.itemId = itemId; + this.quantity = quantity; + this.unitPrice = unitPrice; + } + + public Item() { + this(null, "", 1, 0F); + } + + public Item(String itemId) { + this(null, itemId, 1, 0F); + } + + public Item(Item item, String id) { + this(id, item.itemId, item.quantity, item.unitPrice); + } + + public Item(Item item, int quantity) { + this(item.id(), item.itemId, quantity, item.unitPrice); + } + + public String id() { + return id; + } + + public String itemId() { + return itemId; + } + + public int quantity() { + return quantity; + } + + @Override + public String toString() { + return "Item{" + + "id='" + id + '\'' + + ", itemId='" + itemId + '\'' + + ", quantity=" + quantity + + ", unitPrice=" + unitPrice + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Item item = (Item) o; + + return itemId != null ? itemId.equals(item.itemId) : item.itemId == null; + } + + // ****** Crappy getter/setters for Jackson JSON invoking ******** + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getItemId() { + return itemId; + } + + public void setItemId(String itemId) { + this.itemId = itemId; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + public float getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(float unitPrice) { + this.unitPrice = unitPrice; + } +} \ No newline at end of file diff --git a/src/main/java/works/weave/socks/cart/item/FoundItem.java b/src/main/java/works/weave/socks/cart/item/FoundItem.java new file mode 100644 index 000000000..07c5dbaa3 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/item/FoundItem.java @@ -0,0 +1,37 @@ +package works.weave.socks.cart.item; + +import org.slf4j.Logger; +import works.weave.socks.cart.entities.Item; + +import java.util.List; +import java.util.function.Supplier; + +import static org.slf4j.LoggerFactory.getLogger; + +public class FoundItem implements Supplier { + private final Logger LOG = getLogger(getClass()); + private final Supplier> items; + private final Supplier item; + + public FoundItem(Supplier> items, Supplier item) { + this.items = items; + this.item = item; + } + + @Override + public Item get() { + return items.get().stream() + .filter(item.get()::equals) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Cannot find item in cart")); + } + + public boolean hasItem() { + boolean present = items.get().stream() + .filter(item.get()::equals) + .findFirst() + .isPresent(); + LOG.debug((present ? "Found" : "Didn't find") + " item: " + item.get() + ", in: " + items.get()); + return present; + } +} diff --git a/src/main/java/works/weave/socks/cart/item/ItemDAO.java b/src/main/java/works/weave/socks/cart/item/ItemDAO.java new file mode 100644 index 000000000..5f66760a0 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/item/ItemDAO.java @@ -0,0 +1,35 @@ +package works.weave.socks.cart.item; + +import works.weave.socks.cart.entities.Item; + +import java.util.HashMap; +import java.util.Map; + +public interface ItemDAO { + Item save(Item item); + + void destroy(Item item); + + Item findOne(String id); + + class Fake implements ItemDAO { + private Map store = new HashMap<>(); + + @Override + public Item save(Item item) { + return store.put(item.itemId(), item); + } + + @Override + public void destroy(Item item) { + store.remove(item.itemId()); + + } + + @Override + public Item findOne(String id) { + return store.entrySet().stream().filter(i -> i.getValue().id().equals(id)).map(Map.Entry::getValue) + .findFirst().orElse(null); + } + } +} diff --git a/src/main/java/works/weave/socks/cart/item/ItemResource.java b/src/main/java/works/weave/socks/cart/item/ItemResource.java new file mode 100644 index 000000000..1753f2a13 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/item/ItemResource.java @@ -0,0 +1,36 @@ +package works.weave.socks.cart.item; + +import works.weave.socks.cart.cart.Resource; +import works.weave.socks.cart.entities.Item; + +import java.util.function.Supplier; + +public class ItemResource implements Resource { + private final ItemDAO itemRepository; + private final Supplier item; + + public ItemResource(ItemDAO itemRepository, Supplier item) { + this.itemRepository = itemRepository; + this.item = item; + } + + @Override + public Runnable destroy() { + return () -> itemRepository.destroy(value().get()); + } + + @Override + public Supplier create() { + return () -> itemRepository.save(item.get()); + } + + @Override + public Supplier value() { + return item; // Basically a null method. Gets the item from the supplier. + } + + @Override + public Runnable merge(Item toMerge) { + return () -> itemRepository.save(new Item(value().get(), toMerge.quantity())); + } +} diff --git a/src/main/java/works/weave/socks/cart/repositories/CartRepository.java b/src/main/java/works/weave/socks/cart/repositories/CartRepository.java new file mode 100644 index 000000000..47c68d997 --- /dev/null +++ b/src/main/java/works/weave/socks/cart/repositories/CartRepository.java @@ -0,0 +1,14 @@ +package works.weave.socks.cart.repositories; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import works.weave.socks.cart.entities.Cart; + +import java.util.List; + +@RepositoryRestResource(exported = false) +public interface CartRepository extends MongoRepository { + List findByCustomerId(@Param("custId") String id); +} + diff --git a/src/main/java/works/weave/socks/cart/repositories/ItemRepository.java b/src/main/java/works/weave/socks/cart/repositories/ItemRepository.java new file mode 100644 index 000000000..0c832818a --- /dev/null +++ b/src/main/java/works/weave/socks/cart/repositories/ItemRepository.java @@ -0,0 +1,10 @@ +package works.weave.socks.cart.repositories; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import works.weave.socks.cart.entities.Item; + +@RepositoryRestResource +public interface ItemRepository extends MongoRepository { +} + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 000000000..6001fe6ec --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,2 @@ +server.port=${port:8081} +spring.data.mongodb.uri=mongodb://${db:cart-db}:27017/data diff --git a/src/test/java/works/weave/socks/cart/action/UnitFirstResultOrDefault.java b/src/test/java/works/weave/socks/cart/action/UnitFirstResultOrDefault.java new file mode 100644 index 000000000..b2834b118 --- /dev/null +++ b/src/test/java/works/weave/socks/cart/action/UnitFirstResultOrDefault.java @@ -0,0 +1,33 @@ +package works.weave.socks.cart.action; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +public class UnitFirstResultOrDefault { + @Test + public void whenEmptyUsesDefault() { + String defaultValue = "test"; + FirstResultOrDefault CUT = new FirstResultOrDefault<>(Collections.emptyList(), () -> defaultValue); + assertThat(CUT.get(), equalTo(defaultValue)); + } + + @Test + public void whenNotEmptyUseFirst() { + String testValue = "test"; + FirstResultOrDefault CUT = new FirstResultOrDefault<>(Arrays.asList(testValue), () -> "nonDefault"); + assertThat(CUT.get(), equalTo(testValue)); + } + + @Test + public void whenMultipleNotEmptyUseFirst() { + String testValue = "test"; + FirstResultOrDefault CUT = new FirstResultOrDefault<>(Arrays.asList(testValue, "test2"), () -> + "nonDefault"); + assertThat(CUT.get(), equalTo(testValue)); + } +} diff --git a/src/test/java/works/weave/socks/cart/cart/UnitCartContentsResource.java b/src/test/java/works/weave/socks/cart/cart/UnitCartContentsResource.java new file mode 100644 index 000000000..9d17e7742 --- /dev/null +++ b/src/test/java/works/weave/socks/cart/cart/UnitCartContentsResource.java @@ -0,0 +1,42 @@ +package works.weave.socks.cart.cart; + +import org.hamcrest.collection.IsCollectionWithSize; +import org.junit.Test; +import works.weave.socks.cart.entities.Cart; +import works.weave.socks.cart.entities.Item; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; + +public class UnitCartContentsResource { + private final String customerId = "testId"; + private final CartDAO.Fake fakeDAO = new CartDAO.Fake(); + private final Resource fakeCartResource = new Resource.CartFake(customerId); + + @Test + public void shouldAddAndReturnContents() { + CartContentsResource contentsResource = new CartContentsResource(fakeDAO, () -> fakeCartResource); + Item item = new Item("testId"); + contentsResource.add(() -> item).run(); + assertThat(contentsResource.contents().get(), IsCollectionWithSize.hasSize(1)); + assertThat(contentsResource.contents().get(), containsInAnyOrder(item)); + } + + @Test + public void shouldStartEmpty() { + CartContentsResource contentsResource = new CartContentsResource(fakeDAO, () -> fakeCartResource); + assertThat(contentsResource.contents().get(), IsCollectionWithSize.hasSize(0)); + } + + @Test + public void shouldDeleteItemFromCart() { + CartContentsResource contentsResource = new CartContentsResource(fakeDAO, () -> fakeCartResource); + Item item = new Item("testId"); + contentsResource.add(() -> item).run(); + assertThat(contentsResource.contents().get(), IsCollectionWithSize.hasSize(1)); + assertThat(contentsResource.contents().get(), containsInAnyOrder(item)); + Item item2 = new Item(item.itemId()); + contentsResource.delete(() -> item2).run(); + assertThat(contentsResource.contents().get(), IsCollectionWithSize.hasSize(0)); + } +} diff --git a/src/test/java/works/weave/socks/cart/cart/UnitCartResource.java b/src/test/java/works/weave/socks/cart/cart/UnitCartResource.java new file mode 100644 index 000000000..674f3b4b9 --- /dev/null +++ b/src/test/java/works/weave/socks/cart/cart/UnitCartResource.java @@ -0,0 +1,83 @@ +package works.weave.socks.cart.cart; + +import org.junit.Test; +import works.weave.socks.cart.entities.Cart; +import works.weave.socks.cart.entities.Item; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +public class UnitCartResource { + + private final String customerId = "testId"; + private final CartDAO.Fake fake = new CartDAO.Fake(); + + @Test + public void whenCartExistsUseThat() { + Cart cart = new Cart(customerId); + fake.save(cart); + CartResource cartResource = new CartResource(fake, customerId); + assertThat(cartResource.value().get(), equalTo(cart)); + } + + @Test + public void whenCartDoesntExistCreateNew() { + CartResource cartResource = new CartResource(fake, customerId); + assertThat(cartResource.value().get(), is(notNullValue())); + assertThat(cartResource.value().get().customerId, is(equalTo(customerId))); + } + + @Test + public void whenDestroyRemoveItem() { + Cart cart = new Cart(customerId); + fake.save(cart); + CartResource cartResource = new CartResource(fake, customerId); + cartResource.destroy().run(); + assertThat(fake.findByCustomerId(customerId), is(empty())); + } + + @Test + public void whenDestroyOnEmptyStillEmpty() { + CartResource cartResource = new CartResource(fake, customerId); + cartResource.destroy().run(); + assertThat(fake.findByCustomerId(customerId), is(empty())); + } + + @Test + public void whenCreateDoCreate() { + CartResource cartResource = new CartResource(fake, customerId); + cartResource.create().get(); + assertThat(fake.findByCustomerId(customerId), is(not(empty()))); + } + + @Test + public void contentsShouldBeEmptyWhenNew() { + CartResource cartResource = new CartResource(fake, customerId); + cartResource.create().get(); + assertThat(cartResource.contents().get().contents().get(), is(empty())); + } + + @Test + public void mergedItemsShouldBeInCart() { + String person1 = "person1"; + String person2 = "person2"; + Item person1Item = new Item("item1"); + Item person2Item = new Item("item2"); + CartResource cartResource = new CartResource(fake, person1); + cartResource.contents().get().add(() -> person1Item).run(); + CartResource cartResourceToMerge = new CartResource(fake, person2); + cartResourceToMerge.contents().get().add(() -> person2Item).run(); + cartResource.merge(cartResourceToMerge.value().get()).run(); + assertThat(cartResource.contents().get().contents().get(), hasSize(2)); + assertThat(cartResource.contents().get().contents().get().get(0), anyOf(equalTo(person1Item), equalTo + (person2Item))); + assertThat(cartResource.contents().get().contents().get().get(1), anyOf(equalTo(person1Item), equalTo + (person2Item))); + assertThat(cartResourceToMerge.contents().get().contents().get(), hasSize(1)); + } +} diff --git a/src/test/java/works/weave/socks/cart/controllers/UnitCartsController.java b/src/test/java/works/weave/socks/cart/controllers/UnitCartsController.java new file mode 100644 index 000000000..274dc8233 --- /dev/null +++ b/src/test/java/works/weave/socks/cart/controllers/UnitCartsController.java @@ -0,0 +1,96 @@ +package works.weave.socks.cart.controllers; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import works.weave.socks.cart.cart.CartDAO; +import works.weave.socks.cart.entities.Cart; +import works.weave.socks.cart.entities.Item; +import works.weave.socks.cart.item.ItemDAO; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.junit.Assert.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class UnitCartsController { + + @Autowired + private ItemsController itemsController; + + @Autowired + private CartDAO cartDAO; + + @Autowired + private CartsController cartsController; + + + @Test + public void shouldGetCart() { + String customerId = "customerIdGet"; + Cart cart = new Cart(customerId); + cartDAO.save(cart); + Cart gotCart = cartsController.get(customerId); + assertThat(gotCart, is(equalTo(cart))); + assertThat(cartDAO.findByCustomerId(customerId).get(0), is(equalTo(cart))); + } + + @Test + public void shouldDeleteCart() { + String customerId = "customerIdGet"; + Cart cart = new Cart(customerId); + cartDAO.save(cart); + cartsController.delete(customerId); + assertThat(cartDAO.findByCustomerId(customerId), is(empty())); + } + + @Test + public void shouldMergeItemsInCartsTogether() { + String customerId1 = "customerId1"; + Cart cart1 = new Cart(customerId1); + Item itemId1 = new Item("itemId1"); + cart1.add(itemId1); + cartDAO.save(cart1); + String customerId2 = "customerId2"; + Cart cart2 = new Cart(customerId2); + Item itemId2 = new Item("itemId2"); + cart2.add(itemId2); + cartDAO.save(cart2); + + cartsController.mergeCarts(customerId1, customerId2); + assertThat(cartDAO.findByCustomerId(customerId1).get(0).contents(), is(hasSize(2))); + assertThat(cartDAO.findByCustomerId(customerId1).get(0).contents(), is(containsInAnyOrder(itemId1, itemId2))); + assertThat(cartDAO.findByCustomerId(customerId2), is(empty())); + } + + @Configuration + static class ItemsControllerTestConfiguration { + @Bean + public ItemsController itemsController() { + return new ItemsController(); + } + + @Bean + public CartsController cartsController() { + return new CartsController(); + } + + @Bean + public ItemDAO itemDAO() { + return new ItemDAO.Fake(); + } + + @Bean + public CartDAO cartDAO() { + return new CartDAO.Fake(); + } + } +} diff --git a/src/test/java/works/weave/socks/cart/controllers/UnitItemsController.java b/src/test/java/works/weave/socks/cart/controllers/UnitItemsController.java new file mode 100644 index 000000000..ba07440e7 --- /dev/null +++ b/src/test/java/works/weave/socks/cart/controllers/UnitItemsController.java @@ -0,0 +1,95 @@ +package works.weave.socks.cart.controllers; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import works.weave.socks.cart.cart.CartDAO; +import works.weave.socks.cart.entities.Item; +import works.weave.socks.cart.item.ItemDAO; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.junit.Assert.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class UnitItemsController { + + @Autowired + private ItemsController itemsController; + + @Autowired + private ItemDAO itemDAO; + + @Autowired + private CartsController cartsController; + + @Test + public void whenNewItemAdd() { + Item item = new Item("id", "itemId", 1, 0F); + String customerId = "customerIdAdd"; + itemsController.addToCart(customerId, item); + assertThat(itemsController.getItems(customerId), is(hasSize(1))); + assertThat(itemsController.getItems(customerId), is(org.hamcrest.CoreMatchers.hasItem(item))); + } + + @Test + public void whenExistIncrementQuantity() { + Item item = new Item("id", "itemId", 1, 0F); + String customerId = "customerIdIncrement"; + itemsController.addToCart(customerId, item); + itemsController.addToCart(customerId, item); + assertThat(itemsController.getItems(customerId), is(hasSize(1))); + assertThat(itemsController.getItems(customerId), is(org.hamcrest.CoreMatchers.hasItem(item))); + assertThat(itemDAO.findOne(item.id()).quantity(), is(equalTo(2))); + } + + @Test + public void shouldRemoveItemFromCart() { + Item item = new Item("id", "itemId", 1, 0F); + String customerId = "customerIdRemove"; + itemsController.addToCart(customerId, item); + assertThat(itemsController.getItems(customerId), is(hasSize(1))); + itemsController.removeItem(customerId, item.itemId()); + assertThat(itemsController.getItems(customerId), is(hasSize(0))); + } + + @Test + public void shouldSetQuantity() { + Item item = new Item("id", "itemId", 1, 0F); + String customerId = "customerIdQuantity"; + itemsController.addToCart(customerId, item); + assertThat(itemsController.getItems(customerId).get(0).quantity(), is(equalTo(item.quantity()))); + Item anotherItem = new Item(item, 15); + itemsController.updateItem(customerId, anotherItem); + assertThat(itemDAO.findOne(item.id()).quantity(), is(equalTo(anotherItem.quantity()))); + } + + @Configuration + static class ItemsControllerTestConfiguration { + @Bean + public ItemsController itemsController() { + return new ItemsController(); + } + + @Bean + public CartsController cartsController() { + return new CartsController(); + } + + @Bean + public ItemDAO itemDAO() { + return new ItemDAO.Fake(); + } + + @Bean + public CartDAO cartDAO() { + return new CartDAO.Fake(); + } + } +} diff --git a/src/test/java/works/weave/socks/cart/item/UnitFoundItem.java b/src/test/java/works/weave/socks/cart/item/UnitFoundItem.java new file mode 100644 index 000000000..70ae862f3 --- /dev/null +++ b/src/test/java/works/weave/socks/cart/item/UnitFoundItem.java @@ -0,0 +1,23 @@ +package works.weave.socks.cart.item; + +import org.junit.Test; +import works.weave.socks.cart.entities.Item; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +public class UnitFoundItem { + @Test + public void findOneItem() { + List list = new ArrayList<>(); + String testId = "testId"; + Item testAnswer = new Item(testId); + list.add(testAnswer); + FoundItem foundItem = new FoundItem(() -> list, () -> testAnswer); + assertThat(foundItem.get(), is(equalTo(testAnswer))); + } +} diff --git a/src/test/java/works/weave/socks/cart/item/UnitItemResource.java b/src/test/java/works/weave/socks/cart/item/UnitItemResource.java new file mode 100644 index 000000000..068d9500b --- /dev/null +++ b/src/test/java/works/weave/socks/cart/item/UnitItemResource.java @@ -0,0 +1,34 @@ +package works.weave.socks.cart.item; + +import org.junit.Test; +import works.weave.socks.cart.entities.Item; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + + +public class UnitItemResource { + private ItemDAO itemDAO = new ItemDAO.Fake(); + + @Test + public void testCreateAndDestroy() { + Item item = new Item("itemId", "testId", 1, 0F); + ItemResource itemResource = new ItemResource(itemDAO, () -> item); + itemResource.create().get(); + assertThat(itemDAO.findOne(item.id()), is(equalTo(item))); + itemResource.destroy().run(); + assertThat(itemDAO.findOne(item.id()), is(nullValue())); + } + + @Test + public void mergedItemShouldHaveNewQuantity() { + Item item = new Item("itemId", "testId", 1, 0F); + ItemResource itemResource = new ItemResource(itemDAO, () -> item); + assertThat(itemResource.value().get(), is(equalTo(item))); + Item newItem = new Item(item, 10); + itemResource.merge(newItem).run(); + assertThat(itemDAO.findOne(item.id()).quantity(), is(equalTo(newItem.quantity()))); + } +} diff --git a/src/test/java/works/weave/socks/cart/repositories/ITCartRepository.java b/src/test/java/works/weave/socks/cart/repositories/ITCartRepository.java new file mode 100644 index 000000000..a0f3d6d1a --- /dev/null +++ b/src/test/java/works/weave/socks/cart/repositories/ITCartRepository.java @@ -0,0 +1,53 @@ +package works.weave.socks.cart.repositories; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import works.weave.socks.cart.entities.Cart; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringRunner.class) +@EnableAutoConfiguration +public class ITCartRepository { + @Autowired + private CartRepository cartRepository; + + @Before + public void removeAllData() { + cartRepository.deleteAll(); + } + + @Test + public void testCartSave() { + Cart original = new Cart("customerId"); + Cart saved = cartRepository.save(original); + + assertEquals(1, cartRepository.count()); + assertEquals(original, saved); + } + + @Test + public void testCartGetDefault() { + Cart original = new Cart("customerId"); + Cart saved = cartRepository.save(original); + + assertEquals(1, cartRepository.count()); + assertEquals(original, saved); + } + + @Test + public void testSearchCustomerById() { + Cart original = new Cart("customerId"); + cartRepository.save(original); + + List found = cartRepository.findByCustomerId(original.customerId); + assertEquals(1, found.size()); + assertEquals(original, found.get(0)); + } +} diff --git a/src/test/java/works/weave/socks/cart/repositories/ITItemRepository.java b/src/test/java/works/weave/socks/cart/repositories/ITItemRepository.java new file mode 100644 index 000000000..93bb87afb --- /dev/null +++ b/src/test/java/works/weave/socks/cart/repositories/ITItemRepository.java @@ -0,0 +1,32 @@ +package works.weave.socks.cart.repositories; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import works.weave.socks.cart.entities.Item; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringRunner.class) +@EnableAutoConfiguration +public class ITItemRepository { + @Autowired + private ItemRepository itemRepository; + + @Before + public void removeAllData() { + itemRepository.deleteAll(); + } + + @Test + public void testCustomerSave() { + Item original = new Item("id", "itemId", 1, 0.99F); + Item saved = itemRepository.save(original); + + assertEquals(1, itemRepository.count()); + assertEquals(original, saved); + } +} diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 000000000..8b921b33d --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.6-alpine + +RUN apk add --no-cache \ + ca-certificates \ + curl \ + openssl + +ENV DOCKER_BUCKET get.docker.com +ENV DOCKER_VERSION 1.8.3 + +RUN set -x \ + && curl -fSL "https://${DOCKER_BUCKET}/builds/Linux/x86_64/docker-${DOCKER_VERSION}.tgz" -o docker.tgz \ + && tar -xzvf docker.tgz \ + && docker -v + +RUN pip install requests diff --git a/test/component.py b/test/component.py new file mode 100644 index 000000000..262300705 --- /dev/null +++ b/test/component.py @@ -0,0 +1,19 @@ +import os +import unittest +from os.path import expanduser + +from util.Docker import Docker + + +class JavaServices(unittest.TestCase): + def test_maven(self): + script_dir = os.path.dirname(os.path.realpath(__file__)) + code_dir = script_dir + "/.." + home = expanduser("~") + command = ['docker', 'run', '--rm', '-v', home + '/.m2:/root/.m2', '-v', code_dir + ':/usr/src/mymaven', '-w', + '/usr/src/mymaven', 'maven:3.2-jdk-8', 'mvn', 'integration-test'] + print(Docker().execute(command)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/container.py b/test/container.py new file mode 100644 index 000000000..426fb3f58 --- /dev/null +++ b/test/container.py @@ -0,0 +1,59 @@ +import argparse +import sys +import unittest +from time import sleep + +from util.Api import Api +from util.Docker import Docker +from util.Dredd import Dredd + + +class CartContainerTest(unittest.TestCase): + TAG = "latest" + container_name = Docker().random_container_name('cart') + mongo_container_name = Docker().random_container_name('cart-db') + + def __init__(self, methodName='runTest'): + super(CartContainerTest, self).__init__(methodName) + self.ip = "" + + def setUp(self): + Docker().start_container(container_name=self.mongo_container_name, image="mongo", host="cart-db") + command = ['docker', 'run', + '-d', + '--name', CartContainerTest.container_name, + '-h', 'cart', + '--link', + CartContainerTest.mongo_container_name, + 'weaveworksdemos/cart:' + self.TAG] + Docker().execute(command) + self.ip = Docker().get_container_ip(CartContainerTest.container_name) + + def tearDown(self): + Docker().kill_and_remove(CartContainerTest.container_name) + Docker().kill_and_remove(CartContainerTest.mongo_container_name) + + def test_api_validated(self): + limit = 60 + while Api().noResponse('http://' + self.ip + ':80/carts/579f21ae98684924944651bf'): + if limit == 0: + self.fail("Couldn't get the API running") + limit = limit - 1 + sleep(1) + + out = Dredd().test_against_endpoint("carts/carts.json", CartContainerTest.container_name, "http://cart/", + "mongodb://cart-db:27017/data", self.mongo_container_name) + self.assertGreater(out.find("0 failing"), -1) + self.assertGreater(out.find("0 errors"), -1) + print(out) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--tag', default="latest", help='The tag of the image to use. (default: latest)') + parser.add_argument('unittest_args', nargs='*') + args = parser.parse_args() + CartContainerTest.TAG = args.tag + # Now set the sys.argv to the unittest_args (leaving sys.argv[0] alone) + sys.argv[1:] = args.unittest_args + unittest.main() diff --git a/test/coveralls.py b/test/coveralls.py new file mode 100644 index 000000000..097b10792 --- /dev/null +++ b/test/coveralls.py @@ -0,0 +1,36 @@ +import os +import unittest +from os.path import expanduser + +from util.Docker import Docker + + +class JavaServices(unittest.TestCase): + def test_maven(self): + script_dir = os.path.dirname(os.path.realpath(__file__)) + code_dir = script_dir + "/.." + home = expanduser("~") + command = ['docker', 'run', '--rm', + '-v', home + '/.m2:/root/.m2', + '-v', code_dir + ':/usr/src/mymaven', + '-w', '/usr/src/mymaven', + 'maven:3.2-jdk-8', + 'mvn', + '-DrepoToken=' + os.getenv('COVERALLS_TOKEN'), + '-DserviceJobId=' + os.getenv('TRAVIS_JOB_ID'), + '-Dbranch=' + os.getenv('TRAVIS_BRANCH'), + '-DpullRequest=' + os.getenv('TRAVIS_PULL_REQUEST'), + '-DserviceName=' + os.getenv('TRAVIS'), + 'verify', + 'jacoco:report', + 'coveralls:report'] + print("Coveralls command: ", + '-DserviceJobId=' + os.getenv('TRAVIS_JOB_ID'), + '-Dbranch=' + os.getenv('TRAVIS_BRANCH'), + '-DpullRequest=' + os.getenv('TRAVIS_PULL_REQUEST'), + '-DserviceName=' + 'TRAVIS') + print(Docker().execute(command)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test.sh b/test/test.sh new file mode 100755 index 000000000..f84b64db1 --- /dev/null +++ b/test/test.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -ev + +SCRIPT_DIR=`dirname "$0"` +SCRIPT_NAME=`basename "$0"` +SSH_OPTS=-oStrictHostKeyChecking=no + +if [[ "$(uname)" == "Darwin" ]]; then + DOCKER_CMD=docker +else + DOCKER_CMD="sudo docker" +fi + +if [[ -z $($DOCKER_CMD images | grep test-container) ]] ; then + echo "Building test container" + docker build -t test-container $SCRIPT_DIR > /dev/null +fi + +echo "Testing $1" +CODE_DIR=$(cd $SCRIPT_DIR/..; pwd) +echo "$@" +$DOCKER_CMD run \ + --rm \ + --name test \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $CODE_DIR:$CODE_DIR -w $CODE_DIR \ + -e COVERALLS_TOKEN=$COVERALLS_TOKEN \ + -e TRAVIS_JOB_ID=$TRAVIS_JOB_ID \ + -e TRAVIS_BRANCH=$TRAVIS_BRANCH \ + -e TRAVIS_PULL_REQUEST=$TRAVIS_PULL_REQUEST \ + -e TRAVIS=$TRAVIS \ + test-container \ + sh -c export PYTHONPATH=\$PYTHONPATH:\$PWD/test ; python test/"$@" diff --git a/test/unit.py b/test/unit.py new file mode 100644 index 000000000..0b8acc597 --- /dev/null +++ b/test/unit.py @@ -0,0 +1,19 @@ +import os +import unittest +from os.path import expanduser + +from util.Docker import Docker + + +class JavaServices(unittest.TestCase): + def test_maven(self): + script_dir = os.path.dirname(os.path.realpath(__file__)) + code_dir = script_dir + "/.." + home = expanduser("~") + command = ['docker', 'run', '--rm', '-v', home + '/.m2:/root/.m2', '-v', code_dir + ':/usr/src/mymaven', '-w', + '/usr/src/mymaven', 'maven:3.2-jdk-8', 'mvn', 'test'] + print(Docker().execute(command)) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/util/Api.py b/test/util/Api.py new file mode 100644 index 000000000..34953e00b --- /dev/null +++ b/test/util/Api.py @@ -0,0 +1,10 @@ +import requests + + +class Api: + def noResponse(self, url): + try: + r = requests.get(url, timeout=5) + except requests.exceptions.ConnectionError: + return True + return r.status_code > 299 diff --git a/test/util/Docker.py b/test/util/Docker.py new file mode 100644 index 000000000..c739f03d7 --- /dev/null +++ b/test/util/Docker.py @@ -0,0 +1,39 @@ +import re +from random import random +from subprocess import Popen, PIPE + + +# From http://blog.bordage.pro/avoid-docker-py/ +class Docker: + def kill_and_remove(self, ctr_name): + command = ['docker', 'rm', '-f', ctr_name] + self.execute(command) + + def random_container_name(self, prefix): + retstr = prefix + '-' + for i in range(5): + retstr += chr(int(round(random() * (122 - 97) + 97))) + return retstr + + def get_container_ip(self, ctr_name): + command = ['docker', 'inspect', + '--format', '\'{{.NetworkSettings.IPAddress}}\'', + ctr_name] + return re.sub(r'[^0-9.]*', '', self.execute(command)) + + def execute(self, command): + print("Running: " + ' '.join(command)) + p = Popen(command, stdout=PIPE, stderr=PIPE) + out = p.stdout.read() + stderr = p.stderr.read() + if p.wait() != 0: + p.stdout.close() + p.stderr.close() + raise RuntimeError(str(stderr.decode('utf-8'))) + p.stdout.close() + p.stderr.close() + return str(out.decode('utf-8')) + + def start_container(self, container_name="", image="", cmd="", host=""): + command = ['docker', 'run', '-d', '-h', host, '--name', container_name, image] + self.execute(command) diff --git a/test/util/Dredd.py b/test/util/Dredd.py new file mode 100644 index 000000000..03b0f899a --- /dev/null +++ b/test/util/Dredd.py @@ -0,0 +1,25 @@ +from util.Docker import Docker + + +class Dredd: + image = 'weaveworksdemos/openapi' + container_name = '' + + def test_against_endpoint(self, json_spec, endpoint_container_name, api_endpoint, mongo_endpoint_url, + mongo_container_name): + self.container_name = Docker().random_container_name('openapi') + command = ['docker', 'run', + '-h', 'openapi', + '--name', self.container_name, + '--link', mongo_container_name, + '--link', endpoint_container_name, + '--env', "MONGO_ENDPOINT={0}".format(mongo_endpoint_url), + Dredd.image, + "/usr/src/app/{0}".format(json_spec), + api_endpoint, + "-f", + "/usr/src/app/hooks.js" + ] + out = Docker().execute(command) + Docker().kill_and_remove(self.container_name) + return out diff --git a/test/util/__init__.py b/test/util/__init__.py new file mode 100644 index 000000000..e69de29bb