Compare commits

...

2 Commits

Author SHA1 Message Date
Fran Jurmanović
b9cb315944 add tests 2025-07-06 19:19:42 +02:00
Fran Jurmanović
26a0d33592 implement graphQL and init postgres 2025-07-06 19:19:36 +02:00
33 changed files with 4631 additions and 314 deletions

13
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -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())
}
}

View File

@@ -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.

View 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!
}
`
}

View 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!
}

View 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
}

View File

@@ -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,

View File

@@ -44,22 +44,22 @@ type AuditLogCreateRequest struct {
// AuditLogInfo represents public audit log information // AuditLogInfo represents public audit log information
type AuditLogInfo struct { type AuditLogInfo struct {
ID string `json:"id"` ID string `json:"id"`
UserID string `json:"userId"` UserID string `json:"userId"`
UserEmail string `json:"userEmail,omitempty"` UserEmail string `json:"userEmail,omitempty"`
UserName string `json:"userName,omitempty"` UserName string `json:"userName,omitempty"`
Action string `json:"action"` Action string `json:"action"`
Resource string `json:"resource"` Resource string `json:"resource"`
ResourceID string `json:"resourceId"` ResourceID string `json:"resourceId"`
Details map[string]interface{} `json:"details"` Details map[string]interface{} `json:"details"`
IPAddress string `json:"ipAddress"` IPAddress string `json:"ipAddress"`
UserAgent string `json:"userAgent"` UserAgent string `json:"userAgent"`
Success bool `json:"success"` Success bool `json:"success"`
ErrorMsg string `json:"errorMsg,omitempty"` ErrorMsg string `json:"errorMsg,omitempty"`
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
@@ -122,26 +122,26 @@ func (al *AuditLog) GetDetailsAsJSON() (string, error) {
// ToAuditLogInfo converts AuditLog to AuditLogInfo (public information) // ToAuditLogInfo converts AuditLog to AuditLogInfo (public information)
func (al *AuditLog) ToAuditLogInfo() AuditLogInfo { func (al *AuditLog) ToAuditLogInfo() AuditLogInfo {
info := AuditLogInfo{ info := AuditLogInfo{
ID: al.ID, ID: al.ID,
UserID: al.UserID, UserID: al.UserID,
Action: al.Action, Action: al.Action,
Resource: al.Resource, Resource: al.Resource,
ResourceID: al.ResourceID, ResourceID: al.ResourceID,
Details: al.GetDetails(), Details: al.GetDetails(),
IPAddress: al.IPAddress, IPAddress: al.IPAddress,
UserAgent: al.UserAgent, UserAgent: al.UserAgent,
Success: al.Success, Success: al.Success,
ErrorMsg: al.ErrorMsg, ErrorMsg: al.ErrorMsg,
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

View File

@@ -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
View 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)
}

View File

@@ -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":

View File

@@ -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
View 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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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
View 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"),
}
}

View File

@@ -13,42 +13,98 @@ import (
// User represents a user in the system // User represents a user in the system
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"` Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
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;"`
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"`
@@ -65,16 +121,13 @@ type UserLoginResponse struct {
// UserInfo represents public user information // UserInfo represents public user information
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"` Roles []RoleInfo `json:"roles"`
Active bool `json:"active"` Permissions []string `json:"permissions"`
EmailVerified bool `json:"emailVerified"` CreatedAt time.Time `json:"created_at"`
LastLogin *time.Time `json:"lastLogin"` UpdatedAt time.Time `json:"updated_at"`
Roles []RoleInfo `json:"roles"`
Permissions []string `json:"permissions"`
DateCreated time.Time `json:"dateCreated"`
} }
// 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,55 +215,16 @@ 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, Roles: make([]RoleInfo, len(u.Roles)),
LastLogin: u.LastLogin, Permissions: []string{},
DateCreated: u.DateCreated,
Roles: make([]RoleInfo, len(u.Roles)),
Permissions: []string{},
} }
// Convert roles and collect permissions // Convert roles and collect permissions
@@ -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
} }

View File

@@ -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
} }

View File

@@ -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 {
return nil, err
} }
if req.Password != nil && *req.Password != "" { // Handle roles if provided
// Use the model's SetPassword method to hash the password if len(req.RoleIDs) > 0 {
if err := user.SetPassword(*req.Password); err != nil { roles := make([]model.Role, 0, len(req.RoleIDs))
return nil, err for _, roleID := range req.RoleIDs {
roleUUID, err := uuid.Parse(roleID)
if err != nil {
return nil, errors.New("invalid role ID format: " + roleID)
}
role, err := s.repo.FindRoleByID(ctx, roleUUID)
if err != nil {
return nil, errors.New("role not found: " + roleID)
}
roles = append(roles, *role)
} }
user.Roles = roles
} }
if req.RoleID != nil { // Validate updated user
// Check if role exists if err := user.Validate(); err != nil {
roleUUID, err := uuid.Parse(*req.RoleID) return nil, err
if err != nil {
return nil, errors.New("invalid role ID format")
}
role, err := s.repo.FindRoleByID(ctx, roleUUID)
if err != nil {
return nil, errors.New("role not found")
}
user.Roles = []model.Role{*role}
} }
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
} }

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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
@@ -214,10 +282,9 @@ 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
View 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
View 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
}

View 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
}

View 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")
}
})
}

View 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
View 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)
}

View 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)
}
}
}
}
}
})
}

View 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
}