From 3cbf97433d6a9fda59e10d4608ac8dbff3778212 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 21 Sep 2025 15:52:36 +0200 Subject: [PATCH 01/52] chore: Add required modules for Jaeger --- src/go.mod | 49 +++++++++++------ src/go.sum | 157 ++++++++++++++++++++++++++--------------------------- 2 files changed, 110 insertions(+), 96 deletions(-) diff --git a/src/go.mod b/src/go.mod index 46fe94f..f0a3bfc 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,41 +3,56 @@ module bookem-user-service go 1.24.5 require ( + github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.10.1 github.com/golang-jwt/jwt/v5 v5.2.3 github.com/lib/pq v1.10.9 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 ) require ( + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gin-contrib/cors v1.7.6 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - golang.org/x/sync v0.15.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + golang.org/x/sync v0.16.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.75.0 // indirect ) require ( - github.com/bytedance/sonic v1.13.3 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -45,12 +60,12 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect - golang.org/x/arch v0.18.0 // indirect - golang.org/x/crypto v0.39.0 - golang.org/x/net v0.41.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.41.0 + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.30.1 diff --git a/src/go.sum b/src/go.sum index 1a1459f..e89c882 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,53 +1,48 @@ -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= -github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= -github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= -github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= -github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= 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= @@ -62,14 +57,10 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -83,8 +74,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -101,49 +90,61 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= -golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0 h1:5kSIJ0y8ckZZKoDhZHdVtcyjVi6rXyAwyaR8mp4zLbg= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0/go.mod h1:i+fIMHvcSQtsIY82/xgiVWRklrNt/O6QriHLjzGeY+s= +go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo= +go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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= @@ -154,5 +155,3 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From 3f3def700d0de18a715811c907d6c2a05b93e47f Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 21 Sep 2025 15:54:56 +0200 Subject: [PATCH 02/52] feat: Initialize Jaeger tracer in `main()` --- src/main.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/main.go b/src/main.go index 3cea061..18fcd2e 100644 --- a/src/main.go +++ b/src/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "database/sql" "fmt" "log" @@ -20,6 +21,14 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" + + "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" ) var ( @@ -55,13 +64,38 @@ func connectToDb() { log.Printf("Connected to DB!") } +func initTracer() func(context.Context) error { + ctx := context.Background() + + exp, err := otlptracehttp.New(ctx) + if err != nil { + log.Fatalf("Failed to create an OTLP HTTP exporter: %v", err) + } + + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName("user-service"), + )), + ) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.TraceContext{}) + return tp.Shutdown +} + func main() { + ctx := context.Background() + shutdown := initTracer() + defer shutdown(ctx) + connectToDb() defer rawDB.Close() syncDatabase() server = gin.Default() + server.Use(otelgin.Middleware("user-service")) server.Use(cors.New(cors.Config{ AllowOrigins: []string{"http://localhost:5173", "http://localhost", "http://bookem.local"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, From 91acf0a5e33d49873a5159b2b62d3afaaa56fea6 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 21 Sep 2025 16:13:03 +0200 Subject: [PATCH 03/52] feat: Set tracer variables from the environment These env vars should also be updated in `infrastructure` and `helm-charts`. --- compose.integration.yml | 2 ++ src/main.go | 15 +++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/compose.integration.yml b/compose.integration.yml index f07f947..a513f4d 100644 --- a/compose.integration.yml +++ b/compose.integration.yml @@ -27,6 +27,8 @@ services: JWT_PRIVATE_KEY_PATH: /app/keys/private_key.key JWT_PUBLIC_KEY_PATH: /app/keys/public_key.pem ENABLE_TEST_MODE: "true" + SERVICE_NAME: user-service + DEPLOYMENT_ENV: test depends_on: db: condition: service_healthy diff --git a/src/main.go b/src/main.go index 18fcd2e..61ba3b3 100644 --- a/src/main.go +++ b/src/main.go @@ -64,9 +64,7 @@ func connectToDb() { log.Printf("Connected to DB!") } -func initTracer() func(context.Context) error { - ctx := context.Background() - +func initTracer(ctx context.Context, serviceName, deploymentEnvironment string) func(context.Context) error { exp, err := otlptracehttp.New(ctx) if err != nil { log.Fatalf("Failed to create an OTLP HTTP exporter: %v", err) @@ -76,7 +74,8 @@ func initTracer() func(context.Context) error { sdktrace.WithBatcher(exp), sdktrace.WithResource(resource.NewWithAttributes( semconv.SchemaURL, - semconv.ServiceName("user-service"), + semconv.ServiceName(serviceName), + semconv.DeploymentEnvironment(deploymentEnvironment), )), ) otel.SetTracerProvider(tp) @@ -86,7 +85,11 @@ func initTracer() func(context.Context) error { func main() { ctx := context.Background() - shutdown := initTracer() + shutdown := initTracer( + ctx, + os.Getenv("SERVICE_NAME"), + os.Getenv("DEPLOYMENT_ENV"), + ) defer shutdown(ctx) connectToDb() @@ -95,7 +98,7 @@ func main() { server = gin.Default() - server.Use(otelgin.Middleware("user-service")) + server.Use(otelgin.Middleware(os.Getenv("SERVICE_NAME"))) server.Use(cors.New(cors.Config{ AllowOrigins: []string{"http://localhost:5173", "http://localhost", "http://bookem.local"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, From 5a47732a570112f2bf3f9bd7d214feff2ecdc9c7 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 21 Sep 2025 16:52:19 +0200 Subject: [PATCH 04/52] refactor: Extract tracing code in `utils/` package --- src/main.go | 28 ++------------- src/util/tracing.go | 83 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 src/util/tracing.go diff --git a/src/main.go b/src/main.go index 61ba3b3..8df8a2e 100644 --- a/src/main.go +++ b/src/main.go @@ -18,17 +18,12 @@ import ( domain "bookem-user-service/domain" repo "bookem-user-service/repo" service "bookem-user-service/service" + utils "bookem-user-service/util" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.21.0" ) var ( @@ -64,28 +59,9 @@ func connectToDb() { log.Printf("Connected to DB!") } -func initTracer(ctx context.Context, serviceName, deploymentEnvironment string) func(context.Context) error { - exp, err := otlptracehttp.New(ctx) - if err != nil { - log.Fatalf("Failed to create an OTLP HTTP exporter: %v", err) - } - - tp := sdktrace.NewTracerProvider( - sdktrace.WithBatcher(exp), - sdktrace.WithResource(resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceName(serviceName), - semconv.DeploymentEnvironment(deploymentEnvironment), - )), - ) - otel.SetTracerProvider(tp) - otel.SetTextMapPropagator(propagation.TraceContext{}) - return tp.Shutdown -} - func main() { ctx := context.Background() - shutdown := initTracer( + shutdown := utils.InitTracer( ctx, os.Getenv("SERVICE_NAME"), os.Getenv("DEPLOYMENT_ENV"), diff --git a/src/util/tracing.go b/src/util/tracing.go new file mode 100644 index 0000000..4acaa73 --- /dev/null +++ b/src/util/tracing.go @@ -0,0 +1,83 @@ +package utils + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/gin-gonic/gin" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" + "go.opentelemetry.io/otel/trace" +) + +var tracer trace.Tracer + +// InitTracer initialies an OLTP tracer. Call this once at program startup. +func InitTracer(ctx context.Context, serviceName, deploymentEnvironment string) func(context.Context) error { + exp, err := otlptracehttp.New(ctx) + if err != nil { + log.Fatalf("Failed to create an OTLP HTTP exporter: %v", err) + } + + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(serviceName), + semconv.DeploymentEnvironment(deploymentEnvironment), + )), + ) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.TraceContext{}) + + tracer = otel.Tracer(serviceName) + return tp.Shutdown +} + +// NewSpan creates a new span specifically for use inside a Gin handler +// function. +// +// c is the context. name should be a descriptive operation like "create-user" +// or "fetch-all-requests" or "query-db-for-rooms". +// +// Function returns a context (used when submitting HTTP requests yourself, see +// InjectSpan) and a span object (used in AddEvent) which you should close. +func NewSpan(c *gin.Context, name string, attrs ...attribute.KeyValue) (context.Context, trace.Span) { + return tracer.Start(c.Request.Context(), name, trace.WithAttributes(attrs...)) +} + +// SetSpanUser adds user context to the given span. +// +// By default, spans created with NewSpan don't have a user.id set assuming the +// user is not specified or the method is anonymous. You can add further user +// context with this method. +func SetSpanUser(span trace.Span, userId int) { + span.SetAttributes(attribute.String("user.id", fmt.Sprintf("%d", userId))) +} + +// InjectSpan injects a tracere span inside an outgoing http request. +// +// Use this when sending requests to other microservices. +func InjectSpan(tracerCtx context.Context, outgoingRequest *http.Request) { + otel.GetTextMapPropagator().Inject(tracerCtx, propagation.HeaderCarrier(outgoingRequest.Header)) +} + +// AddEvent submits an event to the given span. +// +// It works like logging but is not a supstitution for logging. +func AddEvent(span trace.Span, msg string, err error) { + if err == nil { + span.AddEvent(msg) + } else { + span.AddEvent(msg, trace.WithAttributes( + attribute.String("error", err.Error()), + )) + } +} From 692c0c547dab298b039cba70c055d955c0ff3a01 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 21 Sep 2025 17:10:28 +0200 Subject: [PATCH 05/52] feat: Add tracing at the API level --- src/api/handler.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/util/tracing.go | 11 ++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/api/handler.go b/src/api/handler.go index 1760b1c..00df0cc 100644 --- a/src/api/handler.go +++ b/src/api/handler.go @@ -4,6 +4,7 @@ import ( "bookem-user-service/api/middleware" domain "bookem-user-service/domain" service "bookem-user-service/service" + utils "bookem-user-service/util" "fmt" "log" "net/http" @@ -21,15 +22,21 @@ func NewHandler(us service.Service) Handler { } func (h *Handler) registerUser(ctx *gin.Context) { + _, span := utils.NewSpan(ctx, "register-user") + defer span.End() + var dto domain.UserCreateDTO + if err := ctx.ShouldBindJSON(&dto); err != nil { ctx.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) + utils.AddEvent(span, "failed binding JSON", err) return } user, err := h.service.Register(&dto) if err != nil { ctx.Error(err) + utils.AddEvent(span, "failed registering user", err) return } @@ -37,16 +44,21 @@ func (h *Handler) registerUser(ctx *gin.Context) { } func (h *Handler) login(ctx *gin.Context) { + _, span := utils.NewSpan(ctx, "login-user") + defer span.End() + var dto domain.LoginDTO if err := ctx.ShouldBindJSON(&dto); err != nil { ctx.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) + utils.AddEvent(span, "failed binding JSON", err) return } jwt, err := h.service.Login(dto) if err != nil { ctx.Error(err) + utils.AddEvent(span, "failed logging in user", err) return } @@ -54,21 +66,29 @@ func (h *Handler) login(ctx *gin.Context) { } func (h *Handler) update(ctx *gin.Context) { + _, span := utils.NewSpan(ctx, "update-user") + defer span.End() + jwt, err := middleware.GetJwt(ctx) if err != nil { ctx.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) + utils.AddEvent(span, "unauthenticated", err) return } var dto domain.UserUpdateDTO if err := ctx.ShouldBindJSON(&dto); err != nil { ctx.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) + utils.AddEvent(span, "failed binding JSON", err) return } + utils.SetSpanUser(span, jwt.ID) + _, err = h.service.Update(jwt.ID, dto) if err != nil { ctx.Error(err) + utils.AddEvent(span, "failed updating user", err) return } @@ -76,21 +96,29 @@ func (h *Handler) update(ctx *gin.Context) { } func (h *Handler) changePassword(ctx *gin.Context) { + _, span := utils.NewSpan(ctx, "change-user-password") + defer span.End() + jwt, err := middleware.GetJwt(ctx) if err != nil { ctx.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) + utils.AddEvent(span, "unauthenticated", err) return } var dto domain.PasswordUpdateDTO if err := ctx.ShouldBindJSON(&dto); err != nil { ctx.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) + utils.AddEvent(span, "failed binding JSON", err) return } + utils.SetSpanUser(span, jwt.ID) + _, err = h.service.ChangePassword(jwt.ID, dto) if err != nil { ctx.Error(err) + utils.AddEvent(span, "failed changing password", err) return } @@ -98,18 +126,25 @@ func (h *Handler) changePassword(ctx *gin.Context) { } func (h *Handler) findById(ctx *gin.Context) { + _, span := utils.NewSpan(ctx, "find-user-by-id") + defer span.End() + id, err := strconv.Atoi(ctx.Param("id")) if err != nil { log.Printf("Could not parse ID: %s", err.Error()) ctx.Error(err) + utils.AddEvent(span, "failed parsing ID", err) return } + utils.AddAttribInt(span, "id", id) + log.Printf("Find user by id %d", id) user, err := h.service.FindById(uint(id)) if err != nil { ctx.Error(err) + utils.AddEvent(span, "failed finding user by ID", err) return } @@ -117,22 +152,32 @@ func (h *Handler) findById(ctx *gin.Context) { } func (h *Handler) deleteById(ctx *gin.Context) { + _, span := utils.NewSpan(ctx, "update-user") + defer span.End() + id, err := strconv.Atoi(ctx.Param("id")) if err != nil { log.Printf("Could not parse ID: %s", err.Error()) ctx.Error(err) + utils.AddEvent(span, "failed parsing ID", err) return } + utils.AddAttribInt(span, "id", id) + jwt, err := middleware.GetJwt(ctx) if err != nil { ctx.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) + utils.AddEvent(span, "unauthenticatetd", err) return } + utils.SetSpanUser(span, jwt.ID) + err = h.service.Delete(jwt.ID, uint(id)) if err != nil { ctx.Error(err) + utils.AddEvent(span, "could not delete user", err) return } diff --git a/src/util/tracing.go b/src/util/tracing.go index 4acaa73..650a03d 100644 --- a/src/util/tracing.go +++ b/src/util/tracing.go @@ -58,10 +58,19 @@ func NewSpan(c *gin.Context, name string, attrs ...attribute.KeyValue) (context. // By default, spans created with NewSpan don't have a user.id set assuming the // user is not specified or the method is anonymous. You can add further user // context with this method. -func SetSpanUser(span trace.Span, userId int) { +func SetSpanUser(span trace.Span, userId uint) { span.SetAttributes(attribute.String("user.id", fmt.Sprintf("%d", userId))) } +// SetSpanUser adds user context to the given span. +// +// By default, spans created with NewSpan don't have a user.id set assuming the +// user is not specified or the method is anonymous. You can add further user +// context with this method. +func AddAttribInt(span trace.Span, key string, val int) { + span.SetAttributes(attribute.Int(key, val)) +} + // InjectSpan injects a tracere span inside an outgoing http request. // // Use this when sending requests to other microservices. From 68ca6e0bd691eaa2473811c7f3688f923532e863 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 21 Sep 2025 17:35:24 +0200 Subject: [PATCH 06/52] feat: Propagate Registration context to service layer (and add service-level tracing) We should've done this from the start. Now I have to add "context.Context" to every single service method (maybe even repo method!) and fix the tests! AAARGHH!! --- src/api/handler.go | 86 +++++++++++++++++----------------- src/service/service.go | 15 +++++- src/test/unit/register_test.go | 9 ++-- src/util/tracing.go | 10 ++-- 4 files changed, 67 insertions(+), 53 deletions(-) diff --git a/src/api/handler.go b/src/api/handler.go index 00df0cc..b0fca34 100644 --- a/src/api/handler.go +++ b/src/api/handler.go @@ -21,64 +21,64 @@ func NewHandler(us service.Service) Handler { return Handler{us} } -func (h *Handler) registerUser(ctx *gin.Context) { - _, span := utils.NewSpan(ctx, "register-user") +func (h *Handler) registerUser(c *gin.Context) { + ctx, span := utils.NewSpan(c.Request.Context(), "register-user") defer span.End() var dto domain.UserCreateDTO - if err := ctx.ShouldBindJSON(&dto); err != nil { - ctx.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) + if err := c.ShouldBindJSON(&dto); err != nil { + c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) utils.AddEvent(span, "failed binding JSON", err) return } - user, err := h.service.Register(&dto) + user, err := h.service.Register(ctx, &dto) if err != nil { - ctx.Error(err) + c.Error(err) utils.AddEvent(span, "failed registering user", err) return } - ctx.JSON(http.StatusCreated, domain.NewUserDTO(user)) + c.JSON(http.StatusCreated, domain.NewUserDTO(user)) } -func (h *Handler) login(ctx *gin.Context) { - _, span := utils.NewSpan(ctx, "login-user") +func (h *Handler) login(c *gin.Context) { + _, span := utils.NewSpan(c.Request.Context(), "login-user") defer span.End() var dto domain.LoginDTO - if err := ctx.ShouldBindJSON(&dto); err != nil { - ctx.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) + if err := c.ShouldBindJSON(&dto); err != nil { + c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) utils.AddEvent(span, "failed binding JSON", err) return } jwt, err := h.service.Login(dto) if err != nil { - ctx.Error(err) + c.Error(err) utils.AddEvent(span, "failed logging in user", err) return } - ctx.JSON(http.StatusOK, domain.JWTDTO{Jwt: jwt}) + c.JSON(http.StatusOK, domain.JWTDTO{Jwt: jwt}) } -func (h *Handler) update(ctx *gin.Context) { - _, span := utils.NewSpan(ctx, "update-user") +func (h *Handler) update(c *gin.Context) { + _, span := utils.NewSpan(c.Request.Context(), "update-user") defer span.End() - jwt, err := middleware.GetJwt(ctx) + jwt, err := middleware.GetJwt(c) if err != nil { - ctx.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) + c.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) utils.AddEvent(span, "unauthenticated", err) return } var dto domain.UserUpdateDTO - if err := ctx.ShouldBindJSON(&dto); err != nil { - ctx.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) + if err := c.ShouldBindJSON(&dto); err != nil { + c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) utils.AddEvent(span, "failed binding JSON", err) return } @@ -87,28 +87,28 @@ func (h *Handler) update(ctx *gin.Context) { _, err = h.service.Update(jwt.ID, dto) if err != nil { - ctx.Error(err) + c.Error(err) utils.AddEvent(span, "failed updating user", err) return } - ctx.JSON(http.StatusNoContent, nil) + c.JSON(http.StatusNoContent, nil) } -func (h *Handler) changePassword(ctx *gin.Context) { - _, span := utils.NewSpan(ctx, "change-user-password") +func (h *Handler) changePassword(c *gin.Context) { + _, span := utils.NewSpan(c.Request.Context(), "change-user-password") defer span.End() - jwt, err := middleware.GetJwt(ctx) + jwt, err := middleware.GetJwt(c) if err != nil { - ctx.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) + c.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) utils.AddEvent(span, "unauthenticated", err) return } var dto domain.PasswordUpdateDTO - if err := ctx.ShouldBindJSON(&dto); err != nil { - ctx.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) + if err := c.ShouldBindJSON(&dto); err != nil { + c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) utils.AddEvent(span, "failed binding JSON", err) return } @@ -117,22 +117,22 @@ func (h *Handler) changePassword(ctx *gin.Context) { _, err = h.service.ChangePassword(jwt.ID, dto) if err != nil { - ctx.Error(err) + c.Error(err) utils.AddEvent(span, "failed changing password", err) return } - ctx.JSON(http.StatusNoContent, nil) + c.JSON(http.StatusNoContent, nil) } -func (h *Handler) findById(ctx *gin.Context) { - _, span := utils.NewSpan(ctx, "find-user-by-id") +func (h *Handler) findById(c *gin.Context) { + _, span := utils.NewSpan(c.Request.Context(), "find-user-by-id") defer span.End() - id, err := strconv.Atoi(ctx.Param("id")) + id, err := strconv.Atoi(c.Param("id")) if err != nil { log.Printf("Could not parse ID: %s", err.Error()) - ctx.Error(err) + c.Error(err) utils.AddEvent(span, "failed parsing ID", err) return } @@ -143,31 +143,31 @@ func (h *Handler) findById(ctx *gin.Context) { user, err := h.service.FindById(uint(id)) if err != nil { - ctx.Error(err) + c.Error(err) utils.AddEvent(span, "failed finding user by ID", err) return } - ctx.JSON(http.StatusOK, domain.NewUserDTO(user)) + c.JSON(http.StatusOK, domain.NewUserDTO(user)) } -func (h *Handler) deleteById(ctx *gin.Context) { - _, span := utils.NewSpan(ctx, "update-user") +func (h *Handler) deleteById(c *gin.Context) { + _, span := utils.NewSpan(c.Request.Context(), "update-user") defer span.End() - id, err := strconv.Atoi(ctx.Param("id")) + id, err := strconv.Atoi(c.Param("id")) if err != nil { log.Printf("Could not parse ID: %s", err.Error()) - ctx.Error(err) + c.Error(err) utils.AddEvent(span, "failed parsing ID", err) return } utils.AddAttribInt(span, "id", id) - jwt, err := middleware.GetJwt(ctx) + jwt, err := middleware.GetJwt(c) if err != nil { - ctx.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) + c.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) utils.AddEvent(span, "unauthenticatetd", err) return } @@ -176,10 +176,10 @@ func (h *Handler) deleteById(ctx *gin.Context) { err = h.service.Delete(jwt.ID, uint(id)) if err != nil { - ctx.Error(err) + c.Error(err) utils.AddEvent(span, "could not delete user", err) return } - ctx.JSON(http.StatusNoContent, nil) + c.JSON(http.StatusNoContent, nil) } diff --git a/src/service/service.go b/src/service/service.go index 47cec5d..da1d13e 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -5,13 +5,14 @@ import ( "bookem-user-service/domain" repo "bookem-user-service/repo" util "bookem-user-service/util" + "context" "fmt" "log" "strings" ) type Service interface { - Register(input *domain.UserCreateDTO) (*domain.User, error) + Register(ctx context.Context, input *domain.UserCreateDTO) (*domain.User, error) Login(dto domain.LoginDTO) (string, error) Update(callerID uint, dto domain.UserUpdateDTO) (*domain.User, error) ChangePassword(callerID uint, dto domain.PasswordUpdateDTO) (*domain.User, error) @@ -32,9 +33,12 @@ func NewService(r repo.Repository, roomClient roomclient.RoomClient) Service { return &service{r, roomClient} } -func (s *service) Register(dto *domain.UserCreateDTO) (*domain.User, error) { +func (s *service) Register(ctx context.Context, dto *domain.UserCreateDTO) (*domain.User, error) { + _, span1 := util.NewSpan(ctx, "hash-password") + defer span1.End() hashed, err := util.HashPassword(dto.Password) if err != nil { + util.AddEvent(span1, "failed hashing password", err) return nil, domain.ErrHashingPassword } @@ -48,18 +52,25 @@ func (s *service) Register(dto *domain.UserCreateDTO) (*domain.User, error) { Address: dto.Address, } + _, span2 := util.NewSpan(ctx, "db-query-user") + defer span2.End() existing, _ := s.repo.FindByUsernameOrEmail(dto.Username, dto.Email) if existing != nil { if existing.Username == dto.Username { + util.AddEvent(span2, "username exists", nil) return nil, domain.ErrUsernameExists } if existing.Email == dto.Email { + util.AddEvent(span2, "email exists", nil) return nil, domain.ErrEmailExists } } + _, span3 := util.NewSpan(ctx, "db-insert-user") + defer span3.End() err = s.repo.Create(user) if err != nil { + util.AddEvent(span3, "failed inserting user", err) return nil, fmt.Errorf("%w: %v", domain.ErrDBInternal, err) } diff --git a/src/test/unit/register_test.go b/src/test/unit/register_test.go index 240d9d2..18dbc36 100644 --- a/src/test/unit/register_test.go +++ b/src/test/unit/register_test.go @@ -2,6 +2,7 @@ package test import ( domain "bookem-user-service/domain" + "context" "errors" "strings" "testing" @@ -18,7 +19,7 @@ func TestSuccess(t *testing.T) { mockRepo.On("FindByUsernameOrEmail", dto.Username, dto.Email).Return(nil, nil) mockRepo.On("Create", mock.AnythingOfType("*domain.User")).Return(nil) - user, err := svc.Register(&dto) + user, err := svc.Register(context.Background(), &dto) assert.NoError(t, err) assert.Equal(t, dto.Username, user.Username) @@ -38,7 +39,7 @@ func TestUsernameExists(t *testing.T) { mockRepo.On("FindByUsernameOrEmail", dto.Username, dto.Email).Return(&existing, nil) - user, err := svc.Register(&dto) + user, err := svc.Register(context.Background(), &dto) assert.Nil(t, user) assert.ErrorIs(t, err, domain.ErrUsernameExists) @@ -57,7 +58,7 @@ func TestEmailExists(t *testing.T) { mockRepo.On("FindByUsernameOrEmail", dto.Username, dto.Email).Return(&existing, nil) - user, err := svc.Register(&dto) + user, err := svc.Register(context.Background(), &dto) assert.Nil(t, user) assert.ErrorIs(t, err, domain.ErrEmailExists) @@ -71,7 +72,7 @@ func TestCreateFails(t *testing.T) { mockRepo.On("FindByUsernameOrEmail", dto.Username, dto.Email).Return(nil, nil) mockRepo.On("Create", mock.Anything).Return(errors.New("db down")) - user, err := svc.Register(&dto) + user, err := svc.Register(context.Background(), &dto) assert.Nil(t, user) assert.ErrorContains(t, err, "db down") diff --git a/src/util/tracing.go b/src/util/tracing.go index 650a03d..f673a89 100644 --- a/src/util/tracing.go +++ b/src/util/tracing.go @@ -6,9 +6,9 @@ import ( "log" "net/http" - "github.com/gin-gonic/gin" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" @@ -49,8 +49,8 @@ func InitTracer(ctx context.Context, serviceName, deploymentEnvironment string) // // Function returns a context (used when submitting HTTP requests yourself, see // InjectSpan) and a span object (used in AddEvent) which you should close. -func NewSpan(c *gin.Context, name string, attrs ...attribute.KeyValue) (context.Context, trace.Span) { - return tracer.Start(c.Request.Context(), name, trace.WithAttributes(attrs...)) +func NewSpan(ctx context.Context, name string, attrs ...attribute.KeyValue) (context.Context, trace.Span) { + return tracer.Start(ctx, name, trace.WithAttributes(attrs...)) } // SetSpanUser adds user context to the given span. @@ -86,7 +86,9 @@ func AddEvent(span trace.Span, msg string, err error) { span.AddEvent(msg) } else { span.AddEvent(msg, trace.WithAttributes( - attribute.String("error", err.Error()), + attribute.String("error.message", err.Error()), + attribute.Bool("error", true), )) + span.SetStatus(codes.Error, err.Error()) } } From 3b721fa9e2e8c19728bccbd940d889ba3d6d8410 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 21 Sep 2025 18:14:19 +0200 Subject: [PATCH 07/52] refactor: Create a "telemetry" object abstracting tracing and logging --- src/api/handler.go | 10 ++--- src/main.go | 11 ++++- src/service/service.go | 21 ++++----- src/util/telemetry.go | 99 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 src/util/telemetry.go diff --git a/src/api/handler.go b/src/api/handler.go index b0fca34..e071116 100644 --- a/src/api/handler.go +++ b/src/api/handler.go @@ -22,21 +22,21 @@ func NewHandler(us service.Service) Handler { } func (h *Handler) registerUser(c *gin.Context) { - ctx, span := utils.NewSpan(c.Request.Context(), "register-user") - defer span.End() + utils.TEL.Push(c.Request.Context(), "register-user") + defer utils.TEL.Pop() var dto domain.UserCreateDTO if err := c.ShouldBindJSON(&dto); err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) - utils.AddEvent(span, "failed binding JSON", err) + utils.TEL.Event("failed binding JSON", err) return } - user, err := h.service.Register(ctx, &dto) + user, err := h.service.Register(utils.TEL.Ctx(), &dto) if err != nil { c.Error(err) - utils.AddEvent(span, "failed registering user", err) + utils.TEL.Event("failed registering user", err) return } diff --git a/src/main.go b/src/main.go index 8df8a2e..5ac8b05 100644 --- a/src/main.go +++ b/src/main.go @@ -61,12 +61,19 @@ func connectToDb() { func main() { ctx := context.Background() - shutdown := utils.InitTracer( + // shutdown := utils.InitTracer( + // ctx, + // os.Getenv("SERVICE_NAME"), + // os.Getenv("DEPLOYMENT_ENV"), + // ) + // defer shutdown(ctx) + + shutdown2 := utils.TEL.Init( ctx, os.Getenv("SERVICE_NAME"), os.Getenv("DEPLOYMENT_ENV"), ) - defer shutdown(ctx) + defer shutdown2(ctx) connectToDb() defer rawDB.Close() diff --git a/src/service/service.go b/src/service/service.go index da1d13e..a784a35 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -34,11 +34,12 @@ func NewService(r repo.Repository, roomClient roomclient.RoomClient) Service { } func (s *service) Register(ctx context.Context, dto *domain.UserCreateDTO) (*domain.User, error) { - _, span1 := util.NewSpan(ctx, "hash-password") - defer span1.End() + util.TEL.Push(ctx, "hash-password") + defer util.TEL.Pop() + hashed, err := util.HashPassword(dto.Password) if err != nil { - util.AddEvent(span1, "failed hashing password", err) + util.TEL.Event("failed hashing password", err) return nil, domain.ErrHashingPassword } @@ -52,25 +53,25 @@ func (s *service) Register(ctx context.Context, dto *domain.UserCreateDTO) (*dom Address: dto.Address, } - _, span2 := util.NewSpan(ctx, "db-query-user") - defer span2.End() + util.TEL.Push(ctx, "db-query-user") + defer util.TEL.Pop() existing, _ := s.repo.FindByUsernameOrEmail(dto.Username, dto.Email) if existing != nil { if existing.Username == dto.Username { - util.AddEvent(span2, "username exists", nil) + util.TEL.Event("username exists", nil) return nil, domain.ErrUsernameExists } if existing.Email == dto.Email { - util.AddEvent(span2, "email exists", nil) + util.TEL.Event("email exists", nil) return nil, domain.ErrEmailExists } } - _, span3 := util.NewSpan(ctx, "db-insert-user") - defer span3.End() + util.TEL.Push(ctx, "db-insert-user") + defer util.TEL.Pop() err = s.repo.Create(user) if err != nil { - util.AddEvent(span3, "failed inserting user", err) + util.TEL.Event("failed inserting user", err) return nil, fmt.Errorf("%w: %v", domain.ErrDBInternal, err) } diff --git a/src/util/telemetry.go b/src/util/telemetry.go new file mode 100644 index 0000000..c7d12eb --- /dev/null +++ b/src/util/telemetry.go @@ -0,0 +1,99 @@ +package utils + +import ( + "context" + "log" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" + "go.opentelemetry.io/otel/trace" +) + +type SpanPair struct { + Ctx context.Context + Span trace.Span +} + +type Telemetry struct { + Tracer trace.Tracer + + SpanStack []SpanPair +} + +var TEL Telemetry + +func (t *Telemetry) Init(ctx context.Context, serviceName, deploymentEnvironment string) func(context.Context) error { + // [1] Init tracer + { + exp, err := otlptracehttp.New(ctx) + if err != nil { + log.Fatalf("Failed to create an OTLP HTTP exporter: %v", err) + } + + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(serviceName), + semconv.DeploymentEnvironment(deploymentEnvironment), + )), + ) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.TraceContext{}) + + t.Tracer = otel.Tracer(serviceName) + return tp.Shutdown + } +} + +func (t *Telemetry) Push(ctx context.Context, name string, attrs ...attribute.KeyValue) { + newCtx, span := t.Tracer.Start(ctx, name, trace.WithAttributes(attrs...)) + + t.SpanStack = append(t.SpanStack, SpanPair{Ctx: newCtx, Span: span}) +} + +func (t *Telemetry) Pop() { + top := t.SpanStack[len(t.SpanStack)-1] + top.Span.End() + t.SpanStack = t.SpanStack[:len(t.SpanStack)-1] +} + +func (t *Telemetry) Top() SpanPair { + return t.SpanStack[len(t.SpanStack)-1] +} + +func (t *Telemetry) Ctx() context.Context { + return t.Top().Ctx +} + +func (t *Telemetry) Event(msg string, err error) { + // Logging + { + if err == nil { + log.Println(msg) + } else { + log.Printf("%s: %v\n", msg, err) + } + } + + // Tracing + if len(t.SpanStack) > 0 { + span := t.SpanStack[len(t.SpanStack)-1].Span + + if err == nil { + span.AddEvent(msg) + } else { + span.AddEvent(msg, trace.WithAttributes( + attribute.String("error.message", err.Error()), + attribute.Bool("error", true), + )) + span.SetStatus(codes.Error, err.Error()) + } + } +} From e8ad598b90e99c7488926680b21120236892a591 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 21 Sep 2025 18:24:32 +0200 Subject: [PATCH 08/52] refactor: Migrate to the telemetry object --- src/api/handler.go | 57 +++++++++++++------------- src/util/telemetry.go | 14 +++++++ src/util/tracing.go | 94 ------------------------------------------- 3 files changed, 43 insertions(+), 122 deletions(-) delete mode 100644 src/util/tracing.go diff --git a/src/api/handler.go b/src/api/handler.go index e071116..e7d0f2d 100644 --- a/src/api/handler.go +++ b/src/api/handler.go @@ -11,6 +11,7 @@ import ( "strconv" "github.com/gin-gonic/gin" + "go.opentelemetry.io/otel/attribute" ) type Handler struct { @@ -44,21 +45,21 @@ func (h *Handler) registerUser(c *gin.Context) { } func (h *Handler) login(c *gin.Context) { - _, span := utils.NewSpan(c.Request.Context(), "login-user") - defer span.End() + utils.TEL.Push(c.Request.Context(), "login-user") + defer utils.TEL.Pop() var dto domain.LoginDTO if err := c.ShouldBindJSON(&dto); err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) - utils.AddEvent(span, "failed binding JSON", err) + utils.TEL.Event("failed binding JSON", err) return } jwt, err := h.service.Login(dto) if err != nil { c.Error(err) - utils.AddEvent(span, "failed logging in user", err) + utils.TEL.Event("failed logging in user", err) return } @@ -66,29 +67,29 @@ func (h *Handler) login(c *gin.Context) { } func (h *Handler) update(c *gin.Context) { - _, span := utils.NewSpan(c.Request.Context(), "update-user") - defer span.End() + utils.TEL.Push(c.Request.Context(), "update-user") + defer utils.TEL.Pop() jwt, err := middleware.GetJwt(c) if err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) - utils.AddEvent(span, "unauthenticated", err) + utils.TEL.Event("unauthenticated", err) return } var dto domain.UserUpdateDTO if err := c.ShouldBindJSON(&dto); err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) - utils.AddEvent(span, "failed binding JSON", err) + utils.TEL.Event("failed binding JSON", err) return } - utils.SetSpanUser(span, jwt.ID) + utils.TEL.SetUser(jwt.ID) _, err = h.service.Update(jwt.ID, dto) if err != nil { c.Error(err) - utils.AddEvent(span, "failed updating user", err) + utils.TEL.Event("failed updating user", err) return } @@ -96,29 +97,29 @@ func (h *Handler) update(c *gin.Context) { } func (h *Handler) changePassword(c *gin.Context) { - _, span := utils.NewSpan(c.Request.Context(), "change-user-password") - defer span.End() + utils.TEL.Push(c.Request.Context(), "change-user-password") + defer utils.TEL.Pop() jwt, err := middleware.GetJwt(c) if err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) - utils.AddEvent(span, "unauthenticated", err) + utils.TEL.Event("unauthenticated", err) return } var dto domain.PasswordUpdateDTO if err := c.ShouldBindJSON(&dto); err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) - utils.AddEvent(span, "failed binding JSON", err) + utils.TEL.Event("failed binding JSON", err) return } - utils.SetSpanUser(span, jwt.ID) + utils.TEL.SetUser(jwt.ID) _, err = h.service.ChangePassword(jwt.ID, dto) if err != nil { c.Error(err) - utils.AddEvent(span, "failed changing password", err) + utils.TEL.Event("failed changing password", err) return } @@ -126,25 +127,25 @@ func (h *Handler) changePassword(c *gin.Context) { } func (h *Handler) findById(c *gin.Context) { - _, span := utils.NewSpan(c.Request.Context(), "find-user-by-id") - defer span.End() + utils.TEL.Push(c.Request.Context(), "find-user-by-id") + defer utils.TEL.Pop() id, err := strconv.Atoi(c.Param("id")) if err != nil { log.Printf("Could not parse ID: %s", err.Error()) c.Error(err) - utils.AddEvent(span, "failed parsing ID", err) + utils.TEL.Event("failed parsing ID", err) return } - utils.AddAttribInt(span, "id", id) + utils.TEL.SetAttrib(attribute.Int("id", id)) log.Printf("Find user by id %d", id) user, err := h.service.FindById(uint(id)) if err != nil { c.Error(err) - utils.AddEvent(span, "failed finding user by ID", err) + utils.TEL.Event("failed finding user by ID", err) return } @@ -152,32 +153,32 @@ func (h *Handler) findById(c *gin.Context) { } func (h *Handler) deleteById(c *gin.Context) { - _, span := utils.NewSpan(c.Request.Context(), "update-user") - defer span.End() + utils.TEL.Push(c.Request.Context(), "update-user") + defer utils.TEL.Pop() id, err := strconv.Atoi(c.Param("id")) if err != nil { log.Printf("Could not parse ID: %s", err.Error()) c.Error(err) - utils.AddEvent(span, "failed parsing ID", err) + utils.TEL.Event("failed parsing ID", err) return } - utils.AddAttribInt(span, "id", id) + utils.TEL.SetAttrib(attribute.Int("id", id)) jwt, err := middleware.GetJwt(c) if err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) - utils.AddEvent(span, "unauthenticatetd", err) + utils.TEL.Event("unauthenticatetd", err) return } - utils.SetSpanUser(span, jwt.ID) + utils.TEL.SetUser(jwt.ID) err = h.service.Delete(jwt.ID, uint(id)) if err != nil { c.Error(err) - utils.AddEvent(span, "could not delete user", err) + utils.TEL.Event("could not delete user", err) return } diff --git a/src/util/telemetry.go b/src/util/telemetry.go index c7d12eb..dc0c3ef 100644 --- a/src/util/telemetry.go +++ b/src/util/telemetry.go @@ -2,7 +2,9 @@ package utils import ( "context" + "fmt" "log" + "net/http" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -97,3 +99,15 @@ func (t *Telemetry) Event(msg string, err error) { } } } + +func (t *Telemetry) SetAttrib(kv ...attribute.KeyValue) { + t.Top().Span.SetAttributes(kv...) +} + +func (t *Telemetry) SetUser(id uint) { + t.Top().Span.SetAttributes(attribute.String("user.id", fmt.Sprintf("%d", id))) +} + +func (t *Telemetry) Inject(outgoingRequest *http.Request) { + otel.GetTextMapPropagator().Inject(t.Ctx(), propagation.HeaderCarrier(outgoingRequest.Header)) +} diff --git a/src/util/tracing.go b/src/util/tracing.go deleted file mode 100644 index f673a89..0000000 --- a/src/util/tracing.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "fmt" - "log" - "net/http" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.21.0" - "go.opentelemetry.io/otel/trace" -) - -var tracer trace.Tracer - -// InitTracer initialies an OLTP tracer. Call this once at program startup. -func InitTracer(ctx context.Context, serviceName, deploymentEnvironment string) func(context.Context) error { - exp, err := otlptracehttp.New(ctx) - if err != nil { - log.Fatalf("Failed to create an OTLP HTTP exporter: %v", err) - } - - tp := sdktrace.NewTracerProvider( - sdktrace.WithBatcher(exp), - sdktrace.WithResource(resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceName(serviceName), - semconv.DeploymentEnvironment(deploymentEnvironment), - )), - ) - otel.SetTracerProvider(tp) - otel.SetTextMapPropagator(propagation.TraceContext{}) - - tracer = otel.Tracer(serviceName) - return tp.Shutdown -} - -// NewSpan creates a new span specifically for use inside a Gin handler -// function. -// -// c is the context. name should be a descriptive operation like "create-user" -// or "fetch-all-requests" or "query-db-for-rooms". -// -// Function returns a context (used when submitting HTTP requests yourself, see -// InjectSpan) and a span object (used in AddEvent) which you should close. -func NewSpan(ctx context.Context, name string, attrs ...attribute.KeyValue) (context.Context, trace.Span) { - return tracer.Start(ctx, name, trace.WithAttributes(attrs...)) -} - -// SetSpanUser adds user context to the given span. -// -// By default, spans created with NewSpan don't have a user.id set assuming the -// user is not specified or the method is anonymous. You can add further user -// context with this method. -func SetSpanUser(span trace.Span, userId uint) { - span.SetAttributes(attribute.String("user.id", fmt.Sprintf("%d", userId))) -} - -// SetSpanUser adds user context to the given span. -// -// By default, spans created with NewSpan don't have a user.id set assuming the -// user is not specified or the method is anonymous. You can add further user -// context with this method. -func AddAttribInt(span trace.Span, key string, val int) { - span.SetAttributes(attribute.Int(key, val)) -} - -// InjectSpan injects a tracere span inside an outgoing http request. -// -// Use this when sending requests to other microservices. -func InjectSpan(tracerCtx context.Context, outgoingRequest *http.Request) { - otel.GetTextMapPropagator().Inject(tracerCtx, propagation.HeaderCarrier(outgoingRequest.Header)) -} - -// AddEvent submits an event to the given span. -// -// It works like logging but is not a supstitution for logging. -func AddEvent(span trace.Span, msg string, err error) { - if err == nil { - span.AddEvent(msg) - } else { - span.AddEvent(msg, trace.WithAttributes( - attribute.String("error.message", err.Error()), - attribute.Bool("error", true), - )) - span.SetStatus(codes.Error, err.Error()) - } -} From daef9874e615c119cfbcd3df75584867693ba46b Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:41:45 +0200 Subject: [PATCH 09/52] feat: Prepare telemetry for tests --- src/util/telemetry.go | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/util/telemetry.go b/src/util/telemetry.go index dc0c3ef..1a02ca4 100644 --- a/src/util/telemetry.go +++ b/src/util/telemetry.go @@ -23,7 +23,11 @@ type SpanPair struct { } type Telemetry struct { - Tracer trace.Tracer + // During tests, the tracer is not set up, so we silently ignore tracing. + // This has to be done manually (i.e. don't call tracer methods, don't touch + // spans etc.) + tracerReady bool + Tracer trace.Tracer SpanStack []SpanPair } @@ -50,19 +54,27 @@ func (t *Telemetry) Init(ctx context.Context, serviceName, deploymentEnvironment otel.SetTextMapPropagator(propagation.TraceContext{}) t.Tracer = otel.Tracer(serviceName) + t.tracerReady = true return tp.Shutdown } } func (t *Telemetry) Push(ctx context.Context, name string, attrs ...attribute.KeyValue) { - newCtx, span := t.Tracer.Start(ctx, name, trace.WithAttributes(attrs...)) - - t.SpanStack = append(t.SpanStack, SpanPair{Ctx: newCtx, Span: span}) + if t.tracerReady { + newCtx, span := t.Tracer.Start(ctx, name, trace.WithAttributes(attrs...)) + t.SpanStack = append(t.SpanStack, SpanPair{Ctx: newCtx, Span: span}) + } else { + newCtx := ctx + var span trace.Span + t.SpanStack = append(t.SpanStack, SpanPair{Ctx: newCtx, Span: span}) + } } func (t *Telemetry) Pop() { top := t.SpanStack[len(t.SpanStack)-1] - top.Span.End() + if t.tracerReady { + top.Span.End() + } t.SpanStack = t.SpanStack[:len(t.SpanStack)-1] } @@ -85,7 +97,7 @@ func (t *Telemetry) Event(msg string, err error) { } // Tracing - if len(t.SpanStack) > 0 { + if t.tracerReady && len(t.SpanStack) > 0 { span := t.SpanStack[len(t.SpanStack)-1].Span if err == nil { @@ -101,11 +113,15 @@ func (t *Telemetry) Event(msg string, err error) { } func (t *Telemetry) SetAttrib(kv ...attribute.KeyValue) { - t.Top().Span.SetAttributes(kv...) + if t.tracerReady { + t.Top().Span.SetAttributes(kv...) + } } func (t *Telemetry) SetUser(id uint) { - t.Top().Span.SetAttributes(attribute.String("user.id", fmt.Sprintf("%d", id))) + if t.tracerReady { + t.Top().Span.SetAttributes(attribute.String("user.id", fmt.Sprintf("%d", id))) + } } func (t *Telemetry) Inject(outgoingRequest *http.Request) { From 6eae6a06ab4215a554075aca9305873d3b9bf47b Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:43:53 +0200 Subject: [PATCH 10/52] feat: Add tracing to the external client methods They need a context. So the service method that calls them needs a context too. So the tests need to be updated. Don't forget to pass the context when mocking! We should've added a context from the very start. It's gonna be a huge pain to rewrite everything... --- src/api/handler.go | 10 ++++---- src/client/roomclient/client.go | 20 +++++++++++---- src/service/service.go | 36 +++++++++++++-------------- src/test/unit/change_password_test.go | 13 +++++----- src/test/unit/delete_test.go | 19 +++++++------- src/test/unit/find_test.go | 5 ++-- src/test/unit/login_test.go | 9 ++++--- src/test/unit/update_test.go | 15 +++++------ src/test/unit/utils.go | 9 ++++--- 9 files changed, 76 insertions(+), 60 deletions(-) diff --git a/src/api/handler.go b/src/api/handler.go index e7d0f2d..3705301 100644 --- a/src/api/handler.go +++ b/src/api/handler.go @@ -56,7 +56,7 @@ func (h *Handler) login(c *gin.Context) { return } - jwt, err := h.service.Login(dto) + jwt, err := h.service.Login(utils.TEL.Ctx(), dto) if err != nil { c.Error(err) utils.TEL.Event("failed logging in user", err) @@ -86,7 +86,7 @@ func (h *Handler) update(c *gin.Context) { utils.TEL.SetUser(jwt.ID) - _, err = h.service.Update(jwt.ID, dto) + _, err = h.service.Update(utils.TEL.Ctx(), jwt.ID, dto) if err != nil { c.Error(err) utils.TEL.Event("failed updating user", err) @@ -116,7 +116,7 @@ func (h *Handler) changePassword(c *gin.Context) { utils.TEL.SetUser(jwt.ID) - _, err = h.service.ChangePassword(jwt.ID, dto) + _, err = h.service.ChangePassword(utils.TEL.Ctx(), jwt.ID, dto) if err != nil { c.Error(err) utils.TEL.Event("failed changing password", err) @@ -142,7 +142,7 @@ func (h *Handler) findById(c *gin.Context) { log.Printf("Find user by id %d", id) - user, err := h.service.FindById(uint(id)) + user, err := h.service.FindById(utils.TEL.Ctx(), uint(id)) if err != nil { c.Error(err) utils.TEL.Event("failed finding user by ID", err) @@ -175,7 +175,7 @@ func (h *Handler) deleteById(c *gin.Context) { utils.TEL.SetUser(jwt.ID) - err = h.service.Delete(jwt.ID, uint(id)) + err = h.service.Delete(utils.TEL.Ctx(), jwt.ID, uint(id)) if err != nil { c.Error(err) utils.TEL.Event("could not delete user", err) diff --git a/src/client/roomclient/client.go b/src/client/roomclient/client.go index 7a8de21..8e773f0 100644 --- a/src/client/roomclient/client.go +++ b/src/client/roomclient/client.go @@ -1,14 +1,18 @@ package roomclient -import "bookem-user-service/domain" +import ( + "bookem-user-service/domain" + utils "bookem-user-service/util" + "context" +) type RoomClient interface { // GetPendingGuestReservations finds all reservations made by `guest` that // haven't completed yet. The user must be a guest. - GetPendingGuestReservations(guest *domain.User) ([]ReservationDTO, error) + GetPendingGuestReservations(ctx context.Context, guest *domain.User) ([]ReservationDTO, error) // GetActiveHostReservations finds all reservations made to rooms owned by // `host` that haven't completed yet. The user must be a host. - GetActiveHostReservations(host *domain.User) ([]ReservationDTO, error) + GetActiveHostReservations(ctx context.Context, host *domain.User) ([]ReservationDTO, error) } type roomClient struct { @@ -21,7 +25,10 @@ func NewRoomClient() RoomClient { } } -func (c *roomClient) GetPendingGuestReservations(guest *domain.User) ([]ReservationDTO, error) { +func (c *roomClient) GetPendingGuestReservations(ctx context.Context, guest *domain.User) ([]ReservationDTO, error) { + utils.TEL.Push(ctx, "get-reservation-requests-made-by-guest") + defer utils.TEL.Pop() + if guest.Role != domain.Guest { return []ReservationDTO{}, domain.ErrUnauthorized } @@ -29,7 +36,10 @@ func (c *roomClient) GetPendingGuestReservations(guest *domain.User) ([]Reservat return []ReservationDTO{}, nil } -func (c *roomClient) GetActiveHostReservations(host *domain.User) ([]ReservationDTO, error) { +func (c *roomClient) GetActiveHostReservations(ctx context.Context, host *domain.User) ([]ReservationDTO, error) { + utils.TEL.Push(ctx, "get-reservation-requests-for-host") + defer utils.TEL.Pop() + if host.Role != domain.Host { return []ReservationDTO{}, domain.ErrUnauthorized } diff --git a/src/service/service.go b/src/service/service.go index a784a35..cbc02ff 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -13,15 +13,15 @@ import ( type Service interface { Register(ctx context.Context, input *domain.UserCreateDTO) (*domain.User, error) - Login(dto domain.LoginDTO) (string, error) - Update(callerID uint, dto domain.UserUpdateDTO) (*domain.User, error) - ChangePassword(callerID uint, dto domain.PasswordUpdateDTO) (*domain.User, error) - FindById(id uint) (*domain.User, error) - Delete(callerID uint, id uint) error + Login(ctx context.Context, dto domain.LoginDTO) (string, error) + Update(ctx context.Context, callerID uint, dto domain.UserUpdateDTO) (*domain.User, error) + ChangePassword(ctx context.Context, callerID uint, dto domain.PasswordUpdateDTO) (*domain.User, error) + FindById(ctx context.Context, id uint) (*domain.User, error) + Delete(ctx context.Context, callerID uint, id uint) error /// canDeleteUser returns an error if the user cannot be deleted right now. /// The error specifies the reason why the operation cannot be done. - canDeleteUser(user *domain.User) error + canDeleteUser(ctx context.Context, user *domain.User) error } type service struct { @@ -82,7 +82,7 @@ func (s *service) Register(ctx context.Context, dto *domain.UserCreateDTO) (*dom // It can accept both an email or a username. // On success, it returns a JWT string. // On error, it returns an empty string. -func (s *service) Login(dto domain.LoginDTO) (string, error) { +func (s *service) Login(ctx context.Context, dto domain.LoginDTO) (string, error) { user, _ := s.repo.FindByUsernameOrEmail(dto.UsernameOrEmail, dto.UsernameOrEmail) if user == nil { @@ -107,7 +107,7 @@ func (s *service) Login(dto domain.LoginDTO) (string, error) { // Update updates the user (specified by his ID in the dto) with the new values // in the DTO. Fields with null values are skipped. -func (s *service) Update(callerID uint, dto domain.UserUpdateDTO) (*domain.User, error) { +func (s *service) Update(ctx context.Context, callerID uint, dto domain.UserUpdateDTO) (*domain.User, error) { log.Printf("User %d wants to update user %d", callerID, dto.Id) // Users can only update themselves. @@ -118,7 +118,7 @@ func (s *service) Update(callerID uint, dto domain.UserUpdateDTO) (*domain.User, // Search for the user. - user, err := s.FindById(dto.Id) + user, err := s.FindById(ctx, dto.Id) if err != nil { log.Printf("User %d not fonud", dto.Id) return nil, domain.ErrNotFound @@ -179,7 +179,7 @@ func (s *service) Update(callerID uint, dto domain.UserUpdateDTO) (*domain.User, } // ChangePassword changes the user's password. -func (s *service) ChangePassword(callerID uint, dto domain.PasswordUpdateDTO) (*domain.User, error) { +func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain.PasswordUpdateDTO) (*domain.User, error) { log.Printf("User %d wants to change password of user %d", callerID, dto.Id) // User can only change his own password. @@ -190,7 +190,7 @@ func (s *service) ChangePassword(callerID uint, dto domain.PasswordUpdateDTO) (* // Search for the user. - user, err := s.FindById(dto.Id) + user, err := s.FindById(ctx, dto.Id) if err != nil { return nil, err } @@ -233,7 +233,7 @@ func (s *service) ChangePassword(callerID uint, dto domain.PasswordUpdateDTO) (* return user, nil } -func (s *service) FindById(id uint) (*domain.User, error) { +func (s *service) FindById(ctx context.Context, id uint) (*domain.User, error) { user, err := s.repo.FindById(id) if err != nil { return nil, domain.ErrNotFound @@ -241,7 +241,7 @@ func (s *service) FindById(id uint) (*domain.User, error) { return user, nil } -func (s *service) Delete(callerID uint, id uint) error { +func (s *service) Delete(ctx context.Context, callerID uint, id uint) error { log.Printf("User %d wants to delete user %d", callerID, id) // User can only delete himself. @@ -252,14 +252,14 @@ func (s *service) Delete(callerID uint, id uint) error { // Search for the user. - user, err := s.FindById(id) + user, err := s.FindById(ctx, id) if err != nil { return err } // Check if user can be deleted. - err = s.canDeleteUser(user) + err = s.canDeleteUser(ctx, user) if err != nil { return err } @@ -272,10 +272,10 @@ func (s *service) Delete(callerID uint, id uint) error { return nil } -func (s *service) canDeleteUser(user *domain.User) error { +func (s *service) canDeleteUser(ctx context.Context, user *domain.User) error { switch user.Role { case domain.Guest: - reservations, err := s.roomClient.GetPendingGuestReservations(user) + reservations, err := s.roomClient.GetPendingGuestReservations(ctx, user) if err != nil { return err } @@ -284,7 +284,7 @@ func (s *service) canDeleteUser(user *domain.User) error { } return nil case domain.Host: - reservations, err := s.roomClient.GetActiveHostReservations(user) + reservations, err := s.roomClient.GetActiveHostReservations(ctx, user) if err != nil { return err } diff --git a/src/test/unit/change_password_test.go b/src/test/unit/change_password_test.go index 74910b2..2506af8 100644 --- a/src/test/unit/change_password_test.go +++ b/src/test/unit/change_password_test.go @@ -1,6 +1,7 @@ package test import ( + "context" "fmt" "testing" @@ -33,7 +34,7 @@ func TestChangePassword_Success(t *testing.T) { // Verify - newUser, err := svc.ChangePassword(1, dto) + newUser, err := svc.ChangePassword(context.Background(), 1, dto) t.Log(oldHashed) @@ -50,7 +51,7 @@ func TestChangePassword_SomeoneElse(t *testing.T) { // Verify - newUser, err := svc.ChangePassword(1, dto) + newUser, err := svc.ChangePassword(context.Background(), 1, dto) assert.Nil(t, newUser) assert.Error(t, err) @@ -70,7 +71,7 @@ func TestChangePassword_UserNotFound(t *testing.T) { // Verify - newUser, err := svc.ChangePassword(1, dto) + newUser, err := svc.ChangePassword(context.Background(), 1, dto) assert.Nil(t, newUser) assert.Error(t, err) @@ -94,7 +95,7 @@ func TestChangePassword_PasswordsNotMatch(t *testing.T) { // Verify - newUser, err := svc.ChangePassword(1, dto) + newUser, err := svc.ChangePassword(context.Background(), 1, dto) assert.Nil(t, newUser) assert.Error(t, err) @@ -119,7 +120,7 @@ func TestChangePassword_BadOldPassword(t *testing.T) { // Verify - newUser, err := svc.ChangePassword(1, dto) + newUser, err := svc.ChangePassword(context.Background(), 1, dto) assert.Nil(t, newUser) assert.Error(t, err) @@ -142,7 +143,7 @@ func TestChangePassword_PasswordIsTheSame(t *testing.T) { // Verify - newUser, err := svc.ChangePassword(1, dto) + newUser, err := svc.ChangePassword(context.Background(), 1, dto) assert.Nil(t, newUser) assert.Error(t, err) diff --git a/src/test/unit/delete_test.go b/src/test/unit/delete_test.go index 43ee20e..5b904ad 100644 --- a/src/test/unit/delete_test.go +++ b/src/test/unit/delete_test.go @@ -1,6 +1,7 @@ package test import ( + "context" "fmt" "testing" @@ -20,9 +21,9 @@ func TestDelete_Success(t *testing.T) { user.Role = domain.Guest mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() - mockRoomClient.On("GetPendingGuestReservations", user).Return([]roomclient.ReservationDTO{}, nil) + mockRoomClient.On("GetPendingGuestReservations", context.Background(), user).Return([]roomclient.ReservationDTO{}, nil) - err := svc.Delete(callerID, id) + err := svc.Delete(context.Background(), callerID, id) assert.NoError(t, err) } @@ -36,7 +37,7 @@ func TestDelete_TriedToDeleteSomeoneElse(t *testing.T) { user.ID = id mockRepo.On("FindById", id).Return(user, nil) - err := svc.Delete(callerID, id) + err := svc.Delete(context.Background(), callerID, id) assert.Error(t, err) assert.Equal(t, domain.ErrUnauthorized, err) @@ -49,7 +50,7 @@ func TestDelete_UserNotFound(t *testing.T) { callerID := uint(1) mockRepo.On("FindById", id).Return(nil, fmt.Errorf("user not found")) - err := svc.Delete(callerID, id) + err := svc.Delete(context.Background(), callerID, id) assert.Error(t, err) assert.Equal(t, domain.ErrNotFound, err) @@ -66,9 +67,9 @@ func TestDelete_GuestHasPendingReservations(t *testing.T) { mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() reservation := roomclient.ReservationDTO{} - mockRoomClient.On("GetPendingGuestReservations", user).Return([]roomclient.ReservationDTO{reservation}, nil) + mockRoomClient.On("GetPendingGuestReservations", context.Background(), user).Return([]roomclient.ReservationDTO{reservation}, nil) - err := svc.Delete(callerID, id) + err := svc.Delete(context.Background(), callerID, id) assert.Error(t, err) assert.Equal(t, domain.ErrGuestHasReservations, err) @@ -85,9 +86,9 @@ func TestDelete_HostHasPendingReservations(t *testing.T) { mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() reservation := roomclient.ReservationDTO{} - mockRoomClient.On("GetActiveHostReservations", user).Return([]roomclient.ReservationDTO{reservation}, nil) + mockRoomClient.On("GetActiveHostReservations", context.Background(), user).Return([]roomclient.ReservationDTO{reservation}, nil) - err := svc.Delete(callerID, id) + err := svc.Delete(context.Background(), callerID, id) assert.Error(t, err) assert.Equal(t, domain.ErrHostHasReservations, err) @@ -104,7 +105,7 @@ func TestDelete_TriedDeletingAdmin(t *testing.T) { mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() - err := svc.Delete(callerID, id) + err := svc.Delete(context.Background(), callerID, id) assert.Error(t, err) assert.Equal(t, domain.ErrCannotDeleteAdmin, err) diff --git a/src/test/unit/find_test.go b/src/test/unit/find_test.go index 2c7828a..9c388ea 100644 --- a/src/test/unit/find_test.go +++ b/src/test/unit/find_test.go @@ -1,6 +1,7 @@ package test import ( + "context" "fmt" "testing" @@ -16,7 +17,7 @@ func TestFindById_Success(t *testing.T) { user.ID = id mockRepo.On("FindById", id).Return(user, nil) - userGot, err := svc.FindById(id) + userGot, err := svc.FindById(context.Background(), id) assert.NoError(t, err) assert.NotNil(t, userGot) @@ -33,7 +34,7 @@ func TestFindById_UserNotFound(t *testing.T) { user.ID = id mockRepo.On("FindById", id).Return(nil, fmt.Errorf("no such user")) - userGot, err := svc.FindById(id) + userGot, err := svc.FindById(context.Background(), id) assert.Error(t, err) assert.Nil(t, userGot) diff --git a/src/test/unit/login_test.go b/src/test/unit/login_test.go index ec43989..184c501 100644 --- a/src/test/unit/login_test.go +++ b/src/test/unit/login_test.go @@ -1,6 +1,7 @@ package test import ( + "context" "fmt" "testing" @@ -45,7 +46,7 @@ func TestLogin_Success(t *testing.T) { // Verify - jwt, err := svc.Login(dto) + jwt, err := svc.Login(context.Background(), dto) assert.NoError(t, err) assert.NotEqual(t, "", jwt) @@ -64,7 +65,7 @@ func TestLogin_UserNotFound(t *testing.T) { dto.UsernameOrEmail, dto.UsernameOrEmail, ).Return(nil, fmt.Errorf("no such user")) - jwt, err := svc.Login(dto) + jwt, err := svc.Login(context.Background(), dto) assert.ErrorIs(t, err, domain.ErrLoginFailed) assert.Equal(t, "", jwt) @@ -96,7 +97,7 @@ func TestLogin_WrongPassword(t *testing.T) { dto.UsernameOrEmail, dto.UsernameOrEmail, ).Return(&user, nil) - jwt, err := svc.Login(dto) + jwt, err := svc.Login(context.Background(), dto) assert.ErrorIs(t, err, domain.ErrLoginFailed) assert.Equal(t, "", jwt) @@ -137,7 +138,7 @@ func TestLogin_JWTFailed(t *testing.T) { // Verify - jwt, err := svc.Login(dto) + jwt, err := svc.Login(context.Background(), dto) assert.Error(t, err) assert.Equal(t, "", jwt) diff --git a/src/test/unit/update_test.go b/src/test/unit/update_test.go index 4304b03..8df83cd 100644 --- a/src/test/unit/update_test.go +++ b/src/test/unit/update_test.go @@ -1,6 +1,7 @@ package test import ( + "context" "fmt" "testing" @@ -33,7 +34,7 @@ func TestUpdate_Success(t *testing.T) { // Verify - newUser, err := svc.Update(1, dto) + newUser, err := svc.Update(context.Background(), 1, dto) assert.NoError(t, err) assert.Equal(t, userAfter, *newUser) @@ -50,7 +51,7 @@ func TestUpdate_SomeoneElse(t *testing.T) { // Verify - newUser, err := svc.Update(1, dto) + newUser, err := svc.Update(context.Background(), 1, dto) assert.Nil(t, newUser) assert.Error(t, err) @@ -72,7 +73,7 @@ func TestUpdate_UserNotFound(t *testing.T) { // Verify - newUser, err := svc.Update(uint(1), dto) + newUser, err := svc.Update(context.Background(), uint(1), dto) assert.Nil(t, newUser) assert.Error(t, err) @@ -98,7 +99,7 @@ func TestUpdate_UsernameTaken(t *testing.T) { // Verify - newUser, err := svc.Update(1, dto) + newUser, err := svc.Update(context.Background(), 1, dto) assert.Nil(t, newUser) assert.Error(t, err) @@ -125,7 +126,7 @@ func TestUpdate_EmailTaken(t *testing.T) { // Verify - newUser, err := svc.Update(1, dto) + newUser, err := svc.Update(context.Background(), 1, dto) assert.Nil(t, newUser) assert.Error(t, err) @@ -155,7 +156,7 @@ func TestUpdate_UsernameTakenEmailOk(t *testing.T) { // Verify - newUser, err := svc.Update(1, dto) + newUser, err := svc.Update(context.Background(), 1, dto) assert.Nil(t, newUser) assert.Error(t, err) @@ -185,7 +186,7 @@ func TestUpdate_UsernameOkEmailTaken(t *testing.T) { // Verify - newUser, err := svc.Update(1, dto) + newUser, err := svc.Update(context.Background(), 1, dto) assert.Nil(t, newUser) assert.Error(t, err) diff --git a/src/test/unit/utils.go b/src/test/unit/utils.go index e38f8e4..7995a39 100644 --- a/src/test/unit/utils.go +++ b/src/test/unit/utils.go @@ -4,6 +4,7 @@ import ( "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" service "bookem-user-service/service" + "context" mock "github.com/stretchr/testify/mock" ) @@ -63,14 +64,14 @@ type MockRoomClient struct { mock.Mock } -func (m *MockRoomClient) GetPendingGuestReservations(guest *domain.User) ([]roomclient.ReservationDTO, error) { - args := m.Called(guest) +func (m *MockRoomClient) GetPendingGuestReservations(ctx context.Context, guest *domain.User) ([]roomclient.ReservationDTO, error) { + args := m.Called(ctx, guest) reservations, _ := args.Get(0).([]roomclient.ReservationDTO) return reservations, args.Error(1) } -func (m *MockRoomClient) GetActiveHostReservations(host *domain.User) ([]roomclient.ReservationDTO, error) { - args := m.Called(host) +func (m *MockRoomClient) GetActiveHostReservations(ctx context.Context, host *domain.User) ([]roomclient.ReservationDTO, error) { + args := m.Called(ctx, host) reservations, _ := args.Get(0).([]roomclient.ReservationDTO) return reservations, args.Error(1) } From 44f22657ad420fb2912b7c442fa39ebb1b59e345 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:17:12 +0200 Subject: [PATCH 11/52] feat: Add tracing to the rest of the microservice --- src/service/service.go | 90 +++++++++++++++++++++++++++++++++++------- src/util/telemetry.go | 5 +++ 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/src/service/service.go b/src/service/service.go index cbc02ff..1e1a27f 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -7,7 +7,6 @@ import ( util "bookem-user-service/util" "context" "fmt" - "log" "strings" ) @@ -83,22 +82,31 @@ func (s *service) Register(ctx context.Context, dto *domain.UserCreateDTO) (*dom // On success, it returns a JWT string. // On error, it returns an empty string. func (s *service) Login(ctx context.Context, dto domain.LoginDTO) (string, error) { + util.TEL.Push(ctx, "find-user") + defer util.TEL.Pop() + user, _ := s.repo.FindByUsernameOrEmail(dto.UsernameOrEmail, dto.UsernameOrEmail) if user == nil { - log.Printf("User %s not found", dto.UsernameOrEmail) + util.TEL.Event(fmt.Sprintf("User %s not found", dto.UsernameOrEmail), nil) return "", domain.ErrLoginFailed } + util.TEL.Push(ctx, "verify-password") + defer util.TEL.Pop() + err := util.VerifyPassword(user.Password, dto.Password) if err != nil { - log.Print(err) + util.TEL.Event("Password verification failed", err) return "", domain.ErrLoginFailed } + util.TEL.Push(ctx, "create-jwt") + defer util.TEL.Pop() + jwt, err := util.CreateJWT(int(user.ID), user.Username, user.Role) if err != nil { - log.Print(err) + util.TEL.Event("JWT Creation failed", err) return "", domain.ErrLoginFailed } @@ -108,24 +116,31 @@ func (s *service) Login(ctx context.Context, dto domain.LoginDTO) (string, error // Update updates the user (specified by his ID in the dto) with the new values // in the DTO. Fields with null values are skipped. func (s *service) Update(ctx context.Context, callerID uint, dto domain.UserUpdateDTO) (*domain.User, error) { - log.Printf("User %d wants to update user %d", callerID, dto.Id) + util.TEL.Eventf("User %d wants to update user %d", nil, callerID, dto.Id) // Users can only update themselves. if callerID != dto.Id { + util.TEL.Eventf("User %d trying to update someone else", nil, callerID) return nil, domain.ErrUnauthorized } // Search for the user. - user, err := s.FindById(ctx, dto.Id) + util.TEL.Push(ctx, "find-user") + defer util.TEL.Pop() + + user, err := s.FindById(util.TEL.Ctx(), dto.Id) if err != nil { - log.Printf("User %d not fonud", dto.Id) + util.TEL.Eventf("User %d not found", err, dto.Id) return nil, domain.ErrNotFound } // Check if the username or email is already taken by someone else. + util.TEL.Push(ctx, "assert-unique-credentials") + defer util.TEL.Pop() + if dto.Username != nil || dto.Email != nil { usernameSafe := "" if dto.Username != nil { @@ -144,8 +159,7 @@ func (s *service) Update(ctx context.Context, callerID uint, dto domain.UserUpda } else if emailSafe == otherUserWithUsernameOrEmail.Email { return nil, domain.ErrEmailExists } else { - log.Printf("DB found user matching [%s] or [%s] but the in-memory comparison failed.", usernameSafe, emailSafe) - log.Printf("User: %+v", otherUserWithUsernameOrEmail) + util.TEL.Eventf("DB found user matching [%s] or [%s] but the in-memory comparison failed.\nFound user: %+v", nil, usernameSafe, emailSafe, otherUserWithUsernameOrEmail) return nil, domain.ErrDBInternal } @@ -170,8 +184,12 @@ func (s *service) Update(ctx context.Context, callerID uint, dto domain.UserUpda user.Address = *dto.Address } + util.TEL.Push(ctx, "update-user") + defer util.TEL.Pop() + err = s.repo.Update(user) if err != nil { + util.TEL.Event("Could not update user in DB", err) return nil, err } @@ -180,24 +198,33 @@ func (s *service) Update(ctx context.Context, callerID uint, dto domain.UserUpda // ChangePassword changes the user's password. func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain.PasswordUpdateDTO) (*domain.User, error) { - log.Printf("User %d wants to change password of user %d", callerID, dto.Id) + util.TEL.Eventf("User %d wants to change password of user %d", nil, callerID, dto.Id) // User can only change his own password. if callerID != dto.Id { + util.TEL.Eventf("User %d trying to update someone else", nil, callerID) return nil, domain.ErrUnauthorized } // Search for the user. - user, err := s.FindById(ctx, dto.Id) + util.TEL.Push(ctx, "find-user") + defer util.TEL.Pop() + + user, err := s.FindById(util.TEL.Ctx(), dto.Id) if err != nil { + util.TEL.Eventf("User %d not found", err, dto.Id) return nil, err } // Check if confirm password is valid. + util.TEL.Push(ctx, "password-validation") + defer util.TEL.Pop() + if dto.NewPasswordConfirm != dto.NewPassword { + util.TEL.Eventf("Passwords do not match", nil) return nil, domain.ErrPasswordsNotMatch } @@ -205,12 +232,14 @@ func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain. err = util.VerifyPassword(user.Password, dto.OldPassword) if err != nil { + util.TEL.Eventf("Old password is incorrect", err) return nil, domain.ErrWrongPassword } // Check if password is new. if dto.NewPassword == dto.OldPassword { + util.TEL.Eventf("New password hasn't changed", nil) return nil, domain.ErrPasswordNotChanged } @@ -218,15 +247,20 @@ func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain. passwordHashed, err := util.HashPassword(dto.NewPassword) if err != nil { + util.TEL.Eventf("Password hashing failed", err) return nil, err } // Update. + util.TEL.Push(ctx, "update-user") + defer util.TEL.Pop() + user.Password = passwordHashed err = s.repo.Update(user) if err != nil { + util.TEL.Event("Could not update user in DB", err) return nil, err } @@ -234,65 +268,91 @@ func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain. } func (s *service) FindById(ctx context.Context, id uint) (*domain.User, error) { + util.TEL.Eventf("Find user by ID %d", nil, id) + + util.TEL.Push(ctx, "find-user-in-db") + defer util.TEL.Pop() + user, err := s.repo.FindById(id) if err != nil { + util.TEL.Eventf("User %d not found", err, id) return nil, domain.ErrNotFound } return user, nil } func (s *service) Delete(ctx context.Context, callerID uint, id uint) error { - log.Printf("User %d wants to delete user %d", callerID, id) + util.TEL.Eventf("User %d wants to delete user %d", nil, callerID, id) // User can only delete himself. if id != callerID { + util.TEL.Eventf("User %d trying to delete someone else", nil, callerID) return domain.ErrUnauthorized } // Search for the user. - user, err := s.FindById(ctx, id) + util.TEL.Push(ctx, "find-user") + defer util.TEL.Pop() + + user, err := s.FindById(util.TEL.Ctx(), id) if err != nil { + util.TEL.Eventf("User %d not found", err, id) return err } // Check if user can be deleted. - err = s.canDeleteUser(ctx, user) + util.TEL.Push(ctx, "delete-safety-check") + defer util.TEL.Pop() + + err = s.canDeleteUser(util.TEL.Ctx(), user) if err != nil { + util.TEL.Eventf("Cannot delete user %d", err, id) return err } // Delete user + util.TEL.Push(ctx, "delete-user-in-db") + defer util.TEL.Pop() + s.repo.Delete(user.ID) - log.Printf("User %d deleted", id) + util.TEL.Eventf("User %d deleted", nil, id) return nil } func (s *service) canDeleteUser(ctx context.Context, user *domain.User) error { + util.TEL.Eventf("Check if user %d can be deleted", nil, user.ID) + switch user.Role { case domain.Guest: + util.TEL.Eventf("User is guest - must not have any reservations", nil) reservations, err := s.roomClient.GetPendingGuestReservations(ctx, user) if err != nil { + util.TEL.Eventf("Could not check", err) return err } if len(reservations) > 0 { + util.TEL.Eventf("Guest has reservations, cannot delete user", nil) return domain.ErrGuestHasReservations } return nil case domain.Host: + util.TEL.Eventf("User is host - rooms must not have any reservations", nil) reservations, err := s.roomClient.GetActiveHostReservations(ctx, user) if err != nil { return err } if len(reservations) > 0 { + util.TEL.Eventf("Host's rooms have reservations, cannot delete user", nil) return domain.ErrHostHasReservations } return nil default: + util.TEL.Eventf("Users with role %d cannot be deleted", nil, user.Role) return domain.ErrCannotDeleteAdmin } } diff --git a/src/util/telemetry.go b/src/util/telemetry.go index 1a02ca4..388071e 100644 --- a/src/util/telemetry.go +++ b/src/util/telemetry.go @@ -112,6 +112,11 @@ func (t *Telemetry) Event(msg string, err error) { } } +func (t *Telemetry) Eventf(msg string, err error, a ...any) { + msgFinal := fmt.Sprintf(msg, a...) + t.Event(msgFinal, err) +} + func (t *Telemetry) SetAttrib(kv ...attribute.KeyValue) { if t.tracerReady { t.Top().Span.SetAttributes(kv...) From 39589d896d13c14ee9127bbea282c73d6aa56cb8 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:03:11 +0200 Subject: [PATCH 12/52] feat: Install `slog-multi` package --- src/go.mod | 5 ++++- src/go.sum | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/go.mod b/src/go.mod index f0a3bfc..26a5118 100644 --- a/src/go.mod +++ b/src/go.mod @@ -12,6 +12,7 @@ require ( go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 ) require ( @@ -29,11 +30,12 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/samber/lo v1.51.0 // indirect + github.com/samber/slog-common v0.19.0 // indirect github.com/stretchr/objx v0.5.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect golang.org/x/sync v0.16.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect @@ -58,6 +60,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/samber/slog-multi v1.5.0 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect golang.org/x/arch v0.20.0 // indirect diff --git a/src/go.sum b/src/go.sum index e89c882..16e6b2e 100644 --- a/src/go.sum +++ b/src/go.sum @@ -80,6 +80,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= +github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI= +github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= +github.com/samber/slog-multi v1.5.0 h1:UDRJdsdb0R5vFQFy3l26rpX3rL3FEPJTJ2yKVjoiT1I= +github.com/samber/slog-multi v1.5.0/go.mod h1:im2Zi3mH/ivSY5XDj6LFcKToRIWPw1OcjSVSdXt+2d0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= From 7e6ee8e3ea5472548a9f0d3557fa663b3ceef904 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:04:09 +0200 Subject: [PATCH 13/52] feat: Add structured logging support in Telemetry My goal is to migrate from Event and Eventf to using Info/Debug/Warn/Error. --- src/util/telemetry.go | 57 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/util/telemetry.go b/src/util/telemetry.go index 388071e..507eb71 100644 --- a/src/util/telemetry.go +++ b/src/util/telemetry.go @@ -4,8 +4,11 @@ import ( "context" "fmt" "log" + "log/slog" "net/http" + "os" + slogmulti "github.com/samber/slog-multi" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" @@ -30,11 +33,22 @@ type Telemetry struct { Tracer trace.Tracer SpanStack []SpanPair + + logger *slog.Logger } var TEL Telemetry func (t *Telemetry) Init(ctx context.Context, serviceName, deploymentEnvironment string) func(context.Context) error { + // [0] Init logger + { + t.logger = slog.New( + slogmulti.Fanout( + // TODO: Save to file and then use promtail, use JSON handler there. + slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}), + ), + ) + } // [1] Init tracer { exp, err := otlptracehttp.New(ctx) @@ -90,9 +104,9 @@ func (t *Telemetry) Event(msg string, err error) { // Logging { if err == nil { - log.Println(msg) + t.logger.Info(msg) } else { - log.Printf("%s: %v\n", msg, err) + t.logger.Error(msg, "error", err) } } @@ -132,3 +146,42 @@ func (t *Telemetry) SetUser(id uint) { func (t *Telemetry) Inject(outgoingRequest *http.Request) { otel.GetTextMapPropagator().Inject(t.Ctx(), propagation.HeaderCarrier(outgoingRequest.Header)) } + +func (t *Telemetry) Info(msg string, attrs ...any) { + t.logger.Info(msg, attrs...) + if span := t.currentSpan(); span != nil { + span.AddEvent(msg) + } +} + +func (t *Telemetry) Warn(msg string, attrs ...any) { + t.logger.Warn(msg, attrs...) + if span := t.currentSpan(); span != nil { + span.AddEvent(msg) + } +} + +func (t *Telemetry) Debug(msg string, attrs ...any) { + t.logger.Debug(msg, attrs...) + if span := t.currentSpan(); span != nil { + span.AddEvent(msg) + } +} + +func (t *Telemetry) Error(msg string, err error, attrs ...any) { + t.logger.Error(msg, attrs...) + if span := t.currentSpan(); span != nil { + span.AddEvent(msg, trace.WithAttributes( + attribute.String("error.message", err.Error()), + attribute.Bool("error", true), + )) + span.SetStatus(codes.Error, err.Error()) + } +} + +func (t *Telemetry) currentSpan() trace.Span { + if t.tracerReady && len(t.SpanStack) > 0 { + return t.SpanStack[len(t.SpanStack)-1].Span + } + return nil +} From 4750a31af653c27d75e46fe4350270a31ccc627a Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:06:16 +0200 Subject: [PATCH 14/52] feat: Pass error message to logger --- src/util/telemetry.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util/telemetry.go b/src/util/telemetry.go index 507eb71..9924a34 100644 --- a/src/util/telemetry.go +++ b/src/util/telemetry.go @@ -169,7 +169,9 @@ func (t *Telemetry) Debug(msg string, attrs ...any) { } func (t *Telemetry) Error(msg string, err error, attrs ...any) { + attrs = append(attrs, slog.Any("error", err)) t.logger.Error(msg, attrs...) + if span := t.currentSpan(); span != nil { span.AddEvent(msg, trace.WithAttributes( attribute.String("error.message", err.Error()), From 6fc9b7208f0367d4c2e9d774adf7887190bb0b5d Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:40:54 +0200 Subject: [PATCH 15/52] feat: Migrate to new tracing/logging API --- src/api/handler.go | 36 ++++++++---------- src/service/service.go | 77 +++++++++++++++++++++----------------- src/util/telemetry.go | 85 ++++++++++++++++++++++++------------------ 3 files changed, 107 insertions(+), 91 deletions(-) diff --git a/src/api/handler.go b/src/api/handler.go index 3705301..efa87f8 100644 --- a/src/api/handler.go +++ b/src/api/handler.go @@ -6,7 +6,6 @@ import ( service "bookem-user-service/service" utils "bookem-user-service/util" "fmt" - "log" "net/http" "strconv" @@ -30,14 +29,14 @@ func (h *Handler) registerUser(c *gin.Context) { if err := c.ShouldBindJSON(&dto); err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) - utils.TEL.Event("failed binding JSON", err) + utils.TEL.Error("failed binding JSON", err) return } user, err := h.service.Register(utils.TEL.Ctx(), &dto) if err != nil { c.Error(err) - utils.TEL.Event("failed registering user", err) + utils.TEL.Error("failed registering user", err) return } @@ -52,14 +51,14 @@ func (h *Handler) login(c *gin.Context) { if err := c.ShouldBindJSON(&dto); err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) - utils.TEL.Event("failed binding JSON", err) + utils.TEL.Error("failed binding JSON", err) return } jwt, err := h.service.Login(utils.TEL.Ctx(), dto) if err != nil { c.Error(err) - utils.TEL.Event("failed logging in user", err) + utils.TEL.Error("failed logging in user", err) return } @@ -73,14 +72,14 @@ func (h *Handler) update(c *gin.Context) { jwt, err := middleware.GetJwt(c) if err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) - utils.TEL.Event("unauthenticated", err) + utils.TEL.Error("unauthenticated", err) return } var dto domain.UserUpdateDTO if err := c.ShouldBindJSON(&dto); err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) - utils.TEL.Event("failed binding JSON", err) + utils.TEL.Error("failed binding JSON", err) return } @@ -89,7 +88,7 @@ func (h *Handler) update(c *gin.Context) { _, err = h.service.Update(utils.TEL.Ctx(), jwt.ID, dto) if err != nil { c.Error(err) - utils.TEL.Event("failed updating user", err) + utils.TEL.Error("failed updating user", err) return } @@ -103,14 +102,14 @@ func (h *Handler) changePassword(c *gin.Context) { jwt, err := middleware.GetJwt(c) if err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) - utils.TEL.Event("unauthenticated", err) + utils.TEL.Error("unauthenticated", err) return } var dto domain.PasswordUpdateDTO if err := c.ShouldBindJSON(&dto); err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrInvalidInput, err)) - utils.TEL.Event("failed binding JSON", err) + utils.TEL.Error("failed binding JSON", err) return } @@ -119,7 +118,7 @@ func (h *Handler) changePassword(c *gin.Context) { _, err = h.service.ChangePassword(utils.TEL.Ctx(), jwt.ID, dto) if err != nil { c.Error(err) - utils.TEL.Event("failed changing password", err) + utils.TEL.Error("failed changing password", err) return } @@ -132,20 +131,18 @@ func (h *Handler) findById(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - log.Printf("Could not parse ID: %s", err.Error()) c.Error(err) - utils.TEL.Event("failed parsing ID", err) + utils.TEL.Error("failed parsing ID", err) return } utils.TEL.SetAttrib(attribute.Int("id", id)) - - log.Printf("Find user by id %d", id) + utils.TEL.Debug("find user", "id", id) user, err := h.service.FindById(utils.TEL.Ctx(), uint(id)) if err != nil { c.Error(err) - utils.TEL.Event("failed finding user by ID", err) + utils.TEL.Error("failed finding user by ID", err) return } @@ -158,9 +155,8 @@ func (h *Handler) deleteById(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - log.Printf("Could not parse ID: %s", err.Error()) c.Error(err) - utils.TEL.Event("failed parsing ID", err) + utils.TEL.Error("failed parsing ID", err) return } @@ -169,7 +165,7 @@ func (h *Handler) deleteById(c *gin.Context) { jwt, err := middleware.GetJwt(c) if err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) - utils.TEL.Event("unauthenticatetd", err) + utils.TEL.Error("unauthenticatetd", err) return } @@ -178,7 +174,7 @@ func (h *Handler) deleteById(c *gin.Context) { err = h.service.Delete(utils.TEL.Ctx(), jwt.ID, uint(id)) if err != nil { c.Error(err) - utils.TEL.Event("could not delete user", err) + utils.TEL.Error("could not delete user", err) return } diff --git a/src/service/service.go b/src/service/service.go index 1e1a27f..edba580 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -38,7 +38,7 @@ func (s *service) Register(ctx context.Context, dto *domain.UserCreateDTO) (*dom hashed, err := util.HashPassword(dto.Password) if err != nil { - util.TEL.Event("failed hashing password", err) + util.TEL.Error("failed hashing password", err) return nil, domain.ErrHashingPassword } @@ -57,11 +57,11 @@ func (s *service) Register(ctx context.Context, dto *domain.UserCreateDTO) (*dom existing, _ := s.repo.FindByUsernameOrEmail(dto.Username, dto.Email) if existing != nil { if existing.Username == dto.Username { - util.TEL.Event("username exists", nil) + util.TEL.Error("username exists", nil, "username", existing.Username, "id", existing.ID) return nil, domain.ErrUsernameExists } if existing.Email == dto.Email { - util.TEL.Event("email exists", nil) + util.TEL.Error("email exists", nil, "email", existing.Email, "id", existing.ID) return nil, domain.ErrEmailExists } } @@ -70,10 +70,12 @@ func (s *service) Register(ctx context.Context, dto *domain.UserCreateDTO) (*dom defer util.TEL.Pop() err = s.repo.Create(user) if err != nil { - util.TEL.Event("failed inserting user", err) + util.TEL.Error("failed inserting user", err) return nil, fmt.Errorf("%w: %v", domain.ErrDBInternal, err) } + util.TEL.Info("Successfully created user", "id", user.ID) + return user, nil } @@ -88,7 +90,7 @@ func (s *service) Login(ctx context.Context, dto domain.LoginDTO) (string, error user, _ := s.repo.FindByUsernameOrEmail(dto.UsernameOrEmail, dto.UsernameOrEmail) if user == nil { - util.TEL.Event(fmt.Sprintf("User %s not found", dto.UsernameOrEmail), nil) + util.TEL.Error("user not found", nil, "username_or_email", dto.UsernameOrEmail) return "", domain.ErrLoginFailed } @@ -97,7 +99,7 @@ func (s *service) Login(ctx context.Context, dto domain.LoginDTO) (string, error err := util.VerifyPassword(user.Password, dto.Password) if err != nil { - util.TEL.Event("Password verification failed", err) + util.TEL.Error("Password verification failed", err) return "", domain.ErrLoginFailed } @@ -106,22 +108,24 @@ func (s *service) Login(ctx context.Context, dto domain.LoginDTO) (string, error jwt, err := util.CreateJWT(int(user.ID), user.Username, user.Role) if err != nil { - util.TEL.Event("JWT Creation failed", err) + util.TEL.Error("JWT Creation failed", err) return "", domain.ErrLoginFailed } + util.TEL.Info("Logged in user", "id", user.ID) + return jwt, nil } // Update updates the user (specified by his ID in the dto) with the new values // in the DTO. Fields with null values are skipped. func (s *service) Update(ctx context.Context, callerID uint, dto domain.UserUpdateDTO) (*domain.User, error) { - util.TEL.Eventf("User %d wants to update user %d", nil, callerID, dto.Id) + util.TEL.Info("user update request", "caller_id", callerID, "user_id", dto.Id) // Users can only update themselves. if callerID != dto.Id { - util.TEL.Eventf("User %d trying to update someone else", nil, callerID) + util.TEL.Error("user trying to update someone else", nil) return nil, domain.ErrUnauthorized } @@ -132,7 +136,7 @@ func (s *service) Update(ctx context.Context, callerID uint, dto domain.UserUpda user, err := s.FindById(util.TEL.Ctx(), dto.Id) if err != nil { - util.TEL.Eventf("User %d not found", err, dto.Id) + util.TEL.Error("user not found", err, "id", dto.Id) return nil, domain.ErrNotFound } @@ -159,8 +163,7 @@ func (s *service) Update(ctx context.Context, callerID uint, dto domain.UserUpda } else if emailSafe == otherUserWithUsernameOrEmail.Email { return nil, domain.ErrEmailExists } else { - util.TEL.Eventf("DB found user matching [%s] or [%s] but the in-memory comparison failed.\nFound user: %+v", nil, usernameSafe, emailSafe, otherUserWithUsernameOrEmail) - + util.TEL.Error("db malfunction, could not compare users", nil) return nil, domain.ErrDBInternal } } @@ -189,21 +192,23 @@ func (s *service) Update(ctx context.Context, callerID uint, dto domain.UserUpda err = s.repo.Update(user) if err != nil { - util.TEL.Event("Could not update user in DB", err) + util.TEL.Error("could not update user in DB", err) return nil, err } + util.TEL.Info("updated user", "user_id", user.ID) + return user, nil } // ChangePassword changes the user's password. func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain.PasswordUpdateDTO) (*domain.User, error) { - util.TEL.Eventf("User %d wants to change password of user %d", nil, callerID, dto.Id) + util.TEL.Info("password change request", "caller_id", callerID, "user_id", dto.Id) // User can only change his own password. if callerID != dto.Id { - util.TEL.Eventf("User %d trying to update someone else", nil, callerID) + util.TEL.Error("user trying to update someone else", nil) return nil, domain.ErrUnauthorized } @@ -214,7 +219,7 @@ func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain. user, err := s.FindById(util.TEL.Ctx(), dto.Id) if err != nil { - util.TEL.Eventf("User %d not found", err, dto.Id) + util.TEL.Error("user not found", err, "id", dto.Id) return nil, err } @@ -224,7 +229,7 @@ func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain. defer util.TEL.Pop() if dto.NewPasswordConfirm != dto.NewPassword { - util.TEL.Eventf("Passwords do not match", nil) + util.TEL.Error("passwords do not match", nil) return nil, domain.ErrPasswordsNotMatch } @@ -232,14 +237,14 @@ func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain. err = util.VerifyPassword(user.Password, dto.OldPassword) if err != nil { - util.TEL.Eventf("Old password is incorrect", err) + util.TEL.Error("old password is incorrect", err) return nil, domain.ErrWrongPassword } // Check if password is new. if dto.NewPassword == dto.OldPassword { - util.TEL.Eventf("New password hasn't changed", nil) + util.TEL.Error("new password hasn't changed", nil) return nil, domain.ErrPasswordNotChanged } @@ -247,7 +252,7 @@ func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain. passwordHashed, err := util.HashPassword(dto.NewPassword) if err != nil { - util.TEL.Eventf("Password hashing failed", err) + util.TEL.Error("password hashing failed", err) return nil, err } @@ -260,34 +265,36 @@ func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain. err = s.repo.Update(user) if err != nil { - util.TEL.Event("Could not update user in DB", err) + util.TEL.Error("could not update user in DB", err) return nil, err } + util.TEL.Info("updated password", "user_id", user.ID) + return user, nil } func (s *service) FindById(ctx context.Context, id uint) (*domain.User, error) { - util.TEL.Eventf("Find user by ID %d", nil, id) + util.TEL.Info("Find user", "id", id) util.TEL.Push(ctx, "find-user-in-db") defer util.TEL.Pop() user, err := s.repo.FindById(id) if err != nil { - util.TEL.Eventf("User %d not found", err, id) + util.TEL.Error("user not found", err, "id", id) return nil, domain.ErrNotFound } return user, nil } func (s *service) Delete(ctx context.Context, callerID uint, id uint) error { - util.TEL.Eventf("User %d wants to delete user %d", nil, callerID, id) + util.TEL.Info("user delete request", "caller_id", callerID, "user_id", id) // User can only delete himself. if id != callerID { - util.TEL.Eventf("User %d trying to delete someone else", nil, callerID) + util.TEL.Error("user trying to update someone else", nil) return domain.ErrUnauthorized } @@ -298,7 +305,7 @@ func (s *service) Delete(ctx context.Context, callerID uint, id uint) error { user, err := s.FindById(util.TEL.Ctx(), id) if err != nil { - util.TEL.Eventf("User %d not found", err, id) + util.TEL.Error("user not found", err, "id", id) return err } @@ -309,7 +316,7 @@ func (s *service) Delete(ctx context.Context, callerID uint, id uint) error { err = s.canDeleteUser(util.TEL.Ctx(), user) if err != nil { - util.TEL.Eventf("Cannot delete user %d", err, id) + util.TEL.Error("cannot delete user", err, "id", id) return err } @@ -319,40 +326,40 @@ func (s *service) Delete(ctx context.Context, callerID uint, id uint) error { defer util.TEL.Pop() s.repo.Delete(user.ID) - util.TEL.Eventf("User %d deleted", nil, id) + util.TEL.Info("User deleted", "id", id) return nil } func (s *service) canDeleteUser(ctx context.Context, user *domain.User) error { - util.TEL.Eventf("Check if user %d can be deleted", nil, user.ID) + util.TEL.Info("check if user can be deleted", "id", user.ID) switch user.Role { case domain.Guest: - util.TEL.Eventf("User is guest - must not have any reservations", nil) + util.TEL.Debug("user is guest - must not have any reservations") reservations, err := s.roomClient.GetPendingGuestReservations(ctx, user) if err != nil { - util.TEL.Eventf("Could not check", err) + util.TEL.Error("could not check", err) return err } if len(reservations) > 0 { - util.TEL.Eventf("Guest has reservations, cannot delete user", nil) + util.TEL.Error("guest has reservations, cannot delete user", nil) return domain.ErrGuestHasReservations } return nil case domain.Host: - util.TEL.Eventf("User is host - rooms must not have any reservations", nil) + util.TEL.Debug("user is host - rooms must not have any reservations") reservations, err := s.roomClient.GetActiveHostReservations(ctx, user) if err != nil { return err } if len(reservations) > 0 { - util.TEL.Eventf("Host's rooms have reservations, cannot delete user", nil) + util.TEL.Error("host's rooms have reservations, cannot delete user", nil) return domain.ErrHostHasReservations } return nil default: - util.TEL.Eventf("Users with role %d cannot be deleted", nil, user.Role) + util.TEL.Error("users with this role cannot be deleted", nil, "role", user.Role) return domain.ErrCannotDeleteAdmin } } diff --git a/src/util/telemetry.go b/src/util/telemetry.go index 9924a34..d0d237f 100644 --- a/src/util/telemetry.go +++ b/src/util/telemetry.go @@ -34,7 +34,8 @@ type Telemetry struct { SpanStack []SpanPair - logger *slog.Logger + loggerReady bool + logger *slog.Logger } var TEL Telemetry @@ -48,6 +49,8 @@ func (t *Telemetry) Init(ctx context.Context, serviceName, deploymentEnvironment slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}), ), ) + + t.loggerReady = true } // [1] Init tracer { @@ -100,36 +103,36 @@ func (t *Telemetry) Ctx() context.Context { return t.Top().Ctx } -func (t *Telemetry) Event(msg string, err error) { - // Logging - { - if err == nil { - t.logger.Info(msg) - } else { - t.logger.Error(msg, "error", err) - } - } - - // Tracing - if t.tracerReady && len(t.SpanStack) > 0 { - span := t.SpanStack[len(t.SpanStack)-1].Span - - if err == nil { - span.AddEvent(msg) - } else { - span.AddEvent(msg, trace.WithAttributes( - attribute.String("error.message", err.Error()), - attribute.Bool("error", true), - )) - span.SetStatus(codes.Error, err.Error()) - } - } -} - -func (t *Telemetry) Eventf(msg string, err error, a ...any) { - msgFinal := fmt.Sprintf(msg, a...) - t.Event(msgFinal, err) -} +// func (t *Telemetry) Event(msg string, err error) { +// // Logging +// { +// if err == nil { +// t.logger.Info(msg) +// } else { +// t.logger.Error(msg, "error", err) +// } +// } + +// // Tracing +// if t.tracerReady && len(t.SpanStack) > 0 { +// span := t.SpanStack[len(t.SpanStack)-1].Span + +// if err == nil { +// span.AddEvent(msg) +// } else { +// span.AddEvent(msg, trace.WithAttributes( +// attribute.String("error.message", err.Error()), +// attribute.Bool("error", true), +// )) +// span.SetStatus(codes.Error, err.Error()) +// } +// } +// } + +// func (t *Telemetry) Eventf(msg string, err error, a ...any) { +// msgFinal := fmt.Sprintf(msg, a...) +// t.Event(msgFinal, err) +// } func (t *Telemetry) SetAttrib(kv ...attribute.KeyValue) { if t.tracerReady { @@ -148,29 +151,39 @@ func (t *Telemetry) Inject(outgoingRequest *http.Request) { } func (t *Telemetry) Info(msg string, attrs ...any) { - t.logger.Info(msg, attrs...) + if t.loggerReady { + t.logger.Info(msg, attrs...) + } if span := t.currentSpan(); span != nil { span.AddEvent(msg) } } func (t *Telemetry) Warn(msg string, attrs ...any) { - t.logger.Warn(msg, attrs...) + if t.loggerReady { + t.logger.Warn(msg, attrs...) + } if span := t.currentSpan(); span != nil { span.AddEvent(msg) } } func (t *Telemetry) Debug(msg string, attrs ...any) { - t.logger.Debug(msg, attrs...) + if t.loggerReady { + t.logger.Debug(msg, attrs...) + } if span := t.currentSpan(); span != nil { span.AddEvent(msg) } } func (t *Telemetry) Error(msg string, err error, attrs ...any) { - attrs = append(attrs, slog.Any("error", err)) - t.logger.Error(msg, attrs...) + if t.loggerReady { + if err != nil { + attrs = append(attrs, slog.Any("error", err)) + } + t.logger.Error(msg, attrs...) + } if span := t.currentSpan(); span != nil { span.AddEvent(msg, trace.WithAttributes( From 5d6eb7e72e71372769bc9ccf96e5c6d2aa52560f Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:49:28 +0200 Subject: [PATCH 16/52] fix: Don't use `err` if it's `nil` --- src/service/service.go | 4 ++-- src/util/telemetry.go | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/service/service.go b/src/service/service.go index edba580..6176d3b 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -208,7 +208,7 @@ func (s *service) ChangePassword(ctx context.Context, callerID uint, dto domain. // User can only change his own password. if callerID != dto.Id { - util.TEL.Error("user trying to update someone else", nil) + util.TEL.Error("user trying to change password of someone else", nil) return nil, domain.ErrUnauthorized } @@ -294,7 +294,7 @@ func (s *service) Delete(ctx context.Context, callerID uint, id uint) error { // User can only delete himself. if id != callerID { - util.TEL.Error("user trying to update someone else", nil) + util.TEL.Error("user trying to delete someone else", nil) return domain.ErrUnauthorized } diff --git a/src/util/telemetry.go b/src/util/telemetry.go index d0d237f..6020de5 100644 --- a/src/util/telemetry.go +++ b/src/util/telemetry.go @@ -186,11 +186,12 @@ func (t *Telemetry) Error(msg string, err error, attrs ...any) { } if span := t.currentSpan(); span != nil { - span.AddEvent(msg, trace.WithAttributes( - attribute.String("error.message", err.Error()), - attribute.Bool("error", true), - )) - span.SetStatus(codes.Error, err.Error()) + span.AddEvent(msg, trace.WithAttributes(attribute.Bool("error", true))) + span.SetStatus(codes.Error, "error") + if err != nil { + span.AddEvent(msg, trace.WithAttributes(attribute.String("error.message", err.Error()))) + span.SetStatus(codes.Error, err.Error()) + } } } From ad705d9795950a6b79ec5105392ea5be3ff6041a Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:40:38 +0200 Subject: [PATCH 17/52] feat: Write logs to log file --- src/main.go | 1 + src/util/telemetry.go | 79 ++++++++++++++++++++++--------------------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/main.go b/src/main.go index 5ac8b05..91a4416 100644 --- a/src/main.go +++ b/src/main.go @@ -82,6 +82,7 @@ func main() { server = gin.Default() server.Use(otelgin.Middleware(os.Getenv("SERVICE_NAME"))) + server.Use(utils.TEL.GetLoggingMiddleware()) server.Use(cors.New(cors.Config{ AllowOrigins: []string{"http://localhost:5173", "http://localhost", "http://bookem.local"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, diff --git a/src/util/telemetry.go b/src/util/telemetry.go index 6020de5..cc957fc 100644 --- a/src/util/telemetry.go +++ b/src/util/telemetry.go @@ -8,6 +8,7 @@ import ( "net/http" "os" + "github.com/gin-gonic/gin" slogmulti "github.com/samber/slog-multi" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -43,14 +44,12 @@ var TEL Telemetry func (t *Telemetry) Init(ctx context.Context, serviceName, deploymentEnvironment string) func(context.Context) error { // [0] Init logger { - t.logger = slog.New( - slogmulti.Fanout( - // TODO: Save to file and then use promtail, use JSON handler there. - slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}), - ), - ) + err := t.initLogger() + if err != nil { + // log.Printf instead of the logger here! + log.Printf("Could not initialize logger: %v", err) + } - t.loggerReady = true } // [1] Init tracer { @@ -76,6 +75,41 @@ func (t *Telemetry) Init(ctx context.Context, serviceName, deploymentEnvironment } } +func (t *Telemetry) initLogger() error { + err := os.MkdirAll("/app/logs", 0755) + if err != nil { + return err + } + + logFile, err := os.OpenFile("/app/logs/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return err + } + + t.logger = slog.New( + slogmulti.Fanout( + slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}), + slog.NewJSONHandler(logFile, &slog.HandlerOptions{Level: slog.LevelDebug}), + ), + ) + + t.loggerReady = true + t.Debug("Logger initialized") + return nil +} + +func (t *Telemetry) GetLoggingMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + t.logger.Info("request", + slog.String("method", c.Request.Method), + slog.String("path", c.Request.URL.Path), + slog.Int("status", c.Writer.Status()), + slog.String("client_ip", c.ClientIP()), + ) + } +} + func (t *Telemetry) Push(ctx context.Context, name string, attrs ...attribute.KeyValue) { if t.tracerReady { newCtx, span := t.Tracer.Start(ctx, name, trace.WithAttributes(attrs...)) @@ -103,37 +137,6 @@ func (t *Telemetry) Ctx() context.Context { return t.Top().Ctx } -// func (t *Telemetry) Event(msg string, err error) { -// // Logging -// { -// if err == nil { -// t.logger.Info(msg) -// } else { -// t.logger.Error(msg, "error", err) -// } -// } - -// // Tracing -// if t.tracerReady && len(t.SpanStack) > 0 { -// span := t.SpanStack[len(t.SpanStack)-1].Span - -// if err == nil { -// span.AddEvent(msg) -// } else { -// span.AddEvent(msg, trace.WithAttributes( -// attribute.String("error.message", err.Error()), -// attribute.Bool("error", true), -// )) -// span.SetStatus(codes.Error, err.Error()) -// } -// } -// } - -// func (t *Telemetry) Eventf(msg string, err error, a ...any) { -// msgFinal := fmt.Sprintf(msg, a...) -// t.Event(msgFinal, err) -// } - func (t *Telemetry) SetAttrib(kv ...attribute.KeyValue) { if t.tracerReady { t.Top().Span.SetAttributes(kv...) From 83a9d2f71c2f6e78635881846a4720f56c4a1e98 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 5 Oct 2025 15:23:01 +0200 Subject: [PATCH 18/52] fix: Add empty stack check in Telemetry --- src/util/telemetry.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/util/telemetry.go b/src/util/telemetry.go index cc957fc..0315c78 100644 --- a/src/util/telemetry.go +++ b/src/util/telemetry.go @@ -134,7 +134,11 @@ func (t *Telemetry) Top() SpanPair { } func (t *Telemetry) Ctx() context.Context { - return t.Top().Ctx + if len(t.SpanStack) > 0 { + return t.Top().Ctx + } else { + return context.Background() // Ehh... + } } func (t *Telemetry) SetAttrib(kv ...attribute.KeyValue) { From 71bd62ee98a3df68c0246b00a29ffb361d6e1aad Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 5 Oct 2025 16:19:07 +0200 Subject: [PATCH 19/52] fix: Trigger the Actions bot I changed the base in my PR (accidentally set it to master instead of develop) which cancelled the jobs. This PR is used to trigger the Github Actions bot again. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 53a0a21..ff92eb0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ test-out/ # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # -# Binaries for programs and plugins +# Binaries for programs and plugins. *.exe *.exe~ *.dll From 483330942841e62c4c2c62bf8b4cb27a567536ad Mon Sep 17 00:00:00 2001 From: Vasilije Date: Sat, 11 Oct 2025 13:17:33 +0200 Subject: [PATCH 20/52] feat: Support `reservation-service` and `room_service` in integration tests --- Makefile | 10 +++- ci.override.env | 7 +++ compose.integration.yml | 119 +++++++++++++++++++++++++++++++++++++++- default.env | 14 +++++ run-integration.sh | 55 +++++++++++++++++++ 5 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 ci.override.env create mode 100644 default.env create mode 100644 run-integration.sh diff --git a/Makefile b/Makefile index 1fdd78f..be7b594 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,12 @@ .PHONY: run +# Usage: +# make [command] {MODE} +# +# command := run (default) / test / test_unit / test_integration +# MODE := ci / local +# + run: echo "Run using infrastructure/" @@ -9,5 +16,4 @@ test_unit: ./run-tests.sh test_integration: - docker compose -f compose.integration.yml up --build --abort-on-container-exit --exit-code-from test-runner - docker compose -f compose.integration.yml down + ./run-integration.sh $(MODE) \ No newline at end of file diff --git a/ci.override.env b/ci.override.env new file mode 100644 index 0000000..7261b24 --- /dev/null +++ b/ci.override.env @@ -0,0 +1,7 @@ +RESERVATION_SERVICE_PATH=./services/reservation-service +RESERVATION_SERVICE_REPO=https://github.com/book-em/reservation-service.git +RESERVATION_SERVICE_BRANCH=develop + +ROOM_SERVICE_PATH=./services/room-service +ROOM_SERVICE_REPO=https://github.com/book-em/room-service.git +ROOM_SERVICE_BRANCH=develop \ No newline at end of file diff --git a/compose.integration.yml b/compose.integration.yml index a513f4d..ff29095 100644 --- a/compose.integration.yml +++ b/compose.integration.yml @@ -1,13 +1,23 @@ services: test-runner: build: - context: ./src + context: ${TEST_RUNNER_PATH}/src dockerfile: test.Dockerfile depends_on: db: condition: service_healthy user-service: condition: service_healthy + room-db: + condition: service_healthy + room-service: + condition: service_healthy + room-images: + condition: service_healthy + reservation-db: + condition: service_healthy + reservation-service: + condition: service_healthy volumes: - go-mod-cache:/go/pkg/mod @@ -58,5 +68,110 @@ services: timeout: 3s retries: 5 + room-service: + build: + context: ${ROOM_SERVICE_PATH}/src + dockerfile: Dockerfile + image: room-service-test + ports: + - "${ROOM_SERVICE_PORT}:8080" + environment: + DB_HOST: room-db + DB_PORT: 5432 + DB_NAME: bookem_roomdb_test + DB_USER: bookem_roomdb_user + DB_PASSWORD: testpass + JWT_PUBLIC_KEY_PATH: /app/keys/public_key.pem + ENABLE_TEST_MODE: "true" + depends_on: + room-db: + condition: service_healthy + volumes: + - ${ROOM_SERVICE_PATH}/keys/public_key.pem:/app/keys/public_key.pem:ro + - room_images:/app/images/ + + healthcheck: + test: ["CMD-SHELL", "wget --spider --tries=1 --no-verbose http://room-service:8080/healthz || exit 1"] + interval: 3s + timeout: 3s + retries: 5 + + room-db: + image: postgres:15-alpine + environment: + POSTGRES_DB: bookem_roomdb_test + POSTGRES_USER: bookem_roomdb_user + POSTGRES_PASSWORD: testpass + ports: + - "${ROOM_SERVICE_DB_PORT}:5432" + volumes: + - type: tmpfs + target: /var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 3s + timeout: 3s + retries: 5 + + room-images: + restart: always + build: + context: ${ROOM_SERVICE_PATH}/nginx + dockerfile: Dockerfile + ports: + - "${ROOM_SERVICE_IMAGES_PORT}:80" + volumes: + - room_images:/usr/share/nginx/html/images/ + healthcheck: + test: service nginx status || exit 1 + interval: 15s + timeout: 3s + retries: 2 + + reservation-service: + build: + context: ${RESERVATION_SERVICE_PATH}/src + dockerfile: Dockerfile + image: reservation-service-test + ports: + - "${RESERVATION_SERVICE_PORT}:8080" + environment: + DB_HOST: reservation-db + DB_PORT: 5432 + DB_NAME: bookem_reservationdb_test + DB_USER: bookem_reservationdb_user + DB_PASSWORD: testpass + JWT_PUBLIC_KEY_PATH: /app/keys/public_key.pem + ENABLE_TEST_MODE: "true" + depends_on: + reservation-db: + condition: service_healthy + volumes: + - ${RESERVATION_SERVICE_PATH}/keys/public_key.pem:/app/keys/public_key.pem:ro + + healthcheck: + test: ["CMD-SHELL", "wget --spider --tries=1 --no-verbose http://reservation-service:8080/healthz || exit 1"] + interval: 3s + timeout: 3s + retries: 5 + + reservation-db: + image: postgres:15-alpine + environment: + POSTGRES_DB: bookem_reservationdb_test + POSTGRES_USER: bookem_reservationdb_user + POSTGRES_PASSWORD: testpass + ports: + - "${RESERVATION_SERVICE_DB_PORT}:5432" + volumes: + - type: tmpfs + target: /var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 3s + timeout: 3s + retries: 5 + volumes: - go-mod-cache: \ No newline at end of file + go-mod-cache: + room_images: \ No newline at end of file diff --git a/default.env b/default.env new file mode 100644 index 0000000..3ab1ffc --- /dev/null +++ b/default.env @@ -0,0 +1,14 @@ +TEST_RUNNER_PATH=. + +RESERVATION_SERVICE_PATH=../reservation-service +RESERVATION_SERVICE_PORT=0 +RESERVATION_SERVICE_DB_PORT=0 + +USER_SERVICE_PATH=. +USER_SERVICE_PORT=0 +USER_SERVICE_DB_PORT=0 + +ROOM_SERVICE_PATH=../room-service +ROOM_SERVICE_PORT=0 +ROOM_SERVICE_DB_PORT=0 +ROOM_SERVICE_IMAGES_PORT=0 diff --git a/run-integration.sh b/run-integration.sh new file mode 100644 index 0000000..fcf09a6 --- /dev/null +++ b/run-integration.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -e + +# run-tests.sh [ci|local] +# +# - ci: Loads default + ci.override.env +# - local: Loads default + override.env + +# Determine mode + +mode="$1" +if [[ "$mode" != "ci" && "$mode" != "local" ]]; then + echo "Must specify 'ci' or 'local' as first argument" + exit 1 +fi + +# Determine which env files to read + +ENV_FILES=("./default.env") + +if [[ "$mode" == "ci" ]]; then + echo "Loading CI overrides..." + ENV_FILES+=("./ci.override.env") +else + echo "Loading local overrides..." + ENV_FILES+=("./override.env") +fi + +# Load env files + +for file in "${ENV_FILES[@]}"; do + if [[ -f "$file" ]]; then + echo "Loading env vars from $file" + set -o allexport + source "$file" + set +o allexport + else + echo "Skipping missing optional env file: $file" + fi +done + +# Clone repos if CI + +if [[ "$mode" == "ci" ]]; then + echo "Cloning $RESERVATION_SERVICE_REPO branch $RESERVATION_SERVICE_REPO into $RESERVATION_SERVICE_PATH" + git clone --branch "$RESERVATION_SERVICE_BRANCH" "$RESERVATION_SERVICE_REPO" "$RESERVATION_SERVICE_PATH" + + echo "Cloning $ROOM_SERVICE_REPO branch $ROOM_SERVICE_BRANCH into $ROOM_SERVICE_PATH" + git clone --branch "$ROOM_SERVICE_BRANCH" "$ROOM_SERVICE_REPO" "$ROOM_SERVICE_PATH" +fi + +# Run integration tests + +docker compose -f ./compose.integration.yml up --build --abort-on-container-exit --exit-code-from test-runner +docker compose -f ./compose.integration.yml down \ No newline at end of file From 6e7bbe641efafe6a733679db2c9f0535fa752781 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Sat, 11 Oct 2025 13:17:47 +0200 Subject: [PATCH 21/52] feat: Ignore `services` and `override.env`, and enforce `eol=lf` --- .gitattributes | 1 + .gitignore | 1 + services/.gitignore | 1 + 3 files changed, 3 insertions(+) create mode 100644 services/.gitignore diff --git a/.gitattributes b/.gitattributes index da23d37..0127dc5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ *.sh text eol=lf +*.env text eol=lf Makefile text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore index ff92eb0..0249db8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ go.work.sum # env file .env +override.env # Editor/IDE # .idea/ diff --git a/services/.gitignore b/services/.gitignore new file mode 100644 index 0000000..0a00d70 --- /dev/null +++ b/services/.gitignore @@ -0,0 +1 @@ +*/ \ No newline at end of file From 42cbb8a556494defba2755aac43a539b2d05dcff Mon Sep 17 00:00:00 2001 From: Vasilije Date: Sat, 11 Oct 2025 13:18:18 +0200 Subject: [PATCH 22/52] feat: Run `test_integration` with `MODE=ci` in GitHub Actions --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6009583..375ccf6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,4 +84,6 @@ jobs: run: chmod 400 keys/private_key.key - name: Run tests - run: make test_integration \ No newline at end of file + run: | + chmod +x ./run-integration.sh + make test_integration MODE=ci \ No newline at end of file From b13b285f17e969952f73b9cdaa95a9fdcd31f324 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:34:55 +0200 Subject: [PATCH 23/52] feat: Add required libraries for sending data to Prometheus --- src/go.mod | 8 ++++++++ src/go.sum | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/go.mod b/src/go.mod index 26a5118..284c94c 100644 --- a/src/go.mod +++ b/src/go.mod @@ -7,6 +7,7 @@ require ( github.com/gin-gonic/gin v1.10.1 github.com/golang-jwt/jwt/v5 v5.2.3 github.com/lib/pq v1.10.9 + github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.63.0 go.opentelemetry.io/otel v1.38.0 @@ -16,7 +17,9 @@ require ( ) require ( + github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -28,7 +31,11 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/samber/lo v1.51.0 // indirect github.com/samber/slog-common v0.19.0 // indirect @@ -37,6 +44,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/sync v0.16.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect diff --git a/src/go.sum b/src/go.sum index 16e6b2e..e89c01d 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,9 +1,13 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -57,12 +61,16 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -74,10 +82,20 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 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/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= @@ -128,6 +146,8 @@ go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOV go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= From eade30ed3359459fc3a86361b366d8f5ace0ab3f Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:35:16 +0200 Subject: [PATCH 24/52] feat: Create Prometheus middleware --- src/api/middleware/prometheus_middleware.go | 43 +++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/api/middleware/prometheus_middleware.go diff --git a/src/api/middleware/prometheus_middleware.go b/src/api/middleware/prometheus_middleware.go new file mode 100644 index 0000000..db4ba67 --- /dev/null +++ b/src/api/middleware/prometheus_middleware.go @@ -0,0 +1,43 @@ +package middleware + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus" +) + +var ( + httpRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"method", "status", "endpoint"}, + ) + + httpResponseSizeBytes = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_response_size_bytes", + Help: "Total response size in bytes", + }, + []string{"endpoint", "status"}, + ) +) + +func PrometheusMiddleware() gin.HandlerFunc { + prometheus.MustRegister(httpRequestsTotal) + prometheus.MustRegister(httpResponseSizeBytes) + + return func(c *gin.Context) { + c.Next() + + endpoint := c.FullPath() + status := fmt.Sprintf("%d", c.Writer.Status()) + method := c.Request.Method + size := float64(c.Writer.Size()) + + httpRequestsTotal.WithLabelValues(method, status, endpoint).Inc() + httpResponseSizeBytes.WithLabelValues(endpoint, status).Add(size) + } +} From 5ae983f00eb9e95940f482205283beb91bb24cce Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:35:42 +0200 Subject: [PATCH 25/52] feat: Add Prometheus middleware to the server --- src/main.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main.go b/src/main.go index 91a4416..a51cc5c 100644 --- a/src/main.go +++ b/src/main.go @@ -10,10 +10,12 @@ import ( "time" _ "github.com/lib/pq" + "github.com/prometheus/client_golang/prometheus/promhttp" "gorm.io/driver/postgres" "gorm.io/gorm" api "bookem-user-service/api" + "bookem-user-service/api/middleware" "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" repo "bookem-user-service/repo" @@ -61,12 +63,6 @@ func connectToDb() { func main() { ctx := context.Background() - // shutdown := utils.InitTracer( - // ctx, - // os.Getenv("SERVICE_NAME"), - // os.Getenv("DEPLOYMENT_ENV"), - // ) - // defer shutdown(ctx) shutdown2 := utils.TEL.Init( ctx, @@ -81,6 +77,7 @@ func main() { server = gin.Default() + server.Use(middleware.PrometheusMiddleware()) server.Use(otelgin.Middleware(os.Getenv("SERVICE_NAME"))) server.Use(utils.TEL.GetLoggingMiddleware()) server.Use(cors.New(cors.Config{ @@ -92,6 +89,7 @@ func main() { MaxAge: 12 * time.Hour, })) + server.GET("/metrics", gin.WrapH(promhttp.Handler())) server.GET("/healthz", func(ctx *gin.Context) { err := rawDB.Ping() if err != nil { From ac2614f97d74e226c522ef781e501f11a18802da Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:46:18 +0200 Subject: [PATCH 26/52] feat: Add user agent and timestamp to logs --- src/util/telemetry.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/util/telemetry.go b/src/util/telemetry.go index 0315c78..8857f1d 100644 --- a/src/util/telemetry.go +++ b/src/util/telemetry.go @@ -7,6 +7,7 @@ import ( "log/slog" "net/http" "os" + "time" "github.com/gin-gonic/gin" slogmulti "github.com/samber/slog-multi" @@ -106,6 +107,8 @@ func (t *Telemetry) GetLoggingMiddleware() gin.HandlerFunc { slog.String("path", c.Request.URL.Path), slog.Int("status", c.Writer.Status()), slog.String("client_ip", c.ClientIP()), + slog.String("user_agent", c.Request.UserAgent()), + slog.Time("timestamp", time.Now()), ) } } From b4bab9c678318f0a803f952d8c5a8146ac75e334 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sat, 11 Oct 2025 12:06:25 +0200 Subject: [PATCH 27/52] fix: Don't push response size that's < 0 to Prometheus See commit 1c0f39b in room-service for a description. --- src/api/middleware/prometheus_middleware.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/api/middleware/prometheus_middleware.go b/src/api/middleware/prometheus_middleware.go index db4ba67..bec6ae3 100644 --- a/src/api/middleware/prometheus_middleware.go +++ b/src/api/middleware/prometheus_middleware.go @@ -1,6 +1,7 @@ package middleware import ( + utils "bookem-user-service/util" "fmt" "github.com/gin-gonic/gin" @@ -38,6 +39,11 @@ func PrometheusMiddleware() gin.HandlerFunc { size := float64(c.Writer.Size()) httpRequestsTotal.WithLabelValues(method, status, endpoint).Inc() - httpResponseSizeBytes.WithLabelValues(endpoint, status).Add(size) + + if size >= 0 { + httpResponseSizeBytes.WithLabelValues(endpoint, status).Add(float64(size)) + } else { + utils.TEL.Warn("Response size < 0, cannot push to Prometheus", "size", size) + } } } From e26ce4effaa55a1ffe9f4abdd23a341d5145c718 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Fri, 26 Sep 2025 14:56:37 +0200 Subject: [PATCH 28/52] feat: Add `reservationclient` to directly check active guest reservations --- src/client/reservationclient/client.go | 55 ++++++++++++++++++++++++++ src/client/reservationclient/model.go | 16 ++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/client/reservationclient/client.go create mode 100644 src/client/reservationclient/model.go diff --git a/src/client/reservationclient/client.go b/src/client/reservationclient/client.go new file mode 100644 index 0000000..dca2c8c --- /dev/null +++ b/src/client/reservationclient/client.go @@ -0,0 +1,55 @@ +package reservationclient + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" +) + +type ReservationClient interface { + // GetActiveGuestReservations finds all reservations made by `guest` that + // haven't completed yet. The user must be a guest. + GetActiveGuestReservations(jwt string) ([]ReservationDTO, error) +} + +type reservationClient struct { + baseURL string +} + +func NewReservationClient() ReservationClient { + return &reservationClient{ + baseURL: "http://reservation-service:8080/api", // TODO: This should not be hardcoded + } +} + +func (c *reservationClient) GetActiveGuestReservations(jwt string) ([]ReservationDTO, error) { + log.Printf("Get active guest reservations") + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/reservations/guest/active", c.baseURL), nil) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+jwt) + resp, err := http.DefaultClient.Do(req) + + if err != nil { + log.Printf("Error %v", err) + return nil, err + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Parsing response error: %v", err) + return nil, err + } + + var obj []ReservationDTO + if err := json.Unmarshal(bodyBytes, &obj); err != nil { + log.Printf("JSON Unmarshall error: %v", err) + return nil, err + } + + return obj, nil +} diff --git a/src/client/reservationclient/model.go b/src/client/reservationclient/model.go new file mode 100644 index 0000000..7aa16d0 --- /dev/null +++ b/src/client/reservationclient/model.go @@ -0,0 +1,16 @@ +package reservationclient + +import "time" + +type ReservationDTO struct { + ID uint `gorm:"primaryKey"` + RoomID uint `gorm:"not null"` + RoomAvailabilityID uint `gorm:"not null"` + RoomPriceID uint `gorm:"not null"` + GuestID uint `gorm:"not null"` + DateFrom time.Time `gorm:"not null"` + DateTo time.Time `gorm:"not null"` + GuestCount uint `gorm:"not null"` + Cancelled bool `gorm:"not null"` + Cost uint `gorm:"not null"` +} From 30d2224f628938cecf34c36ac0248b49d2ab9e5f Mon Sep 17 00:00:00 2001 From: Vasilije Date: Fri, 26 Sep 2025 15:02:01 +0200 Subject: [PATCH 29/52] feat: Logged-in user deletes themselves I also replaced 'Pending' with 'Active' in `GetPendingGuestReservations`, as it seemed to provide more consistency with `GetActiveHostReservations`. *Tests also need to be updated. --- src/api/handler.go | 14 ++++------ src/api/route.go | 2 +- src/client/roomclient/client.go | 47 +++++++++++++++++++------------ src/client/roomclient/model.go | 12 ++++++++ src/main.go | 4 ++- src/service/service.go | 49 +++++++++++++++------------------ 6 files changed, 73 insertions(+), 55 deletions(-) diff --git a/src/api/handler.go b/src/api/handler.go index efa87f8..9ad5d79 100644 --- a/src/api/handler.go +++ b/src/api/handler.go @@ -149,20 +149,18 @@ func (h *Handler) findById(c *gin.Context) { c.JSON(http.StatusOK, domain.NewUserDTO(user)) } -func (h *Handler) deleteById(c *gin.Context) { +func (h *Handler) delete(c *gin.Context) { utils.TEL.Push(c.Request.Context(), "update-user") defer utils.TEL.Pop() - id, err := strconv.Atoi(c.Param("id")) + jwt, err := middleware.GetJwt(c) if err != nil { - c.Error(err) - utils.TEL.Error("failed parsing ID", err) + c.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) + utils.TEL.Error("unauthenticatetd", err) return } - utils.TEL.SetAttrib(attribute.Int("id", id)) - - jwt, err := middleware.GetJwt(c) + jwtString, err := middleware.GetJwtString(c) if err != nil { c.Error(fmt.Errorf("%w: %v", domain.ErrUnauthenticated, err)) utils.TEL.Error("unauthenticatetd", err) @@ -171,7 +169,7 @@ func (h *Handler) deleteById(c *gin.Context) { utils.TEL.SetUser(jwt.ID) - err = h.service.Delete(utils.TEL.Ctx(), jwt.ID, uint(id)) + err = h.service.Delete(utils.TEL.Ctx(), jwt.ID, jwtString) if err != nil { c.Error(err) utils.TEL.Error("could not delete user", err) diff --git a/src/api/route.go b/src/api/route.go index bb0c6ed..5696a48 100644 --- a/src/api/route.go +++ b/src/api/route.go @@ -21,5 +21,5 @@ func (r *Route) Route(rg *gin.RouterGroup) { rg.PUT("/update", r.handler.update) rg.PUT("/password", r.handler.changePassword) rg.GET("/:id", r.handler.findById) - rg.DELETE("/:id", r.handler.deleteById) + rg.DELETE("/", r.handler.delete) } diff --git a/src/client/roomclient/client.go b/src/client/roomclient/client.go index 8e773f0..8f0affb 100644 --- a/src/client/roomclient/client.go +++ b/src/client/roomclient/client.go @@ -1,18 +1,18 @@ package roomclient import ( - "bookem-user-service/domain" utils "bookem-user-service/util" "context" + "encoding/json" + "fmt" + "io" + "net/http" ) type RoomClient interface { - // GetPendingGuestReservations finds all reservations made by `guest` that - // haven't completed yet. The user must be a guest. - GetPendingGuestReservations(ctx context.Context, guest *domain.User) ([]ReservationDTO, error) // GetActiveHostReservations finds all reservations made to rooms owned by // `host` that haven't completed yet. The user must be a host. - GetActiveHostReservations(ctx context.Context, host *domain.User) ([]ReservationDTO, error) + GetActiveHostReservations(ctx context.Context, jwt string) ([]ReservationDTO, error) } type roomClient struct { @@ -21,28 +21,39 @@ type roomClient struct { func NewRoomClient() RoomClient { return &roomClient{ - baseURL: "http://localhost:9999", // Placeholder URL for now + baseURL: "http://room-service:8080/api", // TODO: This should not be hardcoded } } -func (c *roomClient) GetPendingGuestReservations(ctx context.Context, guest *domain.User) ([]ReservationDTO, error) { - utils.TEL.Push(ctx, "get-reservation-requests-made-by-guest") +func (c *roomClient) GetActiveHostReservations(ctx context.Context, jwt string) ([]ReservationDTO, error) { + utils.TEL.Push(ctx, "get-active-reservations-for-host") defer utils.TEL.Pop() - if guest.Role != domain.Guest { - return []ReservationDTO{}, domain.ErrUnauthorized + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/reservations/host/active", c.baseURL), nil) + if err != nil { + return nil, err } + req.Header.Add("Authorization", "Bearer "+jwt) + resp, err := http.DefaultClient.Do(req) - return []ReservationDTO{}, nil -} + if err != nil { + utils.TEL.Error("error ", err) + return nil, err + } -func (c *roomClient) GetActiveHostReservations(ctx context.Context, host *domain.User) ([]ReservationDTO, error) { - utils.TEL.Push(ctx, "get-reservation-requests-for-host") - defer utils.TEL.Pop() + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + utils.TEL.Error("parsing response error", err) + + return nil, err + } + + var obj []ReservationDTO + if err := json.Unmarshal(bodyBytes, &obj); err != nil { + utils.TEL.Error("JSON unmarshall error", err) - if host.Role != domain.Host { - return []ReservationDTO{}, domain.ErrUnauthorized + return nil, err } - return []ReservationDTO{}, nil + return obj, nil } diff --git a/src/client/roomclient/model.go b/src/client/roomclient/model.go index 5ff0045..a16382e 100644 --- a/src/client/roomclient/model.go +++ b/src/client/roomclient/model.go @@ -1,7 +1,19 @@ package roomclient +import "time" + type RoomDTO struct { } type ReservationDTO struct { + ID uint `gorm:"primaryKey"` + RoomID uint `gorm:"not null"` + RoomAvailabilityID uint `gorm:"not null"` + RoomPriceID uint `gorm:"not null"` + GuestID uint `gorm:"not null"` + DateFrom time.Time `gorm:"not null"` + DateTo time.Time `gorm:"not null"` + GuestCount uint `gorm:"not null"` + Cancelled bool `gorm:"not null"` + Cost uint `gorm:"not null"` } diff --git a/src/main.go b/src/main.go index a51cc5c..100c1a1 100644 --- a/src/main.go +++ b/src/main.go @@ -16,6 +16,7 @@ import ( api "bookem-user-service/api" "bookem-user-service/api/middleware" + "bookem-user-service/client/reservationclient" "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" repo "bookem-user-service/repo" @@ -100,9 +101,10 @@ func main() { }) roomclient := roomclient.NewRoomClient() + reservationclient := reservationclient.NewReservationClient() repo := repo.NewRepository(dB) - service := service.NewService(repo, roomclient) + service := service.NewService(repo, roomclient, reservationclient) handler := api.NewHandler(service) route := *api.NewRoute(handler) diff --git a/src/service/service.go b/src/service/service.go index 6176d3b..df7f07f 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -1,6 +1,7 @@ package api import ( + "bookem-user-service/client/reservationclient" "bookem-user-service/client/roomclient" "bookem-user-service/domain" repo "bookem-user-service/repo" @@ -16,20 +17,21 @@ type Service interface { Update(ctx context.Context, callerID uint, dto domain.UserUpdateDTO) (*domain.User, error) ChangePassword(ctx context.Context, callerID uint, dto domain.PasswordUpdateDTO) (*domain.User, error) FindById(ctx context.Context, id uint) (*domain.User, error) - Delete(ctx context.Context, callerID uint, id uint) error + Delete(ctx context.Context, userId uint, jwt string) error /// canDeleteUser returns an error if the user cannot be deleted right now. /// The error specifies the reason why the operation cannot be done. - canDeleteUser(ctx context.Context, user *domain.User) error + canDeleteUser(ctx context.Context, user *domain.User, jwt string) error } type service struct { - repo repo.Repository - roomClient roomclient.RoomClient + repo repo.Repository + roomClient roomclient.RoomClient + reservationClient reservationclient.ReservationClient } -func NewService(r repo.Repository, roomClient roomclient.RoomClient) Service { - return &service{r, roomClient} +func NewService(r repo.Repository, roomClient roomclient.RoomClient, reservationClient reservationclient.ReservationClient) Service { + return &service{r, roomClient, reservationClient} } func (s *service) Register(ctx context.Context, dto *domain.UserCreateDTO) (*domain.User, error) { @@ -288,24 +290,17 @@ func (s *service) FindById(ctx context.Context, id uint) (*domain.User, error) { return user, nil } -func (s *service) Delete(ctx context.Context, callerID uint, id uint) error { - util.TEL.Info("user delete request", "caller_id", callerID, "user_id", id) - - // User can only delete himself. - - if id != callerID { - util.TEL.Error("user trying to delete someone else", nil) - return domain.ErrUnauthorized - } +func (s *service) Delete(ctx context.Context, userId uint, jwt string) error { + util.TEL.Info("user wants to delete himself", "user_id", userId) // Search for the user. util.TEL.Push(ctx, "find-user") defer util.TEL.Pop() - user, err := s.FindById(util.TEL.Ctx(), id) + user, err := s.FindById(util.TEL.Ctx(), userId) if err != nil { - util.TEL.Error("user not found", err, "id", id) + util.TEL.Error("user not found", err, "id", userId) return err } @@ -314,9 +309,9 @@ func (s *service) Delete(ctx context.Context, callerID uint, id uint) error { util.TEL.Push(ctx, "delete-safety-check") defer util.TEL.Pop() - err = s.canDeleteUser(util.TEL.Ctx(), user) + err = s.canDeleteUser(util.TEL.Ctx(), user, jwt) if err != nil { - util.TEL.Error("cannot delete user", err, "id", id) + util.TEL.Error("cannot delete user", err, "id", userId) return err } @@ -326,35 +321,35 @@ func (s *service) Delete(ctx context.Context, callerID uint, id uint) error { defer util.TEL.Pop() s.repo.Delete(user.ID) - util.TEL.Info("User deleted", "id", id) + util.TEL.Info("User deleted", "id", userId) return nil } -func (s *service) canDeleteUser(ctx context.Context, user *domain.User) error { +func (s *service) canDeleteUser(ctx context.Context, user *domain.User, jwt string) error { util.TEL.Info("check if user can be deleted", "id", user.ID) switch user.Role { case domain.Guest: - util.TEL.Debug("user is guest - must not have any reservations") - reservations, err := s.roomClient.GetPendingGuestReservations(ctx, user) + util.TEL.Debug("user is guest - must not have any active reservations") + reservations, err := s.reservationClient.GetActiveGuestReservations(ctx, jwt) if err != nil { util.TEL.Error("could not check", err) return err } if len(reservations) > 0 { - util.TEL.Error("guest has reservations, cannot delete user", nil) + util.TEL.Error("guest has active reservations, cannot delete user", nil) return domain.ErrGuestHasReservations } return nil case domain.Host: - util.TEL.Debug("user is host - rooms must not have any reservations") - reservations, err := s.roomClient.GetActiveHostReservations(ctx, user) + util.TEL.Debug("user is host - rooms must not have any active reservations") + reservations, err := s.roomClient.GetActiveHostReservations(ctx, jwt) if err != nil { return err } if len(reservations) > 0 { - util.TEL.Error("host's rooms have reservations, cannot delete user", nil) + util.TEL.Error("host's rooms have active reservations, cannot delete user", nil) return domain.ErrHostHasReservations } return nil From f99abc75f24e2112c28535a349b8f1156b745bd0 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Sun, 28 Sep 2025 22:51:07 +0200 Subject: [PATCH 30/52] test: Retune unit tests mostly due to `ReservationClient` --- src/test/unit/change_password_test.go | 12 ++--- src/test/unit/delete_test.go | 64 +++++++++++---------------- src/test/unit/find_test.go | 4 +- src/test/unit/login_test.go | 8 ++-- src/test/unit/register_test.go | 8 ++-- src/test/unit/update_test.go | 14 +++--- src/test/unit/utils.go | 24 ++++++---- 7 files changed, 64 insertions(+), 70 deletions(-) diff --git a/src/test/unit/change_password_test.go b/src/test/unit/change_password_test.go index 2506af8..1d321e0 100644 --- a/src/test/unit/change_password_test.go +++ b/src/test/unit/change_password_test.go @@ -12,7 +12,7 @@ import ( ) func TestChangePassword_Success(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -43,7 +43,7 @@ func TestChangePassword_Success(t *testing.T) { } func TestChangePassword_SomeoneElse(t *testing.T) { - svc, _, _ := createTestService() + svc, _, _, _ := createTestService() // Prepare @@ -59,7 +59,7 @@ func TestChangePassword_SomeoneElse(t *testing.T) { } func TestChangePassword_UserNotFound(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -78,7 +78,7 @@ func TestChangePassword_UserNotFound(t *testing.T) { } func TestChangePassword_PasswordsNotMatch(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -103,7 +103,7 @@ func TestChangePassword_PasswordsNotMatch(t *testing.T) { } func TestChangePassword_BadOldPassword(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -127,7 +127,7 @@ func TestChangePassword_BadOldPassword(t *testing.T) { } func TestChangePassword_PasswordIsTheSame(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare diff --git a/src/test/unit/delete_test.go b/src/test/unit/delete_test.go index 5b904ad..8afaf1c 100644 --- a/src/test/unit/delete_test.go +++ b/src/test/unit/delete_test.go @@ -1,111 +1,97 @@ package test import ( + "bookem-user-service/client/reservationclient" + "bookem-user-service/client/roomclient" + domain "bookem-user-service/domain" "context" "fmt" "testing" - "bookem-user-service/client/roomclient" - domain "bookem-user-service/domain" - assert "github.com/stretchr/testify/assert" ) func TestDelete_Success(t *testing.T) { - svc, mockRepo, mockRoomClient := createTestService() + svc, mockRepo, _, mockReservationClient := createTestService() id := uint(1) - callerID := uint(1) + jwt := "token" user := defaultUser user.ID = id user.Role = domain.Guest + mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() - mockRoomClient.On("GetPendingGuestReservations", context.Background(), user).Return([]roomclient.ReservationDTO{}, nil) + mockReservationClient.On("GetActiveGuestReservations", jwt).Return([]roomclient.ReservationDTO{}, nil) - err := svc.Delete(context.Background(), callerID, id) + err := svc.Delete(context.Background(), id, jwt) assert.NoError(t, err) } -func TestDelete_TriedToDeleteSomeoneElse(t *testing.T) { - svc, mockRepo, _ := createTestService() - - id := uint(1) - callerID := uint(2) - user := defaultUser - user.ID = id - mockRepo.On("FindById", id).Return(user, nil) - - err := svc.Delete(context.Background(), callerID, id) - - assert.Error(t, err) - assert.Equal(t, domain.ErrUnauthorized, err) -} - func TestDelete_UserNotFound(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() id := uint(1) - callerID := uint(1) + jwt := "token" mockRepo.On("FindById", id).Return(nil, fmt.Errorf("user not found")) - err := svc.Delete(context.Background(), callerID, id) + err := svc.Delete(context.Background(), id, jwt) assert.Error(t, err) assert.Equal(t, domain.ErrNotFound, err) } -func TestDelete_GuestHasPendingReservations(t *testing.T) { - svc, mockRepo, mockRoomClient := createTestService() +func TestDelete_GuestHasActiveReservations(t *testing.T) { + svc, mockRepo, _, mockReservationClient := createTestService() id := uint(1) - callerID := uint(1) + jwt := "token" user := defaultUser user.ID = id user.Role = domain.Guest mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() - reservation := roomclient.ReservationDTO{} - mockRoomClient.On("GetPendingGuestReservations", context.Background(), user).Return([]roomclient.ReservationDTO{reservation}, nil) + reservation := reservationclient.ReservationDTO{} + mockReservationClient.On("GetActiveGuestReservations", jwt).Return([]reservationclient.ReservationDTO{reservation}, nil) - err := svc.Delete(context.Background(), callerID, id) + err := svc.Delete(context.Background(), id, jwt) assert.Error(t, err) assert.Equal(t, domain.ErrGuestHasReservations, err) } -func TestDelete_HostHasPendingReservations(t *testing.T) { - svc, mockRepo, mockRoomClient := createTestService() +func TestDelete_HostHasActiveReservations(t *testing.T) { + svc, mockRepo, mockRoomClient, _ := createTestService() id := uint(1) - callerID := uint(1) + jwt := "token" user := defaultUser user.ID = id user.Role = domain.Host mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() reservation := roomclient.ReservationDTO{} - mockRoomClient.On("GetActiveHostReservations", context.Background(), user).Return([]roomclient.ReservationDTO{reservation}, nil) + mockRoomClient.On("GetActiveHostReservations", jwt).Return([]roomclient.ReservationDTO{reservation}, nil) - err := svc.Delete(context.Background(), callerID, id) + err := svc.Delete(context.Background(), id, jwt) assert.Error(t, err) assert.Equal(t, domain.ErrHostHasReservations, err) } func TestDelete_TriedDeletingAdmin(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() id := uint(1) - callerID := uint(1) + jwt := "token" user := defaultUser user.ID = id user.Role = domain.Admin mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() - err := svc.Delete(context.Background(), callerID, id) + err := svc.Delete(context.Background(), id, jwt) assert.Error(t, err) assert.Equal(t, domain.ErrCannotDeleteAdmin, err) diff --git a/src/test/unit/find_test.go b/src/test/unit/find_test.go index 9c388ea..e4e41a4 100644 --- a/src/test/unit/find_test.go +++ b/src/test/unit/find_test.go @@ -9,7 +9,7 @@ import ( ) func TestFindById_Success(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() id := uint(1) @@ -26,7 +26,7 @@ func TestFindById_Success(t *testing.T) { } func TestFindById_UserNotFound(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() id := uint(1) diff --git a/src/test/unit/login_test.go b/src/test/unit/login_test.go index 184c501..049c2a7 100644 --- a/src/test/unit/login_test.go +++ b/src/test/unit/login_test.go @@ -12,7 +12,7 @@ import ( ) func TestLogin_Success(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -53,7 +53,7 @@ func TestLogin_Success(t *testing.T) { } func TestLogin_UserNotFound(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := domain.LoginDTO{ UsernameOrEmail: "user123", @@ -72,7 +72,7 @@ func TestLogin_UserNotFound(t *testing.T) { } func TestLogin_WrongPassword(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := domain.LoginDTO{ UsernameOrEmail: "user123", @@ -104,7 +104,7 @@ func TestLogin_WrongPassword(t *testing.T) { } func TestLogin_JWTFailed(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare diff --git a/src/test/unit/register_test.go b/src/test/unit/register_test.go index 18dbc36..8ff1227 100644 --- a/src/test/unit/register_test.go +++ b/src/test/unit/register_test.go @@ -12,7 +12,7 @@ import ( ) func TestSuccess(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := *defaultUserDTO @@ -29,7 +29,7 @@ func TestSuccess(t *testing.T) { } func TestUsernameExists(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := *defaultUserDTO dto.Username = "username" @@ -46,7 +46,7 @@ func TestUsernameExists(t *testing.T) { } func TestEmailExists(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := *defaultUserDTO dto.Username = "user1" @@ -65,7 +65,7 @@ func TestEmailExists(t *testing.T) { } func TestCreateFails(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := *defaultUserDTO diff --git a/src/test/unit/update_test.go b/src/test/unit/update_test.go index 8df83cd..f43398a 100644 --- a/src/test/unit/update_test.go +++ b/src/test/unit/update_test.go @@ -11,7 +11,7 @@ import ( ) func TestUpdate_Success(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -41,7 +41,7 @@ func TestUpdate_Success(t *testing.T) { } func TestUpdate_SomeoneElse(t *testing.T) { - svc, _, _ := createTestService() + svc, _, _, _ := createTestService() // Prepare @@ -59,7 +59,7 @@ func TestUpdate_SomeoneElse(t *testing.T) { } func TestUpdate_UserNotFound(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -80,7 +80,7 @@ func TestUpdate_UserNotFound(t *testing.T) { } func TestUpdate_UsernameTaken(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -107,7 +107,7 @@ func TestUpdate_UsernameTaken(t *testing.T) { } func TestUpdate_EmailTaken(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -134,7 +134,7 @@ func TestUpdate_EmailTaken(t *testing.T) { } func TestUpdate_UsernameTakenEmailOk(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -164,7 +164,7 @@ func TestUpdate_UsernameTakenEmailOk(t *testing.T) { } func TestUpdate_UsernameOkEmailTaken(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare diff --git a/src/test/unit/utils.go b/src/test/unit/utils.go index 7995a39..cad0ff5 100644 --- a/src/test/unit/utils.go +++ b/src/test/unit/utils.go @@ -1,6 +1,7 @@ package test import ( + "bookem-user-service/client/reservationclient" "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" service "bookem-user-service/service" @@ -9,13 +10,14 @@ import ( mock "github.com/stretchr/testify/mock" ) -func createTestService() (service.Service, *MockRepo, *MockRoomClient) { +func createTestService() (service.Service, *MockRepo, *MockRoomClient, *MockReservationClient) { mockRepo := new(MockRepo) mockRoomClient := new(MockRoomClient) + mockReservationClient := new(MockReservationClient) - svc := service.NewService(mockRepo, mockRoomClient) + svc := service.NewService(mockRepo, mockRoomClient, mockReservationClient) - return svc, mockRepo, mockRoomClient + return svc, mockRepo, mockRoomClient, mockReservationClient } // ---------------------------------------------- Mock repo @@ -64,15 +66,21 @@ type MockRoomClient struct { mock.Mock } -func (m *MockRoomClient) GetPendingGuestReservations(ctx context.Context, guest *domain.User) ([]roomclient.ReservationDTO, error) { - args := m.Called(ctx, guest) +func (m *MockRoomClient) GetActiveHostReservations(ctx context.Context, jwt string) ([]roomclient.ReservationDTO, error) { + args := m.Called(ctx, jwt) reservations, _ := args.Get(0).([]roomclient.ReservationDTO) return reservations, args.Error(1) } -func (m *MockRoomClient) GetActiveHostReservations(ctx context.Context, host *domain.User) ([]roomclient.ReservationDTO, error) { - args := m.Called(ctx, host) - reservations, _ := args.Get(0).([]roomclient.ReservationDTO) +// ---------------------------------------------- Mock reservation client + +type MockReservationClient struct { + mock.Mock +} + +func (m *MockReservationClient) GetActiveGuestReservations(ctx context.Context, jwt string) ([]reservationclient.ReservationDTO, error) { + args := m.Called(ctx, jwt) + reservations, _ := args.Get(0).([]reservationclient.ReservationDTO) return reservations, args.Error(1) } From d814cf251753f0ff1f53e10693bace75b1c0eaf6 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Thu, 9 Oct 2025 14:32:56 +0200 Subject: [PATCH 31/52] test: Add and update integration tests for `TestIntegration_Delete` --- src/client/roomclient/model.go | 20 ++++++ .../integration/delete_integration_test.go | 68 ++++++++++--------- src/test/integration/util.go | 38 ++++++++++- 3 files changed, 93 insertions(+), 33 deletions(-) diff --git a/src/client/roomclient/model.go b/src/client/roomclient/model.go index a16382e..0112a5a 100644 --- a/src/client/roomclient/model.go +++ b/src/client/roomclient/model.go @@ -3,6 +3,15 @@ package roomclient import "time" type RoomDTO struct { + ID uint `json:"id"` + HostID uint `json:"hostID"` + Name string `json:"name"` + Description string `json:"description"` + Address string `json:"address"` + MinGuests uint `json:"minGuests"` + MaxGuests uint `json:"maxGuests"` + Photos []string `json:"photos"` + Commodities []string `json:"commodities"` } type ReservationDTO struct { @@ -17,3 +26,14 @@ type ReservationDTO struct { Cancelled bool `gorm:"not null"` Cost uint `gorm:"not null"` } + +type CreateRoomDTO struct { + HostID uint `json:"hostID"` + Name string `json:"name"` + Description string `json:"description"` + Address string `json:"address"` + MinGuests uint `json:"minGuests"` + MaxGuests uint `json:"maxGuests"` + PhotosPayload []string `json:"photosPayload"` + Commodities []string `json:"commodities"` +} diff --git a/src/test/integration/delete_integration_test.go b/src/test/integration/delete_integration_test.go index 38a47f1..559b7cd 100644 --- a/src/test/integration/delete_integration_test.go +++ b/src/test/integration/delete_integration_test.go @@ -10,66 +10,72 @@ import ( func TestIntegration_Delete(t *testing.T) { { - resp, _ := registerUser("user_deleted_guest_01", "1234", domain.Guest) - id := getUserFromRegister(resp).Id - jwt := loginUser2("user_deleted_guest_01", "1234") - resp, err := deleteUserById(jwt, id) + resp, _ := registerUser("guest1", "1234", domain.Guest) + jwt := loginUser2("guest1", "1234") + resp, err := deleteUser(jwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } { - resp, _ := registerUser("user_deleted_host_01", "1234", domain.Host) - id := getUserFromRegister(resp).Id - jwt := loginUser2("user_deleted_host_01", "1234") - resp, err := deleteUserById(jwt, id) + resp, _ := registerUser("host1", "1234", domain.Host) + jwt := loginUser2("host1", "1234") + resp, err := deleteUser(jwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } } -func TestIntegration_Delete_WrongUser(t *testing.T) { - registerUser("user_deleted_guest_03", "1234", domain.Guest) - id := uint(999999) - jwt := loginUser2("user_deleted_guest_03", "1234") - resp, err := deleteUserById(jwt, id) +func TestIntegration_Delete_GuestHasActiveReservations(t *testing.T) { + resp, _ := registerUser("guest2", "1234", domain.Guest) + jwt := loginUser2("guest2", "1234") + // TODO: Once we can actually create reservations + // we will add active reservations here. + // Until then, this test will pass. + + resp, err := deleteUser(jwt) require.Nil(t, err) - require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + require.Equal(t, http.StatusNoContent, resp.StatusCode) } -func TestIntegration_Delete_GuestHasPendingReservations(t *testing.T) { - resp, _ := registerUser("user_deleted_guest_02", "1234", domain.Guest) - id := getUserFromRegister(resp).Id - jwt := loginUser2("user_deleted_guest_02", "1234") - resp, err := deleteUserById(jwt, id) +func TestIntegration_Delete_GuestHasNoActiveReservations(t *testing.T) { + resp, _ := registerUser("guest3", "1234", domain.Guest) + jwt := loginUser2("guest3", "1234") - // TODO: Once we can actually create reservations, that needs to happen here so we can trigger a 400. - // Until then, this test will pass. + // TODO: Once we can actually create reservations + // we will add non-active reservations here. + // Until then, this test will pass. + resp, err := deleteUser(jwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } -func TestIntegration_Delete_HostHasPendingReservations(t *testing.T) { - resp, _ := registerUser("user_deleted_host_02", "1234", domain.Host) +func TestIntegration_Delete_HostHasActiveReservations(t *testing.T) { + resp, _ := registerUser("host2", "1234", domain.Host) id := getUserFromRegister(resp).Id - jwt := loginUser2("user_deleted_host_02", "1234") - resp, err := deleteUserById(jwt, id) + jwt := loginUser2("host2", "1234") - // TODO: Once we can actually create reservations, that needs to happen here so we can trigger a 400. - // Until then, this test will pass. + roomDTO := DefaultRoomCreateDTO + roomDTO.HostID = id + resp, _ = createRoom(jwt, roomDTO) + + // TODO: Once we can actually create reservations + // we will add active reservations here. + // Until then, this test will pass. + + resp, err := deleteUser(jwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } func TestIntegration_Delete_UserIsAdmin(t *testing.T) { - resp, _ := registerUser("user_deleted_admin_01", "1234", domain.Admin) - id := getUserFromRegister(resp).Id - jwt := loginUser2("user_deleted_admin_01", "1234") - resp, err := deleteUserById(jwt, id) + resp, _ := registerUser("admin1", "1234", domain.Admin) + jwt := loginUser2("admin1", "1234") + resp, err := deleteUser(jwt) require.Nil(t, err) require.Equal(t, http.StatusBadRequest, resp.StatusCode) diff --git a/src/test/integration/util.go b/src/test/integration/util.go index 703093c..d5fcab5 100644 --- a/src/test/integration/util.go +++ b/src/test/integration/util.go @@ -1,6 +1,7 @@ package test import ( + "bookem-user-service/client/roomclient" "bookem-user-service/domain" "bytes" "encoding/json" @@ -12,6 +13,8 @@ import ( ) const URL = "http://user-service:8080/api/" +const URL_room = "http://room-service:8080/api/" +const URL_reservation = "http://reservation-service:8080/api/" var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") @@ -147,11 +150,42 @@ func findUserById(id uint) (*http.Response, error) { return resp, err } -func deleteUserById(jwt string, id uint) (*http.Response, error) { - req, err := http.NewRequest(http.MethodDelete, URL+fmt.Sprintf("%d", id), nil) +func deleteUser(jwt string) (*http.Response, error) { + req, err := http.NewRequest(http.MethodDelete, URL, nil) if err != nil { return nil, err } req.Header.Add("Authorization", "Bearer "+jwt) return http.DefaultClient.Do(req) } + +func createRoom(jwt string, dto roomclient.CreateRoomDTO) (*http.Response, error) { + jsonBytes, err := json.Marshal(dto) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, URL_room+"new", bytes.NewBuffer(jsonBytes)) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+jwt) + return http.DefaultClient.Do(req) +} + +// ----------------------------------------------- Mock data + +const ( + SMALL_IMG = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AKp//2Q==" +) + +var DefaultRoomCreateDTO = roomclient.CreateRoomDTO{ + HostID: 1, + Name: "Room Name", + Description: "Room Desc", + Address: "Room Address", + MinGuests: 1, + MaxGuests: 5, + PhotosPayload: []string{SMALL_IMG}, + Commodities: []string{"WiFi"}, +} From 3152979088751b3540cb5d5a090d6107b81fcbed Mon Sep 17 00:00:00 2001 From: Vasilije Date: Sat, 11 Oct 2025 18:23:22 +0200 Subject: [PATCH 32/52] feat: Update `reservation-client` to use new logging --- src/client/reservationclient/client.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/client/reservationclient/client.go b/src/client/reservationclient/client.go index dca2c8c..65feff3 100644 --- a/src/client/reservationclient/client.go +++ b/src/client/reservationclient/client.go @@ -1,17 +1,18 @@ package reservationclient import ( + utils "bookem-user-service/util" + "context" "encoding/json" "fmt" "io" - "log" "net/http" ) type ReservationClient interface { // GetActiveGuestReservations finds all reservations made by `guest` that // haven't completed yet. The user must be a guest. - GetActiveGuestReservations(jwt string) ([]ReservationDTO, error) + GetActiveGuestReservations(ctx context.Context, jwt string) ([]ReservationDTO, error) } type reservationClient struct { @@ -24,8 +25,9 @@ func NewReservationClient() ReservationClient { } } -func (c *reservationClient) GetActiveGuestReservations(jwt string) ([]ReservationDTO, error) { - log.Printf("Get active guest reservations") +func (c *reservationClient) GetActiveGuestReservations(ctx context.Context, jwt string) ([]ReservationDTO, error) { + utils.TEL.Push(ctx, "get-active-reservations-for-guest") + defer utils.TEL.Pop() req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/reservations/guest/active", c.baseURL), nil) if err != nil { @@ -35,19 +37,19 @@ func (c *reservationClient) GetActiveGuestReservations(jwt string) ([]Reservatio resp, err := http.DefaultClient.Do(req) if err != nil { - log.Printf("Error %v", err) + utils.TEL.Error("error ", err) return nil, err } bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("Parsing response error: %v", err) + utils.TEL.Error("parsing response error", err) return nil, err } var obj []ReservationDTO if err := json.Unmarshal(bodyBytes, &obj); err != nil { - log.Printf("JSON Unmarshall error: %v", err) + utils.TEL.Error("JSON unmarshall error", err) return nil, err } From a67158e40601cb412d4fefce1bac51b46790e534 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Sat, 11 Oct 2025 20:37:42 +0200 Subject: [PATCH 33/52] feat: Add forgotten error messages in `reservation-client` --- src/client/reservationclient/client.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/reservationclient/client.go b/src/client/reservationclient/client.go index 65feff3..b6a10a3 100644 --- a/src/client/reservationclient/client.go +++ b/src/client/reservationclient/client.go @@ -31,13 +31,14 @@ func (c *reservationClient) GetActiveGuestReservations(ctx context.Context, jwt req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/reservations/guest/active", c.baseURL), nil) if err != nil { + utils.TEL.Error("preparing request error ", err) return nil, err } req.Header.Add("Authorization", "Bearer "+jwt) resp, err := http.DefaultClient.Do(req) if err != nil { - utils.TEL.Error("error ", err) + utils.TEL.Error("request error ", err) return nil, err } From 2723bdbaad91b185d5b230ed87623873b6442302 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Sat, 11 Oct 2025 20:38:45 +0200 Subject: [PATCH 34/52] test: Update unit tests with `context.Background()` parameter --- src/test/unit/delete_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/unit/delete_test.go b/src/test/unit/delete_test.go index 8afaf1c..d4853e9 100644 --- a/src/test/unit/delete_test.go +++ b/src/test/unit/delete_test.go @@ -22,7 +22,7 @@ func TestDelete_Success(t *testing.T) { mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() - mockReservationClient.On("GetActiveGuestReservations", jwt).Return([]roomclient.ReservationDTO{}, nil) + mockReservationClient.On("GetActiveGuestReservations", context.Background(), jwt).Return([]roomclient.ReservationDTO{}, nil) err := svc.Delete(context.Background(), id, jwt) @@ -53,7 +53,7 @@ func TestDelete_GuestHasActiveReservations(t *testing.T) { mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() reservation := reservationclient.ReservationDTO{} - mockReservationClient.On("GetActiveGuestReservations", jwt).Return([]reservationclient.ReservationDTO{reservation}, nil) + mockReservationClient.On("GetActiveGuestReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{reservation}, nil) err := svc.Delete(context.Background(), id, jwt) @@ -72,7 +72,7 @@ func TestDelete_HostHasActiveReservations(t *testing.T) { mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() reservation := roomclient.ReservationDTO{} - mockRoomClient.On("GetActiveHostReservations", jwt).Return([]roomclient.ReservationDTO{reservation}, nil) + mockRoomClient.On("GetActiveHostReservations", context.Background(), jwt).Return([]roomclient.ReservationDTO{reservation}, nil) err := svc.Delete(context.Background(), id, jwt) From ab14c0cffdf5587fd434975e1d9892ac4d193979 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Sat, 11 Oct 2025 22:18:08 +0200 Subject: [PATCH 35/52] fix: Untar private key for CI --- run-integration.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/run-integration.sh b/run-integration.sh index fcf09a6..d23a3a8 100644 --- a/run-integration.sh +++ b/run-integration.sh @@ -42,6 +42,9 @@ done # Clone repos if CI if [[ "$mode" == "ci" ]]; then + echo "Extracting the test private key for $USER_SERVICE_REPO" + tar -xvzf "$USER_SERVICE_PATH/keys/keys.tar.gz" -C "$USER_SERVICE_PATH/keys" + echo "Cloning $RESERVATION_SERVICE_REPO branch $RESERVATION_SERVICE_REPO into $RESERVATION_SERVICE_PATH" git clone --branch "$RESERVATION_SERVICE_BRANCH" "$RESERVATION_SERVICE_REPO" "$RESERVATION_SERVICE_PATH" From 8d02e2684936fbca67a11c56fbcd4873a48b306f Mon Sep 17 00:00:00 2001 From: Vasilije Date: Sat, 11 Oct 2025 22:20:43 +0200 Subject: [PATCH 36/52] feat: Use branches with latest changes I need to use it so the integration tests pass. --- ci.override.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci.override.env b/ci.override.env index 7261b24..e2a8249 100644 --- a/ci.override.env +++ b/ci.override.env @@ -1,7 +1,7 @@ RESERVATION_SERVICE_PATH=./services/reservation-service RESERVATION_SERVICE_REPO=https://github.com/book-em/reservation-service.git -RESERVATION_SERVICE_BRANCH=develop +RESERVATION_SERVICE_BRANCH=feature-delete-user-core ROOM_SERVICE_PATH=./services/room-service ROOM_SERVICE_REPO=https://github.com/book-em/room-service.git -ROOM_SERVICE_BRANCH=develop \ No newline at end of file +ROOM_SERVICE_BRANCH=feature-delete-user-core \ No newline at end of file From 1b669b1e98b3b5aabdd06450e51a9eb079169507 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Mon, 13 Oct 2025 13:14:02 +0200 Subject: [PATCH 37/52] feat: Move logic of `GetActiveHostReservations` to `reservation-service` --- src/client/reservationclient/client.go | 36 ++++++++++++++++ src/client/roomclient/client.go | 59 -------------------------- src/client/roomclient/model.go | 15 ------- src/main.go | 4 +- src/service/service.go | 8 ++-- src/test/unit/change_password_test.go | 12 +++--- src/test/unit/delete_test.go | 37 +++++++++++----- src/test/unit/find_test.go | 4 +- src/test/unit/login_test.go | 8 ++-- src/test/unit/register_test.go | 8 ++-- src/test/unit/update_test.go | 14 +++--- src/test/unit/utils.go | 24 ++++------- 12 files changed, 98 insertions(+), 131 deletions(-) delete mode 100644 src/client/roomclient/client.go diff --git a/src/client/reservationclient/client.go b/src/client/reservationclient/client.go index b6a10a3..b61f08b 100644 --- a/src/client/reservationclient/client.go +++ b/src/client/reservationclient/client.go @@ -13,6 +13,9 @@ type ReservationClient interface { // GetActiveGuestReservations finds all reservations made by `guest` that // haven't completed yet. The user must be a guest. GetActiveGuestReservations(ctx context.Context, jwt string) ([]ReservationDTO, error) + // GetActiveHostReservations finds all reservations made to rooms owned by + // `host` that haven't completed yet. The user must be a host. + GetActiveHostReservations(ctx context.Context, jwt string) ([]ReservationDTO, error) } type reservationClient struct { @@ -56,3 +59,36 @@ func (c *reservationClient) GetActiveGuestReservations(ctx context.Context, jwt return obj, nil } + +func (c *reservationClient) GetActiveHostReservations(ctx context.Context, jwt string) ([]ReservationDTO, error) { + utils.TEL.Push(ctx, "get-active-reservations-for-host") + defer utils.TEL.Pop() + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/reservations/host/active", c.baseURL), nil) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+jwt) + resp, err := http.DefaultClient.Do(req) + + if err != nil { + utils.TEL.Error("error ", err) + return nil, err + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + utils.TEL.Error("parsing response error", err) + + return nil, err + } + + var obj []ReservationDTO + if err := json.Unmarshal(bodyBytes, &obj); err != nil { + utils.TEL.Error("JSON unmarshall error", err) + + return nil, err + } + + return obj, nil +} diff --git a/src/client/roomclient/client.go b/src/client/roomclient/client.go deleted file mode 100644 index 8f0affb..0000000 --- a/src/client/roomclient/client.go +++ /dev/null @@ -1,59 +0,0 @@ -package roomclient - -import ( - utils "bookem-user-service/util" - "context" - "encoding/json" - "fmt" - "io" - "net/http" -) - -type RoomClient interface { - // GetActiveHostReservations finds all reservations made to rooms owned by - // `host` that haven't completed yet. The user must be a host. - GetActiveHostReservations(ctx context.Context, jwt string) ([]ReservationDTO, error) -} - -type roomClient struct { - baseURL string -} - -func NewRoomClient() RoomClient { - return &roomClient{ - baseURL: "http://room-service:8080/api", // TODO: This should not be hardcoded - } -} - -func (c *roomClient) GetActiveHostReservations(ctx context.Context, jwt string) ([]ReservationDTO, error) { - utils.TEL.Push(ctx, "get-active-reservations-for-host") - defer utils.TEL.Pop() - - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/reservations/host/active", c.baseURL), nil) - if err != nil { - return nil, err - } - req.Header.Add("Authorization", "Bearer "+jwt) - resp, err := http.DefaultClient.Do(req) - - if err != nil { - utils.TEL.Error("error ", err) - return nil, err - } - - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - utils.TEL.Error("parsing response error", err) - - return nil, err - } - - var obj []ReservationDTO - if err := json.Unmarshal(bodyBytes, &obj); err != nil { - utils.TEL.Error("JSON unmarshall error", err) - - return nil, err - } - - return obj, nil -} diff --git a/src/client/roomclient/model.go b/src/client/roomclient/model.go index 0112a5a..0d29eb4 100644 --- a/src/client/roomclient/model.go +++ b/src/client/roomclient/model.go @@ -1,7 +1,5 @@ package roomclient -import "time" - type RoomDTO struct { ID uint `json:"id"` HostID uint `json:"hostID"` @@ -14,19 +12,6 @@ type RoomDTO struct { Commodities []string `json:"commodities"` } -type ReservationDTO struct { - ID uint `gorm:"primaryKey"` - RoomID uint `gorm:"not null"` - RoomAvailabilityID uint `gorm:"not null"` - RoomPriceID uint `gorm:"not null"` - GuestID uint `gorm:"not null"` - DateFrom time.Time `gorm:"not null"` - DateTo time.Time `gorm:"not null"` - GuestCount uint `gorm:"not null"` - Cancelled bool `gorm:"not null"` - Cost uint `gorm:"not null"` -} - type CreateRoomDTO struct { HostID uint `json:"hostID"` Name string `json:"name"` diff --git a/src/main.go b/src/main.go index 100c1a1..05ac2c5 100644 --- a/src/main.go +++ b/src/main.go @@ -17,7 +17,6 @@ import ( api "bookem-user-service/api" "bookem-user-service/api/middleware" "bookem-user-service/client/reservationclient" - "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" repo "bookem-user-service/repo" service "bookem-user-service/service" @@ -100,11 +99,10 @@ func main() { ctx.JSON(http.StatusOK, nil) }) - roomclient := roomclient.NewRoomClient() reservationclient := reservationclient.NewReservationClient() repo := repo.NewRepository(dB) - service := service.NewService(repo, roomclient, reservationclient) + service := service.NewService(repo, reservationclient) handler := api.NewHandler(service) route := *api.NewRoute(handler) diff --git a/src/service/service.go b/src/service/service.go index df7f07f..137ef33 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -2,7 +2,6 @@ package api import ( "bookem-user-service/client/reservationclient" - "bookem-user-service/client/roomclient" "bookem-user-service/domain" repo "bookem-user-service/repo" util "bookem-user-service/util" @@ -26,12 +25,11 @@ type Service interface { type service struct { repo repo.Repository - roomClient roomclient.RoomClient reservationClient reservationclient.ReservationClient } -func NewService(r repo.Repository, roomClient roomclient.RoomClient, reservationClient reservationclient.ReservationClient) Service { - return &service{r, roomClient, reservationClient} +func NewService(r repo.Repository, reservationClient reservationclient.ReservationClient) Service { + return &service{r, reservationClient} } func (s *service) Register(ctx context.Context, dto *domain.UserCreateDTO) (*domain.User, error) { @@ -344,7 +342,7 @@ func (s *service) canDeleteUser(ctx context.Context, user *domain.User, jwt stri return nil case domain.Host: util.TEL.Debug("user is host - rooms must not have any active reservations") - reservations, err := s.roomClient.GetActiveHostReservations(ctx, jwt) + reservations, err := s.reservationClient.GetActiveHostReservations(ctx, jwt) if err != nil { return err } diff --git a/src/test/unit/change_password_test.go b/src/test/unit/change_password_test.go index 1d321e0..2506af8 100644 --- a/src/test/unit/change_password_test.go +++ b/src/test/unit/change_password_test.go @@ -12,7 +12,7 @@ import ( ) func TestChangePassword_Success(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare @@ -43,7 +43,7 @@ func TestChangePassword_Success(t *testing.T) { } func TestChangePassword_SomeoneElse(t *testing.T) { - svc, _, _, _ := createTestService() + svc, _, _ := createTestService() // Prepare @@ -59,7 +59,7 @@ func TestChangePassword_SomeoneElse(t *testing.T) { } func TestChangePassword_UserNotFound(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare @@ -78,7 +78,7 @@ func TestChangePassword_UserNotFound(t *testing.T) { } func TestChangePassword_PasswordsNotMatch(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare @@ -103,7 +103,7 @@ func TestChangePassword_PasswordsNotMatch(t *testing.T) { } func TestChangePassword_BadOldPassword(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare @@ -127,7 +127,7 @@ func TestChangePassword_BadOldPassword(t *testing.T) { } func TestChangePassword_PasswordIsTheSame(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare diff --git a/src/test/unit/delete_test.go b/src/test/unit/delete_test.go index d4853e9..3f0f82a 100644 --- a/src/test/unit/delete_test.go +++ b/src/test/unit/delete_test.go @@ -2,7 +2,6 @@ package test import ( "bookem-user-service/client/reservationclient" - "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" "context" "fmt" @@ -11,8 +10,8 @@ import ( assert "github.com/stretchr/testify/assert" ) -func TestDelete_Success(t *testing.T) { - svc, mockRepo, _, mockReservationClient := createTestService() +func TestDelete_GuestSuccess(t *testing.T) { + svc, mockRepo, mockReservationClient := createTestService() id := uint(1) jwt := "token" @@ -22,7 +21,25 @@ func TestDelete_Success(t *testing.T) { mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() - mockReservationClient.On("GetActiveGuestReservations", context.Background(), jwt).Return([]roomclient.ReservationDTO{}, nil) + mockReservationClient.On("GetActiveGuestReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{}, nil) + + err := svc.Delete(context.Background(), id, jwt) + + assert.NoError(t, err) +} + +func TestDelete_HostSuccess(t *testing.T) { + svc, mockRepo, mockReservationClient := createTestService() + + id := uint(1) + jwt := "token" + user := defaultUser + user.ID = id + user.Role = domain.Host + + mockRepo.On("FindById", id).Return(user, nil) + mockRepo.On("Delete", id).Return() + mockReservationClient.On("GetActiveHostReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{}, nil) err := svc.Delete(context.Background(), id, jwt) @@ -30,7 +47,7 @@ func TestDelete_Success(t *testing.T) { } func TestDelete_UserNotFound(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() id := uint(1) jwt := "token" @@ -43,7 +60,7 @@ func TestDelete_UserNotFound(t *testing.T) { } func TestDelete_GuestHasActiveReservations(t *testing.T) { - svc, mockRepo, _, mockReservationClient := createTestService() + svc, mockRepo, mockReservationClient := createTestService() id := uint(1) jwt := "token" @@ -62,7 +79,7 @@ func TestDelete_GuestHasActiveReservations(t *testing.T) { } func TestDelete_HostHasActiveReservations(t *testing.T) { - svc, mockRepo, mockRoomClient, _ := createTestService() + svc, mockRepo, mockReservationClient := createTestService() id := uint(1) jwt := "token" @@ -71,8 +88,8 @@ func TestDelete_HostHasActiveReservations(t *testing.T) { user.Role = domain.Host mockRepo.On("FindById", id).Return(user, nil) mockRepo.On("Delete", id).Return() - reservation := roomclient.ReservationDTO{} - mockRoomClient.On("GetActiveHostReservations", context.Background(), jwt).Return([]roomclient.ReservationDTO{reservation}, nil) + reservation := reservationclient.ReservationDTO{} + mockReservationClient.On("GetActiveHostReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{reservation}, nil) err := svc.Delete(context.Background(), id, jwt) @@ -81,7 +98,7 @@ func TestDelete_HostHasActiveReservations(t *testing.T) { } func TestDelete_TriedDeletingAdmin(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() id := uint(1) jwt := "token" diff --git a/src/test/unit/find_test.go b/src/test/unit/find_test.go index e4e41a4..9c388ea 100644 --- a/src/test/unit/find_test.go +++ b/src/test/unit/find_test.go @@ -9,7 +9,7 @@ import ( ) func TestFindById_Success(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() id := uint(1) @@ -26,7 +26,7 @@ func TestFindById_Success(t *testing.T) { } func TestFindById_UserNotFound(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() id := uint(1) diff --git a/src/test/unit/login_test.go b/src/test/unit/login_test.go index 049c2a7..184c501 100644 --- a/src/test/unit/login_test.go +++ b/src/test/unit/login_test.go @@ -12,7 +12,7 @@ import ( ) func TestLogin_Success(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare @@ -53,7 +53,7 @@ func TestLogin_Success(t *testing.T) { } func TestLogin_UserNotFound(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() dto := domain.LoginDTO{ UsernameOrEmail: "user123", @@ -72,7 +72,7 @@ func TestLogin_UserNotFound(t *testing.T) { } func TestLogin_WrongPassword(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() dto := domain.LoginDTO{ UsernameOrEmail: "user123", @@ -104,7 +104,7 @@ func TestLogin_WrongPassword(t *testing.T) { } func TestLogin_JWTFailed(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare diff --git a/src/test/unit/register_test.go b/src/test/unit/register_test.go index 8ff1227..18dbc36 100644 --- a/src/test/unit/register_test.go +++ b/src/test/unit/register_test.go @@ -12,7 +12,7 @@ import ( ) func TestSuccess(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() dto := *defaultUserDTO @@ -29,7 +29,7 @@ func TestSuccess(t *testing.T) { } func TestUsernameExists(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() dto := *defaultUserDTO dto.Username = "username" @@ -46,7 +46,7 @@ func TestUsernameExists(t *testing.T) { } func TestEmailExists(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() dto := *defaultUserDTO dto.Username = "user1" @@ -65,7 +65,7 @@ func TestEmailExists(t *testing.T) { } func TestCreateFails(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() dto := *defaultUserDTO diff --git a/src/test/unit/update_test.go b/src/test/unit/update_test.go index f43398a..8df83cd 100644 --- a/src/test/unit/update_test.go +++ b/src/test/unit/update_test.go @@ -11,7 +11,7 @@ import ( ) func TestUpdate_Success(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare @@ -41,7 +41,7 @@ func TestUpdate_Success(t *testing.T) { } func TestUpdate_SomeoneElse(t *testing.T) { - svc, _, _, _ := createTestService() + svc, _, _ := createTestService() // Prepare @@ -59,7 +59,7 @@ func TestUpdate_SomeoneElse(t *testing.T) { } func TestUpdate_UserNotFound(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare @@ -80,7 +80,7 @@ func TestUpdate_UserNotFound(t *testing.T) { } func TestUpdate_UsernameTaken(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare @@ -107,7 +107,7 @@ func TestUpdate_UsernameTaken(t *testing.T) { } func TestUpdate_EmailTaken(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare @@ -134,7 +134,7 @@ func TestUpdate_EmailTaken(t *testing.T) { } func TestUpdate_UsernameTakenEmailOk(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare @@ -164,7 +164,7 @@ func TestUpdate_UsernameTakenEmailOk(t *testing.T) { } func TestUpdate_UsernameOkEmailTaken(t *testing.T) { - svc, mockRepo, _, _ := createTestService() + svc, mockRepo, _ := createTestService() // Prepare diff --git a/src/test/unit/utils.go b/src/test/unit/utils.go index cad0ff5..21606da 100644 --- a/src/test/unit/utils.go +++ b/src/test/unit/utils.go @@ -2,7 +2,6 @@ package test import ( "bookem-user-service/client/reservationclient" - "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" service "bookem-user-service/service" "context" @@ -10,14 +9,13 @@ import ( mock "github.com/stretchr/testify/mock" ) -func createTestService() (service.Service, *MockRepo, *MockRoomClient, *MockReservationClient) { +func createTestService() (service.Service, *MockRepo, *MockReservationClient) { mockRepo := new(MockRepo) - mockRoomClient := new(MockRoomClient) mockReservationClient := new(MockReservationClient) - svc := service.NewService(mockRepo, mockRoomClient, mockReservationClient) + svc := service.NewService(mockRepo, mockReservationClient) - return svc, mockRepo, mockRoomClient, mockReservationClient + return svc, mockRepo, mockReservationClient } // ---------------------------------------------- Mock repo @@ -60,25 +58,19 @@ func (m *MockRepo) Delete(id uint) { m.Called(id) } -// ---------------------------------------------- Mock room client +// ---------------------------------------------- Mock reservation client -type MockRoomClient struct { +type MockReservationClient struct { mock.Mock } -func (m *MockRoomClient) GetActiveHostReservations(ctx context.Context, jwt string) ([]roomclient.ReservationDTO, error) { +func (m *MockReservationClient) GetActiveGuestReservations(ctx context.Context, jwt string) ([]reservationclient.ReservationDTO, error) { args := m.Called(ctx, jwt) - reservations, _ := args.Get(0).([]roomclient.ReservationDTO) + reservations, _ := args.Get(0).([]reservationclient.ReservationDTO) return reservations, args.Error(1) } -// ---------------------------------------------- Mock reservation client - -type MockReservationClient struct { - mock.Mock -} - -func (m *MockReservationClient) GetActiveGuestReservations(ctx context.Context, jwt string) ([]reservationclient.ReservationDTO, error) { +func (m *MockReservationClient) GetActiveHostReservations(ctx context.Context, jwt string) ([]reservationclient.ReservationDTO, error) { args := m.Called(ctx, jwt) reservations, _ := args.Get(0).([]reservationclient.ReservationDTO) return reservations, args.Error(1) From dc1ba25ea740b0989d9244fdabf85420be8e89f8 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Mon, 13 Oct 2025 13:50:06 +0200 Subject: [PATCH 38/52] feat: Add empty `client.go` file --- src/client/roomclient/client.go | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/client/roomclient/client.go diff --git a/src/client/roomclient/client.go b/src/client/roomclient/client.go new file mode 100644 index 0000000..4a838d1 --- /dev/null +++ b/src/client/roomclient/client.go @@ -0,0 +1 @@ +package roomclient From 0ac5b3dc2056f285da337567ff50376031812d6d Mon Sep 17 00:00:00 2001 From: Vasilije Date: Mon, 13 Oct 2025 13:52:15 +0200 Subject: [PATCH 39/52] feat: Use develop as latest branch from other microservices This is possible since `room-service` has been discarded (no effective changes), and `reservation-service` has already been merged. --- ci.override.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci.override.env b/ci.override.env index e2a8249..7261b24 100644 --- a/ci.override.env +++ b/ci.override.env @@ -1,7 +1,7 @@ RESERVATION_SERVICE_PATH=./services/reservation-service RESERVATION_SERVICE_REPO=https://github.com/book-em/reservation-service.git -RESERVATION_SERVICE_BRANCH=feature-delete-user-core +RESERVATION_SERVICE_BRANCH=develop ROOM_SERVICE_PATH=./services/room-service ROOM_SERVICE_REPO=https://github.com/book-em/room-service.git -ROOM_SERVICE_BRANCH=feature-delete-user-core \ No newline at end of file +ROOM_SERVICE_BRANCH=develop \ No newline at end of file From c1015772817cf944179b99e8db8ca6e5a876092e Mon Sep 17 00:00:00 2001 From: Vasilije Date: Mon, 13 Oct 2025 14:15:52 +0200 Subject: [PATCH 40/52] feat: Inject `RoomClient` in service and tests --- src/client/roomclient/client.go | 13 +++++++++++++ src/main.go | 4 +++- src/service/service.go | 6 ++++-- src/test/unit/change_password_test.go | 12 ++++++------ src/test/unit/delete_test.go | 12 ++++++------ src/test/unit/find_test.go | 4 ++-- src/test/unit/login_test.go | 8 ++++---- src/test/unit/register_test.go | 8 ++++---- src/test/unit/update_test.go | 14 +++++++------- src/test/unit/utils.go | 13 ++++++++++--- 10 files changed, 59 insertions(+), 35 deletions(-) diff --git a/src/client/roomclient/client.go b/src/client/roomclient/client.go index 4a838d1..4e3d2fa 100644 --- a/src/client/roomclient/client.go +++ b/src/client/roomclient/client.go @@ -1 +1,14 @@ package roomclient + +type RoomClient interface { +} + +type roomClient struct { + baseURL string +} + +func NewRoomClient() RoomClient { + return &roomClient{ + baseURL: "http://room-service:8080/api", // TODO: This should not be hardcoded + } +} diff --git a/src/main.go b/src/main.go index 05ac2c5..100c1a1 100644 --- a/src/main.go +++ b/src/main.go @@ -17,6 +17,7 @@ import ( api "bookem-user-service/api" "bookem-user-service/api/middleware" "bookem-user-service/client/reservationclient" + "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" repo "bookem-user-service/repo" service "bookem-user-service/service" @@ -99,10 +100,11 @@ func main() { ctx.JSON(http.StatusOK, nil) }) + roomclient := roomclient.NewRoomClient() reservationclient := reservationclient.NewReservationClient() repo := repo.NewRepository(dB) - service := service.NewService(repo, reservationclient) + service := service.NewService(repo, roomclient, reservationclient) handler := api.NewHandler(service) route := *api.NewRoute(handler) diff --git a/src/service/service.go b/src/service/service.go index 137ef33..7763b36 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -2,6 +2,7 @@ package api import ( "bookem-user-service/client/reservationclient" + "bookem-user-service/client/roomclient" "bookem-user-service/domain" repo "bookem-user-service/repo" util "bookem-user-service/util" @@ -25,11 +26,12 @@ type Service interface { type service struct { repo repo.Repository + roomClient roomclient.RoomClient reservationClient reservationclient.ReservationClient } -func NewService(r repo.Repository, reservationClient reservationclient.ReservationClient) Service { - return &service{r, reservationClient} +func NewService(r repo.Repository, roomClient roomclient.RoomClient, reservationClient reservationclient.ReservationClient) Service { + return &service{r, roomClient, reservationClient} } func (s *service) Register(ctx context.Context, dto *domain.UserCreateDTO) (*domain.User, error) { diff --git a/src/test/unit/change_password_test.go b/src/test/unit/change_password_test.go index 2506af8..1d321e0 100644 --- a/src/test/unit/change_password_test.go +++ b/src/test/unit/change_password_test.go @@ -12,7 +12,7 @@ import ( ) func TestChangePassword_Success(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -43,7 +43,7 @@ func TestChangePassword_Success(t *testing.T) { } func TestChangePassword_SomeoneElse(t *testing.T) { - svc, _, _ := createTestService() + svc, _, _, _ := createTestService() // Prepare @@ -59,7 +59,7 @@ func TestChangePassword_SomeoneElse(t *testing.T) { } func TestChangePassword_UserNotFound(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -78,7 +78,7 @@ func TestChangePassword_UserNotFound(t *testing.T) { } func TestChangePassword_PasswordsNotMatch(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -103,7 +103,7 @@ func TestChangePassword_PasswordsNotMatch(t *testing.T) { } func TestChangePassword_BadOldPassword(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -127,7 +127,7 @@ func TestChangePassword_BadOldPassword(t *testing.T) { } func TestChangePassword_PasswordIsTheSame(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare diff --git a/src/test/unit/delete_test.go b/src/test/unit/delete_test.go index 3f0f82a..564df42 100644 --- a/src/test/unit/delete_test.go +++ b/src/test/unit/delete_test.go @@ -11,7 +11,7 @@ import ( ) func TestDelete_GuestSuccess(t *testing.T) { - svc, mockRepo, mockReservationClient := createTestService() + svc, mockRepo, _, mockReservationClient := createTestService() id := uint(1) jwt := "token" @@ -29,7 +29,7 @@ func TestDelete_GuestSuccess(t *testing.T) { } func TestDelete_HostSuccess(t *testing.T) { - svc, mockRepo, mockReservationClient := createTestService() + svc, mockRepo, _, mockReservationClient := createTestService() id := uint(1) jwt := "token" @@ -47,7 +47,7 @@ func TestDelete_HostSuccess(t *testing.T) { } func TestDelete_UserNotFound(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() id := uint(1) jwt := "token" @@ -60,7 +60,7 @@ func TestDelete_UserNotFound(t *testing.T) { } func TestDelete_GuestHasActiveReservations(t *testing.T) { - svc, mockRepo, mockReservationClient := createTestService() + svc, mockRepo, _, mockReservationClient := createTestService() id := uint(1) jwt := "token" @@ -79,7 +79,7 @@ func TestDelete_GuestHasActiveReservations(t *testing.T) { } func TestDelete_HostHasActiveReservations(t *testing.T) { - svc, mockRepo, mockReservationClient := createTestService() + svc, mockRepo, _, mockReservationClient := createTestService() id := uint(1) jwt := "token" @@ -98,7 +98,7 @@ func TestDelete_HostHasActiveReservations(t *testing.T) { } func TestDelete_TriedDeletingAdmin(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() id := uint(1) jwt := "token" diff --git a/src/test/unit/find_test.go b/src/test/unit/find_test.go index 9c388ea..e4e41a4 100644 --- a/src/test/unit/find_test.go +++ b/src/test/unit/find_test.go @@ -9,7 +9,7 @@ import ( ) func TestFindById_Success(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() id := uint(1) @@ -26,7 +26,7 @@ func TestFindById_Success(t *testing.T) { } func TestFindById_UserNotFound(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() id := uint(1) diff --git a/src/test/unit/login_test.go b/src/test/unit/login_test.go index 184c501..049c2a7 100644 --- a/src/test/unit/login_test.go +++ b/src/test/unit/login_test.go @@ -12,7 +12,7 @@ import ( ) func TestLogin_Success(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -53,7 +53,7 @@ func TestLogin_Success(t *testing.T) { } func TestLogin_UserNotFound(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := domain.LoginDTO{ UsernameOrEmail: "user123", @@ -72,7 +72,7 @@ func TestLogin_UserNotFound(t *testing.T) { } func TestLogin_WrongPassword(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := domain.LoginDTO{ UsernameOrEmail: "user123", @@ -104,7 +104,7 @@ func TestLogin_WrongPassword(t *testing.T) { } func TestLogin_JWTFailed(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare diff --git a/src/test/unit/register_test.go b/src/test/unit/register_test.go index 18dbc36..8ff1227 100644 --- a/src/test/unit/register_test.go +++ b/src/test/unit/register_test.go @@ -12,7 +12,7 @@ import ( ) func TestSuccess(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := *defaultUserDTO @@ -29,7 +29,7 @@ func TestSuccess(t *testing.T) { } func TestUsernameExists(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := *defaultUserDTO dto.Username = "username" @@ -46,7 +46,7 @@ func TestUsernameExists(t *testing.T) { } func TestEmailExists(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := *defaultUserDTO dto.Username = "user1" @@ -65,7 +65,7 @@ func TestEmailExists(t *testing.T) { } func TestCreateFails(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() dto := *defaultUserDTO diff --git a/src/test/unit/update_test.go b/src/test/unit/update_test.go index 8df83cd..f43398a 100644 --- a/src/test/unit/update_test.go +++ b/src/test/unit/update_test.go @@ -11,7 +11,7 @@ import ( ) func TestUpdate_Success(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -41,7 +41,7 @@ func TestUpdate_Success(t *testing.T) { } func TestUpdate_SomeoneElse(t *testing.T) { - svc, _, _ := createTestService() + svc, _, _, _ := createTestService() // Prepare @@ -59,7 +59,7 @@ func TestUpdate_SomeoneElse(t *testing.T) { } func TestUpdate_UserNotFound(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -80,7 +80,7 @@ func TestUpdate_UserNotFound(t *testing.T) { } func TestUpdate_UsernameTaken(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -107,7 +107,7 @@ func TestUpdate_UsernameTaken(t *testing.T) { } func TestUpdate_EmailTaken(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -134,7 +134,7 @@ func TestUpdate_EmailTaken(t *testing.T) { } func TestUpdate_UsernameTakenEmailOk(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare @@ -164,7 +164,7 @@ func TestUpdate_UsernameTakenEmailOk(t *testing.T) { } func TestUpdate_UsernameOkEmailTaken(t *testing.T) { - svc, mockRepo, _ := createTestService() + svc, mockRepo, _, _ := createTestService() // Prepare diff --git a/src/test/unit/utils.go b/src/test/unit/utils.go index 21606da..6d74a95 100644 --- a/src/test/unit/utils.go +++ b/src/test/unit/utils.go @@ -9,13 +9,14 @@ import ( mock "github.com/stretchr/testify/mock" ) -func createTestService() (service.Service, *MockRepo, *MockReservationClient) { +func createTestService() (service.Service, *MockRepo, *MockRoomClient, *MockReservationClient) { mockRepo := new(MockRepo) + mockRoomClient := new(MockRoomClient) mockReservationClient := new(MockReservationClient) - svc := service.NewService(mockRepo, mockReservationClient) + svc := service.NewService(mockRepo, mockRoomClient, mockReservationClient) - return svc, mockRepo, mockReservationClient + return svc, mockRepo, mockRoomClient, mockReservationClient } // ---------------------------------------------- Mock repo @@ -58,6 +59,12 @@ func (m *MockRepo) Delete(id uint) { m.Called(id) } +// ---------------------------------------------- Mock room client + +type MockRoomClient struct { + mock.Mock +} + // ---------------------------------------------- Mock reservation client type MockReservationClient struct { From d0ac2b6bb571ec23aa2fccb85683dceb44892704 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:19:14 +0200 Subject: [PATCH 41/52] feat: Use Kaniko for building and publishing Docker images to the registry --- .github/workflows/deploy.yml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6ee9dc7..579e4ad 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,21 +32,17 @@ jobs: - name: Set VERSION environment variable run: echo "VERSION=${{ steps.version.outputs.version }}" >> $GITHUB_ENV - - - name: Log in to DockerHub - uses: docker/login-action@v2 + + - name: Build and push Docker image using Kaniko + uses: aevea/action-kaniko@master with: + image: ${{ secrets.USER_SERVICE_NAME }} + tag: ${{ env.VERSION }} + path: ./src/ + build_file: Dockerfile username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build Docker image - run: | - docker build -t ${{ secrets.USER_SERVICE_NAME }}:${{ env.VERSION }} ./src - - - name: Push Docker image - run: | - docker push ${{ secrets.USER_SERVICE_NAME }}:${{ env.VERSION }} - - name: Push tag run: | git config user.name "github-actions" From 23a7b5a1b7225b83395b1ddbd0c044233df2b3d9 Mon Sep 17 00:00:00 2001 From: Nada Zaric Date: Thu, 23 Oct 2025 17:24:44 +0200 Subject: [PATCH 42/52] feat: Add AutoApprove field into room dtos --- src/client/roomclient/model.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/roomclient/model.go b/src/client/roomclient/model.go index 0d29eb4..d2a4505 100644 --- a/src/client/roomclient/model.go +++ b/src/client/roomclient/model.go @@ -10,6 +10,7 @@ type RoomDTO struct { MaxGuests uint `json:"maxGuests"` Photos []string `json:"photos"` Commodities []string `json:"commodities"` + AutoApprove bool `json:"autoApprove"` } type CreateRoomDTO struct { @@ -21,4 +22,5 @@ type CreateRoomDTO struct { MaxGuests uint `json:"maxGuests"` PhotosPayload []string `json:"photosPayload"` Commodities []string `json:"commodities"` + AutoApprove bool `json:"autoApprove"` } From 231862c51b1aa963818974f2ff435d171725c206 Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 26 Oct 2025 21:44:17 +0100 Subject: [PATCH 43/52] feat: Add SonarQube analysis --- .github/workflows/sonarqube.yml | 37 +++++++++++++++++++++++++++++++++ sonar-project.properties | 14 +++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 .github/workflows/sonarqube.yml create mode 100644 sonar-project.properties diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml new file mode 100644 index 0000000..ff71bf6 --- /dev/null +++ b/.github/workflows/sonarqube.yml @@ -0,0 +1,37 @@ +name: Sonarqube Analysis + +on: + push: + branches: + - master + - develop + - feature-sonarqube # Temporary + +jobs: + sonarqube: + name: SonarQube + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.24 + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v6 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..09f44f7 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,14 @@ +sonar.projectKey=book-em_user-service +sonar.organization=book-em + + +# This is the name and version displayed in the SonarCloud UI. +sonar.projectName=user-service +sonar.projectVersion=1.0 + + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 \ No newline at end of file From cca72f991ca2760d46e910596dc4e6a3549aecbb Mon Sep 17 00:00:00 2001 From: magley <60552482+magley@users.noreply.github.com> Date: Sun, 26 Oct 2025 22:12:43 +0100 Subject: [PATCH 44/52] feat: Prepare for PR analysis --- .github/workflows/sonarqube.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index ff71bf6..eb6016e 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -2,10 +2,12 @@ name: Sonarqube Analysis on: push: + branches: + - feature-sonarqube # Temporary + pull_request: branches: - master - develop - - feature-sonarqube # Temporary jobs: sonarqube: From 0a39c77f2644aee8faff21e6a84e8a74326a31d3 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:27:24 +0100 Subject: [PATCH 45/52] feat: Update user (host rooms) deletion and `repo.Delete` to perform logical deletion --- src/client/roomclient/client.go | 44 ++++++++++++++++++++++++++++++++- src/client/roomclient/model.go | 1 + src/domain/user.go | 1 + src/repo/repo.go | 7 +++--- src/service/service.go | 12 ++++++++- 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/client/roomclient/client.go b/src/client/roomclient/client.go index 4e3d2fa..cc53837 100644 --- a/src/client/roomclient/client.go +++ b/src/client/roomclient/client.go @@ -1,6 +1,16 @@ package roomclient +import ( + utils "bookem-user-service/util" + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + type RoomClient interface { + DeleteHostRooms(ctx context.Context, jwt string) ([]RoomDTO, error) } type roomClient struct { @@ -9,6 +19,38 @@ type roomClient struct { func NewRoomClient() RoomClient { return &roomClient{ - baseURL: "http://room-service:8080/api", // TODO: This should not be hardcoded + baseURL: "http://room-service:8080/api/", // TODO: This should not be hardcoded + } +} + +func (c *roomClient) DeleteHostRooms(ctx context.Context, jwt string) ([]RoomDTO, error) { + utils.TEL.Push(ctx, "delete-host-rooms") + defer utils.TEL.Pop() + + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%shost/", c.baseURL), nil) + if err != nil { + utils.TEL.Error("preparing request error ", err) + return nil, err + } + req.Header.Add("Authorization", "Bearer "+jwt) + resp, err := http.DefaultClient.Do(req) + + if err != nil { + utils.TEL.Error("request error ", err) + return nil, err + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + utils.TEL.Error("parsing response error", err) + return nil, err } + + var obj []RoomDTO + if err := json.Unmarshal(bodyBytes, &obj); err != nil { + utils.TEL.Error("JSON unmarshall error", err) + return nil, err + } + + return obj, nil } diff --git a/src/client/roomclient/model.go b/src/client/roomclient/model.go index d2a4505..a66e2be 100644 --- a/src/client/roomclient/model.go +++ b/src/client/roomclient/model.go @@ -11,6 +11,7 @@ type RoomDTO struct { Photos []string `json:"photos"` Commodities []string `json:"commodities"` AutoApprove bool `json:"autoApprove"` + Deleted bool `json:"deleted"` } type CreateRoomDTO struct { diff --git a/src/domain/user.go b/src/domain/user.go index 067c867..7b628ac 100644 --- a/src/domain/user.go +++ b/src/domain/user.go @@ -17,4 +17,5 @@ type User struct { Surname string `json:"surname" gorm:"type:varchar(60);not null;"` Role UserRole `json:"role" gorm:"type:varchar(5);not null;check:role IN ('guest','host','admin')"` Address string `json:"address" gorm:"type:varchar(150);not null"` + Deleted bool `json:"deleted" gorm:"type:boolean;not null;default:false"` } diff --git a/src/repo/repo.go b/src/repo/repo.go index 545a524..8033065 100644 --- a/src/repo/repo.go +++ b/src/repo/repo.go @@ -12,7 +12,7 @@ type Repository interface { FindByUsernameOrEmailNotId(username, email string, id uint) (*domain.User, error) FindById(id uint) (*domain.User, error) Update(user *domain.User) error - Delete(id uint) + Delete(user *domain.User) error } type repository struct { @@ -58,6 +58,7 @@ func (r *repository) Update(user *domain.User) error { return r.db.Save(user).Error } -func (r *repository) Delete(id uint) { - r.db.Delete(&domain.User{}, id) +func (r *repository) Delete(user *domain.User) error { + user.Deleted = true + return r.Update(user) } diff --git a/src/service/service.go b/src/service/service.go index 7763b36..d30bde3 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -315,12 +315,22 @@ func (s *service) Delete(ctx context.Context, userId uint, jwt string) error { return err } + // Delete rooms + + if user.Role == domain.Host { + _, err := s.roomClient.DeleteHostRooms(ctx, jwt) + if err != nil { + util.TEL.Error("could not delete host rooms", err) + return err + } + } + // Delete user util.TEL.Push(ctx, "delete-user-in-db") defer util.TEL.Pop() - s.repo.Delete(user.ID) + s.repo.Delete(user) util.TEL.Info("User deleted", "id", userId) return nil From bb4b1cd86a38b8871e12d9bc4126d58fcd8a6c27 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:29:45 +0100 Subject: [PATCH 46/52] feat: Verify if user account is deleted on login --- src/service/service.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/service/service.go b/src/service/service.go index d30bde3..0bd5821 100644 --- a/src/service/service.go +++ b/src/service/service.go @@ -96,6 +96,13 @@ func (s *service) Login(ctx context.Context, dto domain.LoginDTO) (string, error return "", domain.ErrLoginFailed } + util.TEL.Push(ctx, "check-account-deletion") + defer util.TEL.Pop() + if user.Deleted { + util.TEL.Error("account is deleted", nil, "username_or_email", dto.UsernameOrEmail) + return "", domain.ErrDeletedAccount + } + util.TEL.Push(ctx, "verify-password") defer util.TEL.Pop() From 34a225e76310e1ba9c617c1902d3198b82a5fe82 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:31:03 +0100 Subject: [PATCH 47/52] feat: Retune terminology --- src/domain/error.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/domain/error.go b/src/domain/error.go index c484cb8..a901327 100644 --- a/src/domain/error.go +++ b/src/domain/error.go @@ -9,6 +9,7 @@ var ( ErrDBInternal = errors.New("database internal error") ErrInvalidInput = errors.New("invalid input") ErrLoginFailed = errors.New("invalid user or password") + ErrDeletedAccount = errors.New("deleted account") ErrUnauthorized = errors.New("unauthorized") ErrPasswordsNotMatch = errors.New("confirm password does not match") ErrPasswordNotChanged = errors.New("password must be different") @@ -16,8 +17,8 @@ var ( ErrNotFound = errors.New("not found") ErrWrongPassword = errors.New("incorrect password") - ErrGuestHasReservations = errors.New("user has pending reservations") - ErrHostHasReservations = errors.New("user has room(s) with pending reservations") + ErrGuestHasReservations = errors.New("user has active reservations") + ErrHostHasReservations = errors.New("user has room(s) with active reservations") ErrCannotDeleteAdmin = errors.New("admin accounts cannot be deleted") ) From d0989bcda82b1065d3c77fef67f94c6feb71eb83 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:35:37 +0100 Subject: [PATCH 48/52] feat: Update unit tests for `Delete` of user --- src/test/unit/delete_test.go | 14 ++++++++------ src/test/unit/utils.go | 12 ++++++++++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/test/unit/delete_test.go b/src/test/unit/delete_test.go index 564df42..6097244 100644 --- a/src/test/unit/delete_test.go +++ b/src/test/unit/delete_test.go @@ -2,6 +2,7 @@ package test import ( "bookem-user-service/client/reservationclient" + "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" "context" "fmt" @@ -20,7 +21,7 @@ func TestDelete_GuestSuccess(t *testing.T) { user.Role = domain.Guest mockRepo.On("FindById", id).Return(user, nil) - mockRepo.On("Delete", id).Return() + mockRepo.On("Delete", user).Return(nil) mockReservationClient.On("GetActiveGuestReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{}, nil) err := svc.Delete(context.Background(), id, jwt) @@ -29,7 +30,7 @@ func TestDelete_GuestSuccess(t *testing.T) { } func TestDelete_HostSuccess(t *testing.T) { - svc, mockRepo, _, mockReservationClient := createTestService() + svc, mockRepo, mockRoomClient, mockReservationClient := createTestService() id := uint(1) jwt := "token" @@ -38,8 +39,9 @@ func TestDelete_HostSuccess(t *testing.T) { user.Role = domain.Host mockRepo.On("FindById", id).Return(user, nil) - mockRepo.On("Delete", id).Return() + mockRepo.On("Delete", user).Return(nil) mockReservationClient.On("GetActiveHostReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{}, nil) + mockRoomClient.On("DeleteHostRooms", context.Background(), jwt).Return([]roomclient.RoomDTO{}, nil) err := svc.Delete(context.Background(), id, jwt) @@ -68,7 +70,7 @@ func TestDelete_GuestHasActiveReservations(t *testing.T) { user.ID = id user.Role = domain.Guest mockRepo.On("FindById", id).Return(user, nil) - mockRepo.On("Delete", id).Return() + mockRepo.On("Delete", id).Return(nil) reservation := reservationclient.ReservationDTO{} mockReservationClient.On("GetActiveGuestReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{reservation}, nil) @@ -87,7 +89,7 @@ func TestDelete_HostHasActiveReservations(t *testing.T) { user.ID = id user.Role = domain.Host mockRepo.On("FindById", id).Return(user, nil) - mockRepo.On("Delete", id).Return() + mockRepo.On("Delete", id).Return(nil) reservation := reservationclient.ReservationDTO{} mockReservationClient.On("GetActiveHostReservations", context.Background(), jwt).Return([]reservationclient.ReservationDTO{reservation}, nil) @@ -106,7 +108,7 @@ func TestDelete_TriedDeletingAdmin(t *testing.T) { user.ID = id user.Role = domain.Admin mockRepo.On("FindById", id).Return(user, nil) - mockRepo.On("Delete", id).Return() + mockRepo.On("Delete", id).Return(nil) err := svc.Delete(context.Background(), id, jwt) diff --git a/src/test/unit/utils.go b/src/test/unit/utils.go index 6d74a95..ae704f3 100644 --- a/src/test/unit/utils.go +++ b/src/test/unit/utils.go @@ -2,6 +2,7 @@ package test import ( "bookem-user-service/client/reservationclient" + "bookem-user-service/client/roomclient" domain "bookem-user-service/domain" service "bookem-user-service/service" "context" @@ -55,8 +56,9 @@ func (m *MockRepo) Update(user *domain.User) error { return args.Error(0) } -func (m *MockRepo) Delete(id uint) { - m.Called(id) +func (m *MockRepo) Delete(user *domain.User) error { + args := m.Called(user) + return args.Error(0) } // ---------------------------------------------- Mock room client @@ -65,6 +67,12 @@ type MockRoomClient struct { mock.Mock } +func (m *MockRoomClient) DeleteHostRooms(ctx context.Context, jwt string) ([]roomclient.RoomDTO, error) { + args := m.Called(ctx, jwt) + rooms, _ := args.Get(0).([]roomclient.RoomDTO) + return rooms, args.Error(1) +} + // ---------------------------------------------- Mock reservation client type MockReservationClient struct { From eb2e52b853eef522bf0dc5492bfbc2fe0c14efc7 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:37:31 +0100 Subject: [PATCH 49/52] fix: Add missing error handling for `login` --- src/api/middleware/error_middleware.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/middleware/error_middleware.go b/src/api/middleware/error_middleware.go index d530e24..9bd52f8 100644 --- a/src/api/middleware/error_middleware.go +++ b/src/api/middleware/error_middleware.go @@ -31,6 +31,8 @@ func mapErrorToStatus(err error) int { return http.StatusNotFound case errors.Is(err, domain.ErrWrongPassword): return http.StatusUnauthorized + case errors.Is(err, domain.ErrDeletedAccount): + return http.StatusUnauthorized default: return http.StatusInternalServerError } From b9d94044deba5cb5f655e55b0dd7e8cc881f9ccc Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:39:09 +0100 Subject: [PATCH 50/52] feat: Add JWT utility for integration tests --- compose.integration.yml | 3 ++ src/api/middleware/jwt.go | 44 ++++++++++++++++++++++++++++ src/api/middleware/jwt_middleware.go | 15 ++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/api/middleware/jwt.go diff --git a/compose.integration.yml b/compose.integration.yml index ff29095..3229ea6 100644 --- a/compose.integration.yml +++ b/compose.integration.yml @@ -18,8 +18,11 @@ services: condition: service_healthy reservation-service: condition: service_healthy + environment: + JWT_PUBLIC_KEY_PATH: /app/keys/public_key.pem volumes: - go-mod-cache:/go/pkg/mod + - ${USER_SERVICE_PATH}/keys/public_key.pem:/app/keys/public_key.pem:ro user-service: build: diff --git a/src/api/middleware/jwt.go b/src/api/middleware/jwt.go new file mode 100644 index 0000000..0075dc7 --- /dev/null +++ b/src/api/middleware/jwt.go @@ -0,0 +1,44 @@ +package middleware + +import ( + "fmt" + "os" + + "github.com/golang-jwt/jwt/v5" +) + +var JWT_PUBLIC_KEY_PATH = os.Getenv("JWT_PUBLIC_KEY_PATH") + +var ParseJWT = parseJWT + +// ParseJWT validates and extracts claims from an encoded JWT string. +func parseJWT(tokenString string) (jwt.MapClaims, error) { + publicKeyData, err := os.ReadFile(JWT_PUBLIC_KEY_PATH) + if err != nil { + return nil, fmt.Errorf("could not open public key %s: %w", JWT_PUBLIC_KEY_PATH, err) + } + + publicKey, err := jwt.ParseRSAPublicKeyFromPEM(publicKeyData) + if err != nil { + return nil, fmt.Errorf("could not parse public key: %w", err) + } + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method") + } + return publicKey, nil + }) + + if err != nil { + return nil, err + } + + claims, ok := token.Claims.(jwt.MapClaims) + + if !ok { + return nil, fmt.Errorf("invalid jwt token or claims") + } + + return claims, nil +} diff --git a/src/api/middleware/jwt_middleware.go b/src/api/middleware/jwt_middleware.go index 6b29357..527a234 100644 --- a/src/api/middleware/jwt_middleware.go +++ b/src/api/middleware/jwt_middleware.go @@ -63,3 +63,18 @@ func GetJwt(ctx *gin.Context) (*Jwt, error) { return &jwt, nil } + +func GetJwtFromString(jwtString string) (*Jwt, error) { + jwtData, err := ParseJWT(jwtString) + if err != nil { + return nil, err + } + + jwt := Jwt{ + ID: uint(jwtData["sub"].(float64)), + Username: jwtData["username"].(string), + Role: domain.UserRole(jwtData["role"].(string)), + } + + return &jwt, nil +} From 08b89b900b71b32d36f8a4234e7d959307b58e9b Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 08:41:56 +0100 Subject: [PATCH 51/52] test: Update integration tests for user deletion --- src/client/reservationclient/model.go | 19 ++ src/client/roomclient/model.go | 40 ++++ src/domain/dto.go | 2 + .../integration/delete_integration_test.go | 100 +++++++--- src/test/integration/util.go | 176 ++++++++++++++++++ 5 files changed, 311 insertions(+), 26 deletions(-) diff --git a/src/client/reservationclient/model.go b/src/client/reservationclient/model.go index 7aa16d0..a752013 100644 --- a/src/client/reservationclient/model.go +++ b/src/client/reservationclient/model.go @@ -14,3 +14,22 @@ type ReservationDTO struct { Cancelled bool `gorm:"not null"` Cost uint `gorm:"not null"` } + +type CreateReservationRequestDTO struct { + RoomID uint `json:"roomId"` + DateFrom time.Time `json:"dateFrom"` + DateTo time.Time `json:"dateTo"` + GuestCount uint `json:"guestCount"` +} + +type ReservationRequestDTO struct { + ID uint `json:"id"` + RoomID uint `json:"roomId"` + DateFrom time.Time `json:"dateFrom"` + DateTo time.Time `json:"dateTo"` + GuestCount uint `json:"guestCount"` + GuestID uint `json:"guestId"` + Status string `json:"status"` + Cost uint `json:"cost"` + GuestCancelCount uint `json:"guestCancelCount"` +} diff --git a/src/client/roomclient/model.go b/src/client/roomclient/model.go index a66e2be..9e4529b 100644 --- a/src/client/roomclient/model.go +++ b/src/client/roomclient/model.go @@ -1,5 +1,7 @@ package roomclient +import "time" + type RoomDTO struct { ID uint `json:"id"` HostID uint `json:"hostID"` @@ -25,3 +27,41 @@ type CreateRoomDTO struct { Commodities []string `json:"commodities"` AutoApprove bool `json:"autoApprove"` } + +// --------------------------------------------------------------- + +type CreateRoomAvailabilityListDTO struct { + RoomID uint `json:"roomId"` + Items []CreateRoomAvailabilityItemDTO `json:"items"` +} + +type CreateRoomAvailabilityItemDTO struct { + // ExistingID is either the ID of an RoomAvailabilityItem that already + // exists, or 0 if this is a new item. When 0, a new one will be created in + // the DB. When not 0, it will reuse the existing object. + ExistingID uint `json:"existingId"` + DateFrom time.Time `json:"dateFrom"` + DateTo time.Time `json:"dateTo"` + Available bool `json:"available"` +} + +// --------------------------------------------------------------- + +type CreateRoomPriceListDTO struct { + RoomID uint `json:"roomId"` + Items []CreateRoomPriceItemDTO `json:"items"` + BasePrice uint `json:"basePrice"` + PerGuest bool `json:"perGuest"` +} + +type CreateRoomPriceItemDTO struct { + // ExistingID is either the ID of an RoomPriceItem that already + // exists, or 0 if this is a new item. When 0, a new one will be created in + // the DB. When not 0, it will reuse the existing object. + ExistingID uint `json:"existingId"` + DateFrom time.Time `json:"dateFrom"` + DateTo time.Time `json:"dateTo"` + Price uint `json:"price"` +} + +// --------------------------------------------------------------- diff --git a/src/domain/dto.go b/src/domain/dto.go index 1065f6a..edf4876 100644 --- a/src/domain/dto.go +++ b/src/domain/dto.go @@ -18,6 +18,7 @@ type UserDTO struct { Surname string `json:"surname" ` Address string `json:"address" ` Role string `json:"role" ` + Deleted bool `json:"deleted" ` } type UserUpdateDTO struct { @@ -38,6 +39,7 @@ func NewUserDTO(user *User) UserDTO { Surname: user.Surname, Address: user.Address, Role: string(user.Role), + Deleted: user.Deleted, } } diff --git a/src/test/integration/delete_integration_test.go b/src/test/integration/delete_integration_test.go index 559b7cd..9e71c38 100644 --- a/src/test/integration/delete_integration_test.go +++ b/src/test/integration/delete_integration_test.go @@ -1,25 +1,28 @@ package test import ( + "bookem-user-service/client/reservationclient" "bookem-user-service/domain" "net/http" + "strconv" "testing" + "time" "github.com/stretchr/testify/require" ) func TestIntegration_Delete(t *testing.T) { { - resp, _ := registerUser("guest1", "1234", domain.Guest) - jwt := loginUser2("guest1", "1234") + resp, _ := registerUser("guest_idel_1", "1234", domain.Guest) + jwt := loginUser2("guest_idel_1", "1234") resp, err := deleteUser(jwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } { - resp, _ := registerUser("host1", "1234", domain.Host) - jwt := loginUser2("host1", "1234") + resp, _ := registerUser("host_idel_1", "1234", domain.Host) + jwt := loginUser2("host_idel_1", "1234") resp, err := deleteUser(jwt) require.Nil(t, err) @@ -28,46 +31,91 @@ func TestIntegration_Delete(t *testing.T) { } func TestIntegration_Delete_GuestHasActiveReservations(t *testing.T) { - resp, _ := registerUser("guest2", "1234", domain.Guest) - jwt := loginUser2("guest2", "1234") + hostUsername := "host_idel_2" + _, _, hostJwt, room := setupHostRoomAvailabilityPrice(hostUsername, t) - // TODO: Once we can actually create reservations - // we will add active reservations here. - // Until then, this test will pass. + registerUser("guest_idel_2", "1234", domain.Guest) + guestJwt := loginUser2("guest_idel_2", "1234") - resp, err := deleteUser(jwt) + dto := reservationclient.CreateReservationRequestDTO{ + RoomID: room.ID, + DateFrom: time.Date(2025, 9, 6, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 8, 0, 0, 0, 0, time.UTC), + GuestCount: 2, + } + + resp, err := createReservationRequest(guestJwt, dto) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) + req := responseToReservationRequest(resp) + + approveURL := URL_reservation + "req/" + strconv.FormatUint(uint64(req.ID), 10) + "/approve" + request, err := http.NewRequest(http.MethodPut, approveURL, nil) + require.NoError(t, err) + request.Header.Add("Authorization", "Bearer "+hostJwt) + + approveResp, err := http.DefaultClient.Do(request) + require.NoError(t, err) + require.Equal(t, http.StatusOK, approveResp.StatusCode) + + resp, err = deleteUser(hostJwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } func TestIntegration_Delete_GuestHasNoActiveReservations(t *testing.T) { - resp, _ := registerUser("guest3", "1234", domain.Guest) - jwt := loginUser2("guest3", "1234") + resp, _ := registerUser("guest_idel_3", "1234", domain.Guest) + guestJwt := loginUser2("guest_idel_3", "1234") + + hostUsername := "host_idel_3" + _, _, _, room := setupHostRoomAvailabilityPrice(hostUsername, t) + + // This reservation request is inactive. + dto := reservationclient.CreateReservationRequestDTO{ + RoomID: room.ID, + DateFrom: time.Date(2025, 9, 6, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 8, 0, 0, 0, 0, time.UTC), + GuestCount: 2, + } - // TODO: Once we can actually create reservations - // we will add non-active reservations here. - // Until then, this test will pass. + resp, err := createReservationRequest(guestJwt, dto) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) - resp, err := deleteUser(jwt) + resp, err = deleteUser(guestJwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } func TestIntegration_Delete_HostHasActiveReservations(t *testing.T) { - resp, _ := registerUser("host2", "1234", domain.Host) - id := getUserFromRegister(resp).Id - jwt := loginUser2("host2", "1234") + hostUsername := "host_idel_4" + _, _, hostJwt, room := setupHostRoomAvailabilityPrice(hostUsername, t) + + registerUser("guest_idel_4", "1234", domain.Guest) + guestJwt := loginUser2("guest_idel_4", "1234") - roomDTO := DefaultRoomCreateDTO - roomDTO.HostID = id + dto := reservationclient.CreateReservationRequestDTO{ + RoomID: room.ID, + DateFrom: time.Date(2025, 9, 6, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 8, 0, 0, 0, 0, time.UTC), + GuestCount: 2, + } - resp, _ = createRoom(jwt, roomDTO) + resp, err := createReservationRequest(guestJwt, dto) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) + req := responseToReservationRequest(resp) - // TODO: Once we can actually create reservations - // we will add active reservations here. - // Until then, this test will pass. + approveURL := URL_reservation + "req/" + strconv.FormatUint(uint64(req.ID), 10) + "/approve" + request, err := http.NewRequest(http.MethodPut, approveURL, nil) + require.NoError(t, err) + request.Header.Add("Authorization", "Bearer "+hostJwt) - resp, err := deleteUser(jwt) + approveResp, err := http.DefaultClient.Do(request) + require.NoError(t, err) + require.Equal(t, http.StatusOK, approveResp.StatusCode) + + resp, err = deleteUser(hostJwt) require.Nil(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) } diff --git a/src/test/integration/util.go b/src/test/integration/util.go index d5fcab5..3f2fed6 100644 --- a/src/test/integration/util.go +++ b/src/test/integration/util.go @@ -1,6 +1,8 @@ package test import ( + middleware "bookem-user-service/api/middleware" + "bookem-user-service/client/reservationclient" "bookem-user-service/client/roomclient" "bookem-user-service/domain" "bytes" @@ -10,6 +12,10 @@ import ( "math/rand" "net/http" "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" ) const URL = "http://user-service:8080/api/" @@ -173,6 +179,176 @@ func createRoom(jwt string, dto roomclient.CreateRoomDTO) (*http.Response, error return http.DefaultClient.Do(req) } +func createReservationRequest(jwt string, dto reservationclient.CreateReservationRequestDTO) (*http.Response, error) { + jsonBytes, err := json.Marshal(dto) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, URL_reservation+"req", bytes.NewBuffer(jsonBytes)) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+jwt) + return http.DefaultClient.Do(req) +} + +func responseToReservationRequest(resp *http.Response) reservationclient.ReservationRequestDTO { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + panic(fmt.Sprintf("failed to read response body: %v", err)) + } + + var obj reservationclient.ReservationRequestDTO + if err := json.Unmarshal(bodyBytes, &obj); err != nil { + panic(fmt.Sprintf("failed to unmarshal: %v", err)) + } + + return obj +} + +func responseToRoom(resp *http.Response) roomclient.RoomDTO { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + panic(fmt.Sprintf("failed to read response body: %v", err)) + } + + var obj roomclient.RoomDTO + if err := json.Unmarshal(bodyBytes, &obj); err != nil { + fmt.Print(string(bodyBytes)) + panic(fmt.Sprintf("failed to unmarshal: %v", err)) + } + + return obj +} + +func createRoomAvailability(jwt string, dto roomclient.CreateRoomAvailabilityListDTO) (*http.Response, error) { + jsonBytes, err := json.Marshal(dto) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, URL_room+"available", bytes.NewBuffer(jsonBytes)) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+jwt) + return http.DefaultClient.Do(req) +} + +func createRoomPrice(jwt string, dto roomclient.CreateRoomPriceListDTO) (*http.Response, error) { + jsonBytes, err := json.Marshal(dto) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, URL_room+"price", bytes.NewBuffer(jsonBytes)) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+jwt) + return http.DefaultClient.Do(req) +} + +func setupHostRoomAvailabilityPrice(hostUsername string, t *testing.T) (string, string, string, roomclient.RoomDTO) { + // Step 1: Register unique host + username := hostUsername + password := "pass" + registerUser(username, password, domain.Host) + jwt := loginUser2(username, password) + jwtObj, err := middleware.GetJwtFromString(jwt) + require.NoError(t, err) + + // Step 2: Create room + roomDTO := roomclient.CreateRoomDTO{ + HostID: jwtObj.ID, + Name: "Room_" + genName(6), + Description: "Test room", + Address: "Test address", + MinGuests: 1, + MaxGuests: 4, + PhotosPayload: []string{SMALL_IMG}, + Commodities: []string{"WiFi", "AC"}, + AutoApprove: false, + } + roomResp, err := createRoom(jwt, roomDTO) + require.NoError(t, err) + defer roomResp.Body.Close() + room := responseToRoom(roomResp) + + // Step 3: Create availability list + availabilityDTO := roomclient.CreateRoomAvailabilityListDTO{ + RoomID: room.ID, + Items: []roomclient.CreateRoomAvailabilityItemDTO{ + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 1, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 10, 0, 0, 0, 0, time.UTC), + Available: true, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 15, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 20, 0, 0, 0, 0, time.UTC), + Available: true, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 22, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 30, 0, 0, 0, 0, time.UTC), + Available: true, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 12, 1, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 12, 10, 0, 0, 0, 0, time.UTC), + Available: true, + }, + }, + } + availResp, err := createRoomAvailability(jwt, availabilityDTO) + require.NoError(t, err) + defer availResp.Body.Close() + + // Step 4: Create price list + priceDTO := roomclient.CreateRoomPriceListDTO{ + RoomID: room.ID, + BasePrice: 80, + PerGuest: false, + Items: []roomclient.CreateRoomPriceItemDTO{ + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 1, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 10, 0, 0, 0, 0, time.UTC), + Price: 100, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 15, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 20, 0, 0, 0, 0, time.UTC), + Price: 120, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 9, 22, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 9, 30, 0, 0, 0, 0, time.UTC), + Price: 200, + }, + { + ExistingID: 0, + DateFrom: time.Date(2025, 12, 1, 0, 0, 0, 0, time.UTC), + DateTo: time.Date(2025, 12, 10, 0, 0, 0, 0, time.UTC), + Price: 200, + }, + }, + } + priceResp, err := createRoomPrice(jwt, priceDTO) + require.NoError(t, err) + defer priceResp.Body.Close() + + return username, password, jwt, room +} + // ----------------------------------------------- Mock data const ( From 327c5ff365faa17d981ef329ef87a5cbcd5eb381 Mon Sep 17 00:00:00 2001 From: Vasilije Date: Tue, 28 Oct 2025 09:19:03 +0100 Subject: [PATCH 52/52] fix: Use latest changes during CI integration testing --- ci.override.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci.override.env b/ci.override.env index 7261b24..e86aab0 100644 --- a/ci.override.env +++ b/ci.override.env @@ -1,7 +1,7 @@ RESERVATION_SERVICE_PATH=./services/reservation-service RESERVATION_SERVICE_REPO=https://github.com/book-em/reservation-service.git -RESERVATION_SERVICE_BRANCH=develop +RESERVATION_SERVICE_BRANCH=feature-delete-user-complete ROOM_SERVICE_PATH=./services/room-service ROOM_SERVICE_REPO=https://github.com/book-em/room-service.git -ROOM_SERVICE_BRANCH=develop \ No newline at end of file +ROOM_SERVICE_BRANCH=feature-delete-user-complete \ No newline at end of file