Compare commits
2 Commits
016728532c
...
b9cb315944
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9cb315944 | ||
|
|
26a0d33592 |
13
go.mod
13
go.mod
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
go.uber.org/dig v1.17.1
|
go.uber.org/dig v1.17.1
|
||||||
golang.org/x/crypto v0.39.0
|
golang.org/x/crypto v0.39.0
|
||||||
gorm.io/driver/sqlite v1.5.6
|
gorm.io/driver/postgres v1.5.9
|
||||||
gorm.io/gorm v1.25.11
|
gorm.io/gorm v1.25.11
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,23 +20,28 @@ require (
|
|||||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
github.com/go-openapi/spec v0.21.0 // indirect
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||||
github.com/swaggo/swag v1.16.3 // indirect
|
github.com/swaggo/swag v1.16.3 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.26.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
golang.org/x/tools v0.33.0 // indirect
|
golang.org/x/tools v0.34.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
33
go.sum
33
go.sum
@@ -2,6 +2,7 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
|
|||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
@@ -20,6 +21,14 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe
|
|||||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
@@ -34,23 +43,23 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||||
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||||
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||||
@@ -69,20 +78,20 @@ golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
|||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
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/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"omega-server/local/controller"
|
"omega-server/local/controller"
|
||||||
|
"omega-server/local/graphql/handler"
|
||||||
|
graphqlService "omega-server/local/graphql/service"
|
||||||
|
"omega-server/local/service"
|
||||||
"omega-server/local/utl/common"
|
"omega-server/local/utl/common"
|
||||||
"omega-server/local/utl/configs"
|
"omega-server/local/utl/configs"
|
||||||
"omega-server/local/utl/logging"
|
"omega-server/local/utl/logging"
|
||||||
@@ -33,5 +36,35 @@ func Init(di *dig.Container, app *fiber.App) {
|
|||||||
logging.Panic("unable to bind routes")
|
logging.Panic("unable to bind routes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize GraphQL
|
||||||
|
initGraphQL(di, groups)
|
||||||
|
|
||||||
controller.InitializeControllers(di)
|
controller.InitializeControllers(di)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initGraphQL initializes GraphQL endpoints
|
||||||
|
func initGraphQL(di *dig.Container, groups fiber.Router) {
|
||||||
|
err := di.Invoke(func(membershipService *service.MembershipService) error {
|
||||||
|
// Create GraphQL service
|
||||||
|
gqlService := graphqlService.NewGraphQLService(membershipService)
|
||||||
|
_ = gqlService // Use the service (placeholder)
|
||||||
|
|
||||||
|
// Create GraphQL handler
|
||||||
|
graphqlHandler := handler.NewGraphQLHandler(membershipService)
|
||||||
|
|
||||||
|
// Register GraphQL routes
|
||||||
|
groups.Post("/graphql", graphqlHandler.Handle)
|
||||||
|
|
||||||
|
// GraphQL playground/schema endpoint
|
||||||
|
groups.Get("/graphql", func(c *fiber.Ctx) error {
|
||||||
|
return c.SendString(graphqlHandler.GetSchema())
|
||||||
|
})
|
||||||
|
|
||||||
|
logging.Info("GraphQL endpoint initialized at /graphql")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logging.Panic("failed to initialize GraphQL: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"omega-server/local/middleware"
|
"omega-server/local/middleware"
|
||||||
|
"omega-server/local/model"
|
||||||
"omega-server/local/service"
|
"omega-server/local/service"
|
||||||
"omega-server/local/utl/common"
|
"omega-server/local/utl/common"
|
||||||
"omega-server/local/utl/error_handler"
|
"omega-server/local/utl/error_handler"
|
||||||
@@ -52,7 +53,7 @@ func NewMembershipController(service *service.MembershipService, auth *middlewar
|
|||||||
// Login handles user login.
|
// Login handles user login.
|
||||||
func (c *MembershipController) Login(ctx *fiber.Ctx) error {
|
func (c *MembershipController) Login(ctx *fiber.Ctx) error {
|
||||||
type request struct {
|
type request struct {
|
||||||
Username string `json:"username"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ func (c *MembershipController) Login(ctx *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logging.Debug("Login request received")
|
logging.Debug("Login request received")
|
||||||
token, err := c.service.Login(ctx.UserContext(), req.Username, req.Password)
|
token, err := c.service.Login(ctx.UserContext(), req.Email, req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.errorHandler.HandleAuthError(ctx, err)
|
return c.errorHandler.HandleAuthError(ctx, err)
|
||||||
}
|
}
|
||||||
@@ -72,23 +73,35 @@ func (c *MembershipController) Login(ctx *fiber.Ctx) error {
|
|||||||
|
|
||||||
// CreateUser creates a new user.
|
// CreateUser creates a new user.
|
||||||
func (mc *MembershipController) CreateUser(c *fiber.Ctx) error {
|
func (mc *MembershipController) CreateUser(c *fiber.Ctx) error {
|
||||||
type request struct {
|
var req model.UserCreateRequest
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var req request
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
return mc.errorHandler.HandleParsingError(c, err)
|
return mc.errorHandler.HandleParsingError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := mc.service.CreateUser(c.UserContext(), req.Username, req.Password, req.Role)
|
// Validate request
|
||||||
|
if err := req.Validate(); err != nil {
|
||||||
|
return mc.errorHandler.HandleValidationError(c, err, "user_create_request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map to domain model
|
||||||
|
user, err := req.ToUser()
|
||||||
|
if err != nil {
|
||||||
|
return mc.errorHandler.HandleValidationError(c, err, "user_mapping")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract role names from request
|
||||||
|
roleIDs := req.RoleIDs
|
||||||
|
if len(roleIDs) == 0 {
|
||||||
|
roleIDs = []string{"user"} // default role
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call service with domain model
|
||||||
|
createdUser, err := mc.service.CreateUser(c.UserContext(), user, roleIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mc.errorHandler.HandleServiceError(c, err)
|
return mc.errorHandler.HandleServiceError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(user)
|
return c.JSON(createdUser.ToResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUsers lists all users.
|
// ListUsers lists all users.
|
||||||
@@ -98,7 +111,13 @@ func (mc *MembershipController) ListUsers(c *fiber.Ctx) error {
|
|||||||
return mc.errorHandler.HandleServiceError(c, err)
|
return mc.errorHandler.HandleServiceError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(users)
|
// Convert to response format
|
||||||
|
userResponses := make([]*model.UserResponse, len(users))
|
||||||
|
for i, user := range users {
|
||||||
|
userResponses[i] = user.ToResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(userResponses)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUser gets a single user by ID.
|
// GetUser gets a single user by ID.
|
||||||
@@ -113,7 +132,7 @@ func (mc *MembershipController) GetUser(c *fiber.Ctx) error {
|
|||||||
return mc.errorHandler.HandleNotFoundError(c, "User")
|
return mc.errorHandler.HandleNotFoundError(c, "User")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(user)
|
return c.JSON(user.ToResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMe returns the currently authenticated user's details.
|
// GetMe returns the currently authenticated user's details.
|
||||||
@@ -128,10 +147,7 @@ func (mc *MembershipController) GetMe(c *fiber.Ctx) error {
|
|||||||
return mc.errorHandler.HandleNotFoundError(c, "User")
|
return mc.errorHandler.HandleNotFoundError(c, "User")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize the user object to not expose password
|
return c.JSON(user.ToResponse())
|
||||||
user.PasswordHash = ""
|
|
||||||
|
|
||||||
return c.JSON(user)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser deletes a user.
|
// DeleteUser deletes a user.
|
||||||
@@ -156,17 +172,22 @@ func (mc *MembershipController) UpdateUser(c *fiber.Ctx) error {
|
|||||||
return mc.errorHandler.HandleUUIDError(c, "user ID")
|
return mc.errorHandler.HandleUUIDError(c, "user ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
var req service.UpdateUserRequest
|
var req model.UserUpdateRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
return mc.errorHandler.HandleParsingError(c, err)
|
return mc.errorHandler.HandleParsingError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := mc.service.UpdateUser(c.UserContext(), id, req)
|
// Validate request
|
||||||
|
if err := req.Validate(); err != nil {
|
||||||
|
return mc.errorHandler.HandleValidationError(c, err, "user_update_request")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := mc.service.UpdateUser(c.UserContext(), id, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mc.errorHandler.HandleServiceError(c, err)
|
return mc.errorHandler.HandleServiceError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(user)
|
return c.JSON(user.ToResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRoles returns all available roles.
|
// GetRoles returns all available roles.
|
||||||
|
|||||||
421
local/graphql/handler/handler.go
Normal file
421
local/graphql/handler/handler.go
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"omega-server/local/model"
|
||||||
|
"omega-server/local/service"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GraphQLHandler handles GraphQL requests
|
||||||
|
type GraphQLHandler struct {
|
||||||
|
membershipService *service.MembershipService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGraphQLHandler creates a new GraphQL handler
|
||||||
|
func NewGraphQLHandler(membershipService *service.MembershipService) *GraphQLHandler {
|
||||||
|
return &GraphQLHandler{
|
||||||
|
membershipService: membershipService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQLRequest represents a GraphQL request
|
||||||
|
type GraphQLRequest struct {
|
||||||
|
Query string `json:"query"`
|
||||||
|
Variables map[string]interface{} `json:"variables"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQLResponse represents a GraphQL response
|
||||||
|
type GraphQLResponse struct {
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
Errors []GraphQLError `json:"errors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQLError represents a GraphQL error
|
||||||
|
type GraphQLError struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Path []string `json:"path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle processes GraphQL requests
|
||||||
|
func (h *GraphQLHandler) Handle(c *fiber.Ctx) error {
|
||||||
|
var req GraphQLRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Invalid request body"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic query parsing and handling
|
||||||
|
ctx := c.UserContext()
|
||||||
|
|
||||||
|
// Simple query routing based on query string
|
||||||
|
query := strings.TrimSpace(req.Query)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Contains(query, "mutation") && strings.Contains(query, "login"):
|
||||||
|
return h.handleLogin(c, ctx, req)
|
||||||
|
case strings.Contains(query, "mutation") && strings.Contains(query, "createUser"):
|
||||||
|
return h.handleCreateUser(c, ctx, req)
|
||||||
|
case strings.Contains(query, "mutation") && strings.Contains(query, "createProject"):
|
||||||
|
return h.handleCreateProject(c, ctx, req)
|
||||||
|
case strings.Contains(query, "mutation") && strings.Contains(query, "createTask"):
|
||||||
|
return h.handleCreateTask(c, ctx, req)
|
||||||
|
case strings.Contains(query, "query") && strings.Contains(query, "me"):
|
||||||
|
return h.handleMe(c, ctx, req)
|
||||||
|
case strings.Contains(query, "query") && strings.Contains(query, "users"):
|
||||||
|
return h.handleUsers(c, ctx, req)
|
||||||
|
case strings.Contains(query, "query") && strings.Contains(query, "projects"):
|
||||||
|
return h.handleProjects(c, ctx, req)
|
||||||
|
case strings.Contains(query, "query") && strings.Contains(query, "tasks"):
|
||||||
|
return h.handleTasks(c, ctx, req)
|
||||||
|
default:
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Query not supported"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleLogin handles login mutations
|
||||||
|
func (h *GraphQLHandler) handleLogin(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||||
|
// Extract variables
|
||||||
|
email, ok := req.Variables["email"].(string)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Email is required"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
password, ok := req.Variables["password"].(string)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Password is required"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call service
|
||||||
|
token, err := h.membershipService.Login(ctx, email, password)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(401).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Invalid credentials"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock user data for now
|
||||||
|
userData := map[string]interface{}{
|
||||||
|
"id": "1",
|
||||||
|
"email": email,
|
||||||
|
"fullName": "User",
|
||||||
|
"createdAt": "2024-01-01T00:00:00Z",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"login": map[string]interface{}{
|
||||||
|
"token": token,
|
||||||
|
"user": userData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(GraphQLResponse{Data: response})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreateUser handles user creation mutations
|
||||||
|
func (h *GraphQLHandler) handleCreateUser(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||||
|
// Extract variables
|
||||||
|
email, ok := req.Variables["email"].(string)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Email is required"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
password, ok := req.Variables["password"].(string)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Password is required"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fullName, ok := req.Variables["fullName"].(string)
|
||||||
|
if !ok {
|
||||||
|
fullName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create domain model
|
||||||
|
user := &model.User{
|
||||||
|
Email: email,
|
||||||
|
FullName: fullName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SetPassword(password); err != nil {
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: err.Error()}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call service
|
||||||
|
createdUser, err := h.membershipService.CreateUser(ctx, user, []string{"user"})
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: err.Error()}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert user to response format
|
||||||
|
userData := map[string]interface{}{
|
||||||
|
"id": createdUser.ID,
|
||||||
|
"email": createdUser.Email,
|
||||||
|
"fullName": createdUser.FullName,
|
||||||
|
"createdAt": createdUser.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
"updatedAt": createdUser.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
}
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"createUser": userData,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(GraphQLResponse{Data: response})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMe handles me queries
|
||||||
|
func (h *GraphQLHandler) handleMe(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||||
|
// This would typically extract user ID from JWT token
|
||||||
|
// For now, return mock data
|
||||||
|
userData := map[string]interface{}{
|
||||||
|
"id": "1",
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"fullName": "System Administrator",
|
||||||
|
"createdAt": "2024-01-01T00:00:00Z",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"me": userData,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(GraphQLResponse{Data: response})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUsers handles users queries
|
||||||
|
func (h *GraphQLHandler) handleUsers(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||||
|
// Call service
|
||||||
|
users, err := h.membershipService.ListUsers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Failed to fetch users"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert users to response format
|
||||||
|
usersData := make([]map[string]interface{}, len(users))
|
||||||
|
for i, user := range users {
|
||||||
|
usersData[i] = map[string]interface{}{
|
||||||
|
"id": user.ID,
|
||||||
|
"email": user.Email,
|
||||||
|
"fullName": user.FullName,
|
||||||
|
"createdAt": user.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
"updatedAt": user.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"users": usersData,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(GraphQLResponse{Data: response})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreateProject handles project creation mutations
|
||||||
|
func (h *GraphQLHandler) handleCreateProject(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||||
|
// Extract variables
|
||||||
|
name, ok := req.Variables["name"].(string)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Name is required"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerId, ok := req.Variables["ownerId"].(string)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Owner ID is required"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
description, _ := req.Variables["description"].(string)
|
||||||
|
|
||||||
|
// Mock project data for now
|
||||||
|
projectData := map[string]interface{}{
|
||||||
|
"id": "mock-project-id",
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"ownerId": ownerId,
|
||||||
|
"createdAt": "2024-01-01T00:00:00Z",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"createProject": projectData,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(GraphQLResponse{Data: response})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreateTask handles task creation mutations
|
||||||
|
func (h *GraphQLHandler) handleCreateTask(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||||
|
// Extract variables
|
||||||
|
title, ok := req.Variables["title"].(string)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Title is required"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
projectId, ok := req.Variables["projectId"].(string)
|
||||||
|
if !ok {
|
||||||
|
return c.Status(400).JSON(GraphQLResponse{
|
||||||
|
Errors: []GraphQLError{{Message: "Project ID is required"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
description, _ := req.Variables["description"].(string)
|
||||||
|
status, _ := req.Variables["status"].(string)
|
||||||
|
if status == "" {
|
||||||
|
status = "todo"
|
||||||
|
}
|
||||||
|
priority, _ := req.Variables["priority"].(string)
|
||||||
|
if priority == "" {
|
||||||
|
priority = "medium"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock task data for now
|
||||||
|
taskData := map[string]interface{}{
|
||||||
|
"id": "mock-task-id",
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"status": status,
|
||||||
|
"priority": priority,
|
||||||
|
"projectId": projectId,
|
||||||
|
"createdAt": "2024-01-01T00:00:00Z",
|
||||||
|
"updatedAt": "2024-01-01T00:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"createTask": taskData,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(GraphQLResponse{Data: response})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleProjects handles projects queries
|
||||||
|
func (h *GraphQLHandler) handleProjects(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||||
|
// Mock empty projects list for now
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"projects": []interface{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(GraphQLResponse{Data: response})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTasks handles tasks queries
|
||||||
|
func (h *GraphQLHandler) handleTasks(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||||
|
// Mock empty tasks list for now
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"tasks": []interface{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(GraphQLResponse{Data: response})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSchema returns the GraphQL schema
|
||||||
|
func (h *GraphQLHandler) GetSchema() string {
|
||||||
|
return `
|
||||||
|
# Core Types
|
||||||
|
type User {
|
||||||
|
id: String!
|
||||||
|
email: String!
|
||||||
|
fullName: String
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Project {
|
||||||
|
id: String!
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
ownerId: String!
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task {
|
||||||
|
id: String!
|
||||||
|
title: String!
|
||||||
|
description: String
|
||||||
|
status: String!
|
||||||
|
priority: String!
|
||||||
|
projectId: String!
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input Types
|
||||||
|
input LoginInput {
|
||||||
|
email: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UserCreateInput {
|
||||||
|
email: String!
|
||||||
|
fullName: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input ProjectCreateInput {
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
ownerId: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input TaskCreateInput {
|
||||||
|
title: String!
|
||||||
|
description: String
|
||||||
|
status: String
|
||||||
|
priority: String
|
||||||
|
projectId: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Response Types
|
||||||
|
type AuthResponse {
|
||||||
|
token: String!
|
||||||
|
user: User!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageResponse {
|
||||||
|
message: String!
|
||||||
|
success: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Queries
|
||||||
|
type Query {
|
||||||
|
me: User!
|
||||||
|
users: [User!]!
|
||||||
|
user(id: String!): User
|
||||||
|
projects: [Project!]!
|
||||||
|
project(id: String!): Project
|
||||||
|
tasks(projectId: String): [Task!]!
|
||||||
|
task(id: String!): Task
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mutations
|
||||||
|
type Mutation {
|
||||||
|
login(input: LoginInput!): AuthResponse!
|
||||||
|
createUser(input: UserCreateInput!): User!
|
||||||
|
createProject(input: ProjectCreateInput!): Project!
|
||||||
|
createTask(input: TaskCreateInput!): Task!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
100
local/graphql/schema/schema.graphql
Normal file
100
local/graphql/schema/schema.graphql
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Minimal GraphQL Schema for Phase 1
|
||||||
|
|
||||||
|
# Core Types
|
||||||
|
type User {
|
||||||
|
id: String!
|
||||||
|
email: String!
|
||||||
|
fullName: String
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Project {
|
||||||
|
id: String!
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
ownerId: String!
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task {
|
||||||
|
id: String!
|
||||||
|
title: String!
|
||||||
|
description: String
|
||||||
|
status: String!
|
||||||
|
priority: String!
|
||||||
|
projectId: String!
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Input Types
|
||||||
|
input LoginInput {
|
||||||
|
email: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UserCreateInput {
|
||||||
|
email: String!
|
||||||
|
fullName: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input ProjectCreateInput {
|
||||||
|
name: String!
|
||||||
|
description: String
|
||||||
|
ownerId: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input TaskCreateInput {
|
||||||
|
title: String!
|
||||||
|
description: String
|
||||||
|
status: String
|
||||||
|
priority: String
|
||||||
|
projectId: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Response Types
|
||||||
|
type AuthResponse {
|
||||||
|
token: String!
|
||||||
|
user: User!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageResponse {
|
||||||
|
message: String!
|
||||||
|
success: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
# Queries
|
||||||
|
type Query {
|
||||||
|
# Authentication
|
||||||
|
me: User!
|
||||||
|
|
||||||
|
# Users
|
||||||
|
users: [User!]!
|
||||||
|
user(id: String!): User
|
||||||
|
|
||||||
|
# Projects
|
||||||
|
projects: [Project!]!
|
||||||
|
project(id: String!): Project
|
||||||
|
|
||||||
|
# Tasks
|
||||||
|
tasks(projectId: String): [Task!]!
|
||||||
|
task(id: String!): Task
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mutations
|
||||||
|
type Mutation {
|
||||||
|
# Authentication
|
||||||
|
login(input: LoginInput!): AuthResponse!
|
||||||
|
|
||||||
|
# Users
|
||||||
|
createUser(input: UserCreateInput!): User!
|
||||||
|
|
||||||
|
# Projects
|
||||||
|
createProject(input: ProjectCreateInput!): Project!
|
||||||
|
|
||||||
|
# Tasks
|
||||||
|
createTask(input: TaskCreateInput!): Task!
|
||||||
|
}
|
||||||
173
local/graphql/service/service.go
Normal file
173
local/graphql/service/service.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"omega-server/local/model"
|
||||||
|
"omega-server/local/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GraphQLService provides GraphQL-specific business logic
|
||||||
|
type GraphQLService struct {
|
||||||
|
membershipService *service.MembershipService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGraphQLService creates a new GraphQL service
|
||||||
|
func NewGraphQLService(membershipService *service.MembershipService) *GraphQLService {
|
||||||
|
return &GraphQLService{
|
||||||
|
membershipService: membershipService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthResponse represents authentication response
|
||||||
|
type AuthResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
User *model.User `json:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageResponse represents a generic message response
|
||||||
|
type MessageResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login handles user authentication
|
||||||
|
func (s *GraphQLService) Login(ctx context.Context, email, password string) (*AuthResponse, error) {
|
||||||
|
token, err := s.membershipService.Login(ctx, email, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, return a mock user. In a full implementation, we'd get the user from the token
|
||||||
|
user := &model.User{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: "mock-user-id",
|
||||||
|
},
|
||||||
|
Email: email,
|
||||||
|
FullName: "Mock User",
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AuthResponse{
|
||||||
|
Token: token,
|
||||||
|
User: user,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser handles user creation
|
||||||
|
func (s *GraphQLService) CreateUser(ctx context.Context, email, password, fullName string) (*model.User, error) {
|
||||||
|
// Create domain model
|
||||||
|
user := &model.User{
|
||||||
|
Email: email,
|
||||||
|
FullName: fullName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SetPassword(password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.membershipService.CreateUser(ctx, user, []string{"user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsers retrieves all users
|
||||||
|
func (s *GraphQLService) GetUsers(ctx context.Context) ([]*model.User, error) {
|
||||||
|
return s.membershipService.ListUsers(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser retrieves a specific user by ID
|
||||||
|
func (s *GraphQLService) GetUser(ctx context.Context, id string) (*model.User, error) {
|
||||||
|
// This would need to be implemented in the membership service
|
||||||
|
// For now, return a mock user
|
||||||
|
return &model.User{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
Email: "mock@example.com",
|
||||||
|
FullName: "Mock User",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMe retrieves the current authenticated user
|
||||||
|
func (s *GraphQLService) GetMe(ctx context.Context, userID string) (*model.User, error) {
|
||||||
|
// This would typically extract user ID from JWT token
|
||||||
|
// For now, return a mock user
|
||||||
|
return &model.User{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: userID,
|
||||||
|
},
|
||||||
|
Email: "current@example.com",
|
||||||
|
FullName: "Current User",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProject handles project creation
|
||||||
|
func (s *GraphQLService) CreateProject(ctx context.Context, name, description, ownerID string) (*model.Project, error) {
|
||||||
|
// This would need to be implemented when we have project service
|
||||||
|
// For now, return a mock project
|
||||||
|
return &model.Project{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: "mock-project-id",
|
||||||
|
},
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
OwnerID: ownerID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjects retrieves all projects
|
||||||
|
func (s *GraphQLService) GetProjects(ctx context.Context) ([]*model.Project, error) {
|
||||||
|
// This would need to be implemented when we have project service
|
||||||
|
// For now, return empty slice
|
||||||
|
return []*model.Project{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProject retrieves a specific project by ID
|
||||||
|
func (s *GraphQLService) GetProject(ctx context.Context, id string) (*model.Project, error) {
|
||||||
|
// This would need to be implemented when we have project service
|
||||||
|
// For now, return a mock project
|
||||||
|
return &model.Project{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
Name: "Mock Project",
|
||||||
|
Description: "Mock project description",
|
||||||
|
OwnerID: "mock-owner-id",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTask handles task creation
|
||||||
|
func (s *GraphQLService) CreateTask(ctx context.Context, title, description, status, priority, projectID string) (*model.Task, error) {
|
||||||
|
// This would need to be implemented when we have task service
|
||||||
|
// For now, return a mock task
|
||||||
|
return &model.Task{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: "mock-task-id",
|
||||||
|
},
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
Status: model.TaskStatus(status),
|
||||||
|
Priority: model.TaskPriority(priority),
|
||||||
|
ProjectID: projectID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTasks retrieves tasks, optionally filtered by project ID
|
||||||
|
func (s *GraphQLService) GetTasks(ctx context.Context, projectID *string) ([]*model.Task, error) {
|
||||||
|
// This would need to be implemented when we have task service
|
||||||
|
// For now, return empty slice
|
||||||
|
return []*model.Task{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTask retrieves a specific task by ID
|
||||||
|
func (s *GraphQLService) GetTask(ctx context.Context, id string) (*model.Task, error) {
|
||||||
|
// This would need to be implemented when we have task service
|
||||||
|
// For now, return a mock task
|
||||||
|
return &model.Task{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
Title: "Mock Task",
|
||||||
|
Description: "Mock task description",
|
||||||
|
Status: model.TaskStatusTodo,
|
||||||
|
Priority: model.TaskPriorityMedium,
|
||||||
|
ProjectID: "mock-project-id",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -26,13 +26,13 @@ type CachedUserInfo struct {
|
|||||||
|
|
||||||
// AuthMiddleware provides authentication and permission middleware.
|
// AuthMiddleware provides authentication and permission middleware.
|
||||||
type AuthMiddleware struct {
|
type AuthMiddleware struct {
|
||||||
membershipService service.MembershipServiceInterface
|
membershipService *service.MembershipService
|
||||||
cache *cache.InMemoryCache
|
cache *cache.InMemoryCache
|
||||||
securityMW *security.SecurityMiddleware
|
securityMW *security.SecurityMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthMiddleware creates a new AuthMiddleware.
|
// NewAuthMiddleware creates a new AuthMiddleware.
|
||||||
func NewAuthMiddleware(ms service.MembershipServiceInterface, cache *cache.InMemoryCache) *AuthMiddleware {
|
func NewAuthMiddleware(ms *service.MembershipService, cache *cache.InMemoryCache) *AuthMiddleware {
|
||||||
auth := &AuthMiddleware{
|
auth := &AuthMiddleware{
|
||||||
membershipService: ms,
|
membershipService: ms,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
@@ -201,7 +201,7 @@ func (m *AuthMiddleware) getCachedUserInfo(ctx context.Context, userID string) (
|
|||||||
|
|
||||||
userInfo := &CachedUserInfo{
|
userInfo := &CachedUserInfo{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Username: user.Username,
|
Username: user.FullName,
|
||||||
Roles: roleNames,
|
Roles: roleNames,
|
||||||
RoleNames: roleNames,
|
RoleNames: roleNames,
|
||||||
Permissions: permissions,
|
Permissions: permissions,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ type AuditLogInfo struct {
|
|||||||
Duration int64 `json:"duration,omitempty"`
|
Duration int64 `json:"duration,omitempty"`
|
||||||
SessionID string `json:"sessionId,omitempty"`
|
SessionID string `json:"sessionId,omitempty"`
|
||||||
RequestID string `json:"requestId,omitempty"`
|
RequestID string `json:"requestId,omitempty"`
|
||||||
DateCreated string `json:"dateCreated"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeCreate is called before creating an audit log
|
// BeforeCreate is called before creating an audit log
|
||||||
@@ -135,13 +135,13 @@ func (al *AuditLog) ToAuditLogInfo() AuditLogInfo {
|
|||||||
Duration: al.Duration,
|
Duration: al.Duration,
|
||||||
SessionID: al.SessionID,
|
SessionID: al.SessionID,
|
||||||
RequestID: al.RequestID,
|
RequestID: al.RequestID,
|
||||||
DateCreated: al.DateCreated.Format("2006-01-02T15:04:05Z"),
|
CreatedAt: al.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include user information if available
|
// Include user information if available
|
||||||
if al.User != nil {
|
if al.User != nil {
|
||||||
info.UserEmail = al.User.Email
|
info.UserEmail = al.User.Email
|
||||||
info.UserName = al.User.Name
|
info.UserName = al.User.FullName
|
||||||
}
|
}
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|||||||
@@ -8,22 +8,22 @@ import (
|
|||||||
|
|
||||||
// BaseModel provides common fields for all database models
|
// BaseModel provides common fields for all database models
|
||||||
type BaseModel struct {
|
type BaseModel struct {
|
||||||
ID string `json:"id" gorm:"primary_key;type:varchar(36)"`
|
ID string `json:"id" gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
|
||||||
DateCreated time.Time `json:"dateCreated" gorm:"not null"`
|
CreatedAt time.Time `json:"created_at" gorm:"not null;default:now()"`
|
||||||
DateUpdated time.Time `json:"dateUpdated" gorm:"not null"`
|
UpdatedAt time.Time `json:"updated_at" gorm:"not null;default:now()"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes base model with DateCreated, DateUpdated, and ID values
|
// Init initializes base model with CreatedAt, UpdatedAt, and ID values
|
||||||
func (bm *BaseModel) Init() {
|
func (bm *BaseModel) Init() {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
bm.ID = uuid.NewString()
|
bm.ID = uuid.NewString()
|
||||||
bm.DateCreated = now
|
bm.CreatedAt = now
|
||||||
bm.DateUpdated = now
|
bm.UpdatedAt = now
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTimestamp updates the DateUpdated field
|
// UpdateTimestamp updates the UpdatedAt field
|
||||||
func (bm *BaseModel) UpdateTimestamp() {
|
func (bm *BaseModel) UpdateTimestamp() {
|
||||||
bm.DateUpdated = time.Now().UTC()
|
bm.UpdatedAt = time.Now().UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeCreate is a GORM hook that runs before creating a record
|
// BeforeCreate is a GORM hook that runs before creating a record
|
||||||
@@ -76,7 +76,7 @@ func DefaultParams() Params {
|
|||||||
return Params{
|
return Params{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
Limit: 10,
|
Limit: 10,
|
||||||
SortBy: "dateCreated",
|
SortBy: "created_at",
|
||||||
SortOrder: "desc",
|
SortOrder: "desc",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ func (p *Params) Validate() {
|
|||||||
p.Limit = 10
|
p.Limit = 10
|
||||||
}
|
}
|
||||||
if p.SortBy == "" {
|
if p.SortBy == "" {
|
||||||
p.SortBy = "dateCreated"
|
p.SortBy = "created_at"
|
||||||
}
|
}
|
||||||
if p.SortOrder != "asc" && p.SortOrder != "desc" {
|
if p.SortOrder != "asc" && p.SortOrder != "desc" {
|
||||||
p.SortOrder = "desc"
|
p.SortOrder = "desc"
|
||||||
|
|||||||
144
local/model/integration.go
Normal file
144
local/model/integration.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Integration represents a third-party integration configuration
|
||||||
|
type Integration struct {
|
||||||
|
BaseModel
|
||||||
|
ProjectID string `json:"project_id" gorm:"not null;type:uuid;index;references:projects(id);onDelete:CASCADE"`
|
||||||
|
Type string `json:"type" gorm:"not null;type:varchar(50)"`
|
||||||
|
Config json.RawMessage `json:"config" gorm:"type:jsonb;not null"`
|
||||||
|
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntegrationCreateRequest represents the request to create a new integration
|
||||||
|
type IntegrationCreateRequest struct {
|
||||||
|
ProjectID string `json:"project_id" validate:"required,uuid"`
|
||||||
|
Type string `json:"type" validate:"required,min=1,max=50"`
|
||||||
|
Config map[string]interface{} `json:"config" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntegrationUpdateRequest represents the request to update an integration
|
||||||
|
type IntegrationUpdateRequest struct {
|
||||||
|
Config map[string]interface{} `json:"config" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntegrationInfo represents public integration information
|
||||||
|
type IntegrationInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Config map[string]interface{} `json:"config"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate is called before creating an integration
|
||||||
|
func (i *Integration) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
i.BaseModel.BeforeCreate()
|
||||||
|
|
||||||
|
// Normalize type
|
||||||
|
i.Type = strings.ToLower(strings.TrimSpace(i.Type))
|
||||||
|
|
||||||
|
return i.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate is called before updating an integration
|
||||||
|
func (i *Integration) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
i.BaseModel.BeforeUpdate()
|
||||||
|
|
||||||
|
// Normalize type if it's being updated
|
||||||
|
if i.Type != "" {
|
||||||
|
i.Type = strings.ToLower(strings.TrimSpace(i.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates integration data
|
||||||
|
func (i *Integration) Validate() error {
|
||||||
|
if i.ProjectID == "" {
|
||||||
|
return errors.New("project_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.Type == "" {
|
||||||
|
return errors.New("type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(i.Type) > 50 {
|
||||||
|
return errors.New("type must not exceed 50 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(i.Config) == 0 {
|
||||||
|
return errors.New("config is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that config is valid JSON
|
||||||
|
var configMap map[string]interface{}
|
||||||
|
if err := json.Unmarshal(i.Config, &configMap); err != nil {
|
||||||
|
return errors.New("config must be valid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToIntegrationInfo converts Integration to IntegrationInfo (public information)
|
||||||
|
func (i *Integration) ToIntegrationInfo() IntegrationInfo {
|
||||||
|
integrationInfo := IntegrationInfo{
|
||||||
|
ID: i.ID,
|
||||||
|
ProjectID: i.ProjectID,
|
||||||
|
Type: i.Type,
|
||||||
|
CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
UpdatedAt: i.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse config JSON to map
|
||||||
|
var configMap map[string]interface{}
|
||||||
|
if err := json.Unmarshal(i.Config, &configMap); err == nil {
|
||||||
|
integrationInfo.Config = configMap
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the configuration from a map
|
||||||
|
func (i *Integration) SetConfig(config map[string]interface{}) error {
|
||||||
|
configJSON, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Config = configJSON
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig gets the configuration as a map
|
||||||
|
func (i *Integration) GetConfig() (map[string]interface{}, error) {
|
||||||
|
var configMap map[string]interface{}
|
||||||
|
err := json.Unmarshal(i.Config, &configMap)
|
||||||
|
return configMap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigValue gets a specific configuration value
|
||||||
|
func (i *Integration) GetConfigValue(key string) (interface{}, error) {
|
||||||
|
configMap, err := i.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return configMap[key], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfigValue sets a specific configuration value
|
||||||
|
func (i *Integration) SetConfigValue(key string, value interface{}) error {
|
||||||
|
configMap, err := i.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configMap[key] = value
|
||||||
|
return i.SetConfig(configMap)
|
||||||
|
}
|
||||||
@@ -156,10 +156,10 @@ func (f *MembershipFilter) GetSorting() (field string, desc bool) {
|
|||||||
|
|
||||||
// Map common sort fields to database column names
|
// Map common sort fields to database column names
|
||||||
switch f.SortBy {
|
switch f.SortBy {
|
||||||
case "dateCreated":
|
case "created_at":
|
||||||
field = "date_created"
|
field = "created_at"
|
||||||
case "dateUpdated":
|
case "updated_at":
|
||||||
field = "date_updated"
|
field = "updated_at"
|
||||||
case "username":
|
case "username":
|
||||||
field = "username"
|
field = "username"
|
||||||
case "email":
|
case "email":
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ type PermissionInfo struct {
|
|||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
System bool `json:"system"`
|
System bool `json:"system"`
|
||||||
RoleCount int64 `json:"roleCount"`
|
RoleCount int64 `json:"roleCount"`
|
||||||
DateCreated string `json:"dateCreated"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeCreate is called before creating a permission
|
// BeforeCreate is called before creating a permission
|
||||||
@@ -132,7 +132,7 @@ func (p *Permission) ToPermissionInfo() PermissionInfo {
|
|||||||
Category: p.Category,
|
Category: p.Category,
|
||||||
Active: p.Active,
|
Active: p.Active,
|
||||||
System: p.System,
|
System: p.System,
|
||||||
DateCreated: p.DateCreated.Format("2006-01-02T15:04:05Z"),
|
CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
148
local/model/project.go
Normal file
148
local/model/project.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Project represents a project in the system
|
||||||
|
type Project struct {
|
||||||
|
BaseModel
|
||||||
|
Name string `json:"name" gorm:"not null;type:varchar(255)"`
|
||||||
|
Description string `json:"description" gorm:"type:text"`
|
||||||
|
OwnerID string `json:"owner_id" gorm:"not null;type:uuid;index;references:users(id)"`
|
||||||
|
TypeID string `json:"type_id" gorm:"not null;type:uuid;index;references:types(id)"`
|
||||||
|
Owner User `json:"owner,omitempty" gorm:"foreignKey:OwnerID"`
|
||||||
|
Type Type `json:"type,omitempty" gorm:"foreignKey:TypeID"`
|
||||||
|
Tasks []Task `json:"tasks,omitempty" gorm:"foreignKey:ProjectID"`
|
||||||
|
Members []User `json:"members,omitempty" gorm:"many2many:project_members;"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectCreateRequest represents the request to create a new project
|
||||||
|
type ProjectCreateRequest struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
|
Description string `json:"description" validate:"max=1000"`
|
||||||
|
OwnerID string `json:"owner_id" validate:"required,uuid"`
|
||||||
|
TypeID string `json:"type_id" validate:"required,uuid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectUpdateRequest represents the request to update a project
|
||||||
|
type ProjectUpdateRequest struct {
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
|
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
|
||||||
|
TypeID *string `json:"type_id,omitempty" validate:"omitempty,uuid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectInfo represents public project information
|
||||||
|
type ProjectInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
OwnerID string `json:"owner_id"`
|
||||||
|
TypeID string `json:"type_id"`
|
||||||
|
Owner UserInfo `json:"owner,omitempty"`
|
||||||
|
Type TypeInfo `json:"type,omitempty"`
|
||||||
|
TaskCount int64 `json:"task_count"`
|
||||||
|
MemberCount int64 `json:"member_count"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectMember represents the many-to-many relationship between projects and users
|
||||||
|
type ProjectMember struct {
|
||||||
|
ProjectID string `json:"project_id" gorm:"type:uuid;primaryKey"`
|
||||||
|
UserID string `json:"user_id" gorm:"type:uuid;primaryKey"`
|
||||||
|
RoleID string `json:"role_id" gorm:"type:uuid;not null;references:roles(id)"`
|
||||||
|
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
|
||||||
|
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
Role Role `json:"role,omitempty" gorm:"foreignKey:RoleID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate is called before creating a project
|
||||||
|
func (p *Project) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
p.BaseModel.BeforeCreate()
|
||||||
|
|
||||||
|
// Normalize name and description
|
||||||
|
p.Name = strings.TrimSpace(p.Name)
|
||||||
|
p.Description = strings.TrimSpace(p.Description)
|
||||||
|
|
||||||
|
return p.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate is called before updating a project
|
||||||
|
func (p *Project) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
p.BaseModel.BeforeUpdate()
|
||||||
|
|
||||||
|
// Normalize fields if they're being updated
|
||||||
|
if p.Name != "" {
|
||||||
|
p.Name = strings.TrimSpace(p.Name)
|
||||||
|
}
|
||||||
|
if p.Description != "" {
|
||||||
|
p.Description = strings.TrimSpace(p.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates project data
|
||||||
|
func (p *Project) Validate() error {
|
||||||
|
if p.Name == "" {
|
||||||
|
return errors.New("name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.Name) > 255 {
|
||||||
|
return errors.New("name must not exceed 255 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.OwnerID == "" {
|
||||||
|
return errors.New("owner_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.TypeID == "" {
|
||||||
|
return errors.New("type_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToProjectInfo converts Project to ProjectInfo (public information)
|
||||||
|
func (p *Project) ToProjectInfo() ProjectInfo {
|
||||||
|
projectInfo := ProjectInfo{
|
||||||
|
ID: p.ID,
|
||||||
|
Name: p.Name,
|
||||||
|
Description: p.Description,
|
||||||
|
OwnerID: p.OwnerID,
|
||||||
|
TypeID: p.TypeID,
|
||||||
|
CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
UpdatedAt: p.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add owner info if loaded
|
||||||
|
if p.Owner.ID != "" {
|
||||||
|
projectInfo.Owner = p.Owner.ToUserInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add type info if loaded
|
||||||
|
if p.Type.ID != "" {
|
||||||
|
projectInfo.Type = p.Type.ToTypeInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasMember checks if a user is a member of the project
|
||||||
|
func (p *Project) HasMember(userID string) bool {
|
||||||
|
for _, member := range p.Members {
|
||||||
|
if member.ID == userID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOwner checks if a user is the owner of the project
|
||||||
|
func (p *Project) IsOwner(userID string) bool {
|
||||||
|
return p.OwnerID == userID
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ type RoleInfo struct {
|
|||||||
System bool `json:"system"`
|
System bool `json:"system"`
|
||||||
Permissions []PermissionInfo `json:"permissions"`
|
Permissions []PermissionInfo `json:"permissions"`
|
||||||
UserCount int64 `json:"userCount"`
|
UserCount int64 `json:"userCount"`
|
||||||
DateCreated string `json:"dateCreated"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeCreate is called before creating a role
|
// BeforeCreate is called before creating a role
|
||||||
@@ -120,7 +120,7 @@ func (r *Role) ToRoleInfo() RoleInfo {
|
|||||||
Active: r.Active,
|
Active: r.Active,
|
||||||
System: r.System,
|
System: r.System,
|
||||||
Permissions: make([]PermissionInfo, len(r.Permissions)),
|
Permissions: make([]PermissionInfo, len(r.Permissions)),
|
||||||
DateCreated: r.DateCreated.Format("2006-01-02T15:04:05Z"),
|
CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert permissions
|
// Convert permissions
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ type SecurityEventInfo struct {
|
|||||||
ResolverName string `json:"resolverName,omitempty"`
|
ResolverName string `json:"resolverName,omitempty"`
|
||||||
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
|
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
|
||||||
Notes string `json:"notes,omitempty"`
|
Notes string `json:"notes,omitempty"`
|
||||||
DateCreated string `json:"dateCreated"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeCreate is called before creating a security event
|
// BeforeCreate is called before creating a security event
|
||||||
@@ -202,19 +202,19 @@ func (se *SecurityEvent) ToSecurityEventInfo() SecurityEventInfo {
|
|||||||
ResolvedBy: se.ResolvedBy,
|
ResolvedBy: se.ResolvedBy,
|
||||||
ResolvedAt: se.ResolvedAt,
|
ResolvedAt: se.ResolvedAt,
|
||||||
Notes: se.Notes,
|
Notes: se.Notes,
|
||||||
DateCreated: se.DateCreated.Format("2006-01-02T15:04:05Z"),
|
CreatedAt: se.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include user information if available
|
// Include user information if available
|
||||||
if se.User != nil {
|
if se.User != nil {
|
||||||
info.UserEmail = se.User.Email
|
info.UserEmail = se.User.Email
|
||||||
info.UserName = se.User.Name
|
info.UserName = se.User.FullName
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include resolver information if available
|
// Include resolver information if available
|
||||||
if se.Resolver != nil {
|
if se.Resolver != nil {
|
||||||
info.ResolverEmail = se.Resolver.Email
|
info.ResolverEmail = se.Resolver.Email
|
||||||
info.ResolverName = se.Resolver.Name
|
info.ResolverName = se.Resolver.FullName
|
||||||
}
|
}
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ type SystemConfigInfo struct {
|
|||||||
DataType string `json:"dataType"`
|
DataType string `json:"dataType"`
|
||||||
IsEditable bool `json:"isEditable"`
|
IsEditable bool `json:"isEditable"`
|
||||||
IsSecret bool `json:"isSecret"`
|
IsSecret bool `json:"isSecret"`
|
||||||
DateCreated string `json:"dateCreated"`
|
CreatedAt string `json:"created_at"`
|
||||||
DateModified string `json:"dateModified"`
|
DateModified string `json:"dateModified"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ func (sc *SystemConfig) ToSystemConfigInfo() SystemConfigInfo {
|
|||||||
DataType: sc.DataType,
|
DataType: sc.DataType,
|
||||||
IsEditable: sc.IsEditable,
|
IsEditable: sc.IsEditable,
|
||||||
IsSecret: sc.IsSecret,
|
IsSecret: sc.IsSecret,
|
||||||
DateCreated: sc.DateCreated.Format("2006-01-02T15:04:05Z"),
|
CreatedAt: sc.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
DateModified: sc.DateModified,
|
DateModified: sc.DateModified,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
213
local/model/task.go
Normal file
213
local/model/task.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TaskStatus represents the status of a task
|
||||||
|
type TaskStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskStatusTodo TaskStatus = "todo"
|
||||||
|
TaskStatusInProgress TaskStatus = "in_progress"
|
||||||
|
TaskStatusDone TaskStatus = "done"
|
||||||
|
TaskStatusCanceled TaskStatus = "canceled"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TaskPriority represents the priority of a task
|
||||||
|
type TaskPriority string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskPriorityLow TaskPriority = "low"
|
||||||
|
TaskPriorityMedium TaskPriority = "medium"
|
||||||
|
TaskPriorityHigh TaskPriority = "high"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Task represents a task in the system
|
||||||
|
type Task struct {
|
||||||
|
BaseModel
|
||||||
|
Title string `json:"title" gorm:"not null;type:varchar(255)"`
|
||||||
|
Description string `json:"description" gorm:"type:text"`
|
||||||
|
Status TaskStatus `json:"status" gorm:"not null;default:'todo';type:varchar(20)"`
|
||||||
|
Priority TaskPriority `json:"priority" gorm:"not null;default:'medium';type:varchar(20)"`
|
||||||
|
ProjectID string `json:"project_id" gorm:"not null;type:uuid;index;references:projects(id);onDelete:CASCADE"`
|
||||||
|
DueDate *string `json:"due_date" gorm:"type:date"`
|
||||||
|
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
|
||||||
|
Assignees []User `json:"assignees,omitempty" gorm:"many2many:task_assignees;"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCreateRequest represents the request to create a new task
|
||||||
|
type TaskCreateRequest struct {
|
||||||
|
Title string `json:"title" validate:"required,min=1,max=255"`
|
||||||
|
Description string `json:"description" validate:"max=1000"`
|
||||||
|
Status TaskStatus `json:"status" validate:"omitempty,oneof=todo in_progress done canceled"`
|
||||||
|
Priority TaskPriority `json:"priority" validate:"omitempty,oneof=low medium high"`
|
||||||
|
ProjectID string `json:"project_id" validate:"required,uuid"`
|
||||||
|
DueDate *string `json:"due_date"`
|
||||||
|
AssigneeIDs []string `json:"assignee_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskUpdateRequest represents the request to update a task
|
||||||
|
type TaskUpdateRequest struct {
|
||||||
|
Title *string `json:"title,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
|
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
|
||||||
|
Status *TaskStatus `json:"status,omitempty" validate:"omitempty,oneof=todo in_progress done canceled"`
|
||||||
|
Priority *TaskPriority `json:"priority,omitempty" validate:"omitempty,oneof=low medium high"`
|
||||||
|
DueDate *string `json:"due_date,omitempty"`
|
||||||
|
AssigneeIDs []string `json:"assignee_ids,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskInfo represents public task information
|
||||||
|
type TaskInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Status TaskStatus `json:"status"`
|
||||||
|
Priority TaskPriority `json:"priority"`
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
|
DueDate *string `json:"due_date"`
|
||||||
|
Project ProjectInfo `json:"project,omitempty"`
|
||||||
|
Assignees []UserInfo `json:"assignees,omitempty"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskAssignee represents the many-to-many relationship between tasks and users
|
||||||
|
type TaskAssignee struct {
|
||||||
|
TaskID string `json:"task_id" gorm:"type:uuid;primaryKey"`
|
||||||
|
UserID string `json:"user_id" gorm:"type:uuid;primaryKey"`
|
||||||
|
Task Task `json:"task,omitempty" gorm:"foreignKey:TaskID"`
|
||||||
|
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate is called before creating a task
|
||||||
|
func (t *Task) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
t.BaseModel.BeforeCreate()
|
||||||
|
|
||||||
|
// Normalize title and description
|
||||||
|
t.Title = strings.TrimSpace(t.Title)
|
||||||
|
t.Description = strings.TrimSpace(t.Description)
|
||||||
|
|
||||||
|
// Set default values
|
||||||
|
if t.Status == "" {
|
||||||
|
t.Status = TaskStatusTodo
|
||||||
|
}
|
||||||
|
if t.Priority == "" {
|
||||||
|
t.Priority = TaskPriorityMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate is called before updating a task
|
||||||
|
func (t *Task) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
t.BaseModel.BeforeUpdate()
|
||||||
|
|
||||||
|
// Normalize fields if they're being updated
|
||||||
|
if t.Title != "" {
|
||||||
|
t.Title = strings.TrimSpace(t.Title)
|
||||||
|
}
|
||||||
|
if t.Description != "" {
|
||||||
|
t.Description = strings.TrimSpace(t.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates task data
|
||||||
|
func (t *Task) Validate() error {
|
||||||
|
if t.Title == "" {
|
||||||
|
return errors.New("title is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(t.Title) > 255 {
|
||||||
|
return errors.New("title must not exceed 255 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.ProjectID == "" {
|
||||||
|
return errors.New("project_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate status
|
||||||
|
if t.Status != "" {
|
||||||
|
switch t.Status {
|
||||||
|
case TaskStatusTodo, TaskStatusInProgress, TaskStatusDone, TaskStatusCanceled:
|
||||||
|
// Valid status
|
||||||
|
default:
|
||||||
|
return errors.New("invalid status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate priority
|
||||||
|
if t.Priority != "" {
|
||||||
|
switch t.Priority {
|
||||||
|
case TaskPriorityLow, TaskPriorityMedium, TaskPriorityHigh:
|
||||||
|
// Valid priority
|
||||||
|
default:
|
||||||
|
return errors.New("invalid priority")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTaskInfo converts Task to TaskInfo (public information)
|
||||||
|
func (t *Task) ToTaskInfo() TaskInfo {
|
||||||
|
taskInfo := TaskInfo{
|
||||||
|
ID: t.ID,
|
||||||
|
Title: t.Title,
|
||||||
|
Description: t.Description,
|
||||||
|
Status: t.Status,
|
||||||
|
Priority: t.Priority,
|
||||||
|
ProjectID: t.ProjectID,
|
||||||
|
DueDate: t.DueDate,
|
||||||
|
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
Assignees: make([]UserInfo, len(t.Assignees)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add project info if loaded
|
||||||
|
if t.Project.ID != "" {
|
||||||
|
taskInfo.Project = t.Project.ToProjectInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add assignee info if loaded
|
||||||
|
for i, assignee := range t.Assignees {
|
||||||
|
taskInfo.Assignees[i] = assignee.ToUserInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAssignedTo checks if a user is assigned to this task
|
||||||
|
func (t *Task) IsAssignedTo(userID string) bool {
|
||||||
|
for _, assignee := range t.Assignees {
|
||||||
|
if assignee.ID == userID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCompleted checks if the task is completed
|
||||||
|
func (t *Task) IsCompleted() bool {
|
||||||
|
return t.Status == TaskStatusDone
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCanceled checks if the task is canceled
|
||||||
|
func (t *Task) IsCanceled() bool {
|
||||||
|
return t.Status == TaskStatusCanceled
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInProgress checks if the task is in progress
|
||||||
|
func (t *Task) IsInProgress() bool {
|
||||||
|
return t.Status == TaskStatusInProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTodo checks if the task is todo
|
||||||
|
func (t *Task) IsTodo() bool {
|
||||||
|
return t.Status == TaskStatusTodo
|
||||||
|
}
|
||||||
95
local/model/type.go
Normal file
95
local/model/type.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type represents a project type in the system
|
||||||
|
type Type struct {
|
||||||
|
BaseModel
|
||||||
|
UserID *string `json:"user_id" gorm:"type:uuid;index;references:users(id);onDelete:SET NULL"`
|
||||||
|
Name string `json:"name" gorm:"not null;type:varchar(100)"`
|
||||||
|
Description string `json:"description" gorm:"type:text"`
|
||||||
|
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeCreateRequest represents the request to create a new type
|
||||||
|
type TypeCreateRequest struct {
|
||||||
|
UserID *string `json:"user_id"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=100"`
|
||||||
|
Description string `json:"description" validate:"max=1000"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeUpdateRequest represents the request to update a type
|
||||||
|
type TypeUpdateRequest struct {
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=100"`
|
||||||
|
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeInfo represents public type information
|
||||||
|
type TypeInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID *string `json:"user_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate is called before creating a type
|
||||||
|
func (t *Type) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
t.BaseModel.BeforeCreate()
|
||||||
|
|
||||||
|
// Normalize name
|
||||||
|
t.Name = strings.TrimSpace(t.Name)
|
||||||
|
t.Description = strings.TrimSpace(t.Description)
|
||||||
|
|
||||||
|
return t.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate is called before updating a type
|
||||||
|
func (t *Type) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
t.BaseModel.BeforeUpdate()
|
||||||
|
|
||||||
|
// Normalize fields if they're being updated
|
||||||
|
if t.Name != "" {
|
||||||
|
t.Name = strings.TrimSpace(t.Name)
|
||||||
|
}
|
||||||
|
if t.Description != "" {
|
||||||
|
t.Description = strings.TrimSpace(t.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates type data
|
||||||
|
func (t *Type) Validate() error {
|
||||||
|
if t.Name == "" {
|
||||||
|
return errors.New("name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(t.Name) > 100 {
|
||||||
|
return errors.New("name must not exceed 100 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(t.Description) > 1000 {
|
||||||
|
return errors.New("description must not exceed 1000 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTypeInfo converts Type to TypeInfo (public information)
|
||||||
|
func (t *Type) ToTypeInfo() TypeInfo {
|
||||||
|
return TypeInfo{
|
||||||
|
ID: t.ID,
|
||||||
|
UserID: t.UserID,
|
||||||
|
Name: t.Name,
|
||||||
|
Description: t.Description,
|
||||||
|
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,41 +14,97 @@ import (
|
|||||||
type User struct {
|
type User struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
|
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
|
||||||
Username string `json:"username" gorm:"unique;not null;type:varchar(100)"`
|
PasswordHash string `json:"-" gorm:"not null;type:varchar(255)"`
|
||||||
Name string `json:"name" gorm:"not null;type:varchar(255)"`
|
FullName string `json:"full_name" gorm:"type:varchar(255)"`
|
||||||
PasswordHash string `json:"-" gorm:"not null;type:text"`
|
|
||||||
Active bool `json:"active" gorm:"default:true"`
|
|
||||||
EmailVerified bool `json:"emailVerified" gorm:"default:false"`
|
|
||||||
EmailVerificationToken string `json:"-" gorm:"type:varchar(255)"`
|
|
||||||
PasswordResetToken string `json:"-" gorm:"type:varchar(255)"`
|
|
||||||
PasswordResetExpires *time.Time `json:"-"`
|
|
||||||
LastLogin *time.Time `json:"lastLogin"`
|
|
||||||
LoginAttempts int `json:"-" gorm:"default:0"`
|
|
||||||
LockedUntil *time.Time `json:"-"`
|
|
||||||
TwoFactorEnabled bool `json:"twoFactorEnabled" gorm:"default:false"`
|
|
||||||
TwoFactorSecret string `json:"-" gorm:"type:varchar(255)"`
|
|
||||||
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
|
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
|
||||||
AuditLogs []AuditLog `json:"-" gorm:"foreignKey:UserID"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserCreateRequest represents the request to create a new user
|
// UserCreateRequest represents the request to create a new user
|
||||||
type UserCreateRequest struct {
|
type UserCreateRequest struct {
|
||||||
Email string `json:"email" validate:"required,email"`
|
Email string `json:"email" validate:"required,email"`
|
||||||
Username string `json:"username" validate:"required,min=3,max=50"`
|
FullName string `json:"full_name" validate:"required,min=2,max=100"`
|
||||||
Name string `json:"name" validate:"required,min=2,max=100"`
|
|
||||||
Password string `json:"password" validate:"required,min=8"`
|
Password string `json:"password" validate:"required,min=8"`
|
||||||
RoleIDs []string `json:"roleIds"`
|
RoleIDs []string `json:"roleIds"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToUser converts UserCreateRequest to User domain model
|
||||||
|
func (req *UserCreateRequest) ToUser() (*User, error) {
|
||||||
|
user := &User{
|
||||||
|
Email: req.Email,
|
||||||
|
FullName: req.FullName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle password hashing
|
||||||
|
if err := user.SetPassword(req.Password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Roles will be set by the service layer after validation
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the UserCreateRequest
|
||||||
|
func (req *UserCreateRequest) Validate() error {
|
||||||
|
if req.Email == "" {
|
||||||
|
return errors.New("email is required")
|
||||||
|
}
|
||||||
|
if !isValidEmail(req.Email) {
|
||||||
|
return errors.New("invalid email format")
|
||||||
|
}
|
||||||
|
if len(req.Password) < 8 {
|
||||||
|
return errors.New("password must be at least 8 characters")
|
||||||
|
}
|
||||||
|
if req.FullName == "" {
|
||||||
|
return errors.New("full name is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// UserUpdateRequest represents the request to update a user
|
// UserUpdateRequest represents the request to update a user
|
||||||
type UserUpdateRequest struct {
|
type UserUpdateRequest struct {
|
||||||
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
||||||
Username *string `json:"username,omitempty" validate:"omitempty,min=3,max=50"`
|
FullName *string `json:"full_name,omitempty" validate:"omitempty,min=2,max=100"`
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=2,max=100"`
|
|
||||||
Active *bool `json:"active,omitempty"`
|
|
||||||
RoleIDs []string `json:"roleIds,omitempty"`
|
RoleIDs []string `json:"roleIds,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyToUser applies the UserUpdateRequest to an existing User
|
||||||
|
func (req *UserUpdateRequest) ApplyToUser(user *User) error {
|
||||||
|
if req.Email != nil {
|
||||||
|
user.Email = *req.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.FullName != nil {
|
||||||
|
user.FullName = *req.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Roles will be handled by the service layer
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the UserUpdateRequest
|
||||||
|
func (req *UserUpdateRequest) Validate() error {
|
||||||
|
if req.Email != nil && !isValidEmail(*req.Email) {
|
||||||
|
return errors.New("invalid email format")
|
||||||
|
}
|
||||||
|
if req.FullName != nil && len(*req.FullName) == 0 {
|
||||||
|
return errors.New("full name cannot be empty")
|
||||||
|
}
|
||||||
|
if req.FullName != nil && len(*req.FullName) > 255 {
|
||||||
|
return errors.New("full name must not exceed 255 characters")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserResponse represents the response when returning user data
|
||||||
|
type UserResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
FullName string `json:"fullName"`
|
||||||
|
Roles []string `json:"roles"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
// UserLoginRequest represents a login request
|
// UserLoginRequest represents a login request
|
||||||
type UserLoginRequest struct {
|
type UserLoginRequest struct {
|
||||||
Email string `json:"email" validate:"required,email"`
|
Email string `json:"email" validate:"required,email"`
|
||||||
@@ -67,14 +123,11 @@ type UserLoginResponse struct {
|
|||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Username string `json:"username"`
|
FullName string `json:"full_name"`
|
||||||
Name string `json:"name"`
|
|
||||||
Active bool `json:"active"`
|
|
||||||
EmailVerified bool `json:"emailVerified"`
|
|
||||||
LastLogin *time.Time `json:"lastLogin"`
|
|
||||||
Roles []RoleInfo `json:"roles"`
|
Roles []RoleInfo `json:"roles"`
|
||||||
Permissions []string `json:"permissions"`
|
Permissions []string `json:"permissions"`
|
||||||
DateCreated time.Time `json:"dateCreated"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangePasswordRequest represents a password change request
|
// ChangePasswordRequest represents a password change request
|
||||||
@@ -98,10 +151,9 @@ type ResetPasswordConfirmRequest struct {
|
|||||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||||
u.BaseModel.BeforeCreate()
|
u.BaseModel.BeforeCreate()
|
||||||
|
|
||||||
// Normalize email and username
|
// Normalize email and full name
|
||||||
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
|
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
|
||||||
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
|
u.FullName = strings.TrimSpace(u.FullName)
|
||||||
u.Name = strings.TrimSpace(u.Name)
|
|
||||||
|
|
||||||
return u.Validate()
|
return u.Validate()
|
||||||
}
|
}
|
||||||
@@ -114,11 +166,8 @@ func (u *User) BeforeUpdate(tx *gorm.DB) error {
|
|||||||
if u.Email != "" {
|
if u.Email != "" {
|
||||||
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
|
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
|
||||||
}
|
}
|
||||||
if u.Username != "" {
|
if u.FullName != "" {
|
||||||
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
|
u.FullName = strings.TrimSpace(u.FullName)
|
||||||
}
|
|
||||||
if u.Name != "" {
|
|
||||||
u.Name = strings.TrimSpace(u.Name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return u.Validate()
|
return u.Validate()
|
||||||
@@ -134,24 +183,8 @@ func (u *User) Validate() error {
|
|||||||
return errors.New("invalid email format")
|
return errors.New("invalid email format")
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.Username == "" {
|
if u.FullName != "" && len(u.FullName) > 255 {
|
||||||
return errors.New("username is required")
|
return errors.New("full name must not exceed 255 characters")
|
||||||
}
|
|
||||||
|
|
||||||
if len(u.Username) < 3 || len(u.Username) > 50 {
|
|
||||||
return errors.New("username must be between 3 and 50 characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isValidUsername(u.Username) {
|
|
||||||
return errors.New("username can only contain letters, numbers, underscores, and hyphens")
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Name == "" {
|
|
||||||
return errors.New("name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(u.Name) < 2 || len(u.Name) > 100 {
|
|
||||||
return errors.New("name must be between 2 and 100 characters")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -182,53 +215,14 @@ func (u *User) VerifyPassword(plainPassword string) bool {
|
|||||||
return u.CheckPassword(plainPassword)
|
return u.CheckPassword(plainPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLocked checks if the user account is locked
|
|
||||||
func (u *User) IsLocked() bool {
|
|
||||||
if u.LockedUntil == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return time.Now().Before(*u.LockedUntil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock locks the user account for the specified duration
|
|
||||||
func (u *User) Lock(duration time.Duration) {
|
|
||||||
lockUntil := time.Now().Add(duration)
|
|
||||||
u.LockedUntil = &lockUntil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock unlocks the user account
|
|
||||||
func (u *User) Unlock() {
|
|
||||||
u.LockedUntil = nil
|
|
||||||
u.LoginAttempts = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrementLoginAttempts increments the login attempt counter
|
|
||||||
func (u *User) IncrementLoginAttempts() {
|
|
||||||
u.LoginAttempts++
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetLoginAttempts resets the login attempt counter
|
|
||||||
func (u *User) ResetLoginAttempts() {
|
|
||||||
u.LoginAttempts = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateLastLogin updates the last login timestamp
|
|
||||||
func (u *User) UpdateLastLogin() {
|
|
||||||
now := time.Now()
|
|
||||||
u.LastLogin = &now
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToUserInfo converts User to UserInfo (public information)
|
// ToUserInfo converts User to UserInfo (public information)
|
||||||
func (u *User) ToUserInfo() UserInfo {
|
func (u *User) ToUserInfo() UserInfo {
|
||||||
userInfo := UserInfo{
|
userInfo := UserInfo{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
Email: u.Email,
|
Email: u.Email,
|
||||||
Username: u.Username,
|
FullName: u.FullName,
|
||||||
Name: u.Name,
|
CreatedAt: u.CreatedAt,
|
||||||
Active: u.Active,
|
UpdatedAt: u.UpdatedAt,
|
||||||
EmailVerified: u.EmailVerified,
|
|
||||||
LastLogin: u.LastLogin,
|
|
||||||
DateCreated: u.DateCreated,
|
|
||||||
Roles: make([]RoleInfo, len(u.Roles)),
|
Roles: make([]RoleInfo, len(u.Roles)),
|
||||||
Permissions: []string{},
|
Permissions: []string{},
|
||||||
}
|
}
|
||||||
@@ -250,6 +244,23 @@ func (u *User) ToUserInfo() UserInfo {
|
|||||||
return userInfo
|
return userInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToResponse converts User to UserResponse (for API responses)
|
||||||
|
func (u *User) ToResponse() *UserResponse {
|
||||||
|
roleNames := make([]string, len(u.Roles))
|
||||||
|
for i, role := range u.Roles {
|
||||||
|
roleNames[i] = role.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserResponse{
|
||||||
|
ID: u.ID,
|
||||||
|
Email: u.Email,
|
||||||
|
FullName: u.FullName,
|
||||||
|
Roles: roleNames,
|
||||||
|
CreatedAt: u.CreatedAt,
|
||||||
|
UpdatedAt: u.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HasRole checks if the user has a specific role
|
// HasRole checks if the user has a specific role
|
||||||
func (u *User) HasRole(roleName string) bool {
|
func (u *User) HasRole(roleName string) bool {
|
||||||
for _, role := range u.Roles {
|
for _, role := range u.Roles {
|
||||||
@@ -278,12 +289,6 @@ func isValidEmail(email string) bool {
|
|||||||
return emailRegex.MatchString(email)
|
return emailRegex.MatchString(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isValidUsername validates username format
|
|
||||||
func isValidUsername(username string) bool {
|
|
||||||
usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
|
||||||
return usernameRegex.MatchString(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validatePassword validates password strength
|
// validatePassword validates password strength
|
||||||
func validatePassword(password string) error {
|
func validatePassword(password string) error {
|
||||||
if len(password) < 8 {
|
if len(password) < 8 {
|
||||||
@@ -294,25 +299,5 @@ func validatePassword(password string) error {
|
|||||||
return errors.New("password must not exceed 128 characters")
|
return errors.New("password must not exceed 128 characters")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for at least one lowercase letter
|
|
||||||
if matched, _ := regexp.MatchString(`[a-z]`, password); !matched {
|
|
||||||
return errors.New("password must contain at least one lowercase letter")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for at least one uppercase letter
|
|
||||||
if matched, _ := regexp.MatchString(`[A-Z]`, password); !matched {
|
|
||||||
return errors.New("password must contain at least one uppercase letter")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for at least one digit
|
|
||||||
if matched, _ := regexp.MatchString(`\d`, password); !matched {
|
|
||||||
return errors.New("password must contain at least one digit")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for at least one special character
|
|
||||||
if matched, _ := regexp.MatchString(`[!@#$%^&*(),.?":{}|<>]`, password); !matched {
|
|
||||||
return errors.New("password must contain at least one special character")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ func NewMembershipRepository(db *gorm.DB) *MembershipRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindUserByUsername finds a user by their username.
|
// FindUserByEmail finds a user by their email.
|
||||||
// It preloads the user's role and the role's permissions.
|
// It preloads the user's role and the role's permissions.
|
||||||
func (r *MembershipRepository) FindUserByUsername(ctx context.Context, username string) (*model.User, error) {
|
func (r *MembershipRepository) FindUserByEmail(ctx context.Context, email string) (*model.User, error) {
|
||||||
var user model.User
|
var user model.User
|
||||||
db := r.db.WithContext(ctx)
|
db := r.db.WithContext(ctx)
|
||||||
err := db.Preload("Roles.Permissions").Where("username = ?", username).First(&user).Error
|
err := db.Preload("Roles.Permissions").Where("email = ?", email).First(&user).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"omega-server/local/utl/jwt"
|
"omega-server/local/utl/jwt"
|
||||||
"omega-server/local/utl/logging"
|
"omega-server/local/utl/logging"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@@ -38,8 +39,8 @@ func (s *MembershipService) SetCacheInvalidator(invalidator CacheInvalidator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Login authenticates a user and returns a JWT.
|
// Login authenticates a user and returns a JWT.
|
||||||
func (s *MembershipService) Login(ctx context.Context, username, password string) (string, error) {
|
func (s *MembershipService) Login(ctx context.Context, email, password string) (string, error) {
|
||||||
user, err := s.repo.FindUserByUsername(ctx, username)
|
user, err := s.repo.FindUserByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("invalid credentials")
|
return "", errors.New("invalid credentials")
|
||||||
}
|
}
|
||||||
@@ -55,38 +56,42 @@ func (s *MembershipService) Login(ctx context.Context, username, password string
|
|||||||
roleNames[i] = role.Name
|
roleNames[i] = role.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
return jwt.GenerateToken(user.ID, user.Email, user.Username, roleNames)
|
return jwt.GenerateToken(user.ID, user.Email, user.FullName, roleNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user.
|
// CreateUser creates a new user.
|
||||||
func (s *MembershipService) CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error) {
|
func (s *MembershipService) CreateUser(ctx context.Context, user *model.User, roleIDs []string) (*model.User, error) {
|
||||||
|
// Validate domain model
|
||||||
role, err := s.repo.FindRoleByName(ctx, roleName)
|
if err := user.Validate(); err != nil {
|
||||||
if err != nil {
|
|
||||||
logging.Error("Failed to find role by name: %v", err)
|
|
||||||
return nil, errors.New("role not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
user := &model.User{
|
|
||||||
Username: username,
|
|
||||||
Email: username + "@example.com", // You may want to accept email as parameter
|
|
||||||
Name: username,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set password using the model's SetPassword method
|
|
||||||
if err := user.SetPassword(password); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign roles
|
// Handle roles
|
||||||
user.Roles = []model.Role{*role}
|
if len(roleIDs) > 0 {
|
||||||
|
roles := make([]model.Role, 0, len(roleIDs))
|
||||||
|
for _, roleID := range roleIDs {
|
||||||
|
role, err := s.repo.FindRoleByName(ctx, roleID)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error("Failed to find role by name: %v", err)
|
||||||
|
return nil, errors.New("role not found: " + roleID)
|
||||||
|
}
|
||||||
|
roles = append(roles, *role)
|
||||||
|
}
|
||||||
|
user.Roles = roles
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
if err := s.repo.CreateUser(ctx, user); err != nil {
|
if err := s.repo.CreateUser(ctx, user); err != nil {
|
||||||
logging.Error("Failed to create user: %v", err)
|
logging.Error("Failed to create user: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logging.InfoOperation("USER_CREATE", "Created user: "+user.Username+" (ID: "+user.ID+", Role: "+roleName+")")
|
// Log with role names
|
||||||
|
roleNames := make([]string, len(user.Roles))
|
||||||
|
for i, role := range user.Roles {
|
||||||
|
roleNames[i] = role.Name
|
||||||
|
}
|
||||||
|
logging.InfoOperation("USER_CREATE", "Created user: "+user.Email+" (ID: "+user.ID+", Roles: "+strings.Join(roleNames, ", ")+")")
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,13 +110,6 @@ func (s *MembershipService) GetUserWithPermissions(ctx context.Context, userID s
|
|||||||
return s.repo.FindUserByIDWithPermissions(ctx, userID)
|
return s.repo.FindUserByIDWithPermissions(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUserRequest defines the request body for updating a user.
|
|
||||||
type UpdateUserRequest struct {
|
|
||||||
Username *string `json:"username"`
|
|
||||||
Password *string `json:"password"`
|
|
||||||
RoleID *string `json:"roleId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteUser deletes a user with validation to prevent Super Admin deletion.
|
// DeleteUser deletes a user with validation to prevent Super Admin deletion.
|
||||||
func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) error {
|
func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) error {
|
||||||
// Get user with role information
|
// Get user with role information
|
||||||
@@ -142,34 +140,42 @@ func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser updates a user's details.
|
// UpdateUser updates a user's details.
|
||||||
func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error) {
|
func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, req *model.UserUpdateRequest) (*model.User, error) {
|
||||||
|
// Validate request
|
||||||
|
if err := req.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
user, err := s.repo.FindUserByID(ctx, userID)
|
user, err := s.repo.FindUserByID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("user not found")
|
return nil, errors.New("user not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Username != nil {
|
// Apply update request to user
|
||||||
user.Username = *req.Username
|
if err := req.ApplyToUser(user); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
if req.Password != nil && *req.Password != "" {
|
|
||||||
// Use the model's SetPassword method to hash the password
|
|
||||||
if err := user.SetPassword(*req.Password); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if req.RoleID != nil {
|
// Handle roles if provided
|
||||||
// Check if role exists
|
if len(req.RoleIDs) > 0 {
|
||||||
roleUUID, err := uuid.Parse(*req.RoleID)
|
roles := make([]model.Role, 0, len(req.RoleIDs))
|
||||||
|
for _, roleID := range req.RoleIDs {
|
||||||
|
roleUUID, err := uuid.Parse(roleID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("invalid role ID format")
|
return nil, errors.New("invalid role ID format: " + roleID)
|
||||||
}
|
}
|
||||||
role, err := s.repo.FindRoleByID(ctx, roleUUID)
|
role, err := s.repo.FindRoleByID(ctx, roleUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("role not found")
|
return nil, errors.New("role not found: " + roleID)
|
||||||
}
|
}
|
||||||
user.Roles = []model.Role{*role}
|
roles = append(roles, *role)
|
||||||
|
}
|
||||||
|
user.Roles = roles
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate updated user
|
||||||
|
if err := user.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.repo.UpdateUser(ctx, user); err != nil {
|
if err := s.repo.UpdateUser(ctx, user); err != nil {
|
||||||
@@ -177,11 +183,11 @@ func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate cache if role was changed
|
// Invalidate cache if role was changed
|
||||||
if req.RoleID != nil && s.cacheInvalidator != nil {
|
if len(req.RoleIDs) > 0 && s.cacheInvalidator != nil {
|
||||||
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
|
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
logging.InfoOperation("USER_UPDATE", "Updated user: "+user.Username+" (ID: "+user.ID+")")
|
logging.InfoOperation("USER_UPDATE", "Updated user: "+user.Email+" (ID: "+user.ID+")")
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,10 +295,17 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a default admin user if one doesn't exist
|
// Create a default admin user if one doesn't exist
|
||||||
_, err = s.repo.FindUserByUsername(ctx, "admin")
|
_, err = s.repo.FindUserByEmail(ctx, "admin@example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Debug("Creating default admin user")
|
logging.Debug("Creating default admin user")
|
||||||
_, err = s.CreateUser(ctx, "admin", os.Getenv("PASSWORD"), "Super Admin") // Default password, should be changed
|
adminUser := &model.User{
|
||||||
|
Email: "admin@example.com",
|
||||||
|
FullName: "System Administrator",
|
||||||
|
}
|
||||||
|
if err := adminUser.SetPassword(os.Getenv("PASSWORD")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = s.CreateUser(ctx, adminUser, []string{"Super Admin"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"omega-server/local/model"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MembershipServiceInterface defines the interface for membership-related operations
|
|
||||||
type MembershipServiceInterface interface {
|
|
||||||
// Authentication and Authorization
|
|
||||||
Login(ctx context.Context, username, password string) (string, error)
|
|
||||||
HasPermission(ctx context.Context, userID string, permissionName string) (bool, error)
|
|
||||||
GetUserWithPermissions(ctx context.Context, userID string) (*model.User, error)
|
|
||||||
SetCacheInvalidator(invalidator CacheInvalidator)
|
|
||||||
|
|
||||||
// User Management
|
|
||||||
CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error)
|
|
||||||
ListUsers(ctx context.Context) ([]*model.User, error)
|
|
||||||
GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error)
|
|
||||||
DeleteUser(ctx context.Context, userID uuid.UUID) error
|
|
||||||
UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error)
|
|
||||||
|
|
||||||
// Role Management
|
|
||||||
GetAllRoles(ctx context.Context) ([]*model.Role, error)
|
|
||||||
SetupInitialData(ctx context.Context) error
|
|
||||||
}
|
|
||||||
@@ -107,7 +107,7 @@ func DefaultPagination() PaginationRequest {
|
|||||||
return PaginationRequest{
|
return PaginationRequest{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
Limit: 10,
|
Limit: 10,
|
||||||
Sort: "dateCreated",
|
Sort: "created_at",
|
||||||
Order: "desc",
|
Order: "desc",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ func (p *PaginationRequest) Validate() {
|
|||||||
p.Limit = 10
|
p.Limit = 10
|
||||||
}
|
}
|
||||||
if p.Sort == "" {
|
if p.Sort == "" {
|
||||||
p.Sort = "dateCreated"
|
p.Sort = "created_at"
|
||||||
}
|
}
|
||||||
if p.Order != "asc" && p.Order != "desc" {
|
if p.Order != "asc" && p.Order != "desc" {
|
||||||
p.Order = "desc"
|
p.Order = "desc"
|
||||||
|
|||||||
@@ -1,23 +1,53 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"omega-server/local/model"
|
"omega-server/local/model"
|
||||||
"omega-server/local/utl/logging"
|
"omega-server/local/utl/logging"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/dig"
|
"go.uber.org/dig"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Start(di *dig.Container) {
|
func Start(di *dig.Container) {
|
||||||
|
// PostgreSQL connection configuration
|
||||||
|
host := os.Getenv("DB_HOST")
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
port := os.Getenv("DB_PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "5432"
|
||||||
|
}
|
||||||
|
|
||||||
|
user := os.Getenv("DB_USER")
|
||||||
|
if user == "" {
|
||||||
|
user = "postgres"
|
||||||
|
}
|
||||||
|
|
||||||
|
password := os.Getenv("DB_PASSWORD")
|
||||||
|
if password == "" {
|
||||||
|
password = "password"
|
||||||
|
}
|
||||||
|
|
||||||
dbName := os.Getenv("DB_NAME")
|
dbName := os.Getenv("DB_NAME")
|
||||||
if dbName == "" {
|
if dbName == "" {
|
||||||
dbName = "app.db"
|
dbName = "omega_db"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sslMode := os.Getenv("DB_SSL_MODE")
|
||||||
|
if sslMode == "" {
|
||||||
|
sslMode = "disable"
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=UTC",
|
||||||
|
host, user, password, dbName, port, sslMode)
|
||||||
|
|
||||||
// Configure GORM logger
|
// Configure GORM logger
|
||||||
gormLogger := logger.Default
|
gormLogger := logger.Default
|
||||||
if os.Getenv("LOG_LEVEL") == "DEBUG" {
|
if os.Getenv("LOG_LEVEL") == "DEBUG" {
|
||||||
@@ -26,10 +56,14 @@ func Start(di *dig.Container) {
|
|||||||
gormLogger = logger.Default.LogMode(logger.Silent)
|
gormLogger = logger.Default.LogMode(logger.Silent)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||||
Logger: gormLogger,
|
Logger: gormLogger,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logging.Error("Failed to connect to PostgreSQL database")
|
||||||
|
logging.Error("Connection string: host=%s user=%s dbname=%s port=%s sslmode=%s", host, user, dbName, port, sslMode)
|
||||||
|
logging.Error("Error: %v", err)
|
||||||
|
logging.Error("Make sure PostgreSQL is running and the database exists")
|
||||||
logging.Panic("failed to connect database: " + err.Error())
|
logging.Panic("failed to connect database: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +96,12 @@ func Migrate(db *gorm.DB) {
|
|||||||
&model.User{},
|
&model.User{},
|
||||||
&model.Role{},
|
&model.Role{},
|
||||||
&model.Permission{},
|
&model.Permission{},
|
||||||
|
&model.Type{},
|
||||||
|
&model.Project{},
|
||||||
|
&model.Task{},
|
||||||
|
&model.Integration{},
|
||||||
|
&model.ProjectMember{},
|
||||||
|
&model.TaskAssignee{},
|
||||||
&model.SystemConfig{},
|
&model.SystemConfig{},
|
||||||
&model.AuditLog{},
|
&model.AuditLog{},
|
||||||
&model.SecurityEvent{},
|
&model.SecurityEvent{},
|
||||||
@@ -87,6 +127,9 @@ func Seed(db *gorm.DB) error {
|
|||||||
if err := seedDefaultAdmin(db); err != nil {
|
if err := seedDefaultAdmin(db); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := seedDefaultTypes(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := seedSystemConfigs(db); err != nil {
|
if err := seedSystemConfigs(db); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -193,6 +236,31 @@ func seedPermissions(db *gorm.DB) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func seedDefaultTypes(db *gorm.DB) error {
|
||||||
|
defaultTypes := []model.Type{
|
||||||
|
{Name: "Web Development", Description: "Standard web development project"},
|
||||||
|
{Name: "Mobile App", Description: "Mobile application development"},
|
||||||
|
{Name: "API Development", Description: "API and backend service development"},
|
||||||
|
{Name: "Data Science", Description: "Data analysis and machine learning projects"},
|
||||||
|
{Name: "DevOps", Description: "Infrastructure and deployment projects"},
|
||||||
|
{Name: "Research", Description: "Research and documentation projects"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, projectType := range defaultTypes {
|
||||||
|
var existingType model.Type
|
||||||
|
err := db.Where("name = ? AND user_id IS NULL", projectType.Name).First(&existingType).Error
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
projectType.Init()
|
||||||
|
if err := db.Create(&projectType).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logging.Info("Created default project type: %s", projectType.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func seedDefaultAdmin(db *gorm.DB) error {
|
func seedDefaultAdmin(db *gorm.DB) error {
|
||||||
// Check if admin user already exists
|
// Check if admin user already exists
|
||||||
var existingAdmin model.User
|
var existingAdmin model.User
|
||||||
@@ -215,9 +283,8 @@ func seedDefaultAdmin(db *gorm.DB) error {
|
|||||||
|
|
||||||
admin := model.User{
|
admin := model.User{
|
||||||
Email: "admin@example.com",
|
Email: "admin@example.com",
|
||||||
Username: "admin",
|
FullName: "System Administrator",
|
||||||
Name: "System Administrator",
|
PasswordHash: "",
|
||||||
Active: true,
|
|
||||||
}
|
}
|
||||||
admin.Init()
|
admin.Init()
|
||||||
|
|
||||||
|
|||||||
369
tests/README.md
Normal file
369
tests/README.md
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
# Testing Module
|
||||||
|
|
||||||
|
This directory contains the comprehensive testing framework for the Omega server application. The testing module is organized to support unit tests, integration tests, GraphQL tests, and provides utilities for creating test fixtures and managing test data.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── README.md # This file
|
||||||
|
├── testing.go # Main testing utilities and test suite
|
||||||
|
├── fixtures/ # Test data fixtures
|
||||||
|
│ └── fixtures.go # Predefined test data
|
||||||
|
├── unit/ # Unit testing utilities
|
||||||
|
│ └── unit_test_utils.go # Unit test helpers and assertions
|
||||||
|
├── integration/ # Integration testing utilities
|
||||||
|
│ └── integration_test_utils.go # HTTP request testing and API integration
|
||||||
|
└── graphql/ # GraphQL testing utilities
|
||||||
|
└── graphql_test_utils.go # GraphQL query execution and testing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Before running tests, ensure you have:
|
||||||
|
|
||||||
|
1. **PostgreSQL Test Database**: Set up a dedicated test database
|
||||||
|
2. **Environment Variables**: Configure test-specific environment variables
|
||||||
|
3. **Go Testing Tools**: Standard Go testing framework
|
||||||
|
|
||||||
|
### Environment Configuration
|
||||||
|
|
||||||
|
Set the following environment variables for testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test Database Configuration
|
||||||
|
TEST_DB_HOST=localhost
|
||||||
|
TEST_DB_PORT=5432
|
||||||
|
TEST_DB_USER=postgres
|
||||||
|
TEST_DB_PASSWORD=password
|
||||||
|
TEST_DB_NAME=omega_test
|
||||||
|
TEST_DB_SSL_MODE=disable
|
||||||
|
|
||||||
|
# Optional: Test-specific configurations
|
||||||
|
LOG_LEVEL=DEBUG
|
||||||
|
DEFAULT_ADMIN_PASSWORD=testpassword123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run tests with verbose output
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Run specific test package
|
||||||
|
go test ./tests/unit/...
|
||||||
|
go test ./tests/integration/...
|
||||||
|
go test ./tests/graphql/...
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
go test -cover ./...
|
||||||
|
|
||||||
|
# Run tests with detailed coverage report
|
||||||
|
go test -coverprofile=coverage.out ./...
|
||||||
|
go tool cover -html=coverage.out
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Categories
|
||||||
|
|
||||||
|
### 1. Unit Tests (`tests/unit/`)
|
||||||
|
|
||||||
|
Unit tests focus on testing individual components in isolation:
|
||||||
|
|
||||||
|
- **Model validation**: Testing model methods and validation logic
|
||||||
|
- **Service layer**: Testing business logic without external dependencies
|
||||||
|
- **Utility functions**: Testing helper functions and utilities
|
||||||
|
- **Repository layer**: Testing data access logic with mocked dependencies
|
||||||
|
|
||||||
|
**Example Unit Test Structure:**
|
||||||
|
```go
|
||||||
|
func TestUserValidation(t *testing.T) {
|
||||||
|
utils := unit.NewUnitTestUtils(nil)
|
||||||
|
|
||||||
|
user := utils.MockUser("test-id", "test@example.com", "Test User")
|
||||||
|
|
||||||
|
err := user.Validate()
|
||||||
|
utils.AssertNoError(t, err)
|
||||||
|
utils.AssertStringEqual(t, "test@example.com", user.Email)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Integration Tests (`tests/integration/`)
|
||||||
|
|
||||||
|
Integration tests verify the interaction between different components:
|
||||||
|
|
||||||
|
- **API endpoints**: Testing complete HTTP request/response cycles
|
||||||
|
- **Database operations**: Testing real database interactions
|
||||||
|
- **Authentication flows**: Testing JWT token generation and validation
|
||||||
|
- **Business workflows**: Testing complete user scenarios
|
||||||
|
|
||||||
|
**Example Integration Test Structure:**
|
||||||
|
```go
|
||||||
|
func TestCreateUserAPI(t *testing.T) {
|
||||||
|
testSuite := tests.NewTestSuite()
|
||||||
|
testSuite.Setup(t)
|
||||||
|
defer testSuite.Teardown(t)
|
||||||
|
|
||||||
|
utils := integration.NewIntegrationTestUtils(app, testSuite.DB, membershipService)
|
||||||
|
|
||||||
|
req := integration.TestRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "/v1/users",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"email": "new@example.com",
|
||||||
|
"fullName": "New User",
|
||||||
|
"password": "password123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := utils.ExecuteRequest(t, req)
|
||||||
|
utils.AssertStatusCode(t, resp, 201)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. GraphQL Tests (`tests/graphql/`)
|
||||||
|
|
||||||
|
GraphQL tests specifically target the GraphQL API layer:
|
||||||
|
|
||||||
|
- **Query execution**: Testing GraphQL queries and mutations
|
||||||
|
- **Schema validation**: Ensuring GraphQL schema correctness
|
||||||
|
- **Error handling**: Testing GraphQL error responses
|
||||||
|
- **Authentication**: Testing GraphQL with JWT authentication
|
||||||
|
|
||||||
|
**Example GraphQL Test Structure:**
|
||||||
|
```go
|
||||||
|
func TestGraphQLLogin(t *testing.T) {
|
||||||
|
testSuite := tests.NewTestSuite()
|
||||||
|
testSuite.Setup(t)
|
||||||
|
defer testSuite.Teardown(t)
|
||||||
|
|
||||||
|
utils := graphql.NewGraphQLTestUtils(testSuite.DB, membershipService)
|
||||||
|
|
||||||
|
response := utils.ExecuteLoginMutation(t, "admin@example.com", "password123")
|
||||||
|
|
||||||
|
utils.AssertNoErrors(t, response)
|
||||||
|
utils.AssertDataNotNil(t, response)
|
||||||
|
|
||||||
|
token := utils.ExtractString(t, response, "login", "token")
|
||||||
|
utils.AssertNotEmpty(t, token)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Utilities
|
||||||
|
|
||||||
|
### TestSuite (`testing.go`)
|
||||||
|
|
||||||
|
The main test suite provides:
|
||||||
|
|
||||||
|
- **Database Setup**: Automatic test database configuration
|
||||||
|
- **Migration Management**: Running migrations for tests
|
||||||
|
- **Cleanup**: Automatic cleanup after tests
|
||||||
|
- **Dependency Injection**: DI container setup for tests
|
||||||
|
- **Helper Methods**: Common test operations
|
||||||
|
|
||||||
|
```go
|
||||||
|
testSuite := tests.NewTestSuite()
|
||||||
|
testSuite.Setup(t)
|
||||||
|
defer testSuite.Teardown(t)
|
||||||
|
|
||||||
|
// Create test data
|
||||||
|
user := testSuite.CreateTestUser(t, "test@example.com", "Test User")
|
||||||
|
project := testSuite.CreateTestProject(t, "Test Project", "Description", user.ID, typeID)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fixtures (`fixtures/`)
|
||||||
|
|
||||||
|
Predefined test data for consistent testing:
|
||||||
|
|
||||||
|
- **User Fixtures**: Standard test users with different roles
|
||||||
|
- **Project Fixtures**: Sample projects with various configurations
|
||||||
|
- **Task Fixtures**: Tasks in different states and priorities
|
||||||
|
- **Integration Fixtures**: Sample third-party integrations
|
||||||
|
- **GraphQL Queries**: Common GraphQL operations
|
||||||
|
|
||||||
|
```go
|
||||||
|
fixtures := fixtures.NewFixtures()
|
||||||
|
users := fixtures.Users()
|
||||||
|
adminUser := users["admin"]
|
||||||
|
testProject := fixtures.Projects()["omega_project"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
|
||||||
|
Each test utility provides rich assertion methods:
|
||||||
|
|
||||||
|
- **Model Assertions**: Compare model instances
|
||||||
|
- **Response Assertions**: Validate HTTP responses
|
||||||
|
- **GraphQL Assertions**: Check GraphQL responses
|
||||||
|
- **Database Assertions**: Verify database state
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Test Organization
|
||||||
|
|
||||||
|
- **One test file per source file**: Mirror the source code structure
|
||||||
|
- **Descriptive test names**: Use `TestFunctionName_Scenario_ExpectedBehavior` format
|
||||||
|
- **Group related tests**: Use subtests with `t.Run()` for related scenarios
|
||||||
|
|
||||||
|
### 2. Test Data Management
|
||||||
|
|
||||||
|
- **Use fixtures**: Leverage predefined fixtures for consistent test data
|
||||||
|
- **Clean up**: Always clean up test data after tests
|
||||||
|
- **Isolation**: Ensure tests don't depend on each other
|
||||||
|
- **Transactions**: Use database transactions for rollback capabilities
|
||||||
|
|
||||||
|
### 3. Mocking and Dependencies
|
||||||
|
|
||||||
|
- **Mock external services**: Don't make real API calls in tests
|
||||||
|
- **Inject dependencies**: Use dependency injection for testability
|
||||||
|
- **Test doubles**: Use appropriate test doubles (mocks, stubs, fakes)
|
||||||
|
|
||||||
|
### 4. Error Testing
|
||||||
|
|
||||||
|
- **Test error paths**: Ensure error conditions are properly tested
|
||||||
|
- **Validate error messages**: Check that error messages are meaningful
|
||||||
|
- **Edge cases**: Test boundary conditions and edge cases
|
||||||
|
|
||||||
|
### 5. Performance Considerations
|
||||||
|
|
||||||
|
- **Fast tests**: Keep unit tests fast (< 100ms each)
|
||||||
|
- **Parallel execution**: Use `t.Parallel()` where appropriate
|
||||||
|
- **Database optimization**: Use transactions and minimal data for speed
|
||||||
|
|
||||||
|
## Test Patterns
|
||||||
|
|
||||||
|
### 1. Table-Driven Tests
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestUserValidation(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
email string
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{"valid email", "test@example.com", ""},
|
||||||
|
{"invalid email", "invalid-email", "invalid email format"},
|
||||||
|
{"empty email", "", "email is required"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
user := &model.User{Email: tc.email}
|
||||||
|
err := user.Validate()
|
||||||
|
|
||||||
|
if tc.expectedErr == "" {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
assert.EqualError(t, err, tc.expectedErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Setup and Teardown
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestUserService(t *testing.T) {
|
||||||
|
testSuite := tests.NewTestSuite()
|
||||||
|
testSuite.Setup(t)
|
||||||
|
defer testSuite.Teardown(t)
|
||||||
|
|
||||||
|
t.Run("CreateUser", func(t *testing.T) {
|
||||||
|
// Test implementation
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetUser", func(t *testing.T) {
|
||||||
|
// Test implementation
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Database Transactions
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestDatabaseOperations(t *testing.T) {
|
||||||
|
testSuite := tests.NewTestSuite()
|
||||||
|
testSuite.Setup(t)
|
||||||
|
defer testSuite.Teardown(t)
|
||||||
|
|
||||||
|
testSuite.RunInTestTransaction(t, func(tx *gorm.DB) {
|
||||||
|
// Database operations here will be rolled back
|
||||||
|
user := &model.User{Email: "test@example.com"}
|
||||||
|
tx.Create(user)
|
||||||
|
|
||||||
|
// Assertions
|
||||||
|
var count int64
|
||||||
|
tx.Model(&model.User{}).Where("email = ?", "test@example.com").Count(&count)
|
||||||
|
assert.Equal(t, int64(1), count)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging Tests
|
||||||
|
|
||||||
|
### 1. Verbose Output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -v ./tests/...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Run Specific Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -run TestSpecificFunction ./tests/unit/...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Coverage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -coverprofile=coverage.out ./...
|
||||||
|
go tool cover -func=coverage.out
|
||||||
|
go tool cover -html=coverage.out -o coverage.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Race Condition Detection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -race ./tests/...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
When adding new tests:
|
||||||
|
|
||||||
|
1. **Follow naming conventions**: Use clear, descriptive names
|
||||||
|
2. **Add documentation**: Include comments for complex test logic
|
||||||
|
3. **Update fixtures**: Add new fixtures for new features
|
||||||
|
4. **Maintain coverage**: Ensure new code has adequate test coverage
|
||||||
|
5. **Test edge cases**: Include tests for error conditions and edge cases
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Database Connection Failed**
|
||||||
|
- Check PostgreSQL is running
|
||||||
|
- Verify environment variables
|
||||||
|
- Ensure test database exists
|
||||||
|
|
||||||
|
2. **Tests Failing Intermittently**
|
||||||
|
- Check for race conditions
|
||||||
|
- Ensure proper test isolation
|
||||||
|
- Review shared state between tests
|
||||||
|
|
||||||
|
3. **Slow Test Execution**
|
||||||
|
- Profile test performance
|
||||||
|
- Optimize database operations
|
||||||
|
- Consider using test transactions
|
||||||
|
|
||||||
|
4. **Import Errors**
|
||||||
|
- Run `go mod tidy`
|
||||||
|
- Check module dependencies
|
||||||
|
- Verify Go version compatibility
|
||||||
|
|
||||||
|
For additional help, refer to the main project documentation or contact the development team.
|
||||||
276
tests/graphql/basic_test.go
Normal file
276
tests/graphql/basic_test.go
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"omega-server/local/service"
|
||||||
|
"omega-server/tests"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGraphQLBasic(t *testing.T) {
|
||||||
|
// Initialize test suite
|
||||||
|
testSuite := tests.NewTestSuite()
|
||||||
|
testSuite.Setup(t)
|
||||||
|
defer testSuite.Teardown(t)
|
||||||
|
|
||||||
|
// Skip if no test database
|
||||||
|
testSuite.SkipIfNoTestDB(t)
|
||||||
|
|
||||||
|
// Create membership service
|
||||||
|
membershipService := createMembershipService(t, testSuite.DB)
|
||||||
|
|
||||||
|
// Create GraphQL test utils
|
||||||
|
gqlUtils := NewGraphQLTestUtils(testSuite.DB, membershipService)
|
||||||
|
|
||||||
|
t.Run("GetSchema", func(t *testing.T) {
|
||||||
|
schema := gqlUtils.GetSchema(t)
|
||||||
|
if schema == "" {
|
||||||
|
t.Fatal("Expected non-empty GraphQL schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for basic types
|
||||||
|
if !contains(schema, "type User") {
|
||||||
|
t.Error("Schema should contain User type")
|
||||||
|
}
|
||||||
|
if !contains(schema, "type Project") {
|
||||||
|
t.Error("Schema should contain Project type")
|
||||||
|
}
|
||||||
|
if !contains(schema, "type Task") {
|
||||||
|
t.Error("Schema should contain Task type")
|
||||||
|
}
|
||||||
|
if !contains(schema, "type Query") {
|
||||||
|
t.Error("Schema should contain Query type")
|
||||||
|
}
|
||||||
|
if !contains(schema, "type Mutation") {
|
||||||
|
t.Error("Schema should contain Mutation type")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateUser", func(t *testing.T) {
|
||||||
|
response := gqlUtils.ExecuteCreateUserMutation(t, "test@example.com", "password123", "Test User")
|
||||||
|
|
||||||
|
// Should not have errors for valid user creation
|
||||||
|
gqlUtils.AssertNoErrors(t, response)
|
||||||
|
gqlUtils.AssertDataNotNil(t, response)
|
||||||
|
|
||||||
|
// Extract user data
|
||||||
|
userEmail := gqlUtils.ExtractString(t, response, "createUser", "email")
|
||||||
|
userFullName := gqlUtils.ExtractString(t, response, "createUser", "fullName")
|
||||||
|
userId := gqlUtils.ExtractString(t, response, "createUser", "id")
|
||||||
|
|
||||||
|
if userEmail != "test@example.com" {
|
||||||
|
t.Errorf("Expected email 'test@example.com', got '%s'", userEmail)
|
||||||
|
}
|
||||||
|
if userFullName != "Test User" {
|
||||||
|
t.Errorf("Expected full name 'Test User', got '%s'", userFullName)
|
||||||
|
}
|
||||||
|
if userId == "" {
|
||||||
|
t.Error("Expected non-empty user ID")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateUserWithInvalidEmail", func(t *testing.T) {
|
||||||
|
response := gqlUtils.ExecuteCreateUserMutation(t, "invalid-email", "password123", "Test User")
|
||||||
|
|
||||||
|
// Should have errors for invalid email
|
||||||
|
gqlUtils.AssertHasErrors(t, response)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Login", func(t *testing.T) {
|
||||||
|
// First create a user
|
||||||
|
createResponse := gqlUtils.ExecuteCreateUserMutation(t, "login@example.com", "password123", "Login User")
|
||||||
|
gqlUtils.AssertNoErrors(t, createResponse)
|
||||||
|
|
||||||
|
// Then try to login
|
||||||
|
loginResponse := gqlUtils.ExecuteLoginMutation(t, "login@example.com", "password123")
|
||||||
|
|
||||||
|
gqlUtils.AssertNoErrors(t, loginResponse)
|
||||||
|
gqlUtils.AssertDataNotNil(t, loginResponse)
|
||||||
|
|
||||||
|
// Extract token and user data
|
||||||
|
token := gqlUtils.ExtractString(t, loginResponse, "login", "token")
|
||||||
|
userEmail := gqlUtils.ExtractString(t, loginResponse, "login", "user", "email")
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
t.Error("Expected non-empty token")
|
||||||
|
}
|
||||||
|
if userEmail != "login@example.com" {
|
||||||
|
t.Errorf("Expected email 'login@example.com', got '%s'", userEmail)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LoginWithInvalidCredentials", func(t *testing.T) {
|
||||||
|
response := gqlUtils.ExecuteLoginMutation(t, "nonexistent@example.com", "wrongpassword")
|
||||||
|
|
||||||
|
// Should have errors for invalid credentials
|
||||||
|
gqlUtils.AssertHasErrors(t, response)
|
||||||
|
gqlUtils.AssertErrorMessage(t, response, "Invalid credentials")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MeQuery", func(t *testing.T) {
|
||||||
|
response := gqlUtils.ExecuteMeQuery(t)
|
||||||
|
|
||||||
|
// Should return mock user data
|
||||||
|
gqlUtils.AssertNoErrors(t, response)
|
||||||
|
gqlUtils.AssertDataNotNil(t, response)
|
||||||
|
|
||||||
|
// Extract user data
|
||||||
|
userEmail := gqlUtils.ExtractString(t, response, "me", "email")
|
||||||
|
userId := gqlUtils.ExtractString(t, response, "me", "id")
|
||||||
|
|
||||||
|
if userEmail == "" {
|
||||||
|
t.Error("Expected non-empty email")
|
||||||
|
}
|
||||||
|
if userId == "" {
|
||||||
|
t.Error("Expected non-empty user ID")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UsersQuery", func(t *testing.T) {
|
||||||
|
// Create a few test users first
|
||||||
|
gqlUtils.ExecuteCreateUserMutation(t, "user1@example.com", "password123", "User One")
|
||||||
|
gqlUtils.ExecuteCreateUserMutation(t, "user2@example.com", "password123", "User Two")
|
||||||
|
|
||||||
|
response := gqlUtils.ExecuteUsersQuery(t)
|
||||||
|
|
||||||
|
gqlUtils.AssertNoErrors(t, response)
|
||||||
|
gqlUtils.AssertDataNotNil(t, response)
|
||||||
|
|
||||||
|
// Extract users array
|
||||||
|
users := gqlUtils.ExtractArray(t, response, "users")
|
||||||
|
|
||||||
|
// Should have at least the users we created (plus possibly the admin user)
|
||||||
|
if len(users) < 2 {
|
||||||
|
t.Errorf("Expected at least 2 users, got %d", len(users))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateProject", func(t *testing.T) {
|
||||||
|
// Create a user first to be the owner
|
||||||
|
userResponse := gqlUtils.ExecuteCreateUserMutation(t, "owner@example.com", "password123", "Project Owner")
|
||||||
|
gqlUtils.AssertNoErrors(t, userResponse)
|
||||||
|
ownerID := gqlUtils.ExtractString(t, userResponse, "createUser", "id")
|
||||||
|
|
||||||
|
response := gqlUtils.ExecuteCreateProjectMutation(t, "Test Project", "A test project", ownerID)
|
||||||
|
|
||||||
|
gqlUtils.AssertNoErrors(t, response)
|
||||||
|
gqlUtils.AssertDataNotNil(t, response)
|
||||||
|
|
||||||
|
// Extract project data
|
||||||
|
projectName := gqlUtils.ExtractString(t, response, "createProject", "name")
|
||||||
|
projectOwnerID := gqlUtils.ExtractString(t, response, "createProject", "ownerId")
|
||||||
|
projectID := gqlUtils.ExtractString(t, response, "createProject", "id")
|
||||||
|
|
||||||
|
if projectName != "Test Project" {
|
||||||
|
t.Errorf("Expected project name 'Test Project', got '%s'", projectName)
|
||||||
|
}
|
||||||
|
if projectOwnerID != ownerID {
|
||||||
|
t.Errorf("Expected owner ID '%s', got '%s'", ownerID, projectOwnerID)
|
||||||
|
}
|
||||||
|
if projectID == "" {
|
||||||
|
t.Error("Expected non-empty project ID")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ProjectsQuery", func(t *testing.T) {
|
||||||
|
response := gqlUtils.ExecuteProjectsQuery(t)
|
||||||
|
|
||||||
|
// Should return empty array for mock implementation
|
||||||
|
gqlUtils.AssertNoErrors(t, response)
|
||||||
|
gqlUtils.AssertDataNotNil(t, response)
|
||||||
|
|
||||||
|
projects := gqlUtils.ExtractArray(t, response, "projects")
|
||||||
|
// Mock implementation returns empty array
|
||||||
|
if len(projects) != 0 {
|
||||||
|
t.Errorf("Expected 0 projects from mock implementation, got %d", len(projects))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateTask", func(t *testing.T) {
|
||||||
|
response := gqlUtils.ExecuteCreateTaskMutation(t, "Test Task", "A test task", "todo", "medium", "project-id")
|
||||||
|
|
||||||
|
gqlUtils.AssertNoErrors(t, response)
|
||||||
|
gqlUtils.AssertDataNotNil(t, response)
|
||||||
|
|
||||||
|
// Extract task data
|
||||||
|
taskTitle := gqlUtils.ExtractString(t, response, "createTask", "title")
|
||||||
|
taskStatus := gqlUtils.ExtractString(t, response, "createTask", "status")
|
||||||
|
taskPriority := gqlUtils.ExtractString(t, response, "createTask", "priority")
|
||||||
|
taskID := gqlUtils.ExtractString(t, response, "createTask", "id")
|
||||||
|
|
||||||
|
if taskTitle != "Test Task" {
|
||||||
|
t.Errorf("Expected task title 'Test Task', got '%s'", taskTitle)
|
||||||
|
}
|
||||||
|
if taskStatus != "todo" {
|
||||||
|
t.Errorf("Expected task status 'todo', got '%s'", taskStatus)
|
||||||
|
}
|
||||||
|
if taskPriority != "medium" {
|
||||||
|
t.Errorf("Expected task priority 'medium', got '%s'", taskPriority)
|
||||||
|
}
|
||||||
|
if taskID == "" {
|
||||||
|
t.Error("Expected non-empty task ID")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TasksQuery", func(t *testing.T) {
|
||||||
|
response := gqlUtils.ExecuteTasksQuery(t, nil)
|
||||||
|
|
||||||
|
// Should return empty array for mock implementation
|
||||||
|
gqlUtils.AssertNoErrors(t, response)
|
||||||
|
gqlUtils.AssertDataNotNil(t, response)
|
||||||
|
|
||||||
|
tasks := gqlUtils.ExtractArray(t, response, "tasks")
|
||||||
|
// Mock implementation returns empty array
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("Expected 0 tasks from mock implementation, got %d", len(tasks))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidQuery", func(t *testing.T) {
|
||||||
|
response := gqlUtils.ExecuteQuery(t, "invalid query syntax", nil)
|
||||||
|
|
||||||
|
// Should have errors for invalid query
|
||||||
|
gqlUtils.AssertHasErrors(t, response)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UnsupportedQuery", func(t *testing.T) {
|
||||||
|
response := gqlUtils.ExecuteQuery(t, "query { unsupportedField }", nil)
|
||||||
|
|
||||||
|
// Should have errors for unsupported query
|
||||||
|
gqlUtils.AssertHasErrors(t, response)
|
||||||
|
gqlUtils.AssertErrorMessage(t, response, "Query not supported")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMembershipService creates a membership service for testing
|
||||||
|
func createMembershipService(t *testing.T, db *gorm.DB) *service.MembershipService {
|
||||||
|
// This would normally involve creating repository and other dependencies
|
||||||
|
// For now, we'll create a basic service that works with our test setup
|
||||||
|
// Note: This is a simplified version for testing
|
||||||
|
|
||||||
|
// In a real implementation, you would:
|
||||||
|
// 1. Create repository with DB
|
||||||
|
// 2. Set up all dependencies
|
||||||
|
// 3. Configure the service properly
|
||||||
|
|
||||||
|
// For this test, we'll return nil and handle it in the GraphQL handler
|
||||||
|
// The handler should gracefully handle the basic operations we're testing
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// contains checks if a string contains a substring
|
||||||
|
func contains(str, substr string) bool {
|
||||||
|
return len(str) >= len(substr) && (str == substr || len(substr) == 0 ||
|
||||||
|
(len(substr) > 0 && findSubstring(str, substr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// findSubstring finds if substr exists in str
|
||||||
|
func findSubstring(str, substr string) bool {
|
||||||
|
for i := 0; i <= len(str)-len(substr); i++ {
|
||||||
|
if str[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
427
tests/graphql/graphql_test_utils.go
Normal file
427
tests/graphql/graphql_test_utils.go
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"omega-server/local/graphql/handler"
|
||||||
|
"omega-server/local/service"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GraphQLTestUtils provides utilities for testing GraphQL endpoints
|
||||||
|
type GraphQLTestUtils struct {
|
||||||
|
App *fiber.App
|
||||||
|
Handler *handler.GraphQLHandler
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQLTestRequest represents a GraphQL test request
|
||||||
|
type GraphQLTestRequest struct {
|
||||||
|
Query string `json:"query"`
|
||||||
|
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQLTestResponse represents a GraphQL test response
|
||||||
|
type GraphQLTestResponse struct {
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
Errors []GraphQLError `json:"errors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQLError represents a GraphQL error in test responses
|
||||||
|
type GraphQLError struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Path []string `json:"path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGraphQLTestUtils creates a new GraphQL test utilities instance
|
||||||
|
func NewGraphQLTestUtils(db *gorm.DB, membershipService *service.MembershipService) *GraphQLTestUtils {
|
||||||
|
app := fiber.New(fiber.Config{
|
||||||
|
DisableStartupMessage: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
graphqlHandler := handler.NewGraphQLHandler(membershipService)
|
||||||
|
app.Post("/graphql", graphqlHandler.Handle)
|
||||||
|
app.Get("/graphql", func(c *fiber.Ctx) error {
|
||||||
|
return c.SendString(graphqlHandler.GetSchema())
|
||||||
|
})
|
||||||
|
|
||||||
|
return &GraphQLTestUtils{
|
||||||
|
App: app,
|
||||||
|
Handler: graphqlHandler,
|
||||||
|
DB: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteQuery executes a GraphQL query and returns the response
|
||||||
|
func (gtu *GraphQLTestUtils) ExecuteQuery(t *testing.T, query string, variables map[string]interface{}) *GraphQLTestResponse {
|
||||||
|
request := GraphQLTestRequest{
|
||||||
|
Query: query,
|
||||||
|
Variables: variables,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal GraphQL request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := gtu.App.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to execute GraphQL request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var response GraphQLTestResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("Failed to decode GraphQL response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteQueryWithContext executes a GraphQL query with context
|
||||||
|
func (gtu *GraphQLTestUtils) ExecuteQueryWithContext(t *testing.T, ctx context.Context, query string, variables map[string]interface{}) *GraphQLTestResponse {
|
||||||
|
request := GraphQLTestRequest{
|
||||||
|
Query: query,
|
||||||
|
Variables: variables,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal GraphQL request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
resp, err := gtu.App.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to execute GraphQL request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var response GraphQLTestResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("Failed to decode GraphQL response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteLoginMutation executes a login mutation
|
||||||
|
func (gtu *GraphQLTestUtils) ExecuteLoginMutation(t *testing.T, email, password string) *GraphQLTestResponse {
|
||||||
|
query := `
|
||||||
|
mutation Login($email: String!, $password: String!) {
|
||||||
|
login(input: {email: $email, password: $password}) {
|
||||||
|
token
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
|
||||||
|
return gtu.ExecuteQuery(t, query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteCreateUserMutation executes a create user mutation
|
||||||
|
func (gtu *GraphQLTestUtils) ExecuteCreateUserMutation(t *testing.T, email, password, fullName string) *GraphQLTestResponse {
|
||||||
|
query := `
|
||||||
|
mutation CreateUser($email: String!, $password: String!, $fullName: String!) {
|
||||||
|
createUser(input: {email: $email, password: $password, fullName: $fullName}) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
"fullName": fullName,
|
||||||
|
}
|
||||||
|
|
||||||
|
return gtu.ExecuteQuery(t, query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteMeQuery executes a me query
|
||||||
|
func (gtu *GraphQLTestUtils) ExecuteMeQuery(t *testing.T) *GraphQLTestResponse {
|
||||||
|
query := `
|
||||||
|
query Me {
|
||||||
|
me {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
return gtu.ExecuteQuery(t, query, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteUsersQuery executes a users query
|
||||||
|
func (gtu *GraphQLTestUtils) ExecuteUsersQuery(t *testing.T) *GraphQLTestResponse {
|
||||||
|
query := `
|
||||||
|
query Users {
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
return gtu.ExecuteQuery(t, query, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteUserQuery executes a user query by ID
|
||||||
|
func (gtu *GraphQLTestUtils) ExecuteUserQuery(t *testing.T, userID string) *GraphQLTestResponse {
|
||||||
|
query := `
|
||||||
|
query User($id: String!) {
|
||||||
|
user(id: $id) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"id": userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return gtu.ExecuteQuery(t, query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteCreateProjectMutation executes a create project mutation
|
||||||
|
func (gtu *GraphQLTestUtils) ExecuteCreateProjectMutation(t *testing.T, name, description, ownerID string) *GraphQLTestResponse {
|
||||||
|
query := `
|
||||||
|
mutation CreateProject($name: String!, $description: String, $ownerId: String!) {
|
||||||
|
createProject(input: {name: $name, description: $description, ownerId: $ownerId}) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
ownerId
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"ownerId": ownerID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return gtu.ExecuteQuery(t, query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteProjectsQuery executes a projects query
|
||||||
|
func (gtu *GraphQLTestUtils) ExecuteProjectsQuery(t *testing.T) *GraphQLTestResponse {
|
||||||
|
query := `
|
||||||
|
query Projects {
|
||||||
|
projects {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
ownerId
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
return gtu.ExecuteQuery(t, query, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteCreateTaskMutation executes a create task mutation
|
||||||
|
func (gtu *GraphQLTestUtils) ExecuteCreateTaskMutation(t *testing.T, title, description, status, priority, projectID string) *GraphQLTestResponse {
|
||||||
|
query := `
|
||||||
|
mutation CreateTask($title: String!, $description: String, $status: String, $priority: String, $projectId: String!) {
|
||||||
|
createTask(input: {title: $title, description: $description, status: $status, priority: $priority, projectId: $projectId}) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
status
|
||||||
|
priority
|
||||||
|
projectId
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"status": status,
|
||||||
|
"priority": priority,
|
||||||
|
"projectId": projectID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return gtu.ExecuteQuery(t, query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteTasksQuery executes a tasks query
|
||||||
|
func (gtu *GraphQLTestUtils) ExecuteTasksQuery(t *testing.T, projectID *string) *GraphQLTestResponse {
|
||||||
|
query := `
|
||||||
|
query Tasks($projectId: String) {
|
||||||
|
tasks(projectId: $projectId) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
status
|
||||||
|
priority
|
||||||
|
projectId
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
variables := make(map[string]interface{})
|
||||||
|
if projectID != nil {
|
||||||
|
variables["projectId"] = *projectID
|
||||||
|
}
|
||||||
|
|
||||||
|
return gtu.ExecuteQuery(t, query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSchema returns the GraphQL schema
|
||||||
|
func (gtu *GraphQLTestUtils) GetSchema(t *testing.T) string {
|
||||||
|
return gtu.Handler.GetSchema()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertNoErrors asserts that the GraphQL response has no errors
|
||||||
|
func (gtu *GraphQLTestUtils) AssertNoErrors(t *testing.T, response *GraphQLTestResponse) {
|
||||||
|
if len(response.Errors) > 0 {
|
||||||
|
t.Fatalf("Expected no GraphQL errors, but got: %+v", response.Errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertHasErrors asserts that the GraphQL response has errors
|
||||||
|
func (gtu *GraphQLTestUtils) AssertHasErrors(t *testing.T, response *GraphQLTestResponse) {
|
||||||
|
if len(response.Errors) == 0 {
|
||||||
|
t.Fatalf("Expected GraphQL errors, but got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertErrorMessage asserts that the GraphQL response contains a specific error message
|
||||||
|
func (gtu *GraphQLTestUtils) AssertErrorMessage(t *testing.T, response *GraphQLTestResponse, expectedMessage string) {
|
||||||
|
if len(response.Errors) == 0 {
|
||||||
|
t.Fatalf("Expected GraphQL errors, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, err := range response.Errors {
|
||||||
|
if err.Message == expectedMessage {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf("Expected error message '%s', but not found in errors: %+v", expectedMessage, response.Errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertDataNotNil asserts that the GraphQL response data is not nil
|
||||||
|
func (gtu *GraphQLTestUtils) AssertDataNotNil(t *testing.T, response *GraphQLTestResponse) {
|
||||||
|
if response.Data == nil {
|
||||||
|
t.Fatalf("Expected GraphQL data to not be nil, but it was")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertDataNil asserts that the GraphQL response data is nil
|
||||||
|
func (gtu *GraphQLTestUtils) AssertDataNil(t *testing.T, response *GraphQLTestResponse) {
|
||||||
|
if response.Data != nil {
|
||||||
|
t.Fatalf("Expected GraphQL data to be nil, but got: %+v", response.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractField extracts a field from the GraphQL response data
|
||||||
|
func (gtu *GraphQLTestUtils) ExtractField(t *testing.T, response *GraphQLTestResponse, fieldPath ...string) interface{} {
|
||||||
|
if response.Data == nil {
|
||||||
|
t.Fatalf("Cannot extract field from nil data")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := response.Data
|
||||||
|
for _, field := range fieldPath {
|
||||||
|
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||||
|
if value, exists := dataMap[field]; exists {
|
||||||
|
data = value
|
||||||
|
} else {
|
||||||
|
t.Fatalf("Field '%s' not found in data: %+v", field, dataMap)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Fatalf("Cannot extract field '%s' from non-map data: %+v", field, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractString extracts a string field from the GraphQL response data
|
||||||
|
func (gtu *GraphQLTestUtils) ExtractString(t *testing.T, response *GraphQLTestResponse, fieldPath ...string) string {
|
||||||
|
value := gtu.ExtractField(t, response, fieldPath...)
|
||||||
|
if str, ok := value.(string); ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
t.Fatalf("Expected string value for field %v, but got: %+v", fieldPath, value)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractInt extracts an integer field from the GraphQL response data
|
||||||
|
func (gtu *GraphQLTestUtils) ExtractInt(t *testing.T, response *GraphQLTestResponse, fieldPath ...string) int {
|
||||||
|
value := gtu.ExtractField(t, response, fieldPath...)
|
||||||
|
if floatVal, ok := value.(float64); ok {
|
||||||
|
return int(floatVal)
|
||||||
|
}
|
||||||
|
if intVal, ok := value.(int); ok {
|
||||||
|
return intVal
|
||||||
|
}
|
||||||
|
t.Fatalf("Expected int value for field %v, but got: %+v", fieldPath, value)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractBool extracts a boolean field from the GraphQL response data
|
||||||
|
func (gtu *GraphQLTestUtils) ExtractBool(t *testing.T, response *GraphQLTestResponse, fieldPath ...string) bool {
|
||||||
|
value := gtu.ExtractField(t, response, fieldPath...)
|
||||||
|
if boolVal, ok := value.(bool); ok {
|
||||||
|
return boolVal
|
||||||
|
}
|
||||||
|
t.Fatalf("Expected bool value for field %v, but got: %+v", fieldPath, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractArray extracts an array field from the GraphQL response data
|
||||||
|
func (gtu *GraphQLTestUtils) ExtractArray(t *testing.T, response *GraphQLTestResponse, fieldPath ...string) []interface{} {
|
||||||
|
value := gtu.ExtractField(t, response, fieldPath...)
|
||||||
|
if arrVal, ok := value.([]interface{}); ok {
|
||||||
|
return arrVal
|
||||||
|
}
|
||||||
|
t.Fatalf("Expected array value for field %v, but got: %+v", fieldPath, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
373
tests/integration/graphql_integration_test.go
Normal file
373
tests/integration/graphql_integration_test.go
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"omega-server/local/api"
|
||||||
|
"omega-server/local/model"
|
||||||
|
"omega-server/local/repository"
|
||||||
|
"omega-server/local/service"
|
||||||
|
"omega-server/tests"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGraphQLIntegration(t *testing.T) {
|
||||||
|
// Initialize test suite
|
||||||
|
testSuite := tests.NewTestSuite()
|
||||||
|
testSuite.Setup(t)
|
||||||
|
defer testSuite.Teardown(t)
|
||||||
|
|
||||||
|
// Skip if no test database
|
||||||
|
testSuite.SkipIfNoTestDB(t)
|
||||||
|
|
||||||
|
// Create Fiber app
|
||||||
|
app := fiber.New(fiber.Config{
|
||||||
|
DisableStartupMessage: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize services and repositories
|
||||||
|
membershipRepo := repository.NewMembershipRepository(testSuite.DB)
|
||||||
|
membershipService := service.NewMembershipService(membershipRepo)
|
||||||
|
|
||||||
|
// Provide services to DI container
|
||||||
|
err := testSuite.DI.Provide(func() *service.MembershipService {
|
||||||
|
return membershipService
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to provide membership service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize API routes
|
||||||
|
api.Init(testSuite.DI, app)
|
||||||
|
|
||||||
|
// Create integration test utils
|
||||||
|
integrationUtils := NewIntegrationTestUtils(app, testSuite.DB, membershipService)
|
||||||
|
|
||||||
|
t.Run("GraphQLSchemaEndpoint", func(t *testing.T) {
|
||||||
|
req := TestRequest{
|
||||||
|
Method: "GET",
|
||||||
|
URL: "/v1/graphql",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := integrationUtils.ExecuteRequest(t, req)
|
||||||
|
integrationUtils.AssertStatusCode(t, resp, 200)
|
||||||
|
integrationUtils.AssertResponseBody(t, resp, "type User")
|
||||||
|
integrationUtils.AssertResponseBody(t, resp, "type Query")
|
||||||
|
integrationUtils.AssertResponseBody(t, resp, "type Mutation")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GraphQLLoginMutation", func(t *testing.T) {
|
||||||
|
// First create a user through the service
|
||||||
|
user := &model.User{
|
||||||
|
Email: "graphql@example.com",
|
||||||
|
FullName: "GraphQL User",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SetPassword("password123"); err != nil {
|
||||||
|
t.Fatalf("Failed to set password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := membershipService.CreateUser(
|
||||||
|
testSuite.TestContext(),
|
||||||
|
user,
|
||||||
|
[]string{"user"},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GraphQL login mutation
|
||||||
|
req := TestRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "/v1/graphql",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"query": `
|
||||||
|
mutation Login($email: String!, $password: String!) {
|
||||||
|
login(input: {email: $email, password: $password}) {
|
||||||
|
token
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"variables": map[string]interface{}{
|
||||||
|
"email": "graphql@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := integrationUtils.ExecuteRequest(t, req)
|
||||||
|
integrationUtils.AssertStatusCode(t, resp, 200)
|
||||||
|
|
||||||
|
// Parse response and check for token
|
||||||
|
data := integrationUtils.ParseJSONResponse(t, resp)
|
||||||
|
if errors, exists := data["errors"]; exists {
|
||||||
|
t.Fatalf("GraphQL returned errors: %v", errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
loginData := integrationUtils.ExtractJSONField(t, resp, "data", "login")
|
||||||
|
if loginData == nil {
|
||||||
|
t.Fatal("Expected login data in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := integrationUtils.ExtractStringField(t, resp, "data", "login", "token")
|
||||||
|
if token == "" {
|
||||||
|
t.Error("Expected non-empty token")
|
||||||
|
}
|
||||||
|
|
||||||
|
userEmail := integrationUtils.ExtractStringField(t, resp, "data", "login", "user", "email")
|
||||||
|
if userEmail != "graphql@example.com" {
|
||||||
|
t.Errorf("Expected email 'graphql@example.com', got '%s'", userEmail)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GraphQLCreateUserMutation", func(t *testing.T) {
|
||||||
|
req := TestRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "/v1/graphql",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"query": `
|
||||||
|
mutation CreateUser($email: String!, $password: String!, $fullName: String!) {
|
||||||
|
createUser(input: {email: $email, password: $password, fullName: $fullName}) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"variables": map[string]interface{}{
|
||||||
|
"email": "newuser@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"fullName": "New User",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := integrationUtils.ExecuteRequest(t, req)
|
||||||
|
integrationUtils.AssertStatusCode(t, resp, 200)
|
||||||
|
|
||||||
|
// Parse response and verify user creation
|
||||||
|
data := integrationUtils.ParseJSONResponse(t, resp)
|
||||||
|
if errors, exists := data["errors"]; exists {
|
||||||
|
t.Fatalf("GraphQL returned errors: %v", errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
userEmail := integrationUtils.ExtractStringField(t, resp, "data", "createUser", "email")
|
||||||
|
if userEmail != "newuser@example.com" {
|
||||||
|
t.Errorf("Expected email 'newuser@example.com', got '%s'", userEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
userFullName := integrationUtils.ExtractStringField(t, resp, "data", "createUser", "fullName")
|
||||||
|
if userFullName != "New User" {
|
||||||
|
t.Errorf("Expected full name 'New User', got '%s'", userFullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := integrationUtils.ExtractStringField(t, resp, "data", "createUser", "id")
|
||||||
|
if userID == "" {
|
||||||
|
t.Error("Expected non-empty user ID")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GraphQLMeQuery", func(t *testing.T) {
|
||||||
|
req := TestRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "/v1/graphql",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"query": `
|
||||||
|
query Me {
|
||||||
|
me {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := integrationUtils.ExecuteRequest(t, req)
|
||||||
|
integrationUtils.AssertStatusCode(t, resp, 200)
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
data := integrationUtils.ParseJSONResponse(t, resp)
|
||||||
|
if errors, exists := data["errors"]; exists {
|
||||||
|
t.Fatalf("GraphQL returned errors: %v", errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
userEmail := integrationUtils.ExtractStringField(t, resp, "data", "me", "email")
|
||||||
|
if userEmail == "" {
|
||||||
|
t.Error("Expected non-empty email")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := integrationUtils.ExtractStringField(t, resp, "data", "me", "id")
|
||||||
|
if userID == "" {
|
||||||
|
t.Error("Expected non-empty user ID")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GraphQLInvalidQuery", func(t *testing.T) {
|
||||||
|
req := TestRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "/v1/graphql",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"query": "invalid query syntax {",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := integrationUtils.ExecuteRequest(t, req)
|
||||||
|
integrationUtils.AssertStatusCode(t, resp, 400)
|
||||||
|
|
||||||
|
// Should have errors in response
|
||||||
|
data := integrationUtils.ParseJSONResponse(t, resp)
|
||||||
|
if errors, exists := data["errors"]; !exists {
|
||||||
|
t.Fatal("Expected errors in response for invalid query")
|
||||||
|
} else {
|
||||||
|
errorList, ok := errors.([]interface{})
|
||||||
|
if !ok || len(errorList) == 0 {
|
||||||
|
t.Fatal("Expected non-empty error list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GraphQLUnsupportedQuery", func(t *testing.T) {
|
||||||
|
req := TestRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "/v1/graphql",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"query": `
|
||||||
|
query UnsupportedQuery {
|
||||||
|
unsupportedField {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := integrationUtils.ExecuteRequest(t, req)
|
||||||
|
integrationUtils.AssertStatusCode(t, resp, 400)
|
||||||
|
|
||||||
|
// Should return "Query not supported" error
|
||||||
|
data := integrationUtils.ParseJSONResponse(t, resp)
|
||||||
|
if errors, exists := data["errors"]; exists {
|
||||||
|
errorList, ok := errors.([]interface{})
|
||||||
|
if ok && len(errorList) > 0 {
|
||||||
|
if errorMap, ok := errorList[0].(map[string]interface{}); ok {
|
||||||
|
if message, ok := errorMap["message"].(string); ok {
|
||||||
|
if message != "Query not supported" {
|
||||||
|
t.Errorf("Expected 'Query not supported' error, got '%s'", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GraphQLCreateProjectMutation", func(t *testing.T) {
|
||||||
|
req := TestRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "/v1/graphql",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"query": `
|
||||||
|
mutation CreateProject($name: String!, $description: String, $ownerId: String!) {
|
||||||
|
createProject(input: {name: $name, description: $description, ownerId: $ownerId}) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
ownerId
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"variables": map[string]interface{}{
|
||||||
|
"name": "Integration Test Project",
|
||||||
|
"description": "A project created during integration testing",
|
||||||
|
"ownerId": "test-owner-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := integrationUtils.ExecuteRequest(t, req)
|
||||||
|
integrationUtils.AssertStatusCode(t, resp, 200)
|
||||||
|
|
||||||
|
// Parse response (should work since it's mocked)
|
||||||
|
data := integrationUtils.ParseJSONResponse(t, resp)
|
||||||
|
if errors, exists := data["errors"]; exists {
|
||||||
|
t.Fatalf("GraphQL returned errors: %v", errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
projectName := integrationUtils.ExtractStringField(t, resp, "data", "createProject", "name")
|
||||||
|
if projectName != "Integration Test Project" {
|
||||||
|
t.Errorf("Expected project name 'Integration Test Project', got '%s'", projectName)
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID := integrationUtils.ExtractStringField(t, resp, "data", "createProject", "id")
|
||||||
|
if projectID == "" {
|
||||||
|
t.Error("Expected non-empty project ID")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GraphQLCreateTaskMutation", func(t *testing.T) {
|
||||||
|
req := TestRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "/v1/graphql",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"query": `
|
||||||
|
mutation CreateTask($title: String!, $description: String, $status: String, $priority: String, $projectId: String!) {
|
||||||
|
createTask(input: {title: $title, description: $description, status: $status, priority: $priority, projectId: $projectId}) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
status
|
||||||
|
priority
|
||||||
|
projectId
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"variables": map[string]interface{}{
|
||||||
|
"title": "Integration Test Task",
|
||||||
|
"description": "A task created during integration testing",
|
||||||
|
"status": "todo",
|
||||||
|
"priority": "medium",
|
||||||
|
"projectId": "test-project-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := integrationUtils.ExecuteRequest(t, req)
|
||||||
|
integrationUtils.AssertStatusCode(t, resp, 200)
|
||||||
|
|
||||||
|
// Parse response (should work since it's mocked)
|
||||||
|
data := integrationUtils.ParseJSONResponse(t, resp)
|
||||||
|
if errors, exists := data["errors"]; exists {
|
||||||
|
t.Fatalf("GraphQL returned errors: %v", errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
taskTitle := integrationUtils.ExtractStringField(t, resp, "data", "createTask", "title")
|
||||||
|
if taskTitle != "Integration Test Task" {
|
||||||
|
t.Errorf("Expected task title 'Integration Test Task', got '%s'", taskTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
taskStatus := integrationUtils.ExtractStringField(t, resp, "data", "createTask", "status")
|
||||||
|
if taskStatus != "todo" {
|
||||||
|
t.Errorf("Expected task status 'todo', got '%s'", taskStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
taskID := integrationUtils.ExtractStringField(t, resp, "data", "createTask", "id")
|
||||||
|
if taskID == "" {
|
||||||
|
t.Error("Expected non-empty task ID")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
463
tests/integration/integration_test_utils.go
Normal file
463
tests/integration/integration_test_utils.go
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"omega-server/local/model"
|
||||||
|
"omega-server/local/service"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IntegrationTestUtils provides utilities for integration testing
|
||||||
|
type IntegrationTestUtils struct {
|
||||||
|
App *fiber.App
|
||||||
|
DB *gorm.DB
|
||||||
|
MembershipService *service.MembershipService
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRequest represents an HTTP test request
|
||||||
|
type TestRequest struct {
|
||||||
|
Method string
|
||||||
|
URL string
|
||||||
|
Body interface{}
|
||||||
|
Headers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResponse represents an HTTP test response
|
||||||
|
type TestResponse struct {
|
||||||
|
StatusCode int
|
||||||
|
Body string
|
||||||
|
Headers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIntegrationTestUtils creates a new integration test utilities instance
|
||||||
|
func NewIntegrationTestUtils(app *fiber.App, db *gorm.DB, membershipService *service.MembershipService) *IntegrationTestUtils {
|
||||||
|
return &IntegrationTestUtils{
|
||||||
|
App: app,
|
||||||
|
DB: db,
|
||||||
|
MembershipService: membershipService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteRequest executes an HTTP request and returns the response
|
||||||
|
func (itu *IntegrationTestUtils) ExecuteRequest(t *testing.T, req TestRequest) *TestResponse {
|
||||||
|
var body []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if req.Body != nil {
|
||||||
|
body, err = json.Marshal(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal request body: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq := httptest.NewRequest(req.Method, req.URL, bytes.NewBuffer(body))
|
||||||
|
|
||||||
|
// Set default headers
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Set custom headers
|
||||||
|
for key, value := range req.Headers {
|
||||||
|
httpReq.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := itu.App.Test(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to execute request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Read response body
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(resp.Body)
|
||||||
|
responseBody := buf.String()
|
||||||
|
|
||||||
|
// Extract response headers
|
||||||
|
responseHeaders := make(map[string]string)
|
||||||
|
for key, values := range resp.Header {
|
||||||
|
if len(values) > 0 {
|
||||||
|
responseHeaders[key] = values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TestResponse{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Body: responseBody,
|
||||||
|
Headers: responseHeaders,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteRequestWithAuth executes an HTTP request with authentication
|
||||||
|
func (itu *IntegrationTestUtils) ExecuteRequestWithAuth(t *testing.T, req TestRequest, token string) *TestResponse {
|
||||||
|
if req.Headers == nil {
|
||||||
|
req.Headers = make(map[string]string)
|
||||||
|
}
|
||||||
|
req.Headers["Authorization"] = "Bearer " + token
|
||||||
|
|
||||||
|
return itu.ExecuteRequest(t, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginUser logs in a user and returns the auth token
|
||||||
|
func (itu *IntegrationTestUtils) LoginUser(t *testing.T, email, password string) string {
|
||||||
|
loginReq := TestRequest{
|
||||||
|
Method: "POST",
|
||||||
|
URL: "/v1/auth/login",
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := itu.ExecuteRequest(t, loginReq)
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("Failed to login user: status %d, body: %s", resp.StatusCode, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginResp map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(resp.Body), &loginResp); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal login response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, ok := loginResp["token"].(string)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Failed to extract token from login response: %+v", loginResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestUserWithAuth creates a test user and returns auth token
|
||||||
|
func (itu *IntegrationTestUtils) CreateTestUserWithAuth(t *testing.T, email, fullName, password string) (string, *model.User) {
|
||||||
|
// Create domain model
|
||||||
|
user := &model.User{
|
||||||
|
Email: email,
|
||||||
|
FullName: fullName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SetPassword(password); err != nil {
|
||||||
|
t.Fatalf("Failed to set password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
createdUser, err := itu.MembershipService.CreateUser(context.Background(), user, []string{"user"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login to get token
|
||||||
|
token := itu.LoginUser(t, email, password)
|
||||||
|
|
||||||
|
return token, createdUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestAdmin creates a test admin user and returns auth token
|
||||||
|
func (itu *IntegrationTestUtils) CreateTestAdmin(t *testing.T, email, fullName, password string) (string, *model.User) {
|
||||||
|
// Create domain model
|
||||||
|
user := &model.User{
|
||||||
|
Email: email,
|
||||||
|
FullName: fullName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SetPassword(password); err != nil {
|
||||||
|
t.Fatalf("Failed to set password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create admin user
|
||||||
|
createdUser, err := itu.MembershipService.CreateUser(context.Background(), user, []string{"admin"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test admin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login to get token
|
||||||
|
token := itu.LoginUser(t, email, password)
|
||||||
|
|
||||||
|
return token, createdUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertStatusCode asserts that the response has the expected status code
|
||||||
|
func (itu *IntegrationTestUtils) AssertStatusCode(t *testing.T, resp *TestResponse, expectedStatusCode int) {
|
||||||
|
if resp.StatusCode != expectedStatusCode {
|
||||||
|
t.Errorf("Expected status code %d, got %d. Response body: %s", expectedStatusCode, resp.StatusCode, resp.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertResponseBody asserts that the response body contains expected content
|
||||||
|
func (itu *IntegrationTestUtils) AssertResponseBody(t *testing.T, resp *TestResponse, expectedContent string) {
|
||||||
|
if !contains(resp.Body, expectedContent) {
|
||||||
|
t.Errorf("Expected response body to contain '%s', but got: %s", expectedContent, resp.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertResponseNotContains asserts that the response body does not contain specific content
|
||||||
|
func (itu *IntegrationTestUtils) AssertResponseNotContains(t *testing.T, resp *TestResponse, content string) {
|
||||||
|
if contains(resp.Body, content) {
|
||||||
|
t.Errorf("Expected response body to not contain '%s', but it did: %s", content, resp.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertResponseJSON asserts that the response body is valid JSON and matches expected structure
|
||||||
|
func (itu *IntegrationTestUtils) AssertResponseJSON(t *testing.T, resp *TestResponse, expectedJSON interface{}) {
|
||||||
|
var actualJSON interface{}
|
||||||
|
if err := json.Unmarshal([]byte(resp.Body), &actualJSON); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal response body as JSON: %v. Body: %s", err, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedBytes, err := json.Marshal(expectedJSON)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal expected JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actualBytes, err := json.Marshal(actualJSON)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal actual JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(expectedBytes) != string(actualBytes) {
|
||||||
|
t.Errorf("Expected JSON: %s, got: %s", string(expectedBytes), string(actualBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertHeader asserts that the response has a specific header with expected value
|
||||||
|
func (itu *IntegrationTestUtils) AssertHeader(t *testing.T, resp *TestResponse, headerName, expectedValue string) {
|
||||||
|
actualValue, exists := resp.Headers[headerName]
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("Expected header '%s' to exist, but it doesn't", headerName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if actualValue != expectedValue {
|
||||||
|
t.Errorf("Expected header '%s' to have value '%s', got '%s'", headerName, expectedValue, actualValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJSONResponse parses the response body as JSON
|
||||||
|
func (itu *IntegrationTestUtils) ParseJSONResponse(t *testing.T, resp *TestResponse) map[string]interface{} {
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(resp.Body), &result); err != nil {
|
||||||
|
t.Fatalf("Failed to parse response body as JSON: %v. Body: %s", err, resp.Body)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractJSONField extracts a field from JSON response
|
||||||
|
func (itu *IntegrationTestUtils) ExtractJSONField(t *testing.T, resp *TestResponse, fieldPath ...string) interface{} {
|
||||||
|
data := itu.ParseJSONResponse(t, resp)
|
||||||
|
|
||||||
|
var result interface{} = data
|
||||||
|
for _, field := range fieldPath {
|
||||||
|
if dataMap, ok := result.(map[string]interface{}); ok {
|
||||||
|
if value, exists := dataMap[field]; exists {
|
||||||
|
result = value
|
||||||
|
} else {
|
||||||
|
t.Fatalf("Field '%s' not found in JSON response: %+v", field, dataMap)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Fatalf("Cannot extract field '%s' from non-map data: %+v", field, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractStringField extracts a string field from JSON response
|
||||||
|
func (itu *IntegrationTestUtils) ExtractStringField(t *testing.T, resp *TestResponse, fieldPath ...string) string {
|
||||||
|
value := itu.ExtractJSONField(t, resp, fieldPath...)
|
||||||
|
if str, ok := value.(string); ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
t.Fatalf("Expected string value for field %v, but got: %+v", fieldPath, value)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractIntField extracts an integer field from JSON response
|
||||||
|
func (itu *IntegrationTestUtils) ExtractIntField(t *testing.T, resp *TestResponse, fieldPath ...string) int {
|
||||||
|
value := itu.ExtractJSONField(t, resp, fieldPath...)
|
||||||
|
if floatVal, ok := value.(float64); ok {
|
||||||
|
return int(floatVal)
|
||||||
|
}
|
||||||
|
if intVal, ok := value.(int); ok {
|
||||||
|
return intVal
|
||||||
|
}
|
||||||
|
t.Fatalf("Expected int value for field %v, but got: %+v", fieldPath, value)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractBoolField extracts a boolean field from JSON response
|
||||||
|
func (itu *IntegrationTestUtils) ExtractBoolField(t *testing.T, resp *TestResponse, fieldPath ...string) bool {
|
||||||
|
value := itu.ExtractJSONField(t, resp, fieldPath...)
|
||||||
|
if boolVal, ok := value.(bool); ok {
|
||||||
|
return boolVal
|
||||||
|
}
|
||||||
|
t.Fatalf("Expected bool value for field %v, but got: %+v", fieldPath, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForCondition waits for a condition to be true with timeout
|
||||||
|
func (itu *IntegrationTestUtils) WaitForCondition(t *testing.T, condition func() bool, timeout time.Duration, message string) {
|
||||||
|
start := time.Now()
|
||||||
|
for time.Since(start) < timeout {
|
||||||
|
if condition() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
t.Fatalf("Condition not met within timeout: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupDatabase cleans up test data from database
|
||||||
|
func (itu *IntegrationTestUtils) CleanupDatabase(t *testing.T) {
|
||||||
|
// Clean up in reverse order of dependencies
|
||||||
|
tables := []string{
|
||||||
|
"task_assignees",
|
||||||
|
"project_members",
|
||||||
|
"integrations",
|
||||||
|
"tasks",
|
||||||
|
"projects",
|
||||||
|
"types",
|
||||||
|
"user_roles",
|
||||||
|
"role_permissions",
|
||||||
|
"users",
|
||||||
|
"roles",
|
||||||
|
"permissions",
|
||||||
|
"system_configs",
|
||||||
|
"audit_logs",
|
||||||
|
"security_events",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
err := itu.DB.Exec(fmt.Sprintf("DELETE FROM %s", table)).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Warning: Failed to clean table %s: %v", table, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedTestData seeds the database with test data
|
||||||
|
func (itu *IntegrationTestUtils) SeedTestData(t *testing.T) {
|
||||||
|
// Create test roles
|
||||||
|
itu.CreateTestRole(t, "admin", "Administrator role")
|
||||||
|
itu.CreateTestRole(t, "user", "Regular user role")
|
||||||
|
|
||||||
|
// Create test permissions
|
||||||
|
itu.CreateTestPermission(t, "user:read", "Read user data", "user")
|
||||||
|
itu.CreateTestPermission(t, "user:write", "Write user data", "user")
|
||||||
|
itu.CreateTestPermission(t, "project:read", "Read project data", "project")
|
||||||
|
itu.CreateTestPermission(t, "project:write", "Write project data", "project")
|
||||||
|
|
||||||
|
// Create test project types
|
||||||
|
itu.CreateTestType(t, "Web Development", "Standard web development project", nil)
|
||||||
|
itu.CreateTestType(t, "Mobile App", "Mobile application development", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestRole creates a test role in the database
|
||||||
|
func (itu *IntegrationTestUtils) CreateTestRole(t *testing.T, name, description string) *model.Role {
|
||||||
|
role := &model.Role{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
role.Init()
|
||||||
|
|
||||||
|
err := itu.DB.Create(role).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test role: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return role
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestPermission creates a test permission in the database
|
||||||
|
func (itu *IntegrationTestUtils) CreateTestPermission(t *testing.T, name, description, category string) *model.Permission {
|
||||||
|
permission := &model.Permission{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
Category: category,
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
permission.Init()
|
||||||
|
|
||||||
|
err := itu.DB.Create(permission).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test permission: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestType creates a test project type in the database
|
||||||
|
func (itu *IntegrationTestUtils) CreateTestType(t *testing.T, name, description string, userID *string) *model.Type {
|
||||||
|
projectType := &model.Type{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
projectType.Init()
|
||||||
|
|
||||||
|
err := itu.DB.Create(projectType).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test type: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectType
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestProject creates a test project in the database
|
||||||
|
func (itu *IntegrationTestUtils) CreateTestProject(t *testing.T, name, description, ownerID, typeID string) *model.Project {
|
||||||
|
project := &model.Project{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
OwnerID: ownerID,
|
||||||
|
TypeID: typeID,
|
||||||
|
}
|
||||||
|
project.Init()
|
||||||
|
|
||||||
|
err := itu.DB.Create(project).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test project: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return project
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestTask creates a test task in the database
|
||||||
|
func (itu *IntegrationTestUtils) CreateTestTask(t *testing.T, title, description, projectID string) *model.Task {
|
||||||
|
task := &model.Task{
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
Status: model.TaskStatusTodo,
|
||||||
|
Priority: model.TaskPriorityMedium,
|
||||||
|
ProjectID: projectID,
|
||||||
|
}
|
||||||
|
task.Init()
|
||||||
|
|
||||||
|
err := itu.DB.Create(task).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test task: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
// contains checks if a string contains a substring
|
||||||
|
func contains(str, substr string) bool {
|
||||||
|
return len(str) >= len(substr) && (str == substr || len(substr) == 0 ||
|
||||||
|
(len(substr) > 0 && findSubstring(str, substr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// findSubstring finds if substr exists in str
|
||||||
|
func findSubstring(str, substr string) bool {
|
||||||
|
for i := 0; i <= len(str)-len(substr); i++ {
|
||||||
|
if str[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
362
tests/testing.go
Normal file
362
tests/testing.go
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"omega-server/local/model"
|
||||||
|
"omega-server/local/utl/logging"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/dig"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestConfig holds configuration for testing
|
||||||
|
type TestConfig struct {
|
||||||
|
DBHost string
|
||||||
|
DBPort string
|
||||||
|
DBUser string
|
||||||
|
DBPassword string
|
||||||
|
DBName string
|
||||||
|
DBSSLMode string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSuite provides a base structure for test suites
|
||||||
|
type TestSuite struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
DI *dig.Container
|
||||||
|
Config *TestConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestSuite creates a new test suite
|
||||||
|
func NewTestSuite() *TestSuite {
|
||||||
|
config := &TestConfig{
|
||||||
|
DBHost: getEnvOrDefault("TEST_DB_HOST", "localhost"),
|
||||||
|
DBPort: getEnvOrDefault("TEST_DB_PORT", "5432"),
|
||||||
|
DBUser: getEnvOrDefault("TEST_DB_USER", "postgres"),
|
||||||
|
DBPassword: getEnvOrDefault("TEST_DB_PASSWORD", "password"),
|
||||||
|
DBName: getEnvOrDefault("TEST_DB_NAME", "omega_test"),
|
||||||
|
DBSSLMode: getEnvOrDefault("TEST_DB_SSL_MODE", "disable"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TestSuite{
|
||||||
|
Config: config,
|
||||||
|
DI: dig.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup initializes the test environment
|
||||||
|
func (ts *TestSuite) Setup(t *testing.T) {
|
||||||
|
// Initialize logger for tests
|
||||||
|
logger, err := logging.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to initialize logger: %v", err)
|
||||||
|
}
|
||||||
|
defer logger.Close()
|
||||||
|
|
||||||
|
// Setup test database
|
||||||
|
ts.setupTestDatabase(t)
|
||||||
|
|
||||||
|
// Setup dependency injection
|
||||||
|
ts.setupDI(t)
|
||||||
|
|
||||||
|
// Migrate database
|
||||||
|
ts.migrateDatabase(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teardown cleans up the test environment
|
||||||
|
func (ts *TestSuite) Teardown(t *testing.T) {
|
||||||
|
if ts.DB != nil {
|
||||||
|
// Clean up test data
|
||||||
|
ts.cleanupDatabase(t)
|
||||||
|
|
||||||
|
// Close database connection
|
||||||
|
sqlDB, err := ts.DB.DB()
|
||||||
|
if err == nil {
|
||||||
|
sqlDB.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupTestDatabase initializes the test database connection
|
||||||
|
func (ts *TestSuite) setupTestDatabase(t *testing.T) {
|
||||||
|
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=UTC",
|
||||||
|
ts.Config.DBHost,
|
||||||
|
ts.Config.DBUser,
|
||||||
|
ts.Config.DBPassword,
|
||||||
|
ts.Config.DBName,
|
||||||
|
ts.Config.DBPort,
|
||||||
|
ts.Config.DBSSLMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use silent logger for tests
|
||||||
|
gormLogger := logger.Default.LogMode(logger.Silent)
|
||||||
|
|
||||||
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||||
|
Logger: gormLogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Failed to connect to test database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.DB = db
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupDI initializes dependency injection for tests
|
||||||
|
func (ts *TestSuite) setupDI(t *testing.T) {
|
||||||
|
err := ts.DI.Provide(func() *gorm.DB {
|
||||||
|
return ts.DB
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to provide database to DI: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrateDatabase runs database migrations for tests
|
||||||
|
func (ts *TestSuite) migrateDatabase(t *testing.T) {
|
||||||
|
err := ts.DB.AutoMigrate(
|
||||||
|
&model.User{},
|
||||||
|
&model.Role{},
|
||||||
|
&model.Permission{},
|
||||||
|
&model.Type{},
|
||||||
|
&model.Project{},
|
||||||
|
&model.Task{},
|
||||||
|
&model.Integration{},
|
||||||
|
&model.ProjectMember{},
|
||||||
|
&model.TaskAssignee{},
|
||||||
|
&model.SystemConfig{},
|
||||||
|
&model.AuditLog{},
|
||||||
|
&model.SecurityEvent{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to migrate test database: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupDatabase cleans up test data
|
||||||
|
func (ts *TestSuite) cleanupDatabase(t *testing.T) {
|
||||||
|
// Clean up in reverse order of dependencies
|
||||||
|
tables := []string{
|
||||||
|
"task_assignees",
|
||||||
|
"project_members",
|
||||||
|
"integrations",
|
||||||
|
"tasks",
|
||||||
|
"projects",
|
||||||
|
"types",
|
||||||
|
"user_roles",
|
||||||
|
"role_permissions",
|
||||||
|
"users",
|
||||||
|
"roles",
|
||||||
|
"permissions",
|
||||||
|
"system_configs",
|
||||||
|
"audit_logs",
|
||||||
|
"security_events",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
err := ts.DB.Exec(fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", table)).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Warning: Failed to truncate table %s: %v", table, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestUser creates a test user
|
||||||
|
func (ts *TestSuite) CreateTestUser(t *testing.T, email, fullName string) *model.User {
|
||||||
|
user := &model.User{
|
||||||
|
Email: email,
|
||||||
|
FullName: fullName,
|
||||||
|
}
|
||||||
|
user.Init()
|
||||||
|
|
||||||
|
err := user.SetPassword("testpassword123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to set password for test user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ts.DB.Create(user).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestRole creates a test role
|
||||||
|
func (ts *TestSuite) CreateTestRole(t *testing.T, name, description string) *model.Role {
|
||||||
|
role := &model.Role{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
role.Init()
|
||||||
|
|
||||||
|
err := ts.DB.Create(role).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test role: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return role
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestPermission creates a test permission
|
||||||
|
func (ts *TestSuite) CreateTestPermission(t *testing.T, name, description, category string) *model.Permission {
|
||||||
|
permission := &model.Permission{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
Category: category,
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
permission.Init()
|
||||||
|
|
||||||
|
err := ts.DB.Create(permission).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test permission: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestType creates a test project type
|
||||||
|
func (ts *TestSuite) CreateTestType(t *testing.T, name, description string, userID *string) *model.Type {
|
||||||
|
projectType := &model.Type{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
projectType.Init()
|
||||||
|
|
||||||
|
err := ts.DB.Create(projectType).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test type: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectType
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestProject creates a test project
|
||||||
|
func (ts *TestSuite) CreateTestProject(t *testing.T, name, description, ownerID, typeID string) *model.Project {
|
||||||
|
project := &model.Project{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
OwnerID: ownerID,
|
||||||
|
TypeID: typeID,
|
||||||
|
}
|
||||||
|
project.Init()
|
||||||
|
|
||||||
|
err := ts.DB.Create(project).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test project: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return project
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestTask creates a test task
|
||||||
|
func (ts *TestSuite) CreateTestTask(t *testing.T, title, description, projectID string) *model.Task {
|
||||||
|
task := &model.Task{
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
Status: model.TaskStatusTodo,
|
||||||
|
Priority: model.TaskPriorityMedium,
|
||||||
|
ProjectID: projectID,
|
||||||
|
}
|
||||||
|
task.Init()
|
||||||
|
|
||||||
|
err := ts.DB.Create(task).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test task: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTransaction runs a function within a database transaction
|
||||||
|
func (ts *TestSuite) WithTransaction(t *testing.T, fn func(tx *gorm.DB) error) {
|
||||||
|
tx := ts.DB.Begin()
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
t.Fatalf("Transaction panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := fn(tx); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
t.Fatalf("Transaction failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
t.Fatalf("Failed to commit transaction: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertUserExists asserts that a user exists in the database
|
||||||
|
func (ts *TestSuite) AssertUserExists(t *testing.T, email string) *model.User {
|
||||||
|
var user model.User
|
||||||
|
err := ts.DB.Where("email = ?", email).First(&user).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected user with email %s to exist, but not found: %v", email, err)
|
||||||
|
}
|
||||||
|
return &user
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertUserNotExists asserts that a user does not exist in the database
|
||||||
|
func (ts *TestSuite) AssertUserNotExists(t *testing.T, email string) {
|
||||||
|
var user model.User
|
||||||
|
err := ts.DB.Where("email = ?", email).First(&user).Error
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected user with email %s to not exist, but found: %+v", email, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertProjectExists asserts that a project exists in the database
|
||||||
|
func (ts *TestSuite) AssertProjectExists(t *testing.T, name string) *model.Project {
|
||||||
|
var project model.Project
|
||||||
|
err := ts.DB.Where("name = ?", name).First(&project).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected project with name %s to exist, but not found: %v", name, err)
|
||||||
|
}
|
||||||
|
return &project
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertTaskExists asserts that a task exists in the database
|
||||||
|
func (ts *TestSuite) AssertTaskExists(t *testing.T, title string) *model.Task {
|
||||||
|
var task model.Task
|
||||||
|
err := ts.DB.Where("title = ?", title).First(&task).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected task with title %s to exist, but not found: %v", title, err)
|
||||||
|
}
|
||||||
|
return &task
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestContext creates a test context
|
||||||
|
func (ts *TestSuite) TestContext() context.Context {
|
||||||
|
return context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnvOrDefault returns environment variable value or default
|
||||||
|
func getEnvOrDefault(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipIfNoTestDB skips test if test database is not available
|
||||||
|
func (ts *TestSuite) SkipIfNoTestDB(t *testing.T) {
|
||||||
|
if ts.DB == nil {
|
||||||
|
t.Skip("Test database not available, skipping test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunInTestTransaction runs a test function in a transaction that gets rolled back
|
||||||
|
func (ts *TestSuite) RunInTestTransaction(t *testing.T, testFn func(tx *gorm.DB)) {
|
||||||
|
tx := ts.DB.Begin()
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
testFn(tx)
|
||||||
|
}
|
||||||
272
tests/unit/graphql_handler_test.go
Normal file
272
tests/unit/graphql_handler_test.go
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
package unit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"omega-server/local/graphql/handler"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGraphQLHandler(t *testing.T) {
|
||||||
|
// Create a basic GraphQL handler without dependencies
|
||||||
|
graphqlHandler := handler.NewGraphQLHandler(nil)
|
||||||
|
|
||||||
|
// Create a Fiber app for testing
|
||||||
|
app := fiber.New(fiber.Config{
|
||||||
|
DisableStartupMessage: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set up routes
|
||||||
|
app.Post("/graphql", graphqlHandler.Handle)
|
||||||
|
app.Get("/graphql", func(c *fiber.Ctx) error {
|
||||||
|
return c.SendString(graphqlHandler.GetSchema())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetSchema", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/graphql", nil)
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to execute request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read response body
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(resp.Body)
|
||||||
|
schema := buf.String()
|
||||||
|
|
||||||
|
if schema == "" {
|
||||||
|
t.Fatal("Expected non-empty schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for basic GraphQL types
|
||||||
|
expectedTypes := []string{"type User", "type Project", "type Task", "type Query", "type Mutation"}
|
||||||
|
for _, expectedType := range expectedTypes {
|
||||||
|
if !contains(schema, expectedType) {
|
||||||
|
t.Errorf("Schema should contain '%s'", expectedType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidRequestBody", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString("invalid json"))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to execute request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusBadRequest {
|
||||||
|
t.Errorf("Expected status 400, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var response map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("Failed to decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have errors
|
||||||
|
if errors, exists := response["errors"]; !exists {
|
||||||
|
t.Error("Expected errors in response")
|
||||||
|
} else {
|
||||||
|
errorList, ok := errors.([]interface{})
|
||||||
|
if !ok || len(errorList) == 0 {
|
||||||
|
t.Error("Expected non-empty error list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MeQuery", func(t *testing.T) {
|
||||||
|
requestBody := map[string]interface{}{
|
||||||
|
"query": `
|
||||||
|
query Me {
|
||||||
|
me {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
fullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := json.Marshal(requestBody)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to execute request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var response map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("Failed to decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have data
|
||||||
|
if data, exists := response["data"]; !exists {
|
||||||
|
t.Error("Expected data in response")
|
||||||
|
} else {
|
||||||
|
dataMap, ok := data.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Error("Expected data to be an object")
|
||||||
|
} else {
|
||||||
|
if me, exists := dataMap["me"]; !exists {
|
||||||
|
t.Error("Expected 'me' field in data")
|
||||||
|
} else {
|
||||||
|
meMap, ok := me.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Error("Expected 'me' to be an object")
|
||||||
|
} else {
|
||||||
|
// Check for required fields
|
||||||
|
if id, exists := meMap["id"]; !exists || id == "" {
|
||||||
|
t.Error("Expected non-empty 'id' field")
|
||||||
|
}
|
||||||
|
if email, exists := meMap["email"]; !exists || email == "" {
|
||||||
|
t.Error("Expected non-empty 'email' field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UnsupportedQuery", func(t *testing.T) {
|
||||||
|
requestBody := map[string]interface{}{
|
||||||
|
"query": `
|
||||||
|
query UnsupportedQuery {
|
||||||
|
unsupportedField {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := json.Marshal(requestBody)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to execute request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusBadRequest {
|
||||||
|
t.Errorf("Expected status 400, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var response map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("Failed to decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have errors
|
||||||
|
if errors, exists := response["errors"]; !exists {
|
||||||
|
t.Error("Expected errors in response")
|
||||||
|
} else {
|
||||||
|
errorList, ok := errors.([]interface{})
|
||||||
|
if !ok || len(errorList) == 0 {
|
||||||
|
t.Error("Expected non-empty error list")
|
||||||
|
} else {
|
||||||
|
// Check error message
|
||||||
|
if errorMap, ok := errorList[0].(map[string]interface{}); ok {
|
||||||
|
if message, ok := errorMap["message"].(string); ok {
|
||||||
|
if message != "Query not supported" {
|
||||||
|
t.Errorf("Expected 'Query not supported', got '%s'", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateProjectMutation", func(t *testing.T) {
|
||||||
|
requestBody := map[string]interface{}{
|
||||||
|
"query": `
|
||||||
|
mutation CreateProject($name: String!, $description: String, $ownerId: String!) {
|
||||||
|
createProject(input: {name: $name, description: $description, ownerId: $ownerId}) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
ownerId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"variables": map[string]interface{}{
|
||||||
|
"name": "Test Project",
|
||||||
|
"description": "A test project",
|
||||||
|
"ownerId": "test-owner-id",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := json.Marshal(requestBody)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to execute request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var response map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("Failed to decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have data
|
||||||
|
if data, exists := response["data"]; !exists {
|
||||||
|
t.Error("Expected data in response")
|
||||||
|
} else {
|
||||||
|
dataMap, ok := data.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Error("Expected data to be an object")
|
||||||
|
} else {
|
||||||
|
if createProject, exists := dataMap["createProject"]; !exists {
|
||||||
|
t.Error("Expected 'createProject' field in data")
|
||||||
|
} else {
|
||||||
|
projectMap, ok := createProject.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Error("Expected 'createProject' to be an object")
|
||||||
|
} else {
|
||||||
|
// Check for required fields
|
||||||
|
if id, exists := projectMap["id"]; !exists || id == "" {
|
||||||
|
t.Error("Expected non-empty 'id' field")
|
||||||
|
}
|
||||||
|
if name, exists := projectMap["name"]; !exists || name != "Test Project" {
|
||||||
|
t.Errorf("Expected name 'Test Project', got '%v'", name)
|
||||||
|
}
|
||||||
|
if ownerId, exists := projectMap["ownerId"]; !exists || ownerId != "test-owner-id" {
|
||||||
|
t.Errorf("Expected ownerId 'test-owner-id', got '%v'", ownerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
376
tests/unit/unit_test_utils.go
Normal file
376
tests/unit/unit_test_utils.go
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
package unit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"omega-server/local/model"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnitTestUtils provides utilities for unit testing
|
||||||
|
type UnitTestUtils struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnitTestUtils creates a new unit test utilities instance
|
||||||
|
func NewUnitTestUtils(db *gorm.DB) *UnitTestUtils {
|
||||||
|
return &UnitTestUtils{
|
||||||
|
DB: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockUser creates a mock user for testing
|
||||||
|
func (utu *UnitTestUtils) MockUser(id, email, fullName string) *model.User {
|
||||||
|
user := &model.User{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
Email: email,
|
||||||
|
FullName: fullName,
|
||||||
|
}
|
||||||
|
user.Init()
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockRole creates a mock role for testing
|
||||||
|
func (utu *UnitTestUtils) MockRole(id, name, description string) *model.Role {
|
||||||
|
role := &model.Role{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
Active: true,
|
||||||
|
System: false,
|
||||||
|
}
|
||||||
|
role.Init()
|
||||||
|
return role
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockPermission creates a mock permission for testing
|
||||||
|
func (utu *UnitTestUtils) MockPermission(id, name, description, category string) *model.Permission {
|
||||||
|
permission := &model.Permission{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
Category: category,
|
||||||
|
Active: true,
|
||||||
|
System: false,
|
||||||
|
}
|
||||||
|
permission.Init()
|
||||||
|
return permission
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockType creates a mock project type for testing
|
||||||
|
func (utu *UnitTestUtils) MockType(id, name, description string, userID *string) *model.Type {
|
||||||
|
projectType := &model.Type{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
projectType.Init()
|
||||||
|
return projectType
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockProject creates a mock project for testing
|
||||||
|
func (utu *UnitTestUtils) MockProject(id, name, description, ownerID, typeID string) *model.Project {
|
||||||
|
project := &model.Project{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
OwnerID: ownerID,
|
||||||
|
TypeID: typeID,
|
||||||
|
}
|
||||||
|
project.Init()
|
||||||
|
return project
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockTask creates a mock task for testing
|
||||||
|
func (utu *UnitTestUtils) MockTask(id, title, description, projectID string) *model.Task {
|
||||||
|
task := &model.Task{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
Status: model.TaskStatusTodo,
|
||||||
|
Priority: model.TaskPriorityMedium,
|
||||||
|
ProjectID: projectID,
|
||||||
|
}
|
||||||
|
task.Init()
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockIntegration creates a mock integration for testing
|
||||||
|
func (utu *UnitTestUtils) MockIntegration(id, projectID, integrationType string, config map[string]interface{}) *model.Integration {
|
||||||
|
integration := &model.Integration{
|
||||||
|
BaseModel: model.BaseModel{
|
||||||
|
ID: id,
|
||||||
|
},
|
||||||
|
ProjectID: projectID,
|
||||||
|
Type: integrationType,
|
||||||
|
}
|
||||||
|
integration.Init()
|
||||||
|
integration.SetConfig(config)
|
||||||
|
return integration
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertUserEqual asserts that two users are equal
|
||||||
|
func (utu *UnitTestUtils) AssertUserEqual(t *testing.T, expected, actual *model.User) {
|
||||||
|
if expected.ID != actual.ID {
|
||||||
|
t.Errorf("Expected user ID %s, got %s", expected.ID, actual.ID)
|
||||||
|
}
|
||||||
|
if expected.Email != actual.Email {
|
||||||
|
t.Errorf("Expected user email %s, got %s", expected.Email, actual.Email)
|
||||||
|
}
|
||||||
|
if expected.FullName != actual.FullName {
|
||||||
|
t.Errorf("Expected user full name %s, got %s", expected.FullName, actual.FullName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertProjectEqual asserts that two projects are equal
|
||||||
|
func (utu *UnitTestUtils) AssertProjectEqual(t *testing.T, expected, actual *model.Project) {
|
||||||
|
if expected.ID != actual.ID {
|
||||||
|
t.Errorf("Expected project ID %s, got %s", expected.ID, actual.ID)
|
||||||
|
}
|
||||||
|
if expected.Name != actual.Name {
|
||||||
|
t.Errorf("Expected project name %s, got %s", expected.Name, actual.Name)
|
||||||
|
}
|
||||||
|
if expected.Description != actual.Description {
|
||||||
|
t.Errorf("Expected project description %s, got %s", expected.Description, actual.Description)
|
||||||
|
}
|
||||||
|
if expected.OwnerID != actual.OwnerID {
|
||||||
|
t.Errorf("Expected project owner ID %s, got %s", expected.OwnerID, actual.OwnerID)
|
||||||
|
}
|
||||||
|
if expected.TypeID != actual.TypeID {
|
||||||
|
t.Errorf("Expected project type ID %s, got %s", expected.TypeID, actual.TypeID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertTaskEqual asserts that two tasks are equal
|
||||||
|
func (utu *UnitTestUtils) AssertTaskEqual(t *testing.T, expected, actual *model.Task) {
|
||||||
|
if expected.ID != actual.ID {
|
||||||
|
t.Errorf("Expected task ID %s, got %s", expected.ID, actual.ID)
|
||||||
|
}
|
||||||
|
if expected.Title != actual.Title {
|
||||||
|
t.Errorf("Expected task title %s, got %s", expected.Title, actual.Title)
|
||||||
|
}
|
||||||
|
if expected.Description != actual.Description {
|
||||||
|
t.Errorf("Expected task description %s, got %s", expected.Description, actual.Description)
|
||||||
|
}
|
||||||
|
if expected.Status != actual.Status {
|
||||||
|
t.Errorf("Expected task status %s, got %s", expected.Status, actual.Status)
|
||||||
|
}
|
||||||
|
if expected.Priority != actual.Priority {
|
||||||
|
t.Errorf("Expected task priority %s, got %s", expected.Priority, actual.Priority)
|
||||||
|
}
|
||||||
|
if expected.ProjectID != actual.ProjectID {
|
||||||
|
t.Errorf("Expected task project ID %s, got %s", expected.ProjectID, actual.ProjectID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertRoleEqual asserts that two roles are equal
|
||||||
|
func (utu *UnitTestUtils) AssertRoleEqual(t *testing.T, expected, actual *model.Role) {
|
||||||
|
if expected.ID != actual.ID {
|
||||||
|
t.Errorf("Expected role ID %s, got %s", expected.ID, actual.ID)
|
||||||
|
}
|
||||||
|
if expected.Name != actual.Name {
|
||||||
|
t.Errorf("Expected role name %s, got %s", expected.Name, actual.Name)
|
||||||
|
}
|
||||||
|
if expected.Description != actual.Description {
|
||||||
|
t.Errorf("Expected role description %s, got %s", expected.Description, actual.Description)
|
||||||
|
}
|
||||||
|
if expected.Active != actual.Active {
|
||||||
|
t.Errorf("Expected role active %t, got %t", expected.Active, actual.Active)
|
||||||
|
}
|
||||||
|
if expected.System != actual.System {
|
||||||
|
t.Errorf("Expected role system %t, got %t", expected.System, actual.System)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertPermissionEqual asserts that two permissions are equal
|
||||||
|
func (utu *UnitTestUtils) AssertPermissionEqual(t *testing.T, expected, actual *model.Permission) {
|
||||||
|
if expected.ID != actual.ID {
|
||||||
|
t.Errorf("Expected permission ID %s, got %s", expected.ID, actual.ID)
|
||||||
|
}
|
||||||
|
if expected.Name != actual.Name {
|
||||||
|
t.Errorf("Expected permission name %s, got %s", expected.Name, actual.Name)
|
||||||
|
}
|
||||||
|
if expected.Description != actual.Description {
|
||||||
|
t.Errorf("Expected permission description %s, got %s", expected.Description, actual.Description)
|
||||||
|
}
|
||||||
|
if expected.Category != actual.Category {
|
||||||
|
t.Errorf("Expected permission category %s, got %s", expected.Category, actual.Category)
|
||||||
|
}
|
||||||
|
if expected.Active != actual.Active {
|
||||||
|
t.Errorf("Expected permission active %t, got %t", expected.Active, actual.Active)
|
||||||
|
}
|
||||||
|
if expected.System != actual.System {
|
||||||
|
t.Errorf("Expected permission system %t, got %t", expected.System, actual.System)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertTypeEqual asserts that two types are equal
|
||||||
|
func (utu *UnitTestUtils) AssertTypeEqual(t *testing.T, expected, actual *model.Type) {
|
||||||
|
if expected.ID != actual.ID {
|
||||||
|
t.Errorf("Expected type ID %s, got %s", expected.ID, actual.ID)
|
||||||
|
}
|
||||||
|
if expected.Name != actual.Name {
|
||||||
|
t.Errorf("Expected type name %s, got %s", expected.Name, actual.Name)
|
||||||
|
}
|
||||||
|
if expected.Description != actual.Description {
|
||||||
|
t.Errorf("Expected type description %s, got %s", expected.Description, actual.Description)
|
||||||
|
}
|
||||||
|
if (expected.UserID == nil) != (actual.UserID == nil) {
|
||||||
|
t.Errorf("Expected type user ID nullability mismatch")
|
||||||
|
} else if expected.UserID != nil && actual.UserID != nil && *expected.UserID != *actual.UserID {
|
||||||
|
t.Errorf("Expected type user ID %s, got %s", *expected.UserID, *actual.UserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertIntegrationEqual asserts that two integrations are equal
|
||||||
|
func (utu *UnitTestUtils) AssertIntegrationEqual(t *testing.T, expected, actual *model.Integration) {
|
||||||
|
if expected.ID != actual.ID {
|
||||||
|
t.Errorf("Expected integration ID %s, got %s", expected.ID, actual.ID)
|
||||||
|
}
|
||||||
|
if expected.ProjectID != actual.ProjectID {
|
||||||
|
t.Errorf("Expected integration project ID %s, got %s", expected.ProjectID, actual.ProjectID)
|
||||||
|
}
|
||||||
|
if expected.Type != actual.Type {
|
||||||
|
t.Errorf("Expected integration type %s, got %s", expected.Type, actual.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestContext creates a test context
|
||||||
|
func (utu *UnitTestUtils) CreateTestContext() context.Context {
|
||||||
|
return context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertNoError asserts that there is no error
|
||||||
|
func (utu *UnitTestUtils) AssertNoError(t *testing.T, err error) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertError asserts that there is an error
|
||||||
|
func (utu *UnitTestUtils) AssertError(t *testing.T, err error) {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected an error, but got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertErrorMessage asserts that the error has a specific message
|
||||||
|
func (utu *UnitTestUtils) AssertErrorMessage(t *testing.T, err error, expectedMessage string) {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected an error with message '%s', but got no error", expectedMessage)
|
||||||
|
}
|
||||||
|
if err.Error() != expectedMessage {
|
||||||
|
t.Fatalf("Expected error message '%s', but got '%s'", expectedMessage, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertStringEqual asserts that two strings are equal
|
||||||
|
func (utu *UnitTestUtils) AssertStringEqual(t *testing.T, expected, actual string) {
|
||||||
|
if expected != actual {
|
||||||
|
t.Errorf("Expected string '%s', got '%s'", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertIntEqual asserts that two integers are equal
|
||||||
|
func (utu *UnitTestUtils) AssertIntEqual(t *testing.T, expected, actual int) {
|
||||||
|
if expected != actual {
|
||||||
|
t.Errorf("Expected int %d, got %d", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertBoolEqual asserts that two booleans are equal
|
||||||
|
func (utu *UnitTestUtils) AssertBoolEqual(t *testing.T, expected, actual bool) {
|
||||||
|
if expected != actual {
|
||||||
|
t.Errorf("Expected bool %t, got %t", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertNotNil asserts that a value is not nil
|
||||||
|
func (utu *UnitTestUtils) AssertNotNil(t *testing.T, value interface{}) {
|
||||||
|
if value == nil {
|
||||||
|
t.Fatalf("Expected value to not be nil, but it was")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertNil asserts that a value is nil
|
||||||
|
func (utu *UnitTestUtils) AssertNil(t *testing.T, value interface{}) {
|
||||||
|
if value != nil {
|
||||||
|
t.Fatalf("Expected value to be nil, but got: %+v", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertSliceLength asserts that a slice has a specific length
|
||||||
|
func (utu *UnitTestUtils) AssertSliceLength(t *testing.T, slice interface{}, expectedLength int) {
|
||||||
|
switch s := slice.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
if len(s) != expectedLength {
|
||||||
|
t.Errorf("Expected slice length %d, got %d", expectedLength, len(s))
|
||||||
|
}
|
||||||
|
case []*model.User:
|
||||||
|
if len(s) != expectedLength {
|
||||||
|
t.Errorf("Expected slice length %d, got %d", expectedLength, len(s))
|
||||||
|
}
|
||||||
|
case []*model.Project:
|
||||||
|
if len(s) != expectedLength {
|
||||||
|
t.Errorf("Expected slice length %d, got %d", expectedLength, len(s))
|
||||||
|
}
|
||||||
|
case []*model.Task:
|
||||||
|
if len(s) != expectedLength {
|
||||||
|
t.Errorf("Expected slice length %d, got %d", expectedLength, len(s))
|
||||||
|
}
|
||||||
|
case []*model.Role:
|
||||||
|
if len(s) != expectedLength {
|
||||||
|
t.Errorf("Expected slice length %d, got %d", expectedLength, len(s))
|
||||||
|
}
|
||||||
|
case []*model.Permission:
|
||||||
|
if len(s) != expectedLength {
|
||||||
|
t.Errorf("Expected slice length %d, got %d", expectedLength, len(s))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("Unsupported slice type for length assertion: %T", slice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertContains asserts that a string contains a substring
|
||||||
|
func (utu *UnitTestUtils) AssertContains(t *testing.T, str, substr string) {
|
||||||
|
if !contains(str, substr) {
|
||||||
|
t.Errorf("Expected string '%s' to contain '%s'", str, substr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertNotContains asserts that a string does not contain a substring
|
||||||
|
func (utu *UnitTestUtils) AssertNotContains(t *testing.T, str, substr string) {
|
||||||
|
if contains(str, substr) {
|
||||||
|
t.Errorf("Expected string '%s' to not contain '%s'", str, substr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// contains checks if a string contains a substring
|
||||||
|
func contains(str, substr string) bool {
|
||||||
|
return len(str) >= len(substr) && (str == substr || len(substr) == 0 ||
|
||||||
|
(len(substr) > 0 && findSubstring(str, substr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// findSubstring finds if substr exists in str
|
||||||
|
func findSubstring(str, substr string) bool {
|
||||||
|
for i := 0; i <= len(str)-len(substr); i++ {
|
||||||
|
if str[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user