From 89b22d5a36ae20eb5d937aa3169f90ad2edaf702 Mon Sep 17 00:00:00 2001 From: Xi Xu Date: Sun, 29 Mar 2026 19:35:50 +0800 Subject: [PATCH] fix(api): resolve docker dependabot alerts --- apps/api/go.mod | 53 +----- apps/api/go.sum | 148 +-------------- .../api/internal/http/api_integration_test.go | 116 +++++------- .../ratelimit/limiter_integration_test.go | 39 +--- .../repo/postgres_integration_test.go | 49 ++--- .../internal/storage/s3_integration_test.go | 59 +++--- apps/api/internal/testutil/docker.go | 178 ++++++++++++++++++ apps/api/internal/testutil/docker_test.go | 35 ++++ 8 files changed, 315 insertions(+), 362 deletions(-) create mode 100644 apps/api/internal/testutil/docker.go create mode 100644 apps/api/internal/testutil/docker_test.go diff --git a/apps/api/go.mod b/apps/api/go.mod index 25306b7..4f6b617 100644 --- a/apps/api/go.mod +++ b/apps/api/go.mod @@ -13,14 +13,9 @@ require ( github.com/jackc/pgx/v5 v5.9.1 github.com/redis/go-redis/v9 v9.18.0 github.com/stretchr/testify v1.11.1 - github.com/testcontainers/testcontainers-go v0.41.0 - github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 ) require ( - dario.cat/mergo v1.0.2 // indirect - github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect @@ -36,61 +31,17 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect github.com/aws/smithy-go v1.24.2 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/errdefs v1.0.0 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.5.2+incompatible // indirect - github.com/docker/go-connections v0.6.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/ebitengine/purego v0.10.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/klauspost/compress v1.18.2 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/magiconair/properties v1.8.10 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/go-archive v0.2.0 // indirect - github.com/moby/patternmatcher v0.6.0 // indirect - github.com/moby/sys/sequential v0.6.0 // indirect - github.com/moby/sys/user v0.4.0 // indirect - github.com/moby/sys/userns v0.1.0 // indirect - github.com/moby/term v0.5.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/shirou/gopsutil/v4 v4.26.2 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/tklauser/go-sysconf v0.3.16 // indirect - github.com/tklauser/numcpus v0.11.0 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.48.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect - google.golang.org/grpc v1.79.3 // indirect - google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/apps/api/go.sum b/apps/api/go.sum index 5b1c94c..2e42dfa 100644 --- a/apps/api/go.sum +++ b/apps/api/go.sum @@ -1,11 +1,3 @@ -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY= @@ -50,59 +42,18 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -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/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= -github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= -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/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= -github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -111,129 +62,36 @@ github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= 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/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= -github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= -github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= -github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais= -github.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI= -github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 h1:AOtFXssrDlLm84A2sTTR/AhvJiYbrIuCO59d+Ro9Tb0= -github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0/go.mod h1:k2a09UKhgSp6vNpliIY0QSgm4Hi7GXVTzWvWgUemu/8= -github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= -github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= -github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= -github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/apps/api/internal/http/api_integration_test.go b/apps/api/internal/http/api_integration_test.go index 1559d55..b20abba 100644 --- a/apps/api/internal/http/api_integration_test.go +++ b/apps/api/internal/http/api_integration_test.go @@ -10,22 +10,18 @@ import ( "log/slog" nethttp "net/http" "net/http/httptest" - "os/exec" - "runtime" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" - "github.com/testcontainers/testcontainers-go/wait" "github.com/xdrop/monorepo/internal/config" "github.com/xdrop/monorepo/internal/ratelimit" "github.com/xdrop/monorepo/internal/repo" "github.com/xdrop/monorepo/internal/service" "github.com/xdrop/monorepo/internal/storage" + "github.com/xdrop/monorepo/internal/testutil" ) func TestAPITransferLifecycleEndToEnd(t *testing.T) { @@ -179,26 +175,27 @@ func startHTTPIntegrationStack(t *testing.T, ctx context.Context) httpIntegratio func startHTTPPostgresDB(t *testing.T, ctx context.Context) *pgxpool.Pool { t.Helper() - container, err := tcpostgres.Run( - ctx, - "postgres:16-alpine", - tcpostgres.WithDatabase("xdrop"), - tcpostgres.WithUsername("xdrop"), - tcpostgres.WithPassword("xdrop"), - tcpostgres.BasicWaitStrategies(), - ) - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, testcontainers.TerminateContainer(container)) + container := testutil.StartDockerContainer(t, ctx, testutil.DockerRunRequest{ + NamePrefix: "xdrop-http-postgres", + Image: "postgres:16-alpine", + Env: map[string]string{ + "POSTGRES_DB": "xdrop", + "POSTGRES_USER": "xdrop", + "POSTGRES_PASSWORD": "xdrop", + }, + ExposedPorts: []string{"5432/tcp"}, }) - connectionString, err := container.ConnectionString(ctx, "sslmode=disable") - require.NoError(t, err) - + connectionString := fmt.Sprintf( + "postgres://xdrop:xdrop@127.0.0.1:%s/xdrop?sslmode=disable", + container.PublishedPort(t, ctx, "5432/tcp"), + ) db, err := pgxpool.New(ctx, connectionString) require.NoError(t, err) - require.NoError(t, db.Ping(ctx)) t.Cleanup(db.Close) + require.NoError(t, testutil.WaitForCondition(ctx, 60*time.Second, 500*time.Millisecond, func() error { + return db.Ping(ctx) + })) return db } @@ -206,29 +203,19 @@ func startHTTPPostgresDB(t *testing.T, ctx context.Context) *pgxpool.Pool { func startHTTPRedisClient(t *testing.T, ctx context.Context) *redis.Client { t.Helper() - container, err := testcontainers.Run( - ctx, - "redis:7-alpine", - testcontainers.WithExposedPorts("6379/tcp"), - testcontainers.WithWaitStrategy( - wait.ForLog("Ready to accept connections").WithStartupTimeout(60*time.Second), - ), - ) - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, testcontainers.TerminateContainer(container)) + container := testutil.StartDockerContainer(t, ctx, testutil.DockerRunRequest{ + NamePrefix: "xdrop-http-redis", + Image: "redis:7-alpine", + ExposedPorts: []string{"6379/tcp"}, }) - host, err := container.Host(ctx) - require.NoError(t, err) - port, err := container.MappedPort(ctx, "6379/tcp") - require.NoError(t, err) - client := redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%s", host, port.Port()), + Addr: fmt.Sprintf("127.0.0.1:%s", container.PublishedPort(t, ctx, "6379/tcp")), DB: 0, }) - require.NoError(t, client.Ping(ctx).Err()) + require.NoError(t, testutil.WaitForCondition(ctx, 60*time.Second, 500*time.Millisecond, func() error { + return client.Ping(ctx).Err() + })) t.Cleanup(func() { require.NoError(t, client.Close()) }) @@ -239,32 +226,29 @@ func startHTTPRedisClient(t *testing.T, ctx context.Context) *redis.Client { func startHTTPStorage(t *testing.T, ctx context.Context) (*storage.S3Storage, config.Config) { t.Helper() - container, err := testcontainers.Run( - ctx, - "minio/minio:latest", - testcontainers.WithEnv(map[string]string{ + container := testutil.StartDockerContainer(t, ctx, testutil.DockerRunRequest{ + NamePrefix: "xdrop-http-minio", + Image: "minio/minio:latest", + Env: map[string]string{ "MINIO_ROOT_USER": "minioadmin", "MINIO_ROOT_PASSWORD": "minioadmin", - }), - testcontainers.WithExposedPorts("9000/tcp"), - testcontainers.WithCmd("server", "/data"), - testcontainers.WithWaitStrategy( - wait.ForHTTP("/minio/health/live"). - WithPort("9000/tcp"). - WithStartupTimeout(90*time.Second), - ), - ) - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, testcontainers.TerminateContainer(container)) + }, + ExposedPorts: []string{"9000/tcp"}, + Command: []string{"server", "/data"}, }) - host, err := container.Host(ctx) - require.NoError(t, err) - port, err := container.MappedPort(ctx, "9000/tcp") - require.NoError(t, err) - - endpoint := fmt.Sprintf("http://%s:%s", host, port.Port()) + endpoint := fmt.Sprintf("http://127.0.0.1:%s", container.PublishedPort(t, ctx, "9000/tcp")) + require.NoError(t, testutil.WaitForCondition(ctx, 90*time.Second, 500*time.Millisecond, func() error { + response, err := nethttp.Get(endpoint + "/minio/health/live") + if err != nil { + return err + } + defer response.Body.Close() + if response.StatusCode >= 400 { + return fmt.Errorf("minio returned status %d", response.StatusCode) + } + return nil + })) objectStorage, err := storage.NewS3Storage(ctx, storage.Config{ Endpoint: endpoint, PublicEndpoint: endpoint, @@ -325,15 +309,5 @@ func doJSON(t *testing.T, client *nethttp.Client, method string, url string, bea func skipIfDockerUnavailable(t *testing.T) { t.Helper() - - if testing.Short() { - t.Skip("skipping docker-backed integration test in short mode") - } - if runtime.GOOS == "windows" { - t.Skip("skipping docker-backed integration test on windows") - } - - if err := exec.Command("docker", "info").Run(); err != nil { - t.Skipf("skipping docker-backed integration test: %v", err) - } + testutil.SkipIfDockerUnavailable(t, true) } diff --git a/apps/api/internal/ratelimit/limiter_integration_test.go b/apps/api/internal/ratelimit/limiter_integration_test.go index 1198e37..061d331 100644 --- a/apps/api/internal/ratelimit/limiter_integration_test.go +++ b/apps/api/internal/ratelimit/limiter_integration_test.go @@ -3,14 +3,12 @@ package ratelimit import ( "context" "fmt" - "os/exec" "testing" "time" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" + "github.com/xdrop/monorepo/internal/testutil" ) func TestRedisLimiterBlocksThenResetsAfterWindow(t *testing.T) { @@ -39,29 +37,19 @@ func TestRedisLimiterBlocksThenResetsAfterWindow(t *testing.T) { func startRedisClient(t *testing.T, ctx context.Context) *redis.Client { t.Helper() - container, err := testcontainers.Run( - ctx, - "redis:7-alpine", - testcontainers.WithExposedPorts("6379/tcp"), - testcontainers.WithWaitStrategy( - wait.ForLog("Ready to accept connections").WithStartupTimeout(60*time.Second), - ), - ) - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, testcontainers.TerminateContainer(container)) + container := testutil.StartDockerContainer(t, ctx, testutil.DockerRunRequest{ + NamePrefix: "xdrop-redis", + Image: "redis:7-alpine", + ExposedPorts: []string{"6379/tcp"}, }) - host, err := container.Host(ctx) - require.NoError(t, err) - port, err := container.MappedPort(ctx, "6379/tcp") - require.NoError(t, err) - client := redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%s", host, port.Port()), + Addr: fmt.Sprintf("127.0.0.1:%s", container.PublishedPort(t, ctx, "6379/tcp")), DB: 0, }) - require.NoError(t, client.Ping(ctx).Err()) + require.NoError(t, testutil.WaitForCondition(ctx, 60*time.Second, 500*time.Millisecond, func() error { + return client.Ping(ctx).Err() + })) t.Cleanup(func() { require.NoError(t, client.Close()) }) @@ -71,12 +59,5 @@ func startRedisClient(t *testing.T, ctx context.Context) *redis.Client { func skipIfDockerUnavailable(t *testing.T) { t.Helper() - - if testing.Short() { - t.Skip("skipping docker-backed integration test in short mode") - } - - if err := exec.Command("docker", "info").Run(); err != nil { - t.Skipf("skipping docker-backed integration test: %v", err) - } + testutil.SkipIfDockerUnavailable(t, false) } diff --git a/apps/api/internal/repo/postgres_integration_test.go b/apps/api/internal/repo/postgres_integration_test.go index 3a43d2d..9cabaf8 100644 --- a/apps/api/internal/repo/postgres_integration_test.go +++ b/apps/api/internal/repo/postgres_integration_test.go @@ -2,16 +2,14 @@ package repo import ( "context" - "os/exec" - "runtime" + "fmt" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/xdrop/monorepo/internal/models" + "github.com/xdrop/monorepo/internal/testutil" ) func TestRunMigrationsAndRepositoryLifecycle(t *testing.T) { @@ -216,26 +214,27 @@ func TestPostgresRepositoryCleanupCandidatesAndPurging(t *testing.T) { func startPostgresTestDB(t *testing.T, ctx context.Context) *pgxpool.Pool { t.Helper() - container, err := tcpostgres.Run( - ctx, - "postgres:16-alpine", - tcpostgres.WithDatabase("xdrop"), - tcpostgres.WithUsername("xdrop"), - tcpostgres.WithPassword("xdrop"), - tcpostgres.BasicWaitStrategies(), - ) - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, testcontainers.TerminateContainer(container)) + container := testutil.StartDockerContainer(t, ctx, testutil.DockerRunRequest{ + NamePrefix: "xdrop-postgres", + Image: "postgres:16-alpine", + Env: map[string]string{ + "POSTGRES_DB": "xdrop", + "POSTGRES_USER": "xdrop", + "POSTGRES_PASSWORD": "xdrop", + }, + ExposedPorts: []string{"5432/tcp"}, }) - connectionString, err := container.ConnectionString(ctx, "sslmode=disable") - require.NoError(t, err) - + connectionString := fmt.Sprintf( + "postgres://xdrop:xdrop@127.0.0.1:%s/xdrop?sslmode=disable", + container.PublishedPort(t, ctx, "5432/tcp"), + ) db, err := pgxpool.New(ctx, connectionString) require.NoError(t, err) - require.NoError(t, db.Ping(ctx)) t.Cleanup(db.Close) + require.NoError(t, testutil.WaitForCondition(ctx, 60*time.Second, 500*time.Millisecond, func() error { + return db.Ping(ctx) + })) return db } @@ -258,15 +257,5 @@ func int64ptr(value int64) *int64 { func skipIfDockerUnavailable(t *testing.T) { t.Helper() - - if testing.Short() { - t.Skip("skipping docker-backed integration test in short mode") - } - if runtime.GOOS == "windows" { - t.Skip("skipping docker-backed integration test on windows") - } - - if err := exec.Command("docker", "info").Run(); err != nil { - t.Skipf("skipping docker-backed integration test: %v", err) - } + testutil.SkipIfDockerUnavailable(t, true) } diff --git a/apps/api/internal/storage/s3_integration_test.go b/apps/api/internal/storage/s3_integration_test.go index cb0fffe..beb5232 100644 --- a/apps/api/internal/storage/s3_integration_test.go +++ b/apps/api/internal/storage/s3_integration_test.go @@ -3,8 +3,7 @@ package storage import ( "context" "fmt" - "os/exec" - "runtime" + nethttp "net/http" "strings" "testing" "time" @@ -12,8 +11,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" + "github.com/xdrop/monorepo/internal/testutil" ) func TestS3StorageLifecycleAgainstMinIO(t *testing.T) { @@ -84,32 +82,31 @@ func TestReadAllReadsEntireStream(t *testing.T) { func startMinIOConfig(t *testing.T, ctx context.Context) Config { t.Helper() - container, err := testcontainers.Run( - ctx, - "minio/minio:latest", - testcontainers.WithEnv(map[string]string{ + container := testutil.StartDockerContainer(t, ctx, testutil.DockerRunRequest{ + NamePrefix: "xdrop-minio", + Image: "minio/minio:latest", + Env: map[string]string{ "MINIO_ROOT_USER": "minioadmin", "MINIO_ROOT_PASSWORD": "minioadmin", - }), - testcontainers.WithExposedPorts("9000/tcp"), - testcontainers.WithCmd("server", "/data"), - testcontainers.WithWaitStrategy( - wait.ForHTTP("/minio/health/live"). - WithPort("9000/tcp"). - WithStartupTimeout(90*time.Second), - ), - ) - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, testcontainers.TerminateContainer(container)) + }, + ExposedPorts: []string{"9000/tcp"}, + Command: []string{"server", "/data"}, }) - host, err := container.Host(ctx) - require.NoError(t, err) - port, err := container.MappedPort(ctx, "9000/tcp") - require.NoError(t, err) + port := container.PublishedPort(t, ctx, "9000/tcp") + internalEndpoint := fmt.Sprintf("http://127.0.0.1:%s", port) + require.NoError(t, testutil.WaitForCondition(ctx, 90*time.Second, 500*time.Millisecond, func() error { + response, err := nethttp.Get(internalEndpoint + "/minio/health/live") + if err != nil { + return err + } + defer response.Body.Close() + if response.StatusCode >= 400 { + return fmt.Errorf("minio returned status %d", response.StatusCode) + } + return nil + })) - internalEndpoint := fmt.Sprintf("http://%s:%s", host, port.Port()) return Config{ Endpoint: internalEndpoint, PublicEndpoint: "http://public.example.test:9000", @@ -158,15 +155,5 @@ func listObjectKeys(t *testing.T, ctx context.Context, store *S3Storage) []strin func skipIfDockerUnavailable(t *testing.T) { t.Helper() - - if testing.Short() { - t.Skip("skipping docker-backed integration test in short mode") - } - if runtime.GOOS == "windows" { - t.Skip("skipping docker-backed integration test on windows") - } - - if err := exec.Command("docker", "info").Run(); err != nil { - t.Skipf("skipping docker-backed integration test: %v", err) - } + testutil.SkipIfDockerUnavailable(t, true) } diff --git a/apps/api/internal/testutil/docker.go b/apps/api/internal/testutil/docker.go new file mode 100644 index 0000000..f180405 --- /dev/null +++ b/apps/api/internal/testutil/docker.go @@ -0,0 +1,178 @@ +package testutil + +import ( + "context" + "fmt" + "io" + "net" + "os/exec" + "runtime" + "slices" + "strings" + "testing" + "time" +) + +type DockerRunRequest struct { + NamePrefix string + Image string + Env map[string]string + ExposedPorts []string + Command []string +} + +type DockerContainer struct { + name string +} + +func SkipIfDockerUnavailable(t *testing.T, skipOnWindows bool) { + t.Helper() + + if testing.Short() { + t.Skip("skipping docker-backed integration test in short mode") + } + if skipOnWindows && runtime.GOOS == "windows" { + t.Skip("skipping docker-backed integration test on windows") + } + + cmd := exec.Command("docker", "info") + cmd.Stdout = io.Discard + cmd.Stderr = io.Discard + if err := cmd.Run(); err != nil { + t.Skipf("skipping docker-backed integration test: %v", err) + } +} + +func StartDockerContainer(t *testing.T, ctx context.Context, request DockerRunRequest) *DockerContainer { + t.Helper() + + name := uniqueContainerName(request.NamePrefix) + args := []string{"run", "-d", "--rm", "--name", name} + + if len(request.Env) > 0 { + keys := make([]string, 0, len(request.Env)) + for key := range request.Env { + keys = append(keys, key) + } + slices.Sort(keys) + for _, key := range keys { + args = append(args, "-e", fmt.Sprintf("%s=%s", key, request.Env[key])) + } + } + + for _, port := range request.ExposedPorts { + containerPort := strings.TrimSuffix(port, "/tcp") + args = append(args, "-p", fmt.Sprintf("127.0.0.1::%s", containerPort)) + } + + args = append(args, request.Image) + args = append(args, request.Command...) + + cmd := exec.CommandContext(ctx, "docker", args...) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("docker run failed: %v\n%s", err, strings.TrimSpace(string(output))) + } + + container := &DockerContainer{name: name} + t.Cleanup(func() { + terminateCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + if err := container.Terminate(terminateCtx); err != nil { + t.Fatalf("failed to terminate docker container %s: %v", name, err) + } + }) + + return container +} + +func (container *DockerContainer) PublishedPort(t *testing.T, ctx context.Context, port string) string { + t.Helper() + + cmd := exec.CommandContext(ctx, "docker", "port", container.name, port) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("docker port failed: %v\n%s", err, strings.TrimSpace(string(output))) + } + + publishedPort, err := parsePublishedPort(string(output)) + if err != nil { + t.Fatalf("failed to parse published port for %s: %v", port, err) + } + + return publishedPort +} + +func (container *DockerContainer) Terminate(ctx context.Context) error { + cmd := exec.CommandContext(ctx, "docker", "rm", "-f", container.name) + output, err := cmd.CombinedOutput() + if err != nil { + trimmed := strings.TrimSpace(string(output)) + if strings.Contains(trimmed, "No such container") { + return nil + } + return fmt.Errorf("docker rm -f %s: %w: %s", container.name, err, trimmed) + } + + return nil +} + +func WaitForCondition(ctx context.Context, timeout time.Duration, interval time.Duration, check func() error) error { + deadline := time.Now().Add(timeout) + var lastErr error + + for { + if err := check(); err == nil { + return nil + } else { + lastErr = err + } + + if time.Now().After(deadline) { + break + } + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(interval): + } + } + + if lastErr == nil { + lastErr = fmt.Errorf("condition did not become ready") + } + + return fmt.Errorf("timed out waiting for condition: %w", lastErr) +} + +func parsePublishedPort(output string) (string, error) { + line := strings.TrimSpace(output) + if line == "" { + return "", fmt.Errorf("empty docker port output") + } + + if newline := strings.Index(line, "\n"); newline >= 0 { + line = line[:newline] + } + + if _, port, err := net.SplitHostPort(line); err == nil { + return port, nil + } + + lastColon := strings.LastIndex(line, ":") + if lastColon >= 0 && lastColon < len(line)-1 { + return line[lastColon+1:], nil + } + + return "", fmt.Errorf("unexpected docker port output %q", output) +} + +func uniqueContainerName(prefix string) string { + cleanPrefix := strings.TrimSpace(prefix) + if cleanPrefix == "" { + cleanPrefix = "xdrop-test" + } + + return fmt.Sprintf("%s-%d", cleanPrefix, time.Now().UnixNano()) +} diff --git a/apps/api/internal/testutil/docker_test.go b/apps/api/internal/testutil/docker_test.go new file mode 100644 index 0000000..78ec94d --- /dev/null +++ b/apps/api/internal/testutil/docker_test.go @@ -0,0 +1,35 @@ +package testutil + +import "testing" + +func TestParsePublishedPortParsesIPv4Binding(t *testing.T) { + t.Parallel() + + port, err := parsePublishedPort("127.0.0.1:49154\n") + if err != nil { + t.Fatalf("parsePublishedPort returned error: %v", err) + } + if port != "49154" { + t.Fatalf("parsePublishedPort returned %q, want %q", port, "49154") + } +} + +func TestParsePublishedPortParsesIPv6Binding(t *testing.T) { + t.Parallel() + + port, err := parsePublishedPort("[::]:32768\n") + if err != nil { + t.Fatalf("parsePublishedPort returned error: %v", err) + } + if port != "32768" { + t.Fatalf("parsePublishedPort returned %q, want %q", port, "32768") + } +} + +func TestParsePublishedPortRejectsUnexpectedFormat(t *testing.T) { + t.Parallel() + + if _, err := parsePublishedPort("not-a-port"); err == nil { + t.Fatal("parsePublishedPort succeeded for malformed output") + } +}