diff --git a/go.mod b/go.mod index fbdffeae..6399fc28 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,11 @@ require ( github.com/oapi-codegen/runtime v1.1.2 github.com/streadway/amqp v1.1.0 github.com/testcontainers/testcontainers-go v0.39.0 + github.com/testcontainers/testcontainers-go/modules/elasticsearch v0.39.0 + github.com/testcontainers/testcontainers-go/modules/kafka v0.39.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0 + github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.39.0 + github.com/testcontainers/testcontainers-go/modules/redis v0.39.0 github.com/testcontainers/testcontainers-go/modules/vault v0.39.0 go.uber.org/zap v1.27.0 ) @@ -35,7 +39,7 @@ require ( github.com/benbjohnson/clock v1.3.0 // indirect github.com/cenkalti/backoff/v3 v3.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/console v1.0.5 // indirect github.com/containerd/continuity v0.4.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect @@ -77,6 +81,7 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect + github.com/mdelapenya/tlscert v0.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -111,6 +116,7 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.36.0 // indirect diff --git a/go.sum b/go.sum index c3b77fa0..c94397a8 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/IBM/sarama v1.42.1 h1:wugyWa15TDEHh2kvq2gAy1IHLjEjuYOYgXz/ruC/OSQ= +github.com/IBM/sarama v1.42.1/go.mod h1:Xxho9HkHd4K/MDUo/T/sOqwtX/17D33++E9Wib6hUdQ= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= @@ -62,8 +64,8 @@ github.com/cenkalti/backoff/v3 v3.1.1 h1:UBHElAnr3ODEbpqPzX8g5sBcASjoLFtt3L/xwJ0 github.com/cenkalti/backoff/v3 v3.1.1/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/compose-spec/compose-go/v2 v2.1.3 h1:bD67uqLuL/XgkAK6ir3xZvNLFPxPScEi1KW7R5esrLE= github.com/compose-spec/compose-go/v2 v2.1.3/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= github.com/confluentinc/confluent-kafka-go/v2 v2.12.0 h1:If5Bi+oJVehEdjuhHa7QEFppQtyexvBXJiuZIloJtIw= @@ -121,12 +123,22 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/eapache/go-resiliency v1.4.0 h1:3OK9bWpPk5q6pbFAaYSEwD9CLUSHG8bnZuqX2yMt3B0= +github.com/eapache/go-resiliency v1.4.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= +github.com/elastic/elastic-transport-go/v8 v8.4.0 h1:EKYiH8CHd33BmMna2Bos1rDNMM89+hdgcymI+KzJCGE= +github.com/elastic/elastic-transport-go/v8 v8.4.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk= github.com/elastic/go-elasticsearch/v7 v7.17.10 h1:TCQ8i4PmIJuBunvBS6bwT2ybzVFxxUhhltAs3Gyu1yo= github.com/elastic/go-elasticsearch/v7 v7.17.10/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4= +github.com/elastic/go-elasticsearch/v8 v8.12.1 h1:QcuFK5LaZS0pSIj/eAEsxmJWmMo7tUs1aVBbzdIgtnE= +github.com/elastic/go-elasticsearch/v8 v8.12.1/go.mod h1:wSzJYrrKPZQ8qPuqAqc6KMR4HrBfHnZORvyL+FMFqq0= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -174,6 +186,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -210,6 +224,8 @@ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= @@ -236,6 +252,16 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/tern/v2 v2.3.3 h1:d6QNRyjk9HttJtSF5pUB8UaXrHwCgEai3/yxYjgci/k= github.com/jackc/tern/v2 v2.3.3/go.mod h1:0/9jqEreuC+ywjB7C5ta6Xkhl+HSaxFmCAggEDcp6v0= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= @@ -346,6 +372,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -362,6 +390,12 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc h1:zAsgcP8MhzAbhMnB1QQ2O7ZhWYVGYSR2iVcjzQuPV+o= github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8= +github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= +github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -404,8 +438,16 @@ github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= github.com/testcontainers/testcontainers-go/modules/compose v0.33.0 h1:PyrUOF+zG+xrS3p+FesyVxMI+9U+7pwhZhyFozH3jKY= github.com/testcontainers/testcontainers-go/modules/compose v0.33.0/go.mod h1:oqZaUnFEskdZriO51YBquku/jhgzoXHPot6xe1DqKV4= +github.com/testcontainers/testcontainers-go/modules/elasticsearch v0.39.0 h1:rf35NQMlo1YxfCrv8HNsSFbZc3EejOc0TOHopHrSUaE= +github.com/testcontainers/testcontainers-go/modules/elasticsearch v0.39.0/go.mod h1:/6i0qhcP1IC/m7dV9yWg1nl5P6deVh3tS09AmNamOAU= +github.com/testcontainers/testcontainers-go/modules/kafka v0.39.0 h1:Nkrk5fjoHbj1bqE8OkMT25Y8bcSDgS5smdVaX3Xkfyc= +github.com/testcontainers/testcontainers-go/modules/kafka v0.39.0/go.mod h1:9Si8E8u8DWMUPQpHSSDseA3lXfhyMgVnCfdMWjoqNNw= github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0 h1:REJz+XwNpGC/dCgTfYvM4SKqobNqDBfvhq74s2oHTUM= github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0/go.mod h1:4K2OhtHEeT+JSIFX4V8DkGKsyLa96Y2vLdd3xsxD5HE= +github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.39.0 h1:1bZYBo/Gj8XFIXwOMZOCKR2cj5KR7834HRQiXld1qLY= +github.com/testcontainers/testcontainers-go/modules/rabbitmq v0.39.0/go.mod h1:6QrVnYo9ZclD5lUutAAtQAFx7YNNoufJYvKPgfH+7hs= +github.com/testcontainers/testcontainers-go/modules/redis v0.39.0 h1:p54qELdCx4Gftkxzf44k9RJRRhaO/S5ehP9zo8SUTLM= +github.com/testcontainers/testcontainers-go/modules/redis v0.39.0/go.mod h1:P1mTbHruHqAU2I26y0RADz1BitF59FLbQr7ceqN9bt4= github.com/testcontainers/testcontainers-go/modules/vault v0.39.0 h1:2FdaAcV6qzjh00LwGei9f6EuxdHGvfZ87zAyK+1b/6Q= github.com/testcontainers/testcontainers-go/modules/vault v0.39.0/go.mod h1:noXKEtMDYUMhNL8wPnuRQtMgUzAE2cNYA9M1ZYLv6zk= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= @@ -487,6 +529,8 @@ golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= diff --git a/internal/elasticsearch/task_integration_test.go b/internal/elasticsearch/task_integration_test.go new file mode 100644 index 00000000..50296b08 --- /dev/null +++ b/internal/elasticsearch/task_integration_test.go @@ -0,0 +1,171 @@ +package elasticsearch_test + +import ( + "context" + "testing" + "time" + + esv7 "github.com/elastic/go-elasticsearch/v7" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/elasticsearch" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + esTask "github.com/MarioCarrion/todo-api-microservice-example/internal/elasticsearch" +) + +// setupElasticsearchContainer starts an Elasticsearch container and returns a configured client. +func setupElasticsearchContainer(ctx context.Context, t *testing.T) (*esv7.Client, func()) { + t.Helper() + + esContainer, err := elasticsearch.Run(ctx, "docker.elastic.co/elasticsearch/elasticsearch:7.17.10") + if err != nil { + t.Fatalf("failed to start elasticsearch container: %v", err) + } + + cleanup := func() { + if err := testcontainers.TerminateContainer(esContainer); err != nil { + t.Logf("failed to terminate container: %v", err) + } + } + + cfg := esv7.Config{ + Addresses: []string{esContainer.Settings.Address}, + } + client, err := esv7.NewClient(cfg) + + if err != nil { + cleanup() + t.Fatalf("failed to create elasticsearch client: %v", err) + } + + return client, cleanup +} + +func TestTask_Index_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + client, cleanup := setupElasticsearchContainer(ctx, t) + t.Cleanup(cleanup) + + // Create task repository + taskRepo := esTask.NewTask(client) + + // Test Index method + now := time.Now() + task := internal.Task{ + ID: "test-123", + Description: "Test task for elasticsearch", + Priority: internal.PriorityHigh.Pointer(), + IsDone: false, + Dates: &internal.Dates{ + Start: &now, + Due: &now, + }, + } + + err := taskRepo.Index(ctx, task) + if err != nil { + t.Fatalf("Failed to index task: %v", err) + } + + // Give ES a moment to index + time.Sleep(500 * time.Millisecond) + + // Test Search to verify it was indexed + results, err := taskRepo.Search(ctx, internal.SearchParams{ + Description: internal.ValueToPointer("Test"), + From: 0, + Size: 10, + }) + if err != nil { + t.Fatalf("Failed to search: %v", err) + } + + if len(results.Tasks) == 0 { + t.Error("Expected to find indexed task") + } +} + +func TestTask_Delete_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + client, cleanup := setupElasticsearchContainer(ctx, t) + t.Cleanup(cleanup) + + taskRepo := esTask.NewTask(client) + + // First index a task + task := internal.Task{ + ID: "test-delete", + Description: "Task to delete", + IsDone: false, + } + + err := taskRepo.Index(ctx, task) + if err != nil { + t.Fatalf("Failed to index task: %v", err) + } + + time.Sleep(500 * time.Millisecond) + + // Now delete it + err = taskRepo.Delete(ctx, task.ID) + if err != nil { + t.Fatalf("Failed to delete task: %v", err) + } +} + +func TestTask_Search_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + client, cleanup := setupElasticsearchContainer(ctx, t) + t.Cleanup(cleanup) + + taskRepo := esTask.NewTask(client) + + // Index multiple tasks + tasks := []internal.Task{ + {ID: "1", Description: "High priority task", Priority: internal.PriorityHigh.Pointer(), IsDone: false}, + {ID: "2", Description: "Low priority task", Priority: internal.PriorityLow.Pointer(), IsDone: false}, + {ID: "3", Description: "Completed task", Priority: internal.PriorityMedium.Pointer(), IsDone: true}, + } + + for _, task := range tasks { + if err := taskRepo.Index(ctx, task); err != nil { + t.Fatalf("Failed to index task: %v", err) + } + } + + time.Sleep(1 * time.Second) + + // Search for high priority tasks + results, err := taskRepo.Search(ctx, internal.SearchParams{ + Priority: internal.PriorityHigh.Pointer(), + From: 0, + Size: 10, + }) + if err != nil { + t.Fatalf("Failed to search: %v", err) + } + + if len(results.Tasks) == 0 { + t.Error("Expected to find high priority tasks") + } + + // Search for completed tasks + results, err = taskRepo.Search(ctx, internal.SearchParams{ + IsDone: internal.ValueToPointer(true), + From: 0, + Size: 10, + }) + if err != nil { + t.Fatalf("Failed to search: %v", err) + } + + if len(results.Tasks) == 0 { + t.Error("Expected to find completed tasks") + } +} diff --git a/internal/errors_test.go b/internal/errors_test.go new file mode 100644 index 00000000..c079ab9b --- /dev/null +++ b/internal/errors_test.go @@ -0,0 +1,217 @@ +package internal_test + +import ( + "errors" + "testing" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" +) + +func TestWrapErrorf(t *testing.T) { + t.Parallel() + + originalErr := errors.New("original error") + + tests := []struct { + name string + orig error + code internal.ErrorCode + format string + args []any + expectedMsg string + expectedCode internal.ErrorCode + }{ + { + name: "wrap error with message", + orig: originalErr, + code: internal.ErrorCodeNotFound, + format: "failed to find: %s", + args: []any{"resource"}, + expectedMsg: "failed to find: resource: original error", + expectedCode: internal.ErrorCodeNotFound, + }, + { + name: "wrap error with multiple args", + orig: originalErr, + code: internal.ErrorCodeInvalidArgument, + format: "validation failed for %s on field %s", + args: []any{"Task", "description"}, + expectedMsg: "validation failed for Task on field description: original error", + expectedCode: internal.ErrorCodeInvalidArgument, + }, + { + name: "wrap nil error", + orig: nil, + code: internal.ErrorCodeUnknown, + format: "some operation failed", + args: nil, + expectedMsg: "some operation failed", + expectedCode: internal.ErrorCodeUnknown, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := internal.WrapErrorf(tt.orig, tt.code, tt.format, tt.args...) + + if err.Error() != tt.expectedMsg { + t.Errorf("expected message %q, got %q", tt.expectedMsg, err.Error()) + } + + var ierr *internal.Error + if !errors.As(err, &ierr) { + t.Fatalf("expected error to be *internal.Error, got %T", err) + } + + if ierr.Code() != tt.expectedCode { + t.Errorf("expected code %d, got %d", tt.expectedCode, ierr.Code()) + } + + if tt.orig != nil { + if !errors.Is(err, tt.orig) { + t.Errorf("expected error to wrap original error") + } + + if !errors.Is(ierr, tt.orig) { + t.Errorf("expected Unwrap() to return original error") + } + } + }) + } +} + +func TestNewErrorf(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + code internal.ErrorCode + format string + args []any + expectedMsg string + expectedCode internal.ErrorCode + }{ + { + name: "create error with not found code", + code: internal.ErrorCodeNotFound, + format: "task %s not found", + args: []any{"123"}, + expectedMsg: "task 123 not found", + expectedCode: internal.ErrorCodeNotFound, + }, + { + name: "create error with invalid argument code", + code: internal.ErrorCodeInvalidArgument, + format: "invalid value: %d", + args: []any{42}, + expectedMsg: "invalid value: 42", + expectedCode: internal.ErrorCodeInvalidArgument, + }, + { + name: "create error without args", + code: internal.ErrorCodeUnknown, + format: "unknown error occurred", + args: nil, + expectedMsg: "unknown error occurred", + expectedCode: internal.ErrorCodeUnknown, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := internal.NewErrorf(tt.code, tt.format, tt.args...) + + if err.Error() != tt.expectedMsg { + t.Errorf("expected message %q, got %q", tt.expectedMsg, err.Error()) + } + + var ierr *internal.Error + if !errors.As(err, &ierr) { + t.Fatalf("expected error to be *internal.Error, got %T", err) + } + + if ierr.Code() != tt.expectedCode { + t.Errorf("expected code %d, got %d", tt.expectedCode, ierr.Code()) + } + + if ierr.Unwrap() != nil { + t.Errorf("expected Unwrap() to return nil, got %v", ierr.Unwrap()) + } + }) + } +} + +func TestError_Error(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + err error + expectedMsg string + }{ + { + name: "error without wrapped error", + err: internal.NewErrorf(internal.ErrorCodeNotFound, "not found"), + expectedMsg: "not found", + }, + { + name: "error with wrapped error", + err: internal.WrapErrorf(errors.New("database error"), internal.ErrorCodeUnknown, "operation failed"), + expectedMsg: "operation failed: database error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + if tt.err.Error() != tt.expectedMsg { + t.Errorf("expected message %q, got %q", tt.expectedMsg, tt.err.Error()) + } + }) + } +} + +func TestErrorCode(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + code internal.ErrorCode + }{ + { + name: "ErrorCodeUnknown", + code: internal.ErrorCodeUnknown, + }, + { + name: "ErrorCodeNotFound", + code: internal.ErrorCodeNotFound, + }, + { + name: "ErrorCodeInvalidArgument", + code: internal.ErrorCodeInvalidArgument, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := internal.NewErrorf(tt.code, "test error") + + var ierr *internal.Error + + if !errors.As(err, &ierr) { + t.Fatalf("expected error to be *internal.Error") + } + + if ierr.Code() != tt.code { + t.Errorf("expected code %d, got %d", tt.code, ierr.Code()) + } + }) + } +} diff --git a/internal/kafka/task_integration_test.go b/internal/kafka/task_integration_test.go new file mode 100644 index 00000000..5a55b38e --- /dev/null +++ b/internal/kafka/task_integration_test.go @@ -0,0 +1,117 @@ +package kafka_test + +import ( + "context" + "testing" + + "github.com/confluentinc/confluent-kafka-go/v2/kafka" + "github.com/testcontainers/testcontainers-go" + kafkamodule "github.com/testcontainers/testcontainers-go/modules/kafka" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + kafkaTask "github.com/MarioCarrion/todo-api-microservice-example/internal/kafka" +) + +// setupKafkaProducer starts a Kafka container and returns a configured producer. +func setupKafkaProducer(ctx context.Context, t *testing.T) (*kafka.Producer, func()) { + t.Helper() + + kafkaContainer, err := kafkamodule.Run(ctx, "confluentinc/confluent-local:7.5.0") + if err != nil { + t.Fatalf("failed to start kafka container: %v", err) + } + + cleanup := func() { + if err := testcontainers.TerminateContainer(kafkaContainer); err != nil { + t.Logf("failed to terminate container: %v", err) + } + } + + brokers, err := kafkaContainer.Brokers(ctx) + if err != nil { + cleanup() + t.Fatalf("failed to get brokers: %v", err) + } + + producer, err := kafka.NewProducer(&kafka.ConfigMap{ + "bootstrap.servers": brokers[0], + }) + if err != nil { + cleanup() + t.Fatalf("failed to create producer: %v", err) + } + + cleanupAll := func() { + producer.Close() + cleanup() + } + + return producer, cleanupAll +} + +func TestTask_Created_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + producer, cleanup := setupKafkaProducer(ctx, t) + t.Cleanup(cleanup) + + // Create task publisher + taskPub := kafkaTask.NewTask(producer, "test-tasks") + + // Test Created method + task := internal.Task{ + ID: "test-123", + Description: "Test task", + Priority: internal.PriorityHigh.Pointer(), + IsDone: false, + } + + err := taskPub.Created(ctx, task) + if err != nil { + t.Fatalf("Failed to publish created event: %v", err) + } + + // Flush to ensure message is sent + producer.Flush(5000) +} + +func TestTask_Updated_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + producer, cleanup := setupKafkaProducer(ctx, t) + t.Cleanup(cleanup) + + taskPub := kafkaTask.NewTask(producer, "test-tasks") + + task := internal.Task{ + ID: "test-456", + Description: "Updated task", + IsDone: true, + } + + err := taskPub.Updated(ctx, task) + if err != nil { + t.Fatalf("Failed to publish updated event: %v", err) + } + + producer.Flush(5000) +} + +func TestTask_Deleted_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + producer, cleanup := setupKafkaProducer(ctx, t) + t.Cleanup(cleanup) + + taskPub := kafkaTask.NewTask(producer, "test-tasks") + + err := taskPub.Deleted(ctx, "test-789") + if err != nil { + t.Fatalf("Failed to publish deleted event: %v", err) + } + + producer.Flush(5000) +} diff --git a/internal/memcached/memcachedtesting/searchable_task_store.gen.go b/internal/memcached/memcachedtesting/searchable_task_store.gen.go new file mode 100644 index 00000000..9f56cc45 --- /dev/null +++ b/internal/memcached/memcachedtesting/searchable_task_store.gen.go @@ -0,0 +1,266 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package memcachedtesting + +import ( + "context" + "sync" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + "github.com/MarioCarrion/todo-api-microservice-example/internal/memcached" +) + +type FakeSearchableTaskStore struct { + DeleteStub func(context.Context, string) error + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + arg1 context.Context + arg2 string + } + deleteReturns struct { + result1 error + } + deleteReturnsOnCall map[int]struct { + result1 error + } + IndexStub func(context.Context, internal.Task) error + indexMutex sync.RWMutex + indexArgsForCall []struct { + arg1 context.Context + arg2 internal.Task + } + indexReturns struct { + result1 error + } + indexReturnsOnCall map[int]struct { + result1 error + } + SearchStub func(context.Context, internal.SearchParams) (internal.SearchResults, error) + searchMutex sync.RWMutex + searchArgsForCall []struct { + arg1 context.Context + arg2 internal.SearchParams + } + searchReturns struct { + result1 internal.SearchResults + result2 error + } + searchReturnsOnCall map[int]struct { + result1 internal.SearchResults + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeSearchableTaskStore) Delete(arg1 context.Context, arg2 string) error { + fake.deleteMutex.Lock() + ret, specificReturn := fake.deleteReturnsOnCall[len(fake.deleteArgsForCall)] + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.DeleteStub + fakeReturns := fake.deleteReturns + fake.recordInvocation("Delete", []interface{}{arg1, arg2}) + fake.deleteMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeSearchableTaskStore) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeSearchableTaskStore) DeleteCalls(stub func(context.Context, string) error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = stub +} + +func (fake *FakeSearchableTaskStore) DeleteArgsForCall(i int) (context.Context, string) { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + argsForCall := fake.deleteArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeSearchableTaskStore) DeleteReturns(result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + fake.deleteReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSearchableTaskStore) DeleteReturnsOnCall(i int, result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + if fake.deleteReturnsOnCall == nil { + fake.deleteReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeSearchableTaskStore) Index(arg1 context.Context, arg2 internal.Task) error { + fake.indexMutex.Lock() + ret, specificReturn := fake.indexReturnsOnCall[len(fake.indexArgsForCall)] + fake.indexArgsForCall = append(fake.indexArgsForCall, struct { + arg1 context.Context + arg2 internal.Task + }{arg1, arg2}) + stub := fake.IndexStub + fakeReturns := fake.indexReturns + fake.recordInvocation("Index", []interface{}{arg1, arg2}) + fake.indexMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeSearchableTaskStore) IndexCallCount() int { + fake.indexMutex.RLock() + defer fake.indexMutex.RUnlock() + return len(fake.indexArgsForCall) +} + +func (fake *FakeSearchableTaskStore) IndexCalls(stub func(context.Context, internal.Task) error) { + fake.indexMutex.Lock() + defer fake.indexMutex.Unlock() + fake.IndexStub = stub +} + +func (fake *FakeSearchableTaskStore) IndexArgsForCall(i int) (context.Context, internal.Task) { + fake.indexMutex.RLock() + defer fake.indexMutex.RUnlock() + argsForCall := fake.indexArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeSearchableTaskStore) IndexReturns(result1 error) { + fake.indexMutex.Lock() + defer fake.indexMutex.Unlock() + fake.IndexStub = nil + fake.indexReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSearchableTaskStore) IndexReturnsOnCall(i int, result1 error) { + fake.indexMutex.Lock() + defer fake.indexMutex.Unlock() + fake.IndexStub = nil + if fake.indexReturnsOnCall == nil { + fake.indexReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.indexReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeSearchableTaskStore) Search(arg1 context.Context, arg2 internal.SearchParams) (internal.SearchResults, error) { + fake.searchMutex.Lock() + ret, specificReturn := fake.searchReturnsOnCall[len(fake.searchArgsForCall)] + fake.searchArgsForCall = append(fake.searchArgsForCall, struct { + arg1 context.Context + arg2 internal.SearchParams + }{arg1, arg2}) + stub := fake.SearchStub + fakeReturns := fake.searchReturns + fake.recordInvocation("Search", []interface{}{arg1, arg2}) + fake.searchMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSearchableTaskStore) SearchCallCount() int { + fake.searchMutex.RLock() + defer fake.searchMutex.RUnlock() + return len(fake.searchArgsForCall) +} + +func (fake *FakeSearchableTaskStore) SearchCalls(stub func(context.Context, internal.SearchParams) (internal.SearchResults, error)) { + fake.searchMutex.Lock() + defer fake.searchMutex.Unlock() + fake.SearchStub = stub +} + +func (fake *FakeSearchableTaskStore) SearchArgsForCall(i int) (context.Context, internal.SearchParams) { + fake.searchMutex.RLock() + defer fake.searchMutex.RUnlock() + argsForCall := fake.searchArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeSearchableTaskStore) SearchReturns(result1 internal.SearchResults, result2 error) { + fake.searchMutex.Lock() + defer fake.searchMutex.Unlock() + fake.SearchStub = nil + fake.searchReturns = struct { + result1 internal.SearchResults + result2 error + }{result1, result2} +} + +func (fake *FakeSearchableTaskStore) SearchReturnsOnCall(i int, result1 internal.SearchResults, result2 error) { + fake.searchMutex.Lock() + defer fake.searchMutex.Unlock() + fake.SearchStub = nil + if fake.searchReturnsOnCall == nil { + fake.searchReturnsOnCall = make(map[int]struct { + result1 internal.SearchResults + result2 error + }) + } + fake.searchReturnsOnCall[i] = struct { + result1 internal.SearchResults + result2 error + }{result1, result2} +} + +func (fake *FakeSearchableTaskStore) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeSearchableTaskStore) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ memcached.SearchableTaskStore = new(FakeSearchableTaskStore) diff --git a/internal/memcached/memcachedtesting/task_store.gen.go b/internal/memcached/memcachedtesting/task_store.gen.go new file mode 100644 index 00000000..1e0cf1e0 --- /dev/null +++ b/internal/memcached/memcachedtesting/task_store.gen.go @@ -0,0 +1,347 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package memcachedtesting + +import ( + "context" + "sync" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + "github.com/MarioCarrion/todo-api-microservice-example/internal/memcached" +) + +type FakeTaskStore struct { + CreateStub func(context.Context, internal.CreateParams) (internal.Task, error) + createMutex sync.RWMutex + createArgsForCall []struct { + arg1 context.Context + arg2 internal.CreateParams + } + createReturns struct { + result1 internal.Task + result2 error + } + createReturnsOnCall map[int]struct { + result1 internal.Task + result2 error + } + DeleteStub func(context.Context, string) error + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + arg1 context.Context + arg2 string + } + deleteReturns struct { + result1 error + } + deleteReturnsOnCall map[int]struct { + result1 error + } + FindStub func(context.Context, string) (internal.Task, error) + findMutex sync.RWMutex + findArgsForCall []struct { + arg1 context.Context + arg2 string + } + findReturns struct { + result1 internal.Task + result2 error + } + findReturnsOnCall map[int]struct { + result1 internal.Task + result2 error + } + UpdateStub func(context.Context, string, internal.UpdateParams) error + updateMutex sync.RWMutex + updateArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 internal.UpdateParams + } + updateReturns struct { + result1 error + } + updateReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeTaskStore) Create(arg1 context.Context, arg2 internal.CreateParams) (internal.Task, error) { + fake.createMutex.Lock() + ret, specificReturn := fake.createReturnsOnCall[len(fake.createArgsForCall)] + fake.createArgsForCall = append(fake.createArgsForCall, struct { + arg1 context.Context + arg2 internal.CreateParams + }{arg1, arg2}) + stub := fake.CreateStub + fakeReturns := fake.createReturns + fake.recordInvocation("Create", []interface{}{arg1, arg2}) + fake.createMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeTaskStore) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeTaskStore) CreateCalls(stub func(context.Context, internal.CreateParams) (internal.Task, error)) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = stub +} + +func (fake *FakeTaskStore) CreateArgsForCall(i int) (context.Context, internal.CreateParams) { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + argsForCall := fake.createArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeTaskStore) CreateReturns(result1 internal.Task, result2 error) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = nil + fake.createReturns = struct { + result1 internal.Task + result2 error + }{result1, result2} +} + +func (fake *FakeTaskStore) CreateReturnsOnCall(i int, result1 internal.Task, result2 error) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = nil + if fake.createReturnsOnCall == nil { + fake.createReturnsOnCall = make(map[int]struct { + result1 internal.Task + result2 error + }) + } + fake.createReturnsOnCall[i] = struct { + result1 internal.Task + result2 error + }{result1, result2} +} + +func (fake *FakeTaskStore) Delete(arg1 context.Context, arg2 string) error { + fake.deleteMutex.Lock() + ret, specificReturn := fake.deleteReturnsOnCall[len(fake.deleteArgsForCall)] + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.DeleteStub + fakeReturns := fake.deleteReturns + fake.recordInvocation("Delete", []interface{}{arg1, arg2}) + fake.deleteMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeTaskStore) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeTaskStore) DeleteCalls(stub func(context.Context, string) error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = stub +} + +func (fake *FakeTaskStore) DeleteArgsForCall(i int) (context.Context, string) { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + argsForCall := fake.deleteArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeTaskStore) DeleteReturns(result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + fake.deleteReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskStore) DeleteReturnsOnCall(i int, result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + if fake.deleteReturnsOnCall == nil { + fake.deleteReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskStore) Find(arg1 context.Context, arg2 string) (internal.Task, error) { + fake.findMutex.Lock() + ret, specificReturn := fake.findReturnsOnCall[len(fake.findArgsForCall)] + fake.findArgsForCall = append(fake.findArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.FindStub + fakeReturns := fake.findReturns + fake.recordInvocation("Find", []interface{}{arg1, arg2}) + fake.findMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeTaskStore) FindCallCount() int { + fake.findMutex.RLock() + defer fake.findMutex.RUnlock() + return len(fake.findArgsForCall) +} + +func (fake *FakeTaskStore) FindCalls(stub func(context.Context, string) (internal.Task, error)) { + fake.findMutex.Lock() + defer fake.findMutex.Unlock() + fake.FindStub = stub +} + +func (fake *FakeTaskStore) FindArgsForCall(i int) (context.Context, string) { + fake.findMutex.RLock() + defer fake.findMutex.RUnlock() + argsForCall := fake.findArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeTaskStore) FindReturns(result1 internal.Task, result2 error) { + fake.findMutex.Lock() + defer fake.findMutex.Unlock() + fake.FindStub = nil + fake.findReturns = struct { + result1 internal.Task + result2 error + }{result1, result2} +} + +func (fake *FakeTaskStore) FindReturnsOnCall(i int, result1 internal.Task, result2 error) { + fake.findMutex.Lock() + defer fake.findMutex.Unlock() + fake.FindStub = nil + if fake.findReturnsOnCall == nil { + fake.findReturnsOnCall = make(map[int]struct { + result1 internal.Task + result2 error + }) + } + fake.findReturnsOnCall[i] = struct { + result1 internal.Task + result2 error + }{result1, result2} +} + +func (fake *FakeTaskStore) Update(arg1 context.Context, arg2 string, arg3 internal.UpdateParams) error { + fake.updateMutex.Lock() + ret, specificReturn := fake.updateReturnsOnCall[len(fake.updateArgsForCall)] + fake.updateArgsForCall = append(fake.updateArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 internal.UpdateParams + }{arg1, arg2, arg3}) + stub := fake.UpdateStub + fakeReturns := fake.updateReturns + fake.recordInvocation("Update", []interface{}{arg1, arg2, arg3}) + fake.updateMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeTaskStore) UpdateCallCount() int { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return len(fake.updateArgsForCall) +} + +func (fake *FakeTaskStore) UpdateCalls(stub func(context.Context, string, internal.UpdateParams) error) { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.UpdateStub = stub +} + +func (fake *FakeTaskStore) UpdateArgsForCall(i int) (context.Context, string, internal.UpdateParams) { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + argsForCall := fake.updateArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeTaskStore) UpdateReturns(result1 error) { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.UpdateStub = nil + fake.updateReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskStore) UpdateReturnsOnCall(i int, result1 error) { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.UpdateStub = nil + if fake.updateReturnsOnCall == nil { + fake.updateReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updateReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskStore) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeTaskStore) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ memcached.TaskStore = new(FakeTaskStore) diff --git a/internal/memcached/searchable_task.go b/internal/memcached/searchable_task.go index f05b047a..807d7d8c 100644 --- a/internal/memcached/searchable_task.go +++ b/internal/memcached/searchable_task.go @@ -11,12 +11,16 @@ import ( "github.com/MarioCarrion/todo-api-microservice-example/internal" ) +//go:generate counterfeiter -generate + // SearchableTask ... type SearchableTask struct { client *memcache.Client orig SearchableTaskStore } +//counterfeiter:generate -o memcachedtesting/searchable_task_store.gen.go . SearchableTaskStore + type SearchableTaskStore interface { Delete(ctx context.Context, id string) error Index(ctx context.Context, task internal.Task) error diff --git a/internal/memcached/task.go b/internal/memcached/task.go index 9da37251..5db93c4e 100644 --- a/internal/memcached/task.go +++ b/internal/memcached/task.go @@ -10,6 +10,8 @@ import ( "github.com/MarioCarrion/todo-api-microservice-example/internal" ) +//counterfeiter:generate -o memcachedtesting/task_store.gen.go . TaskStore + type Task struct { client *memcache.Client orig TaskStore diff --git a/internal/memcached/task_integration_test.go b/internal/memcached/task_integration_test.go new file mode 100644 index 00000000..ea608dfa --- /dev/null +++ b/internal/memcached/task_integration_test.go @@ -0,0 +1,215 @@ +package memcached_test + +import ( + "testing" + + "github.com/bradfitz/gomemcache/memcache" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + "go.uber.org/zap" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + taskMemcached "github.com/MarioCarrion/todo-api-microservice-example/internal/memcached" + "github.com/MarioCarrion/todo-api-microservice-example/internal/memcached/memcachedtesting" +) + +func TestTask_Find_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + req := testcontainers.ContainerRequest{ + Image: "memcached:1.6-alpine", + ExposedPorts: []string{"11211/tcp"}, + WaitingFor: wait.ForListeningPort("11211/tcp"), + } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + t.Fatalf("failed to start memcached container: %v", err) + } + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Logf("failed to terminate container: %v", err) + } + }) + + // Get host and port + host, err := container.Host(ctx) + if err != nil { + t.Fatalf("failed to get host: %v", err) + } + + port, err := container.MappedPort(ctx, "11211") + if err != nil { + t.Fatalf("failed to get port: %v", err) + } + + // Create memcache client + mcClient := memcache.New(host + ":" + port.Port()) + logger := zap.NewNop() + + // Create mock store that returns a task + mockStore := &memcachedtesting.FakeTaskStore{} + testTask := internal.Task{ + ID: "test-123", + Description: "Test task", + Priority: internal.PriorityHigh.Pointer(), + } + mockStore.FindReturns(testTask, nil) + + // Create task with cache + taskCache := taskMemcached.NewTask(mcClient, mockStore, logger) + + // First call should go to store and cache + result, err := taskCache.Find(ctx, "test-123") + if err != nil { + t.Fatalf("Failed to find task: %v", err) + } + + if result.ID != testTask.ID { + t.Errorf("Expected task ID %s, got %s", testTask.ID, result.ID) + } + + // Verify store was called once + if mockStore.FindCallCount() != 1 { + t.Errorf("Expected store.Find to be called once, got %d", mockStore.FindCallCount()) + } + + // Second call should hit cache + _, err = taskCache.Find(ctx, "test-123") + if err != nil { + t.Fatalf("Failed to find task from cache: %v", err) + } + + // Store should still be called only once (cache hit) + if mockStore.FindCallCount() != 1 { + t.Errorf("Expected store.Find to still be called once (cache hit), got %d", mockStore.FindCallCount()) + } +} + +func TestTask_Create_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + req := testcontainers.ContainerRequest{ + Image: "memcached:1.6-alpine", + ExposedPorts: []string{"11211/tcp"}, + WaitingFor: wait.ForListeningPort("11211/tcp"), + } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + t.Fatalf("failed to start memcached container: %v", err) + } + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Logf("failed to terminate container: %v", err) + } + }) + + host, err := container.Host(ctx) + if err != nil { + t.Fatalf("failed to get host: %v", err) + } + + port, err := container.MappedPort(ctx, "11211") + if err != nil { + t.Fatalf("failed to get port: %v", err) + } + + mcClient := memcache.New(host + ":" + port.Port()) + logger := zap.NewNop() + + mockStore := &memcachedtesting.FakeTaskStore{} + testTask := internal.Task{ + ID: "test-create", + Description: "Created task", + } + mockStore.CreateReturns(testTask, nil) + + taskCache := taskMemcached.NewTask(mcClient, mockStore, logger) + + params := internal.CreateParams{ + Description: "Created task", + } + + result, err := taskCache.Create(ctx, params) + if err != nil { + t.Fatalf("Failed to create task: %v", err) + } + + if result.ID != testTask.ID { + t.Errorf("Expected task ID %s, got %s", testTask.ID, result.ID) + } + + // Verify the task is now in cache by trying to get it + item, err := mcClient.Get(testTask.ID) + if err != nil { + t.Logf("Task not found in cache (expected for write-through): %v", err) + // This is OK - the cache set might not complete immediately + } + + if item != nil { + t.Logf("Task successfully cached with key: %s", item.Key) + } +} + +func TestTask_Delete_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + req := testcontainers.ContainerRequest{ + Image: "memcached:1.6-alpine", + ExposedPorts: []string{"11211/tcp"}, + WaitingFor: wait.ForListeningPort("11211/tcp"), + } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + t.Fatalf("failed to start memcached container: %v", err) + } + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Logf("failed to terminate container: %v", err) + } + }) + + host, err := container.Host(ctx) + if err != nil { + t.Fatalf("failed to get host: %v", err) + } + + port, err := container.MappedPort(ctx, "11211") + if err != nil { + t.Fatalf("failed to get port: %v", err) + } + + mcClient := memcache.New(host + ":" + port.Port()) + logger := zap.NewNop() + + mockStore := &memcachedtesting.FakeTaskStore{} + mockStore.DeleteReturns(nil) + + taskCache := taskMemcached.NewTask(mcClient, mockStore, logger) + + err = taskCache.Delete(ctx, "test-delete") + if err != nil { + t.Fatalf("Failed to delete task: %v", err) + } + + if mockStore.DeleteCallCount() != 1 { + t.Errorf("Expected store.Delete to be called once, got %d", mockStore.DeleteCallCount()) + } +} diff --git a/internal/rabbitmq/task_integration_test.go b/internal/rabbitmq/task_integration_test.go new file mode 100644 index 00000000..4b468378 --- /dev/null +++ b/internal/rabbitmq/task_integration_test.go @@ -0,0 +1,173 @@ +package rabbitmq_test + +import ( + "testing" + + "github.com/streadway/amqp" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/rabbitmq" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + rabbitmqTask "github.com/MarioCarrion/todo-api-microservice-example/internal/rabbitmq" +) + +func TestTask_Created_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + rmqContainer, err := rabbitmq.Run(ctx, "rabbitmq:3.12-management-alpine") + if err != nil { + t.Fatalf("failed to start rabbitmq container: %v", err) + } + t.Cleanup(func() { + if err := testcontainers.TerminateContainer(rmqContainer); err != nil { + t.Logf("failed to terminate container: %v", err) + } + }) + + // Get connection string + connStr, err := rmqContainer.AmqpURL(ctx) + if err != nil { + t.Fatalf("failed to get connection string: %v", err) + } + + // Create RabbitMQ connection + conn, err := amqp.Dial(connStr) + if err != nil { + t.Fatalf("failed to connect to rabbitmq: %v", err) + } + t.Cleanup(func() { conn.Close() }) + + channel, err := conn.Channel() + if err != nil { + t.Fatalf("failed to open channel: %v", err) + } + t.Cleanup(func() { channel.Close() }) + + // Declare the exchange + err = channel.ExchangeDeclare( + "tasks", // name + "topic", // type + true, // durable + false, // auto-deleted + false, // internal + false, // no-wait + nil, // arguments + ) + if err != nil { + t.Fatalf("failed to declare exchange: %v", err) + } + + // Create task publisher + taskPub := rabbitmqTask.NewTask(channel) + + // Test Created method + task := internal.Task{ + ID: "test-123", + Description: "Test task", + Priority: internal.PriorityHigh.Pointer(), + IsDone: false, + } + + err = taskPub.Created(ctx, task) + if err != nil { + t.Fatalf("Failed to publish created event: %v", err) + } +} + +func TestTask_Updated_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + rmqContainer, err := rabbitmq.Run(ctx, "rabbitmq:3.12-management-alpine") + if err != nil { + t.Fatalf("failed to start rabbitmq container: %v", err) + } + t.Cleanup(func() { + if err := testcontainers.TerminateContainer(rmqContainer); err != nil { + t.Logf("failed to terminate container: %v", err) + } + }) + + connStr, err := rmqContainer.AmqpURL(ctx) + if err != nil { + t.Fatalf("failed to get connection string: %v", err) + } + + conn, err := amqp.Dial(connStr) + if err != nil { + t.Fatalf("failed to connect to rabbitmq: %v", err) + } + defer conn.Close() + + channel, err := conn.Channel() + if err != nil { + t.Fatalf("failed to open channel: %v", err) + } + t.Cleanup(func() { channel.Close() }) + + err = channel.ExchangeDeclare("tasks", "topic", true, false, false, false, nil) + if err != nil { + t.Fatalf("failed to declare exchange: %v", err) + } + + taskPub := rabbitmqTask.NewTask(channel) + + task := internal.Task{ + ID: "test-456", + Description: "Updated task", + IsDone: true, + } + + err = taskPub.Updated(ctx, task) + if err != nil { + t.Fatalf("Failed to publish updated event: %v", err) + } +} + +func TestTask_Deleted_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + rmqContainer, err := rabbitmq.Run(ctx, "rabbitmq:3.12-management-alpine") + if err != nil { + t.Fatalf("failed to start rabbitmq container: %v", err) + } + t.Cleanup(func() { + if err := testcontainers.TerminateContainer(rmqContainer); err != nil { + t.Logf("failed to terminate container: %v", err) + } + }) + + connStr, err := rmqContainer.AmqpURL(ctx) + if err != nil { + t.Fatalf("failed to get connection string: %v", err) + } + + conn, err := amqp.Dial(connStr) + if err != nil { + t.Fatalf("failed to connect to rabbitmq: %v", err) + } + defer conn.Close() + + channel, err := conn.Channel() + if err != nil { + t.Fatalf("failed to open channel: %v", err) + } + t.Cleanup(func() { channel.Close() }) + + err = channel.ExchangeDeclare("tasks", "topic", true, false, false, false, nil) + if err != nil { + t.Fatalf("failed to declare exchange: %v", err) + } + + taskPub := rabbitmqTask.NewTask(channel) + + err = taskPub.Deleted(ctx, "test-789") + if err != nil { + t.Fatalf("Failed to publish deleted event: %v", err) + } +} diff --git a/internal/redis/task_integration_test.go b/internal/redis/task_integration_test.go new file mode 100644 index 00000000..6d6c8405 --- /dev/null +++ b/internal/redis/task_integration_test.go @@ -0,0 +1,109 @@ +package redis_test + +import ( + "context" + "testing" + + "github.com/go-redis/redis/v8" + "github.com/testcontainers/testcontainers-go" + redismodule "github.com/testcontainers/testcontainers-go/modules/redis" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + redisTask "github.com/MarioCarrion/todo-api-microservice-example/internal/redis" +) + +// setupRedisClient starts a Redis container and returns a configured client. +func setupRedisClient(ctx context.Context, t *testing.T) (*redis.Client, func()) { + t.Helper() + + redisContainer, err := redismodule.Run(ctx, "redis:7-alpine") + if err != nil { + t.Fatalf("failed to start redis container: %v", err) + } + + cleanup := func() { + if err := testcontainers.TerminateContainer(redisContainer); err != nil { + t.Logf("failed to terminate container: %v", err) + } + } + + connStr, err := redisContainer.ConnectionString(ctx) + if err != nil { + cleanup() + t.Fatalf("failed to get connection string: %v", err) + } + + client := redis.NewClient(&redis.Options{ + Addr: connStr, + }) + + cleanupAll := func() { + client.Close() + cleanup() + } + + return client, cleanupAll +} + +func TestTask_Created_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + client, cleanup := setupRedisClient(ctx, t) + t.Cleanup(cleanup) + + taskPub := redisTask.NewTask(client) + + task := internal.Task{ + ID: "test-123", + Description: "Test task", + IsDone: false, + } + + err := taskPub.Created(ctx, task) + if err != nil { + t.Fatalf("Failed to publish created event: %v", err) + } + + // Verify message was published (subscribe and check) + pubsub := client.Subscribe(ctx, "Task.Created") + t.Cleanup(func() { pubsub.Close() }) + // Note: In a real test, you'd need a separate goroutine to publish after subscribing + // This test verifies the method doesn't error +} + +func TestTask_Updated_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + client, cleanup := setupRedisClient(ctx, t) + t.Cleanup(cleanup) + + taskPub := redisTask.NewTask(client) + + task := internal.Task{ + ID: "test-456", + Description: "Updated task", + IsDone: true, + } + + err := taskPub.Updated(ctx, task) + if err != nil { + t.Fatalf("Failed to publish updated event: %v", err) + } +} + +func TestTask_Deleted_Integration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + client, cleanup := setupRedisClient(ctx, t) + t.Cleanup(cleanup) + + taskPub := redisTask.NewTask(client) + + err := taskPub.Deleted(ctx, "test-789") + if err != nil { + t.Fatalf("Failed to publish deleted event: %v", err) + } +} diff --git a/internal/rest/task_test.go b/internal/rest/task_test.go new file mode 100644 index 00000000..f2f9d016 --- /dev/null +++ b/internal/rest/task_test.go @@ -0,0 +1,433 @@ +package rest_test + +import ( + "errors" + "testing" + + "github.com/google/uuid" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + "github.com/MarioCarrion/todo-api-microservice-example/internal/rest" + "github.com/MarioCarrion/todo-api-microservice-example/internal/rest/resttesting" +) + +func TestNewTaskHandler(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + }{ + { + name: "creates new task handler", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + svc := &resttesting.FakeTaskService{} + handler := rest.NewTaskHandler(svc) + + if handler == nil { + t.Fatal("expected non-nil handler") + } + }) + } +} + +func TestTaskHandler_CreateTask(t *testing.T) { + t.Parallel() + + taskID := uuid.New() + + tests := []struct { + name string + request rest.CreateTaskRequestObject + setupMock func(*resttesting.FakeTaskService) + expectError bool + validateResp func(t *testing.T, resp rest.CreateTaskResponseObject) + }{ + { + name: "successful creation", + request: rest.CreateTaskRequestObject{ + Body: &rest.CreateTaskJSONRequestBody{ + Description: "test task", + Priority: (*rest.Priority)(internal.ValueToPointer("high")), + }, + }, + setupMock: func(m *resttesting.FakeTaskService) { + m.CreateReturns(internal.Task{ + ID: taskID.String(), + Description: "test task", + Priority: internal.PriorityHigh.Pointer(), + }, nil) + }, + expectError: false, + validateResp: func(t *testing.T, resp rest.CreateTaskResponseObject) { + t.Helper() + r, ok := resp.(rest.CreateTask201JSONResponse) + if !ok { + t.Fatalf("expected CreateTask201JSONResponse, got %T", resp) + } + if r.Task.ID != taskID { + t.Errorf("expected task ID %v, got %v", taskID, r.Task.ID) + } + }, + }, + { + name: "service error", + request: rest.CreateTaskRequestObject{ + Body: &rest.CreateTaskJSONRequestBody{ + Description: "test task", + }, + }, + setupMock: func(m *resttesting.FakeTaskService) { + m.CreateReturns(internal.Task{}, errors.New("service error")) + }, + expectError: false, + validateResp: func(t *testing.T, resp rest.CreateTaskResponseObject) { + t.Helper() + _, ok := resp.(rest.CreateTask500JSONResponse) + if !ok { + t.Fatalf("expected CreateTask500JSONResponse, got %T", resp) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockService := &resttesting.FakeTaskService{} + tt.setupMock(mockService) + handler := rest.NewTaskHandler(mockService) + resp, err := handler.CreateTask(t.Context(), tt.request) + + if tt.expectError && err == nil { + t.Fatal("expected error, got nil") + } + + if !tt.expectError && err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if tt.validateResp != nil { + tt.validateResp(t, resp) + } + }) + } +} + +func TestTaskHandler_ReadTask(t *testing.T) { + t.Parallel() + + taskID := uuid.New() + + tests := []struct { + name string + request rest.ReadTaskRequestObject + setupMock func(*resttesting.FakeTaskService) + expectError bool + validateResp func(t *testing.T, resp rest.ReadTaskResponseObject) + }{ + { + name: "successful read", + request: rest.ReadTaskRequestObject{ + Id: taskID, + }, + setupMock: func(m *resttesting.FakeTaskService) { + m.ByIDReturns(internal.Task{ + ID: taskID.String(), + Description: "test task", + }, nil) + }, + expectError: false, + validateResp: func(t *testing.T, resp rest.ReadTaskResponseObject) { + t.Helper() + r, ok := resp.(rest.ReadTask200JSONResponse) + if !ok { + t.Fatalf("expected ReadTask200JSONResponse, got %T", resp) + } + if r.Task.ID != taskID { + t.Errorf("expected task ID %v, got %v", taskID, r.Task.ID) + } + }, + }, + { + name: "service error", + request: rest.ReadTaskRequestObject{ + Id: taskID, + }, + setupMock: func(m *resttesting.FakeTaskService) { + m.ByIDReturns(internal.Task{}, errors.New("not found")) + }, + expectError: false, + validateResp: func(t *testing.T, resp rest.ReadTaskResponseObject) { + t.Helper() + _, ok := resp.(rest.ReadTask500JSONResponse) + if !ok { + t.Fatalf("expected ReadTask500JSONResponse, got %T", resp) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockService := &resttesting.FakeTaskService{} + tt.setupMock(mockService) + handler := rest.NewTaskHandler(mockService) + resp, err := handler.ReadTask(t.Context(), tt.request) + + if tt.expectError && err == nil { + t.Fatal("expected error, got nil") + } + if !tt.expectError && err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if tt.validateResp != nil { + tt.validateResp(t, resp) + } + }) + } +} + +func TestTaskHandler_DeleteTask(t *testing.T) { + t.Parallel() + + taskID := uuid.New() + + tests := []struct { + name string + request rest.DeleteTaskRequestObject + setupMock func(*resttesting.FakeTaskService) + expectError bool + validateResp func(t *testing.T, resp rest.DeleteTaskResponseObject) + }{ + { + name: "successful delete", + request: rest.DeleteTaskRequestObject{ + Id: taskID, + }, + setupMock: func(m *resttesting.FakeTaskService) { + m.DeleteReturns(nil) + }, + expectError: false, + validateResp: func(t *testing.T, resp rest.DeleteTaskResponseObject) { + t.Helper() + _, ok := resp.(rest.DeleteTask200Response) + if !ok { + t.Fatalf("expected DeleteTask200Response, got %T", resp) + } + }, + }, + { + name: "service error", + request: rest.DeleteTaskRequestObject{ + Id: taskID, + }, + setupMock: func(m *resttesting.FakeTaskService) { + m.DeleteReturns(errors.New("delete error")) + }, + expectError: false, + validateResp: func(t *testing.T, resp rest.DeleteTaskResponseObject) { + t.Helper() + _, ok := resp.(rest.DeleteTask500JSONResponse) + if !ok { + t.Fatalf("expected DeleteTask500JSONResponse, got %T", resp) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockService := &resttesting.FakeTaskService{} + tt.setupMock(mockService) + handler := rest.NewTaskHandler(mockService) + resp, err := handler.DeleteTask(t.Context(), tt.request) + + if tt.expectError && err == nil { + t.Fatal("expected error, got nil") + } + if !tt.expectError && err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if tt.validateResp != nil { + tt.validateResp(t, resp) + } + }) + } +} + +func TestTaskHandler_UpdateTask(t *testing.T) { + t.Parallel() + + taskID := uuid.New() + + tests := []struct { + name string + request rest.UpdateTaskRequestObject + setupMock func(*resttesting.FakeTaskService) + expectError bool + validateResp func(t *testing.T, resp rest.UpdateTaskResponseObject) + }{ + { + name: "successful update", + request: rest.UpdateTaskRequestObject{ + Id: taskID, + Body: &rest.UpdateTaskJSONRequestBody{ + Description: internal.ValueToPointer("updated task"), + }, + }, + setupMock: func(m *resttesting.FakeTaskService) { + m.UpdateReturns(nil) + }, + expectError: false, + validateResp: func(t *testing.T, resp rest.UpdateTaskResponseObject) { + t.Helper() + _, ok := resp.(rest.UpdateTask200Response) + if !ok { + t.Fatalf("expected UpdateTask200Response, got %T", resp) + } + }, + }, + { + name: "service error", + request: rest.UpdateTaskRequestObject{ + Id: taskID, + Body: &rest.UpdateTaskJSONRequestBody{ + Description: internal.ValueToPointer("updated task"), + }, + }, + setupMock: func(m *resttesting.FakeTaskService) { + m.UpdateReturns(errors.New("update error")) + }, + expectError: false, + validateResp: func(t *testing.T, resp rest.UpdateTaskResponseObject) { + t.Helper() + _, ok := resp.(rest.UpdateTask500JSONResponse) + if !ok { + t.Fatalf("expected UpdateTask500JSONResponse, got %T", resp) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockService := &resttesting.FakeTaskService{} + tt.setupMock(mockService) + handler := rest.NewTaskHandler(mockService) + resp, err := handler.UpdateTask(t.Context(), tt.request) + + if tt.expectError && err == nil { + t.Fatal("expected error, got nil") + } + if !tt.expectError && err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if tt.validateResp != nil { + tt.validateResp(t, resp) + } + }) + } +} + +func TestTaskHandler_SearchTask(t *testing.T) { + t.Parallel() + + taskID1 := uuid.New() + taskID2 := uuid.New() + + tests := []struct { + name string + request rest.SearchTaskRequestObject + setupMock func(*resttesting.FakeTaskService) + expectError bool + validateResp func(t *testing.T, resp rest.SearchTaskResponseObject) + }{ + { + name: "successful search", + request: rest.SearchTaskRequestObject{ + Body: &rest.SearchTaskJSONRequestBody{ + Description: internal.ValueToPointer("test"), + From: 0, + Size: 10, + }, + }, + setupMock: func(m *resttesting.FakeTaskService) { + m.ByReturns(internal.SearchResults{ + Tasks: []internal.Task{ + {ID: taskID1.String(), Description: "test task 1"}, + {ID: taskID2.String(), Description: "test task 2"}, + }, + Total: 2, + }, nil) + }, + expectError: false, + validateResp: func(t *testing.T, resp rest.SearchTaskResponseObject) { + t.Helper() + r, ok := resp.(rest.SearchTask200JSONResponse) + if !ok { + t.Fatalf("expected SearchTask200JSONResponse, got %T", resp) + } + if r.Tasks == nil || len(*r.Tasks) != 2 { + t.Errorf("expected 2 tasks, got %v", r.Tasks) + } + }, + }, + { + name: "service error", + request: rest.SearchTaskRequestObject{ + Body: &rest.SearchTaskJSONRequestBody{ + Description: internal.ValueToPointer("test"), + }, + }, + setupMock: func(m *resttesting.FakeTaskService) { + m.ByReturns(internal.SearchResults{}, errors.New("search error")) + }, + expectError: false, + validateResp: func(t *testing.T, resp rest.SearchTaskResponseObject) { + t.Helper() + _, ok := resp.(rest.SearchTask500JSONResponse) + if !ok { + t.Fatalf("expected SearchTask500JSONResponse, got %T", resp) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockService := &resttesting.FakeTaskService{} + tt.setupMock(mockService) + handler := rest.NewTaskHandler(mockService) + resp, err := handler.SearchTask(t.Context(), tt.request) + + if tt.expectError && err == nil { + t.Fatal("expected error, got nil") + } + if !tt.expectError && err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if tt.validateResp != nil { + tt.validateResp(t, resp) + } + }) + } +} diff --git a/internal/service/servicetesting/task_message_broker_publisher.gen.go b/internal/service/servicetesting/task_message_broker_publisher.gen.go new file mode 100644 index 00000000..e4101ab8 --- /dev/null +++ b/internal/service/servicetesting/task_message_broker_publisher.gen.go @@ -0,0 +1,261 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package servicetesting + +import ( + "context" + "sync" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + "github.com/MarioCarrion/todo-api-microservice-example/internal/service" +) + +type FakeTaskMessageBrokerPublisher struct { + CreatedStub func(context.Context, internal.Task) error + createdMutex sync.RWMutex + createdArgsForCall []struct { + arg1 context.Context + arg2 internal.Task + } + createdReturns struct { + result1 error + } + createdReturnsOnCall map[int]struct { + result1 error + } + DeletedStub func(context.Context, string) error + deletedMutex sync.RWMutex + deletedArgsForCall []struct { + arg1 context.Context + arg2 string + } + deletedReturns struct { + result1 error + } + deletedReturnsOnCall map[int]struct { + result1 error + } + UpdatedStub func(context.Context, internal.Task) error + updatedMutex sync.RWMutex + updatedArgsForCall []struct { + arg1 context.Context + arg2 internal.Task + } + updatedReturns struct { + result1 error + } + updatedReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeTaskMessageBrokerPublisher) Created(arg1 context.Context, arg2 internal.Task) error { + fake.createdMutex.Lock() + ret, specificReturn := fake.createdReturnsOnCall[len(fake.createdArgsForCall)] + fake.createdArgsForCall = append(fake.createdArgsForCall, struct { + arg1 context.Context + arg2 internal.Task + }{arg1, arg2}) + stub := fake.CreatedStub + fakeReturns := fake.createdReturns + fake.recordInvocation("Created", []interface{}{arg1, arg2}) + fake.createdMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeTaskMessageBrokerPublisher) CreatedCallCount() int { + fake.createdMutex.RLock() + defer fake.createdMutex.RUnlock() + return len(fake.createdArgsForCall) +} + +func (fake *FakeTaskMessageBrokerPublisher) CreatedCalls(stub func(context.Context, internal.Task) error) { + fake.createdMutex.Lock() + defer fake.createdMutex.Unlock() + fake.CreatedStub = stub +} + +func (fake *FakeTaskMessageBrokerPublisher) CreatedArgsForCall(i int) (context.Context, internal.Task) { + fake.createdMutex.RLock() + defer fake.createdMutex.RUnlock() + argsForCall := fake.createdArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeTaskMessageBrokerPublisher) CreatedReturns(result1 error) { + fake.createdMutex.Lock() + defer fake.createdMutex.Unlock() + fake.CreatedStub = nil + fake.createdReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskMessageBrokerPublisher) CreatedReturnsOnCall(i int, result1 error) { + fake.createdMutex.Lock() + defer fake.createdMutex.Unlock() + fake.CreatedStub = nil + if fake.createdReturnsOnCall == nil { + fake.createdReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.createdReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskMessageBrokerPublisher) Deleted(arg1 context.Context, arg2 string) error { + fake.deletedMutex.Lock() + ret, specificReturn := fake.deletedReturnsOnCall[len(fake.deletedArgsForCall)] + fake.deletedArgsForCall = append(fake.deletedArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.DeletedStub + fakeReturns := fake.deletedReturns + fake.recordInvocation("Deleted", []interface{}{arg1, arg2}) + fake.deletedMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeTaskMessageBrokerPublisher) DeletedCallCount() int { + fake.deletedMutex.RLock() + defer fake.deletedMutex.RUnlock() + return len(fake.deletedArgsForCall) +} + +func (fake *FakeTaskMessageBrokerPublisher) DeletedCalls(stub func(context.Context, string) error) { + fake.deletedMutex.Lock() + defer fake.deletedMutex.Unlock() + fake.DeletedStub = stub +} + +func (fake *FakeTaskMessageBrokerPublisher) DeletedArgsForCall(i int) (context.Context, string) { + fake.deletedMutex.RLock() + defer fake.deletedMutex.RUnlock() + argsForCall := fake.deletedArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeTaskMessageBrokerPublisher) DeletedReturns(result1 error) { + fake.deletedMutex.Lock() + defer fake.deletedMutex.Unlock() + fake.DeletedStub = nil + fake.deletedReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskMessageBrokerPublisher) DeletedReturnsOnCall(i int, result1 error) { + fake.deletedMutex.Lock() + defer fake.deletedMutex.Unlock() + fake.DeletedStub = nil + if fake.deletedReturnsOnCall == nil { + fake.deletedReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deletedReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskMessageBrokerPublisher) Updated(arg1 context.Context, arg2 internal.Task) error { + fake.updatedMutex.Lock() + ret, specificReturn := fake.updatedReturnsOnCall[len(fake.updatedArgsForCall)] + fake.updatedArgsForCall = append(fake.updatedArgsForCall, struct { + arg1 context.Context + arg2 internal.Task + }{arg1, arg2}) + stub := fake.UpdatedStub + fakeReturns := fake.updatedReturns + fake.recordInvocation("Updated", []interface{}{arg1, arg2}) + fake.updatedMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeTaskMessageBrokerPublisher) UpdatedCallCount() int { + fake.updatedMutex.RLock() + defer fake.updatedMutex.RUnlock() + return len(fake.updatedArgsForCall) +} + +func (fake *FakeTaskMessageBrokerPublisher) UpdatedCalls(stub func(context.Context, internal.Task) error) { + fake.updatedMutex.Lock() + defer fake.updatedMutex.Unlock() + fake.UpdatedStub = stub +} + +func (fake *FakeTaskMessageBrokerPublisher) UpdatedArgsForCall(i int) (context.Context, internal.Task) { + fake.updatedMutex.RLock() + defer fake.updatedMutex.RUnlock() + argsForCall := fake.updatedArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeTaskMessageBrokerPublisher) UpdatedReturns(result1 error) { + fake.updatedMutex.Lock() + defer fake.updatedMutex.Unlock() + fake.UpdatedStub = nil + fake.updatedReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskMessageBrokerPublisher) UpdatedReturnsOnCall(i int, result1 error) { + fake.updatedMutex.Lock() + defer fake.updatedMutex.Unlock() + fake.UpdatedStub = nil + if fake.updatedReturnsOnCall == nil { + fake.updatedReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updatedReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskMessageBrokerPublisher) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeTaskMessageBrokerPublisher) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ service.TaskMessageBrokerPublisher = new(FakeTaskMessageBrokerPublisher) diff --git a/internal/service/servicetesting/task_repository.gen.go b/internal/service/servicetesting/task_repository.gen.go new file mode 100644 index 00000000..293d11a4 --- /dev/null +++ b/internal/service/servicetesting/task_repository.gen.go @@ -0,0 +1,347 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package servicetesting + +import ( + "context" + "sync" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + "github.com/MarioCarrion/todo-api-microservice-example/internal/service" +) + +type FakeTaskRepository struct { + CreateStub func(context.Context, internal.CreateParams) (internal.Task, error) + createMutex sync.RWMutex + createArgsForCall []struct { + arg1 context.Context + arg2 internal.CreateParams + } + createReturns struct { + result1 internal.Task + result2 error + } + createReturnsOnCall map[int]struct { + result1 internal.Task + result2 error + } + DeleteStub func(context.Context, string) error + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + arg1 context.Context + arg2 string + } + deleteReturns struct { + result1 error + } + deleteReturnsOnCall map[int]struct { + result1 error + } + FindStub func(context.Context, string) (internal.Task, error) + findMutex sync.RWMutex + findArgsForCall []struct { + arg1 context.Context + arg2 string + } + findReturns struct { + result1 internal.Task + result2 error + } + findReturnsOnCall map[int]struct { + result1 internal.Task + result2 error + } + UpdateStub func(context.Context, string, internal.UpdateParams) error + updateMutex sync.RWMutex + updateArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 internal.UpdateParams + } + updateReturns struct { + result1 error + } + updateReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeTaskRepository) Create(arg1 context.Context, arg2 internal.CreateParams) (internal.Task, error) { + fake.createMutex.Lock() + ret, specificReturn := fake.createReturnsOnCall[len(fake.createArgsForCall)] + fake.createArgsForCall = append(fake.createArgsForCall, struct { + arg1 context.Context + arg2 internal.CreateParams + }{arg1, arg2}) + stub := fake.CreateStub + fakeReturns := fake.createReturns + fake.recordInvocation("Create", []interface{}{arg1, arg2}) + fake.createMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeTaskRepository) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeTaskRepository) CreateCalls(stub func(context.Context, internal.CreateParams) (internal.Task, error)) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = stub +} + +func (fake *FakeTaskRepository) CreateArgsForCall(i int) (context.Context, internal.CreateParams) { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + argsForCall := fake.createArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeTaskRepository) CreateReturns(result1 internal.Task, result2 error) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = nil + fake.createReturns = struct { + result1 internal.Task + result2 error + }{result1, result2} +} + +func (fake *FakeTaskRepository) CreateReturnsOnCall(i int, result1 internal.Task, result2 error) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = nil + if fake.createReturnsOnCall == nil { + fake.createReturnsOnCall = make(map[int]struct { + result1 internal.Task + result2 error + }) + } + fake.createReturnsOnCall[i] = struct { + result1 internal.Task + result2 error + }{result1, result2} +} + +func (fake *FakeTaskRepository) Delete(arg1 context.Context, arg2 string) error { + fake.deleteMutex.Lock() + ret, specificReturn := fake.deleteReturnsOnCall[len(fake.deleteArgsForCall)] + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.DeleteStub + fakeReturns := fake.deleteReturns + fake.recordInvocation("Delete", []interface{}{arg1, arg2}) + fake.deleteMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeTaskRepository) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeTaskRepository) DeleteCalls(stub func(context.Context, string) error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = stub +} + +func (fake *FakeTaskRepository) DeleteArgsForCall(i int) (context.Context, string) { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + argsForCall := fake.deleteArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeTaskRepository) DeleteReturns(result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + fake.deleteReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskRepository) DeleteReturnsOnCall(i int, result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + if fake.deleteReturnsOnCall == nil { + fake.deleteReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskRepository) Find(arg1 context.Context, arg2 string) (internal.Task, error) { + fake.findMutex.Lock() + ret, specificReturn := fake.findReturnsOnCall[len(fake.findArgsForCall)] + fake.findArgsForCall = append(fake.findArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.FindStub + fakeReturns := fake.findReturns + fake.recordInvocation("Find", []interface{}{arg1, arg2}) + fake.findMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeTaskRepository) FindCallCount() int { + fake.findMutex.RLock() + defer fake.findMutex.RUnlock() + return len(fake.findArgsForCall) +} + +func (fake *FakeTaskRepository) FindCalls(stub func(context.Context, string) (internal.Task, error)) { + fake.findMutex.Lock() + defer fake.findMutex.Unlock() + fake.FindStub = stub +} + +func (fake *FakeTaskRepository) FindArgsForCall(i int) (context.Context, string) { + fake.findMutex.RLock() + defer fake.findMutex.RUnlock() + argsForCall := fake.findArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeTaskRepository) FindReturns(result1 internal.Task, result2 error) { + fake.findMutex.Lock() + defer fake.findMutex.Unlock() + fake.FindStub = nil + fake.findReturns = struct { + result1 internal.Task + result2 error + }{result1, result2} +} + +func (fake *FakeTaskRepository) FindReturnsOnCall(i int, result1 internal.Task, result2 error) { + fake.findMutex.Lock() + defer fake.findMutex.Unlock() + fake.FindStub = nil + if fake.findReturnsOnCall == nil { + fake.findReturnsOnCall = make(map[int]struct { + result1 internal.Task + result2 error + }) + } + fake.findReturnsOnCall[i] = struct { + result1 internal.Task + result2 error + }{result1, result2} +} + +func (fake *FakeTaskRepository) Update(arg1 context.Context, arg2 string, arg3 internal.UpdateParams) error { + fake.updateMutex.Lock() + ret, specificReturn := fake.updateReturnsOnCall[len(fake.updateArgsForCall)] + fake.updateArgsForCall = append(fake.updateArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 internal.UpdateParams + }{arg1, arg2, arg3}) + stub := fake.UpdateStub + fakeReturns := fake.updateReturns + fake.recordInvocation("Update", []interface{}{arg1, arg2, arg3}) + fake.updateMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeTaskRepository) UpdateCallCount() int { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return len(fake.updateArgsForCall) +} + +func (fake *FakeTaskRepository) UpdateCalls(stub func(context.Context, string, internal.UpdateParams) error) { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.UpdateStub = stub +} + +func (fake *FakeTaskRepository) UpdateArgsForCall(i int) (context.Context, string, internal.UpdateParams) { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + argsForCall := fake.updateArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeTaskRepository) UpdateReturns(result1 error) { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.UpdateStub = nil + fake.updateReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskRepository) UpdateReturnsOnCall(i int, result1 error) { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.UpdateStub = nil + if fake.updateReturnsOnCall == nil { + fake.updateReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updateReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeTaskRepository) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeTaskRepository) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ service.TaskRepository = new(FakeTaskRepository) diff --git a/internal/service/servicetesting/task_search_repository.gen.go b/internal/service/servicetesting/task_search_repository.gen.go new file mode 100644 index 00000000..6927d9e8 --- /dev/null +++ b/internal/service/servicetesting/task_search_repository.gen.go @@ -0,0 +1,118 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package servicetesting + +import ( + "context" + "sync" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + "github.com/MarioCarrion/todo-api-microservice-example/internal/service" +) + +type FakeTaskSearchRepository struct { + SearchStub func(context.Context, internal.SearchParams) (internal.SearchResults, error) + searchMutex sync.RWMutex + searchArgsForCall []struct { + arg1 context.Context + arg2 internal.SearchParams + } + searchReturns struct { + result1 internal.SearchResults + result2 error + } + searchReturnsOnCall map[int]struct { + result1 internal.SearchResults + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeTaskSearchRepository) Search(arg1 context.Context, arg2 internal.SearchParams) (internal.SearchResults, error) { + fake.searchMutex.Lock() + ret, specificReturn := fake.searchReturnsOnCall[len(fake.searchArgsForCall)] + fake.searchArgsForCall = append(fake.searchArgsForCall, struct { + arg1 context.Context + arg2 internal.SearchParams + }{arg1, arg2}) + stub := fake.SearchStub + fakeReturns := fake.searchReturns + fake.recordInvocation("Search", []interface{}{arg1, arg2}) + fake.searchMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeTaskSearchRepository) SearchCallCount() int { + fake.searchMutex.RLock() + defer fake.searchMutex.RUnlock() + return len(fake.searchArgsForCall) +} + +func (fake *FakeTaskSearchRepository) SearchCalls(stub func(context.Context, internal.SearchParams) (internal.SearchResults, error)) { + fake.searchMutex.Lock() + defer fake.searchMutex.Unlock() + fake.SearchStub = stub +} + +func (fake *FakeTaskSearchRepository) SearchArgsForCall(i int) (context.Context, internal.SearchParams) { + fake.searchMutex.RLock() + defer fake.searchMutex.RUnlock() + argsForCall := fake.searchArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeTaskSearchRepository) SearchReturns(result1 internal.SearchResults, result2 error) { + fake.searchMutex.Lock() + defer fake.searchMutex.Unlock() + fake.SearchStub = nil + fake.searchReturns = struct { + result1 internal.SearchResults + result2 error + }{result1, result2} +} + +func (fake *FakeTaskSearchRepository) SearchReturnsOnCall(i int, result1 internal.SearchResults, result2 error) { + fake.searchMutex.Lock() + defer fake.searchMutex.Unlock() + fake.SearchStub = nil + if fake.searchReturnsOnCall == nil { + fake.searchReturnsOnCall = make(map[int]struct { + result1 internal.SearchResults + result2 error + }) + } + fake.searchReturnsOnCall[i] = struct { + result1 internal.SearchResults + result2 error + }{result1, result2} +} + +func (fake *FakeTaskSearchRepository) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeTaskSearchRepository) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ service.TaskSearchRepository = new(FakeTaskSearchRepository) diff --git a/internal/service/task.go b/internal/service/task.go index 8c87d530..87ca431e 100644 --- a/internal/service/task.go +++ b/internal/service/task.go @@ -10,6 +10,10 @@ import ( "github.com/MarioCarrion/todo-api-microservice-example/internal" ) +//go:generate counterfeiter -generate + +//counterfeiter:generate -o servicetesting/task_repository.gen.go . TaskRepository + // TaskRepository defines the datastore handling persisting Task records. type TaskRepository interface { Create(ctx context.Context, params internal.CreateParams) (internal.Task, error) @@ -18,11 +22,15 @@ type TaskRepository interface { Update(ctx context.Context, id string, params internal.UpdateParams) error } +//counterfeiter:generate -o servicetesting/task_search_repository.gen.go . TaskSearchRepository + // TaskSearchRepository defines the datastore handling searching Task records. type TaskSearchRepository interface { Search(ctx context.Context, args internal.SearchParams) (internal.SearchResults, error) } +//counterfeiter:generate -o servicetesting/task_message_broker_publisher.gen.go . TaskMessageBrokerPublisher + // TaskMessageBrokerPublisher defines the datastore used to publish Searchable Task records. type TaskMessageBrokerPublisher interface { Created(ctx context.Context, task internal.Task) error diff --git a/internal/service/task_test.go b/internal/service/task_test.go new file mode 100644 index 00000000..bacea072 --- /dev/null +++ b/internal/service/task_test.go @@ -0,0 +1,498 @@ +package service_test + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "go.uber.org/zap" + + "github.com/MarioCarrion/todo-api-microservice-example/internal" + "github.com/MarioCarrion/todo-api-microservice-example/internal/service" +) + +// mockTaskRepository is a mock implementation of TaskRepository for testing. +type mockTaskRepository struct { + createFn func(_ context.Context, params internal.CreateParams) (internal.Task, error) + deleteFn func(_ context.Context, id string) error + findFn func(_ context.Context, id string) (internal.Task, error) + updateFn func(_ context.Context, id string, params internal.UpdateParams) error +} + +func (m *mockTaskRepository) Create(ctx context.Context, params internal.CreateParams) (internal.Task, error) { + if m.createFn != nil { + return m.createFn(ctx, params) + } + + return internal.Task{}, nil +} + +func (m *mockTaskRepository) Delete(ctx context.Context, id string) error { + if m.deleteFn != nil { + return m.deleteFn(ctx, id) + } + + return nil +} + +func (m *mockTaskRepository) Find(ctx context.Context, id string) (internal.Task, error) { + if m.findFn != nil { + return m.findFn(ctx, id) + } + + return internal.Task{}, nil +} + +func (m *mockTaskRepository) Update(ctx context.Context, id string, params internal.UpdateParams) error { + if m.updateFn != nil { + return m.updateFn(ctx, id, params) + } + + return nil +} + +// mockTaskSearchRepository is a mock implementation of TaskSearchRepository. +type mockTaskSearchRepository struct { + searchFn func(_ context.Context, args internal.SearchParams) (internal.SearchResults, error) +} + +func (m *mockTaskSearchRepository) Search(ctx context.Context, args internal.SearchParams) (internal.SearchResults, error) { + if m.searchFn != nil { + return m.searchFn(ctx, args) + } + + return internal.SearchResults{}, nil +} + +// mockTaskMessageBrokerPublisher is a mock implementation of TaskMessageBrokerPublisher. +type mockTaskMessageBrokerPublisher struct { + createdFn func(_ context.Context, task internal.Task) error + deletedFn func(_ context.Context, id string) error + updatedFn func(_ context.Context, task internal.Task) error +} + +func (m *mockTaskMessageBrokerPublisher) Created(ctx context.Context, task internal.Task) error { + if m.createdFn != nil { + return m.createdFn(ctx, task) + } + + return nil +} + +func (m *mockTaskMessageBrokerPublisher) Deleted(ctx context.Context, id string) error { + if m.deletedFn != nil { + return m.deletedFn(ctx, id) + } + + return nil +} + +func (m *mockTaskMessageBrokerPublisher) Updated(ctx context.Context, task internal.Task) error { + if m.updatedFn != nil { + return m.updatedFn(ctx, task) + } + + return nil +} + +func TestTask_Create(t *testing.T) { + t.Parallel() + + logger := zap.NewNop() + + tests := []struct { + name string + params internal.CreateParams + mockRepo *mockTaskRepository + mockMsgBroker *mockTaskMessageBrokerPublisher + verify func(*testing.T, internal.Task, error) + }{ + { + name: "successful create", + params: internal.CreateParams{ + Description: "test task", + Priority: internal.PriorityHigh.Pointer(), + }, + mockRepo: &mockTaskRepository{ + createFn: func(_ context.Context, params internal.CreateParams) (internal.Task, error) { + return internal.Task{ + ID: "123", + Description: params.Description, + Priority: params.Priority, + }, nil + }, + }, + mockMsgBroker: &mockTaskMessageBrokerPublisher{}, + verify: func(t *testing.T, task internal.Task, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expectedTask := internal.Task{ + ID: "123", + Description: "test task", + Priority: internal.PriorityHigh.Pointer(), + } + if diff := cmp.Diff(expectedTask, task); diff != "" { + t.Errorf("task mismatch (-want +got):\n%s", diff) + } + }, + }, + { + name: "validation error", + params: internal.CreateParams{ + Description: "", // Invalid - empty description + }, + mockRepo: &mockTaskRepository{}, + mockMsgBroker: &mockTaskMessageBrokerPublisher{}, + verify: func(t *testing.T, _ internal.Task, err error) { + t.Helper() + if err == nil { + t.Fatalf("expected error containing %q, got nil", "params.Validate") + } + if !strings.Contains(err.Error(), "params.Validate") { + t.Errorf("expected error containing %q, got %q", "params.Validate", err.Error()) + } + }, + }, + { + name: "repository error", + params: internal.CreateParams{ + Description: "test task", + Priority: internal.PriorityHigh.Pointer(), + }, + mockRepo: &mockTaskRepository{ + createFn: func(_ context.Context, _ internal.CreateParams) (internal.Task, error) { + return internal.Task{}, errors.New("database error") + }, + }, + mockMsgBroker: &mockTaskMessageBrokerPublisher{}, + verify: func(t *testing.T, _ internal.Task, err error) { + t.Helper() + if err == nil { + t.Fatalf("expected error containing %q, got nil", "repo.Create") + } + if !strings.Contains(err.Error(), "repo.Create") { + t.Errorf("expected error containing %q, got %q", "repo.Create", err.Error()) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + svc := service.NewTask(logger, tt.mockRepo, &mockTaskSearchRepository{}, tt.mockMsgBroker) + task, err := svc.Create(t.Context(), tt.params) + tt.verify(t, task, err) + }) + } +} + +func TestTask_Delete(t *testing.T) { + t.Parallel() + + logger := zap.NewNop() + + tests := []struct { + name string + id string + mockRepo *mockTaskRepository + mockMsgBroker *mockTaskMessageBrokerPublisher + verify func(*testing.T, error) + }{ + { + name: "successful delete", + id: "123", + mockRepo: &mockTaskRepository{ + deleteFn: func(_ context.Context, _ string) error { + return nil + }, + }, + mockMsgBroker: &mockTaskMessageBrokerPublisher{}, + verify: func(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }, + }, + { + name: "repository error", + id: "123", + mockRepo: &mockTaskRepository{ + deleteFn: func(_ context.Context, _ string) error { + return errors.New("database error") + }, + }, + mockMsgBroker: &mockTaskMessageBrokerPublisher{}, + verify: func(t *testing.T, err error) { + t.Helper() + if err == nil { + t.Fatalf("expected error containing %q, got nil", "Delete") + } + if !strings.Contains(err.Error(), "Delete") { + t.Errorf("expected error containing %q, got %q", "Delete", err.Error()) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + svc := service.NewTask(logger, tt.mockRepo, &mockTaskSearchRepository{}, tt.mockMsgBroker) + err := svc.Delete(t.Context(), tt.id) + tt.verify(t, err) + }) + } +} + +func TestTask_ByID(t *testing.T) { + t.Parallel() + + logger := zap.NewNop() + + tests := []struct { + name string + id string + mockRepo *mockTaskRepository + verify func(*testing.T, internal.Task, error) + }{ + { + name: "successful find", + id: "123", + mockRepo: &mockTaskRepository{ + findFn: func(_ context.Context, id string) (internal.Task, error) { + return internal.Task{ + ID: id, + Description: "test task", + }, nil + }, + }, + verify: func(t *testing.T, task internal.Task, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expectedTask := internal.Task{ + ID: "123", + Description: "test task", + } + if diff := cmp.Diff(expectedTask, task); diff != "" { + t.Errorf("task mismatch (-want +got):\n%s", diff) + } + }, + }, + { + name: "repository error", + id: "123", + mockRepo: &mockTaskRepository{ + findFn: func(_ context.Context, _ string) (internal.Task, error) { + return internal.Task{}, errors.New("not found") + }, + }, + verify: func(t *testing.T, _ internal.Task, err error) { + t.Helper() + if err == nil { + t.Fatalf("expected error containing %q, got nil", "Find") + } + if !strings.Contains(err.Error(), "Find") { + t.Errorf("expected error containing %q, got %q", "Find", err.Error()) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + svc := service.NewTask(logger, tt.mockRepo, &mockTaskSearchRepository{}, &mockTaskMessageBrokerPublisher{}) + task, err := svc.ByID(t.Context(), tt.id) + tt.verify(t, task, err) + }) + } +} + +func TestTask_Update(t *testing.T) { + t.Parallel() + + logger := zap.NewNop() + + tests := []struct { + name string + id string + params internal.UpdateParams + mockRepo *mockTaskRepository + mockMsgBroker *mockTaskMessageBrokerPublisher + verify func(*testing.T, error) + }{ + { + name: "successful update", + id: "123", + params: internal.UpdateParams{ + Description: internal.ValueToPointer("updated task"), + }, + mockRepo: &mockTaskRepository{ + updateFn: func(_ context.Context, _ string, _ internal.UpdateParams) error { + return nil + }, + findFn: func(_ context.Context, id string) (internal.Task, error) { + return internal.Task{ID: id, Description: "updated task"}, nil + }, + }, + mockMsgBroker: &mockTaskMessageBrokerPublisher{}, + verify: func(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }, + }, + { + name: "repository update error", + id: "123", + params: internal.UpdateParams{ + Description: internal.ValueToPointer("updated task"), + }, + mockRepo: &mockTaskRepository{ + updateFn: func(_ context.Context, _ string, _ internal.UpdateParams) error { + return errors.New("database error") + }, + }, + mockMsgBroker: &mockTaskMessageBrokerPublisher{}, + verify: func(t *testing.T, err error) { + t.Helper() + if err == nil { + t.Fatalf("expected error containing %q, got nil", "repo.Update") + } + if !strings.Contains(err.Error(), "repo.Update") { + t.Errorf("expected error containing %q, got %q", "repo.Update", err.Error()) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + svc := service.NewTask(logger, tt.mockRepo, &mockTaskSearchRepository{}, tt.mockMsgBroker) + err := svc.Update(t.Context(), tt.id, tt.params) + tt.verify(t, err) + }) + } +} + +func TestTask_By(t *testing.T) { + t.Parallel() + + logger := zap.NewNop() + + tests := []struct { + name string + params internal.SearchParams + mockSearch *mockTaskSearchRepository + verify func(*testing.T, internal.SearchResults, error) + }{ + { + name: "successful search", + params: internal.SearchParams{ + Description: internal.ValueToPointer("test"), + From: 0, + Size: 10, + }, + mockSearch: &mockTaskSearchRepository{ + searchFn: func(_ context.Context, _ internal.SearchParams) (internal.SearchResults, error) { + return internal.SearchResults{ + Tasks: []internal.Task{ + {ID: "1", Description: "test task 1"}, + {ID: "2", Description: "test task 2"}, + }, + Total: 2, + }, nil + }, + }, + verify: func(t *testing.T, result internal.SearchResults, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expectedResult := internal.SearchResults{ + Tasks: []internal.Task{ + {ID: "1", Description: "test task 1"}, + {ID: "2", Description: "test task 2"}, + }, + Total: 2, + } + if diff := cmp.Diff(expectedResult, result); diff != "" { + t.Errorf("result mismatch (-want +got):\n%s", diff) + } + }, + }, + { + name: "search error", + params: internal.SearchParams{ + Description: internal.ValueToPointer("test"), + }, + mockSearch: &mockTaskSearchRepository{ + searchFn: func(_ context.Context, _ internal.SearchParams) (internal.SearchResults, error) { + return internal.SearchResults{}, errors.New("search error") + }, + }, + verify: func(t *testing.T, _ internal.SearchResults, err error) { + t.Helper() + if err == nil { + t.Fatalf("expected error containing %q, got nil", "search") + } + if !strings.Contains(err.Error(), "search") { + t.Errorf("expected error containing %q, got %q", "search", err.Error()) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + svc := service.NewTask(logger, &mockTaskRepository{}, tt.mockSearch, &mockTaskMessageBrokerPublisher{}) + result, err := svc.By(t.Context(), tt.params) + tt.verify(t, result, err) + }) + } +} + +func TestNewTask(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + }{ + { + name: "creates new task service", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + logger := zap.NewNop() + repo := &mockTaskRepository{} + search := &mockTaskSearchRepository{} + msgBroker := &mockTaskMessageBrokerPublisher{} + + svc := service.NewTask(logger, repo, search, msgBroker) + + if svc == nil { + t.Fatal("expected non-nil service") + } + }) + } +} diff --git a/internal/todo_test.go b/internal/todo_test.go index dad8e83d..7cfa91b8 100644 --- a/internal/todo_test.go +++ b/internal/todo_test.go @@ -60,6 +60,99 @@ func TestPriority_Validate(t *testing.T) { } } +func TestPriority_Pointer(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + priority internal.Priority + }{ + { + name: "PriorityNone", + priority: internal.PriorityNone, + }, + { + name: "PriorityLow", + priority: internal.PriorityLow, + }, + { + name: "PriorityMedium", + priority: internal.PriorityMedium, + }, + { + name: "PriorityHigh", + priority: internal.PriorityHigh, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ptr := tt.priority.Pointer() + if ptr == nil { + t.Fatal("expected non-nil pointer") + } + + if *ptr != tt.priority { + t.Errorf("expected *%v, got *%v", tt.priority, *ptr) + } + }) + } +} + +func TestPriority_Value(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input *internal.Priority + expected internal.Priority + }{ + { + name: "nil pointer", + input: nil, + expected: internal.PriorityNone, + }, + { + name: "PriorityNone pointer", + input: internal.PriorityNone.Pointer(), + expected: internal.PriorityNone, + }, + { + name: "PriorityLow pointer", + input: internal.PriorityLow.Pointer(), + expected: internal.PriorityLow, + }, + { + name: "PriorityMedium pointer", + input: internal.PriorityMedium.Pointer(), + expected: internal.PriorityMedium, + }, + { + name: "PriorityHigh pointer", + input: internal.PriorityHigh.Pointer(), + expected: internal.PriorityHigh, + }, + { + name: "invalid priority pointer returns PriorityNone", + input: internal.Priority(-1).Pointer(), + expected: internal.PriorityNone, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := tt.input.Value() + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} + func TestDates_Validate(t *testing.T) { t.Parallel()