diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acb68f5f8..9f75dd234 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,10 @@ jobs: uses: coursier/cache-action@v6 - name: Run tests run: ./sbt ++${{ matrix.scala }}! test + - name: Run test container + run: docker-compose -f docker-compose.yml up -d + - name: Run integration tests + run: ./sbt ++${{ matrix.scala }}! it:test website: runs-on: ubuntu-20.04 diff --git a/build.sbt b/build.sbt index 1d1f35712..ff73868fb 100644 --- a/build.sbt +++ b/build.sbt @@ -29,7 +29,9 @@ lazy val library = .in(file("modules/library")) .settings(stdSettings("zio-elasticsearch")) .settings(scalacOptions += "-language:higherKinds") + .configs(IntegrationTest) .settings( + Defaults.itSettings, libraryDependencies ++= List( "com.softwaremill.sttp.client3" %% "zio" % "3.8.3", "com.softwaremill.sttp.client3" %% "zio-json" % "3.8.3", @@ -37,8 +39,11 @@ lazy val library = "dev.zio" %% "zio-prelude" % "1.0.0-RC16", "dev.zio" %% "zio-schema" % "0.3.1", "dev.zio" %% "zio-schema-json" % "0.3.1", - "org.apache.commons" % "commons-lang3" % "3.12.0" - ) + "org.apache.commons" % "commons-lang3" % "3.12.0", + "dev.zio" %% "zio-test" % "2.0.4" % IntegrationTest, + "dev.zio" %% "zio-test-sbt" % "2.0.4" % IntegrationTest + ), + testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")) ) lazy val example = diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..d6796a14b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.8' + +services: + elasticsearch: + image: elasticsearch:7.17.6 + container_name: zio-elasticsearch-test + ports: + - "9200:9200" + environment: + discovery.type: "single-node" + xpack.security.enabled: "false" + ES_JAVA_OPTS: "-Xms512m -Xmx512m" diff --git a/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala b/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala new file mode 100644 index 000000000..c26ea94d9 --- /dev/null +++ b/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala @@ -0,0 +1,42 @@ +package zio.elasticsearch + +import zio.elasticsearch.ElasticError.DocumentRetrievingError.{DecoderError, DocumentNotFound} +import zio.test.Assertion.equalTo +import zio.test.TestAspect.nondeterministic +import zio.test._ + +object HttpExecutorSpec extends IntegrationSpec { + + override def spec: Spec[TestEnvironment, Any] = + suite("HTTP Executor")( + suite("retrieving document by ID")( + test("successfully return document") { + checkOnce(genDocumentId, genCustomer) { (documentId, customer) => + val result = for { + _ <- ElasticRequest.upsert[CustomerDocument](index, documentId, customer).execute + document <- ElasticRequest.getById[CustomerDocument](index, documentId).execute + } yield document + + assertZIO(result)(Assertion.isRight(equalTo(customer))) + } + }, + test("return DocumentNotFound if the document does not exist") { + checkOnce(genDocumentId) { documentId => + assertZIO(ElasticRequest.getById[CustomerDocument](index, documentId).execute)( + Assertion.isLeft(equalTo(DocumentNotFound)) + ) + } + }, + test("fail with decoding error") { + checkOnce(genDocumentId, genEmployee) { (documentId, employee) => + val result = for { + _ <- ElasticRequest.upsert[EmployeeDocument](index, documentId, employee).execute + document <- ElasticRequest.getById[CustomerDocument](index, documentId).execute + } yield document + + assertZIO(result)(Assertion.isLeft(equalTo(DecoderError(".address(missing)")))) + } + } + ) @@ nondeterministic + ).provideShared(elasticsearchLayer) +} diff --git a/modules/library/src/it/scala/zio/elasticsearch/IntegrationSpec.scala b/modules/library/src/it/scala/zio/elasticsearch/IntegrationSpec.scala new file mode 100644 index 000000000..1a19661a9 --- /dev/null +++ b/modules/library/src/it/scala/zio/elasticsearch/IntegrationSpec.scala @@ -0,0 +1,30 @@ +package zio.elasticsearch + +import sttp.client3.httpclient.zio.HttpClientZioBackend +import zio.ZLayer +import zio.test.CheckVariants.CheckN +import zio.test.{Gen, ZIOSpecDefault, checkN} + +trait IntegrationSpec extends ZIOSpecDefault { + val elasticsearchLayer: ZLayer[Any, Throwable, ElasticExecutor] = + HttpClientZioBackend.layer() >>> ElasticExecutor.local + + val index: IndexName = IndexName("users") + + def genDocumentId: Gen[Any, DocumentId] = Gen.stringBounded(10, 40)(Gen.alphaNumericChar).map(DocumentId(_)) + + def genCustomer: Gen[Any, CustomerDocument] = for { + id <- Gen.stringBounded(5, 10)(Gen.alphaNumericChar) + name <- Gen.stringBounded(5, 10)(Gen.alphaChar) + address <- Gen.stringBounded(5, 10)(Gen.alphaNumericChar) + balance <- Gen.bigDecimal(100, 10000) + } yield CustomerDocument(id = id, name = name, address = address, balance = balance) + + def genEmployee: Gen[Any, EmployeeDocument] = for { + id <- Gen.stringBounded(5, 10)(Gen.alphaNumericChar) + name <- Gen.stringBounded(5, 10)(Gen.alphaChar) + degree <- Gen.stringBounded(5, 10)(Gen.alphaChar) + } yield EmployeeDocument(id = id, name = name, degree = degree) + + def checkOnce: CheckN = checkN(1) +} diff --git a/modules/library/src/it/scala/zio/elasticsearch/UserDocument.scala b/modules/library/src/it/scala/zio/elasticsearch/UserDocument.scala new file mode 100644 index 000000000..3dbd69478 --- /dev/null +++ b/modules/library/src/it/scala/zio/elasticsearch/UserDocument.scala @@ -0,0 +1,15 @@ +package zio.elasticsearch + +import zio.schema.{DeriveSchema, Schema} + +final case class CustomerDocument(id: String, name: String, address: String, balance: BigDecimal) + +final case class EmployeeDocument(id: String, name: String, degree: String) + +object CustomerDocument { + implicit val schema: Schema[CustomerDocument] = DeriveSchema.gen[CustomerDocument] +} + +object EmployeeDocument { + implicit val schema: Schema[EmployeeDocument] = DeriveSchema.gen[EmployeeDocument] +} diff --git a/modules/library/src/main/scala/zio/elasticsearch/package.scala b/modules/library/src/main/scala/zio/elasticsearch/package.scala index 8eb771391..5687e07a3 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/package.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/package.scala @@ -1,7 +1,8 @@ package zio +import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils._ -import zio.prelude.Assertion._ +import zio.prelude.Assertion.isEmptyString import zio.prelude.AssertionError.failure import zio.prelude.Newtype @@ -19,7 +20,7 @@ package object elasticsearch { if ( name.toLowerCase != name || startsWithAny(name, "+", "-", "_") || - containsAny(name, '\\', '/', '*', '?', '"', '/', '<', '>', '|', ' ', ',', '#', ':') || + containsAny(name, List("*", "?", "\"", "<", ">", "|", " ", ",", "#", ":")) || equalsAny(name, ".", "..") || name.getBytes().length > 255 ) @@ -42,4 +43,7 @@ package object elasticsearch { } type IndexName = IndexName.Type + def containsAny(name: String, params: List[String]): Boolean = + params.exists(StringUtils.contains(name, _)) + }