diff --git a/.gitea/workflows/ build_check.yaml b/.gitea/workflows/ build_check.yaml new file mode 100644 index 0000000..4d35c68 --- /dev/null +++ b/.gitea/workflows/ build_check.yaml @@ -0,0 +1,39 @@ +name: Gitea Build Check +run-name: ${{ gitea.actor }} is testing the build +on: + push: + branches: + - develop/postgresql + pull_request: + branches: [ "main" ] + +jobs: + Build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Setup Go environment + uses: https://github.com/actions/setup-go@v5 + with: + # The Go version to download (if necessary) and use. Supports semver spec and ranges. + go-version: 1.22.0 # optional + # Path to the go.mod file. + go-version-file: ./go.mod # optional + # Set this option to true if you want the action to always check for the latest available version that satisfies the version spec + check-latest: true # optional + # Used to specify whether caching is needed. Set to true, if you'd like to enable caching. + cache: true # optional + + - name: Execute Go Test files with coverage report + run: TESTCONTAINERS_RYUK_DISABLED=true go test -v ./... -json -coverprofile="coverage.out" | tee "test-report.out" + + - uses: sonarsource/sonarqube-scan-action@master + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} + with: + args: > + -Dsonar.projectKey=Anthrove---OtterSpace-SDK \ No newline at end of file diff --git a/README.md b/README.md index 126a1b6..57ec96e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ + +![Build Check Runner](https://git.dragse.it/anthrove/otter-space-sdk/actions/workflows/build_check.yaml/badge.svg) +[![Bugs](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=bugs&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK) +[![Code Smells](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=code_smells&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK) +[![Coverage](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=coverage&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK) + +[![Duplicated Lines (%)](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=duplicated_lines_density&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK) +[![Lines of Code](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=ncloc&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK) +[![Maintainability Rating](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=sqale_rating&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK) +[![Quality Gate Status](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=alert_status&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK) + +[![Reliability Rating](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=reliability_rating&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK) +[![Security Hotspots](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=security_hotspots&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK) +[![Security Rating](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=security_rating&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK) +[![Vulnerabilities](https://sonarqube.dragse.de/api/project_badges/measure?project=Anthrove---OtterSpace-SDK&metric=vulnerabilities&token=sqb_96012ffdd64ce721d7f9c82bfa77aa27a5c1fd38)](https://sonarqube.dragse.de/dashboard?id=Anthrove---OtterSpace-SDK) + # OtterSpace SDK The OtterSpace SDK is a Go package for interacting with the OtterSpace API. It provides methods for connecting to the API, adding and linking users, posts, and sources, and retrieving information about users and posts. @@ -17,20 +33,24 @@ Here's a simple usage example: package main import ( - "context" - "fmt" - "git.dragse.it/anthrove/otter-space-sdk/pkg/models" - "git.dragse.it/anthrove/otter-space-sdk/pkg/graph" + "context" + "fmt" + "git.dragse.it/anthrove/otter-space-sdk/pkg/database" + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" ) func main() { - client := graph.NewGraphConnection() - err := client.Connect(context.Background(), "your-endpoint", "your-username", "your-password") - if err != nil { - fmt.Println(err) - return - } - // further usage of the client... + var err error + dbDebug := false + ctx := context.Background() + + pgClient := database.NewPostgresqlConnection(dbDebug) + err = pgClient.Connect(ctx, "your-endpoint", "your-username", "your-password", "anthrove", 5432, "disable", "Europe/Berlin") + if err != nil { + fmt.Println(err) + return + } + // further usage of the client... } ``` diff --git a/go.mod b/go.mod index 8593a08..fe9ef40 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,71 @@ module git.dragse.it/anthrove/otter-space-sdk go 1.22.0 require ( - github.com/neo4j/neo4j-go-driver/v5 v5.17.0 + github.com/lib/pq v1.10.9 + github.com/matoous/go-nanoid/v2 v2.1.0 + github.com/rubenv/sql-migrate v1.6.1 github.com/sirupsen/logrus v1.9.3 + github.com/testcontainers/testcontainers-go v0.31.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 + gorm.io/driver/postgres v1.5.7 + gorm.io/gorm v1.25.10 ) -require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/hcsshim v0.11.4 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/containerd v1.7.15 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/docker v25.0.5+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.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/now v1.1.5 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/mod v0.16.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.13.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d // indirect + google.golang.org/grpc v1.58.3 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) diff --git a/go.sum b/go.sum index b7d2bdf..a54ced4 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,229 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes= +github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/neo4j/neo4j-go-driver/v5 v5.17.0 h1:Bdqg1Y8Hd3uLYToXtBjysDYXTdMiP7zeUNUEwfbJkSo= -github.com/neo4j/neo4j-go-driver/v5 v5.17.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= +github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +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/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= +github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= +github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rubenv/sql-migrate v1.6.1 h1:bo6/sjsan9HaXAsNxYP/jCEDUGibHp8JmOBw7NTGRos= +github.com/rubenv/sql-migrate v1.6.1/go.mod h1:tPzespupJS0jacLfhbwto/UjSX+8h2FdWB7ar+QlHa0= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U= +github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI= +github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 h1:isAwFS3KNKRbJMbWv+wolWqOFUECmjYZ+sIRZCIBc/E= +github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0/go.mod h1:ZNYY8vumNCEG9YI59A9d6/YaMY49uwRhmeU563EzFGw= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d h1:pgIUhmqwKOUlnKna4r6amKdUngdL8DrkpFeV8+VBElY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= +gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/internal/logger.go b/internal/logger.go deleted file mode 100644 index 446869d..0000000 --- a/internal/logger.go +++ /dev/null @@ -1,46 +0,0 @@ -package internal - -import ( - "fmt" - - neo4jLog "github.com/neo4j/neo4j-go-driver/v5/neo4j/log" - log "github.com/sirupsen/logrus" -) - -type graphLogger struct { - graphDebug bool -} - -func NewGraphLogger(graphDebug bool) neo4jLog.Logger { - return &graphLogger{graphDebug: graphDebug} -} - -func (n graphLogger) Error(name string, id string, err error) { - log.WithFields(log.Fields{ - "name": name, - "id": id, - }).Errorf("graph: %s", err) -} - -func (n graphLogger) Warnf(name string, id string, msg string, args ...any) { - log.WithFields(log.Fields{ - "name": name, - "id": id, - }).Warnf("graph: %v", fmt.Sprintf(msg, args...)) -} - -func (n graphLogger) Infof(name string, id string, msg string, args ...any) { - log.WithFields(log.Fields{ - "name": name, - "id": id, - }).Infof("graph: %v", fmt.Sprintf(msg, args...)) -} - -func (n graphLogger) Debugf(name string, id string, msg string, args ...any) { - if n.graphDebug { - log.WithFields(log.Fields{ - "name": name, - "id": id, - }).Debugf("graph: %v", fmt.Sprintf(msg, args...)) - } -} diff --git a/internal/post.go b/internal/post.go deleted file mode 100644 index 021a841..0000000 --- a/internal/post.go +++ /dev/null @@ -1,116 +0,0 @@ -package internal - -import ( - "context" - - "git.dragse.it/anthrove/otter-space-sdk/pkg/models" - "github.com/neo4j/neo4j-go-driver/v5/neo4j" - log "github.com/sirupsen/logrus" -) - -func CreateAnthrovePostNode(ctx context.Context, driver neo4j.DriverWithContext, anthrovePost *models.AnthrovePost) error { - query := ` - CREATE (newPostNode:AnthrovePost {post_id: $anthrove_post_id, rating: $anthrove_rating}) - ` - - params := map[string]any{ - "anthrove_post_id": anthrovePost.PostID, - "anthrove_rating": anthrovePost.Rating, - } - - _, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return err - } - - log.WithFields(log.Fields{ - "anthrove_post_id": anthrovePost.PostID, - "anthrove_post_rating": anthrovePost.Rating, - }).Trace("graph: created anthrove post") - - return nil -} - -func CheckIfAnthrovePostNodeExistsByAnthroveID(ctx context.Context, driver neo4j.DriverWithContext, anthrovePost *models.AnthrovePost) (*models.AnthrovePost, bool, error) { - query := ` - OPTIONAL MATCH (postNode:AnthrovePost {post_id: $anthrove_post_id}) - RETURN postNode.post_id AS AnthrovePostID - ` - - params := map[string]any{ - "anthrove_post_id": anthrovePost.PostID, - } - - anthrovePost, exists, err := executeCheckQuery(ctx, driver, query, params) - if err != nil { - return nil, false, err - } - - return anthrovePost, exists, nil -} - -func CheckIfAnthrovePostNodeExistsBySourceURl(ctx context.Context, driver neo4j.DriverWithContext, sourceUrl string) (*models.AnthrovePost, bool, error) { - query := ` - OPTIONAL MATCH (postNode:AnthrovePost)<-[:REFERENCE {url: $source_url}]-() - RETURN postNode.post_id AS AnthrovePostID - ` - - params := map[string]any{ - "source_url": sourceUrl, - } - anthrovePost, exists, err := executeCheckQuery(ctx, driver, query, params) - if err != nil { - return nil, false, err - } - - return anthrovePost, exists, nil -} - -func CheckIfAnthrovePostNodeExistsBySourceID(ctx context.Context, driver neo4j.DriverWithContext, sourcePostID string) (*models.AnthrovePost, bool, error) { - query := ` - OPTIONAL MATCH (postNode:AnthrovePost)<-[:REFERENCE {source_post_id: $source_post_id}]-() - RETURN postNode.post_id AS AnthrovePostID - ` - - params := map[string]any{ - "source_post_id": sourcePostID, - } - - anthrovePost, exists, err := executeCheckQuery(ctx, driver, query, params) - if err != nil { - return nil, false, err - } - - return anthrovePost, exists, nil -} - -func executeCheckQuery(ctx context.Context, driver neo4j.DriverWithContext, query string, params map[string]any) (*models.AnthrovePost, bool, error) { - - var anthrovePost models.AnthrovePost - - result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return &anthrovePost, false, err - } - - record := result.Records - - anthrovePostID, isNil, err := neo4j.GetRecordValue[string](record[0], "AnthrovePostID") - exists := !isNil - if err != nil { - return &anthrovePost, exists, err - } - - anthrovePost.PostID = models.AnthrovePostID(anthrovePostID) - - log.WithFields(log.Fields{ - "anthrove_post_id": anthrovePost.PostID, - "anthrove_post_exists": exists, - }).Trace("graph: checked if post exists") - - if !exists { - return nil, exists, nil - } - - return &anthrovePost, exists, nil -} diff --git a/internal/postgres/post.go b/internal/postgres/post.go new file mode 100644 index 0000000..6c08cf3 --- /dev/null +++ b/internal/postgres/post.go @@ -0,0 +1,98 @@ +package postgres + +import ( + "context" + "errors" + + otterError "git.dragse.it/anthrove/otter-space-sdk/pkg/error" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +func CreatePost(ctx context.Context, db *gorm.DB, anthrovePost *models.Post) error { + + if anthrovePost == nil { + return &otterError.EntityValidationFailed{Reason: "anthrovePost is nil"} + } + + result := db.WithContext(ctx).Create(&anthrovePost) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + return &otterError.EntityAlreadyExists{} + } + return result.Error + } + + if result.RowsAffected == 0 { + return &otterError.NoDataWritten{} + } + + log.WithFields(log.Fields{ + "anthrove_post_id": anthrovePost.ID, + "anthrove_post_rating": anthrovePost.Rating, + }).Trace("database: created anthrove post") + + return nil +} + +func GetPostByAnthroveID(ctx context.Context, db *gorm.DB, anthrovePostID models.AnthrovePostID) (*models.Post, error) { + + if anthrovePostID == "" { + return nil, &otterError.EntityValidationFailed{Reason: "anthrovePostID is not set"} + } + + if len(anthrovePostID) != 25 { + return nil, &otterError.EntityValidationFailed{Reason: "anthrovePostID needs to be 25 characters long"} + } + + var post models.Post + result := db.WithContext(ctx).First(&post, "id = ?", anthrovePostID) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + return &post, nil +} + +func GetPostBySourceURL(ctx context.Context, db *gorm.DB, sourceURL string) (*models.Post, error) { + + if sourceURL == "" { + return nil, &otterError.EntityValidationFailed{Reason: "sourceURL is not set"} + } + + var post models.Post + result := db.WithContext(ctx).Raw(`SELECT p.id AS id, p.rating as rating FROM "Post" AS p INNER JOIN "PostReference" AS pr ON p.id = pr.post_id AND pr.url = $1 LIMIT 1`, sourceURL).First(&post) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + return &post, nil +} + +func GetPostBySourceID(ctx context.Context, db *gorm.DB, sourceID models.AnthroveSourceID) (*models.Post, error) { + + if sourceID == "" { + return nil, &otterError.EntityValidationFailed{Reason: "sourceID is not set"} + } + + var post models.Post + result := db.WithContext(ctx).Raw(`SELECT p.id AS id, p.rating as rating FROM "Post" AS p INNER JOIN "PostReference" AS pr ON p.id = pr.post_id AND pr.source_id = $1 LIMIT 1`, sourceID).First(&post) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + return &post, nil +} diff --git a/internal/postgres/post_test.go b/internal/postgres/post_test.go new file mode 100644 index 0000000..bb525c8 --- /dev/null +++ b/internal/postgres/post_test.go @@ -0,0 +1,381 @@ +package postgres + +import ( + "context" + "fmt" + "testing" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/test" + _ "github.com/lib/pq" + "gorm.io/gorm" +) + +func TestCreateAnthrovePostNode(t *testing.T) { + + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Tests + + validPost := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: models.AnthrovePostID(fmt.Sprintf("%025s", "1")), + }, + Rating: "safe", + } + + invalidPost := &models.Post{ + Rating: "error", + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthrovePost *models.Post + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid AnthrovePostID and Rating", + args: args{ + ctx: context.Background(), + db: gormDB, + anthrovePost: validPost, + }, + wantErr: false, + }, + { + name: "Test 2: Invalid Rating", + args: args{ + ctx: context.Background(), + db: gormDB, + anthrovePost: invalidPost, + }, + wantErr: true, + }, + { + name: "Test 3: Nill", + args: args{ + ctx: context.Background(), + db: gormDB, + anthrovePost: nil, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreatePost(tt.args.ctx, tt.args.db, tt.args.anthrovePost); (err != nil) != tt.wantErr { + t.Errorf("CreatePost() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetPostByAnthroveID(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Tests + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: models.AnthrovePostID(fmt.Sprintf("%025s", "1")), + }, + Rating: "safe", + } + + err = CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal("Could not create post", err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthrovePostID models.AnthrovePostID + } + tests := []struct { + name string + args args + want *models.Post + wantErr bool + }{ + { + name: "Test 1: Valid anthrovePostID", + args: args{ + ctx: ctx, + db: gormDB, + anthrovePostID: post.ID, + }, + want: post, + wantErr: false, + }, + { + name: "Test 2: Invalid anthrovePostID", + args: args{ + ctx: ctx, + db: gormDB, + anthrovePostID: "1234", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: No anthrovePostID", + args: args{ + ctx: ctx, + db: gormDB, + anthrovePostID: "", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetPostByAnthroveID(tt.args.ctx, tt.args.db, tt.args.anthrovePostID) + if (err != nil) != tt.wantErr { + t.Errorf("GetPostByAnthroveID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkPost(got, tt.want) { + t.Errorf("GetPostByAnthroveID() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetPostBySourceURL(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Tests + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: models.AnthrovePostID(fmt.Sprintf("%025s", "1")), + }, + + Rating: "safe", + } + + err = CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal("Could not create post", err) + } + + source := models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: models.AnthroveSourceID(fmt.Sprintf("%025s", "1")), + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.net/icon.ico", + } + + err = CreateSource(ctx, gormDB, &source) + if err != nil { + t.Fatal("Could not create source", err) + } + + err = CreateReferenceBetweenPostAndSource(ctx, gormDB, post.ID, models.AnthroveSourceDomain(source.Domain), "http://test.org", models.PostReferenceConfig{}) + if err != nil { + t.Fatal("Could not create source reference", err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + sourceURL string + } + tests := []struct { + name string + args args + want *models.Post + wantErr bool + }{ + { + name: "Test 1: Valid sourceURL", + args: args{ + ctx: ctx, + db: gormDB, + sourceURL: "http://test.org", + }, + want: post, + wantErr: false, + }, + { + name: "Test 2: Invalid sourceURL", + args: args{ + ctx: ctx, + db: gormDB, + sourceURL: "1234", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: No sourceURL", + args: args{ + ctx: ctx, + db: gormDB, + sourceURL: "", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetPostBySourceURL(tt.args.ctx, tt.args.db, tt.args.sourceURL) + if (err != nil) != tt.wantErr { + t.Errorf("GetPostBySourceURL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkPost(got, tt.want) { + t.Errorf("GetPostBySourceURL() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetPostBySourceID(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Tests + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: models.AnthrovePostID(fmt.Sprintf("%025s", "1")), + }, + Rating: "safe", + } + + err = CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal("Could not create post", err) + } + + source := models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: models.AnthroveSourceID(fmt.Sprintf("%025s", "1")), + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.net/icon.ico", + } + + err = CreateSource(ctx, gormDB, &source) + if err != nil { + t.Fatal("Could not create source", err) + } + + err = CreateReferenceBetweenPostAndSource(ctx, gormDB, post.ID, models.AnthroveSourceDomain(source.Domain), "http://test.otg", models.PostReferenceConfig{}) + if err != nil { + t.Fatal("Could not create source reference", err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + sourceID models.AnthroveSourceID + } + + tests := []struct { + name string + args args + want *models.Post + wantErr bool + }{ + { + name: "Test 1: Valid sourceID", + args: args{ + ctx: ctx, + db: gormDB, + sourceID: source.ID, + }, + want: post, + wantErr: false, + }, + { + name: "Test 2: Invalid sourceID", + args: args{ + ctx: ctx, + db: gormDB, + sourceID: "1234", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: No sourceID", + args: args{ + ctx: ctx, + db: gormDB, + sourceID: "", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetPostBySourceID(tt.args.ctx, tt.args.db, tt.args.sourceID) + if (err != nil) != tt.wantErr { + t.Errorf("GetPostBySourceID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkPost(got, tt.want) { + t.Errorf("GetPostBySourceID() got = %v, want %v", got, tt.want) + } + }) + } +} + +func checkPost(got *models.Post, want *models.Post) bool { + + if got == nil && want == nil { + return true + } else if got == nil || want == nil { + return false + } + + if got.ID != want.ID { + return false + } + + if got.Rating != want.Rating { + return false + } + + return true +} diff --git a/internal/postgres/relationships.go b/internal/postgres/relationships.go new file mode 100644 index 0000000..bda9569 --- /dev/null +++ b/internal/postgres/relationships.go @@ -0,0 +1,126 @@ +package postgres + +import ( + "context" + "errors" + + otterError "git.dragse.it/anthrove/otter-space-sdk/pkg/error" + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +func CreateReferenceBetweenPostAndSource(ctx context.Context, db *gorm.DB, anthrovePostID models.AnthrovePostID, sourceDomain models.AnthroveSourceDomain, postURL models.AnthrovePostURL, config models.PostReferenceConfig) error { + if anthrovePostID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthrovePostID) != 25 { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + if sourceDomain == "" { + return &otterError.EntityValidationFailed{Reason: "sourceDomain cannot be empty"} + } + + result := db.WithContext(ctx).Exec(`INSERT INTO "PostReference" (post_id, source_id, url, full_file_url, preview_file_url, sample_file_url, source_post_id) SELECT $1, source.id, $2, $4, $5, $6, $7 FROM "Source" AS source WHERE domain = $3;`, anthrovePostID, postURL, sourceDomain, config.FullFileURL, config.PreviewFileURL, config.SampleFileURL, config.SourcePostID) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return &otterError.NoDataFound{} + } + if errors.Is(result.Error, gorm.ErrCheckConstraintViolated) { + return &otterError.EntityAlreadyExists{} + } + + return result.Error + } + + if result.RowsAffected == 0 { + return &otterError.NoDataWritten{} + } + + log.WithFields(log.Fields{ + "anthrove_post_id": anthrovePostID, + "anthrove_source_domain": sourceDomain, + }).Trace("database: created anthrove post to source link") + + return nil +} + +func CreateReferenceBetweenUserAndPost(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, anthrovePostID models.AnthrovePostID) error { + + if anthrovePostID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthrovePostID) != 25 { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + if anthroveUserID == "" { + return &otterError.EntityValidationFailed{Reason: "anthroveUserID cannot be empty"} + } + + userFavorite := models.UserFavorites{ + UserID: string(anthroveUserID), + PostID: string(anthrovePostID), + } + + result := db.WithContext(ctx).Create(&userFavorite) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + return &otterError.EntityAlreadyExists{} + } + return result.Error + } + + if result.RowsAffected == 0 { + return &otterError.NoDataWritten{} + } + + log.WithFields(log.Fields{ + "anthrove_user_id": anthroveUserID, + "anthrove_post_id": anthrovePostID, + }).Trace("database: created user to post link") + + return nil +} + +func CheckReferenceBetweenUserAndPost(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, anthrovePostID models.AnthrovePostID) (bool, error) { + var count int64 + + if anthrovePostID == "" { + return false, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthrovePostID) != 25 { + return false, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + if anthroveUserID == "" { + return false, &otterError.EntityValidationFailed{Reason: "anthroveUserID cannot be empty"} + } + + if len(anthroveUserID) != 25 { + return false, &otterError.EntityValidationFailed{Reason: "anthroveUserID needs to be 25 characters long"} + } + + result := db.WithContext(ctx).Model(&models.UserFavorites{}).Where("user_id = ? AND post_id = ?", string(anthroveUserID), string(anthrovePostID)).Count(&count) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return false, &otterError.NoDataFound{} + } + return false, result.Error + } + + exists := count > 0 + + log.WithFields(log.Fields{ + "relationship_exists": exists, + "relationship_anthrove_user_id": anthroveUserID, + "relationship_anthrove_post_id": anthrovePostID, + }).Trace("database: checked user post relationship") + + return exists, nil +} diff --git a/internal/postgres/relationships_test.go b/internal/postgres/relationships_test.go new file mode 100644 index 0000000..dd67e2a --- /dev/null +++ b/internal/postgres/relationships_test.go @@ -0,0 +1,392 @@ +package postgres + +import ( + "context" + "fmt" + "testing" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/test" + "gorm.io/gorm" +) + +func TestCheckUserToPostLink(t *testing.T) { + + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validPostID := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + + err = CreateUser(ctx, gormDB, validUserID) + if err != nil { + t.Fatal(err) + } + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: validPostID, + }, + Rating: "safe", + } + + err = CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal(err) + } + + err = CreateReferenceBetweenUserAndPost(ctx, gormDB, validUserID, post.ID) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + anthrovePostID models.AnthrovePostID + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveUserID and AnthrovePostID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + anthrovePostID: post.ID, + }, + want: true, + wantErr: false, + }, + { + name: "Test 2: Valid AnthroveUserID and invalid AnthrovePostID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + anthrovePostID: "qadw", + }, + want: false, + wantErr: true, + }, + { + name: "Test 3: Valid AnthrovePostID and invalid AnthroveUserID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + anthrovePostID: post.ID, + }, + want: false, + wantErr: true, + }, + { + name: "Test 4: Invalid AnthrovePostID and invalid AnthroveUserID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + anthrovePostID: "123456", + }, + want: false, + wantErr: true, + }, + { + name: "Test 5: No AnthrovePostID given", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + anthrovePostID: "123456", + }, + want: false, + wantErr: true, + }, + { + name: "Test 6: No anthrovePostID given", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + anthrovePostID: "", + }, + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CheckReferenceBetweenUserAndPost(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.anthrovePostID) + if (err != nil) != tt.wantErr { + t.Errorf("CheckIfUserHasPostAsFavorite() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("CheckIfUserHasPostAsFavorite() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCheckUserToPostLinkWithNoData(t *testing.T) { + + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validPostID := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + + err = CreateUser(ctx, gormDB, validUserID) + if err != nil { + t.Fatal(err) + } + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: validPostID, + }, + Rating: "safe", + } + + err = CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal(err) + } + + err = CreateReferenceBetweenUserAndPost(ctx, gormDB, validUserID, post.ID) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + anthrovePostID models.AnthrovePostID + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveUserID and AnthrovePostID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + anthrovePostID: post.ID, + }, + want: true, + wantErr: false, + }, + { + name: "Test 2: Valid AnthroveUserID and invalid AnthrovePostID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + anthrovePostID: "qadw", + }, + want: false, + wantErr: true, + }, + { + name: "Test 3: Valid AnthrovePostID and invalid AnthroveUserID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + anthrovePostID: post.ID, + }, + want: false, + wantErr: true, + }, + { + name: "Test 4: Invalid AnthrovePostID and invalid AnthroveUserID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + anthrovePostID: "123456", + }, + want: false, + wantErr: true, + }, + { + name: "Test 5: No AnthrovePostID given", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + anthrovePostID: "123456", + }, + want: false, + wantErr: true, + }, + { + name: "Test 6: No anthrovePostID given", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + anthrovePostID: "", + }, + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CheckReferenceBetweenUserAndPost(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.anthrovePostID) + if (err != nil) != tt.wantErr { + t.Errorf("CheckIfUserHasPostAsFavorite() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("CheckIfUserHasPostAsFavorite() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEstablishUserToPostLink(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validPostID := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + + err = CreateUser(ctx, gormDB, validUserID) + if err != nil { + t.Fatal(err) + } + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: validPostID, + }, + Rating: "safe", + } + + err = CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + anthrovePostID models.AnthrovePostID + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveUserID and AnthrovePostID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + anthrovePostID: post.ID, + }, + wantErr: false, + }, + { + name: "Test 2: Valid AnthroveUserID and invalid AnthrovePostID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + anthrovePostID: "123456", + }, + wantErr: true, + }, + { + name: "Test 3: invalid AnthroveUserID and valid AnthrovePostID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + anthrovePostID: post.ID, + }, + wantErr: true, + }, + { + name: "Test 4: Invalid AnthrovePostID and invalid AnthroveUserID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + anthrovePostID: "123456", + }, + wantErr: true, + }, + { + name: "Test 5: AnthrovePostID is empty", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + anthrovePostID: "", + }, + wantErr: true, + }, + { + name: "Test 6: anthroveUserID is empty", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + anthrovePostID: validPostID, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateReferenceBetweenUserAndPost(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.anthrovePostID); (err != nil) != tt.wantErr { + t.Errorf("CreateReferenceBetweenUserAndPost() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/postgres/source.go b/internal/postgres/source.go new file mode 100644 index 0000000..bdb2122 --- /dev/null +++ b/internal/postgres/source.go @@ -0,0 +1,85 @@ +package postgres + +import ( + "context" + "errors" + + otterError "git.dragse.it/anthrove/otter-space-sdk/pkg/error" + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +// CreateSource creates a pgModels.Source +func CreateSource(ctx context.Context, db *gorm.DB, anthroveSource *models.Source) error { + + if anthroveSource.Domain == "" { + return &otterError.EntityValidationFailed{Reason: "Domain is required"} + } + + result := db.WithContext(ctx).Where(models.Source{Domain: anthroveSource.Domain}).FirstOrCreate(anthroveSource) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + return &otterError.EntityAlreadyExists{} + } + return result.Error + } + + if result.RowsAffected == 0 { + return &otterError.NoDataWritten{} + } + + log.WithFields(log.Fields{ + "node_source_url": anthroveSource.Domain, + "node_source_displayName": anthroveSource.DisplayName, + "node_source_icon": anthroveSource.Icon, + }).Trace("database: created source node") + + return nil +} + +// GetAllSource returns a list of all pgModels.Source +func GetAllSource(ctx context.Context, db *gorm.DB) ([]models.Source, error) { + var sources []models.Source + + result := db.WithContext(ctx).Find(&sources) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + log.WithFields(log.Fields{ + "tag_amount": result.RowsAffected, + }).Trace("database: get all source nodes") + + return sources, nil +} + +// GetSourceByDomain returns the first source it finds based on the domain +func GetSourceByDomain(ctx context.Context, db *gorm.DB, sourceDomain models.AnthroveSourceDomain) (*models.Source, error) { + var sources models.Source + + if sourceDomain == "" { + return nil, &otterError.EntityValidationFailed{Reason: "AnthroveSourceDomain is not set"} + } + + result := db.WithContext(ctx).Where("domain = ?", sourceDomain).First(&sources) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + log.WithFields(log.Fields{ + "tag_amount": result.RowsAffected, + }).Trace("database: get all source nodes") + + return &sources, nil +} diff --git a/internal/postgres/source_test.go b/internal/postgres/source_test.go new file mode 100644 index 0000000..dc5c58e --- /dev/null +++ b/internal/postgres/source_test.go @@ -0,0 +1,270 @@ +package postgres + +import ( + "context" + "fmt" + "testing" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/test" + "gorm.io/gorm" +) + +func TestCreateSourceNode(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validPostID := models.AnthroveSourceID(fmt.Sprintf("%025s", "Post1")) + + validSource := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validPostID}, + DisplayName: "e621", + Domain: "e621.net", + Icon: "icon.e621.net", + } + + invalidSource := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validPostID}, + Domain: "notfound.intern", + } + + invalidSourceDomain := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validPostID}, + Domain: "", + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveSource *models.Source + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid anthroveSource", + args: args{ + ctx: ctx, + db: gormDB, + anthroveSource: validSource, + }, + wantErr: false, + }, + { + name: "Test 2: inValid anthroveSource", + args: args{ + ctx: ctx, + db: gormDB, + anthroveSource: invalidSourceDomain, + }, + wantErr: true, + }, + { + name: "Test 3: unique anthroveSource", + args: args{ + ctx: ctx, + db: gormDB, + anthroveSource: invalidSource, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateSource(tt.args.ctx, tt.args.db, tt.args.anthroveSource); (err != nil) != tt.wantErr { + t.Errorf("CreateSource() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetAllSourceNodes(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + sources := []models.Source{ + { + DisplayName: "e621", + Domain: "e621.net", + Icon: "icon.e621.net", + }, + { + DisplayName: "furaffinity", + Domain: "furaffinity.net", + Icon: "icon.furaffinity.net", + }, + { + DisplayName: "fenpaws", + Domain: "fenpa.ws", + Icon: "icon.fenpa.ws", + }, + } + + for _, source := range sources { + err = CreateSource(ctx, gormDB, &source) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + } + tests := []struct { + name string + args args + want []models.Source + wantErr bool + }{ + { + name: "Test 1: Get all entries", + args: args{ + ctx: ctx, + db: gormDB, + }, + want: sources, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetAllSource(tt.args.ctx, tt.args.db) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllSource() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkSourcesNode(got, tt.want) { + t.Errorf("GetAllSource() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetSourceNodesByURL(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + source := &models.Source{ + DisplayName: "e621", + Domain: "e621.net", + Icon: "icon.e621.net", + } + + err = CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + domain models.AnthroveSourceDomain + } + tests := []struct { + name string + args args + want *models.Source + wantErr bool + }{ + { + name: "Test 1: Valid URL", + args: args{ + ctx: ctx, + db: gormDB, + domain: "e621.net", + }, + want: source, + wantErr: false, + }, + { + name: "Test 2: Invalid URL", + args: args{ + ctx: ctx, + db: gormDB, + domain: "eeeee.net", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 2: No URL", + args: args{ + ctx: ctx, + db: gormDB, + domain: "", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetSourceByDomain(tt.args.ctx, tt.args.db, tt.args.domain) + if (err != nil) != tt.wantErr { + t.Errorf("GetSourceByDomain() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkSourceNode(got, tt.want) { + t.Errorf("GetSourceByDomain() got = %v, want %v", got, tt.want) + } + }) + } +} + +func checkSourcesNode(got []models.Source, want []models.Source) bool { + for i, source := range want { + if source.DisplayName != got[i].DisplayName { + return false + } + if source.Domain != got[i].Domain { + return false + } + if source.Icon != got[i].Icon { + return false + } + } + + return true + +} + +func checkSourceNode(got *models.Source, want *models.Source) bool { + + if want == nil && got == nil { + return true + } + + if got.Domain != want.Domain { + return false + } + + return true + +} diff --git a/internal/postgres/tag.go b/internal/postgres/tag.go new file mode 100644 index 0000000..7ca240c --- /dev/null +++ b/internal/postgres/tag.go @@ -0,0 +1,329 @@ +package postgres + +import ( + "context" + "errors" + + otterError "git.dragse.it/anthrove/otter-space-sdk/pkg/error" + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +func CreateTag(ctx context.Context, db *gorm.DB, tagName models.AnthroveTagName, tagType models.TagType) error { + + if tagName == "" { + return &otterError.EntityValidationFailed{Reason: "tagName cannot be empty"} + } + + if tagType == "" { + return &otterError.EntityValidationFailed{Reason: "tagType cannot be empty"} + } + + result := db.WithContext(ctx).Create(&models.Tag{Name: string(tagName), Type: tagType}) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + return &otterError.EntityAlreadyExists{} + } + return result.Error + } + + if result.RowsAffected == 0 { + return &otterError.NoDataWritten{} + } + + log.WithFields(log.Fields{ + "tag_name": tagName, + "tag_type": tagType, + }).Trace("database: created tag node") + + return nil +} + +func DeleteTag(ctx context.Context, db *gorm.DB, tagName models.AnthroveTagName) error { + + if tagName == "" { + return &otterError.EntityValidationFailed{Reason: "tagName cannot be empty"} + } + + result := db.WithContext(ctx).Delete(&models.Tag{Name: string(tagName)}) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return &otterError.NoDataFound{} + } + return result.Error + } + + log.WithFields(log.Fields{ + "tag_name": tagName, + }).Trace("database: deleted tag") + + return nil +} + +func GetAllTagByTagsType(ctx context.Context, db *gorm.DB, tagType models.TagType) ([]models.Tag, error) { + var tags []models.Tag + + if tagType == "" { + return nil, &otterError.EntityValidationFailed{Reason: "tagType cannot be empty"} + } + + result := db.WithContext(ctx).Model(&models.Tag{}).Where("tag_type = ?", tagType).Scan(&tags) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + log.WithFields(log.Fields{ + "tags_length": len(tags), + }).Trace("database: got tag") + + return tags, nil +} + +func CreateTagAndReferenceToPost(ctx context.Context, db *gorm.DB, anthrovePostID models.AnthrovePostID, tag *models.Tag) error { + + if anthrovePostID == "" { + return &otterError.EntityValidationFailed{Reason: "anthrovePostID cannot be empty"} + } + + if len(anthrovePostID) != 25 { + return &otterError.EntityValidationFailed{Reason: "anthrovePostID needs to be 25 characters long"} + } + + if tag == nil { + return &otterError.EntityValidationFailed{Reason: "Tag is nil"} + } + + pgPost := models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: anthrovePostID, + }, + } + + err := db.WithContext(ctx).Model(&pgPost).Association("Tags").Append(tag) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return &otterError.NoDataFound{} + } + return errors.Join(err, &otterError.NoRelationCreated{}) + } + + log.WithFields(log.Fields{ + "anthrove_post_id": anthrovePostID, + "tag_name": tag.Name, + "tag_type": tag.Type, + }).Trace("database: created tag node") + + return nil +} + +func GetTags(ctx context.Context, db *gorm.DB) ([]models.Tag, error) { + var tags []models.Tag + + result := db.WithContext(ctx).Find(&tags) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + log.WithFields(log.Fields{ + "tag_amount": len(tags), + }).Trace("database: got tags") + + return tags, nil +} + +func CreateTagAlias(ctx context.Context, db *gorm.DB, tagAliasName models.AnthroveTagAliasName, tagID models.AnthroveTagID) error { + + if tagAliasName == "" { + return &otterError.EntityValidationFailed{Reason: "tagAliasName cannot be empty"} + } + if tagID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveTagIDEmpty} + } + + result := db.WithContext(ctx).Create(&models.TagAlias{ + Name: string(tagAliasName), + TagID: string(tagID), + }) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + return &otterError.EntityAlreadyExists{} + } + return result.Error + } + + log.WithFields(log.Fields{ + "tag_alias_name": tagAliasName, + "tag_alias_tag_id": tagID, + }).Trace("database: created tagAlias") + + return nil +} + +func GetAllTagAlias(ctx context.Context, db *gorm.DB) ([]models.TagAlias, error) { + var tagAliases []models.TagAlias + + result := db.WithContext(ctx).Find(&tagAliases) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + log.WithFields(log.Fields{ + "tag_alias_length": len(tagAliases), + }).Trace("database: created tagAlias") + + return tagAliases, nil +} + +func GetAllTagAliasByTag(ctx context.Context, db *gorm.DB, tagID models.AnthroveTagID) ([]models.TagAlias, error) { + var tagAliases []models.TagAlias + + if tagID == "" { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveTagIDEmpty} + } + + result := db.WithContext(ctx).Where("tag_id = ?", tagID).Find(&tagAliases) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + log.WithFields(log.Fields{ + "tag_alias_length": len(tagAliases), + "tag_alias_tag_id": tagID, + }).Trace("database: get specific tagAlias") + + return tagAliases, nil +} + +func DeleteTagAlias(ctx context.Context, db *gorm.DB, tagAliasName models.AnthroveTagAliasName) error { + + if tagAliasName == "" { + return &otterError.EntityValidationFailed{Reason: "tagAliasName cannot be empty"} + } + + result := db.WithContext(ctx).Delete(&models.TagAlias{Name: string(tagAliasName)}) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return &otterError.NoDataFound{} + } + return result.Error + } + + log.WithFields(log.Fields{ + "tag_alias_name": tagAliasName, + }).Trace("database: deleted tagAlias") + + return nil +} + +func CreateTagGroup(ctx context.Context, db *gorm.DB, tagGroupName models.AnthroveTagGroupName, tagID models.AnthroveTagID) error { + + if tagGroupName == "" { + return &otterError.EntityValidationFailed{Reason: "tagGroupName cannot be empty"} + } + if tagID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveTagIDEmpty} + } + + result := db.WithContext(ctx).Create(&models.TagGroup{ + Name: string(tagGroupName), + TagID: string(tagID), + }) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + return &otterError.EntityAlreadyExists{} + } + return result.Error + } + + log.WithFields(log.Fields{ + "tag_group_name": tagGroupName, + "tag_group_tag_id": tagID, + }).Trace("database: created tagGroup") + + return nil +} + +func GetAllTagGroup(ctx context.Context, db *gorm.DB) ([]models.TagGroup, error) { + var tagGroups []models.TagGroup + + result := db.WithContext(ctx).Find(&tagGroups) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + log.WithFields(log.Fields{ + "tag_alias_length": len(tagGroups), + }).Trace("database: created tagGroup") + + return tagGroups, nil +} + +func GetAllTagGroupByTag(ctx context.Context, db *gorm.DB, tagID models.AnthroveTagID) ([]models.TagGroup, error) { + var tagGroups []models.TagGroup + + if tagID == "" { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveTagIDEmpty} + } + + result := db.WithContext(ctx).Where("tag_id = ?", tagID).Find(&tagGroups) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + log.WithFields(log.Fields{ + "tag_alias_length": len(tagGroups), + "tag_alias_tag_id": tagID, + }).Trace("database: get specific tagGroup") + + return tagGroups, nil +} + +func DeleteTagGroup(ctx context.Context, db *gorm.DB, tagGroupName models.AnthroveTagGroupName) error { + + if tagGroupName == "" { + return &otterError.EntityValidationFailed{Reason: "tagGroupName cannot be empty"} + } + + result := db.WithContext(ctx).Delete(&models.TagGroup{Name: string(tagGroupName)}) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return &otterError.NoDataFound{} + } + return result.Error + } + + log.WithFields(log.Fields{ + "tag_alias_name": tagGroupName, + }).Trace("database: deleted tagAlias") + + return nil +} diff --git a/internal/postgres/tag_test.go b/internal/postgres/tag_test.go new file mode 100644 index 0000000..ab15604 --- /dev/null +++ b/internal/postgres/tag_test.go @@ -0,0 +1,1130 @@ +package postgres + +import ( + "context" + "fmt" + "reflect" + "testing" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/test" + "gorm.io/gorm" +) + +func TestCreateTagNodeWitRelation(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: models.AnthrovePostID(fmt.Sprintf("%025s", "1")), + }, + Rating: "safe", + } + + err = CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal(err) + } + + tag := &models.Tag{ + Name: "JayTheFerret", + Type: "artist", + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + PostID models.AnthrovePostID + tag *models.Tag + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid PostID and Tag", + args: args{ + ctx: ctx, + db: gormDB, + PostID: post.ID, + tag: tag, + }, + wantErr: false, + }, + { + name: "Test 2: Valid PostID and no Tag", + args: args{ + ctx: ctx, + db: gormDB, + PostID: post.ID, + tag: nil, + }, + wantErr: true, + }, + { + name: "Test 3: Invalid PostID and valid Tag", + args: args{ + ctx: ctx, + db: gormDB, + PostID: "123456", + tag: tag, + }, + wantErr: true, + }, + { + name: "Test 4: No PostID and valid Tag", + args: args{ + ctx: ctx, + db: gormDB, + PostID: "", + tag: tag, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateTagAndReferenceToPost(tt.args.ctx, tt.args.db, tt.args.PostID, tt.args.tag); (err != nil) != tt.wantErr { + t.Errorf("CreatePostWithReferenceToTagAnd() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetTags(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + tags := []models.Tag{ + { + Name: "JayTheFerret", + Type: "artist", + }, + { + Name: "anthro", + Type: "general", + }, + { + Name: "soxx", + Type: "character", + }, + } + + for _, tag := range tags { + err = CreateTag(ctx, gormDB, models.AnthroveTagName(tag.Name), tag.Type) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + } + tests := []struct { + name string + args args + want []models.Tag + wantErr bool + }{ + { + name: "Test 1: Get Tags", + args: args{ + ctx: ctx, + db: gormDB, + }, + want: tags, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetTags(tt.args.ctx, tt.args.db) + if (err != nil) != tt.wantErr { + t.Errorf("GetTags() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkTag(got, tt.want) { + t.Errorf("GetTags() got = %v, want %v", got, tt.want) + } + }) + } +} + +func checkTag(got []models.Tag, want []models.Tag) bool { + for i, tag := range want { + if tag.Type != got[i].Type { + return false + } + if tag.Name != got[i].Name { + return false + } + } + + return true + +} + +func TestCreateTag(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTag := models.Tag{ + Name: "JayTheFerret", + Type: "artist", + } + + invalidTag := models.Tag{} + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tagName models.AnthroveTagName + tagType models.TagType + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Tag", + args: args{ + ctx: ctx, + db: gormDB, + tagName: models.AnthroveTagName(validTag.Name), + tagType: validTag.Type, + }, + wantErr: false, + }, + { + name: "Test 2: Duplicate Tag", + args: args{ + ctx: ctx, + db: gormDB, + tagName: models.AnthroveTagName(validTag.Name), + tagType: validTag.Type, + }, + wantErr: true, + }, + { + name: "Test 3: Invalid Tag", + args: args{ + ctx: ctx, + db: gormDB, + tagName: models.AnthroveTagName(invalidTag.Name), + tagType: invalidTag.Type, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateTag(tt.args.ctx, tt.args.db, tt.args.tagName, tt.args.tagType); (err != nil) != tt.wantErr { + t.Errorf("CreateTag() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCreateTagAlias(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validTagAliasName01 := models.AnthroveTagAliasName("httyd") + validTagAliasName02 := models.AnthroveTagAliasName("dragon") + + validTagID := models.AnthroveTagID("toothless") + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + err = CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tagAliasName models.AnthroveTagAliasName + tagID models.AnthroveTagID + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + db: gormDB, + tagAliasName: validTagAliasName01, + tagID: validTagID, + }, + wantErr: false, + }, + { + name: "Test 2: No TagAliasName", + args: args{ + ctx: ctx, + db: gormDB, + tagAliasName: "", + tagID: validTagID, + }, + wantErr: true, + }, + { + name: "Test 4: No tagID", + args: args{ + ctx: ctx, + db: gormDB, + tagAliasName: validTagAliasName01, + tagID: "", + }, + wantErr: true, + }, + { + name: "Test 5: Duplicate tagID", + args: args{ + ctx: ctx, + db: gormDB, + tagAliasName: validTagAliasName01, + tagID: validTagID, + }, + wantErr: true, + }, + { + name: "Test 6: Invalide tagID", + args: args{ + ctx: ctx, + db: gormDB, + tagAliasName: validTagAliasName02, + tagID: "aaa", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateTagAlias(tt.args.ctx, tt.args.db, tt.args.tagAliasName, tt.args.tagID); (err != nil) != tt.wantErr { + t.Errorf("CreateTagAlias() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetAllTagAlias(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagAliases := []models.AnthroveTagAliasName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + expectedResult := []models.TagAlias{ + { + Name: string(validTagAliases[0]), + TagID: string(validTagID), + }, + { + Name: string(validTagAliases[1]), + TagID: string(validTagID), + }, + { + Name: string(validTagAliases[2]), + TagID: string(validTagID), + }, + } + + err = CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagAliasName := range validTagAliases { + err = CreateTagAlias(ctx, gormDB, tagAliasName, validTagID) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + } + tests := []struct { + name string + args args + want []models.TagAlias + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ + ctx: ctx, + db: gormDB, + }, + want: expectedResult, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetAllTagAlias(tt.args.ctx, tt.args.db) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagAlias() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTagAlias() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetAllTagAliasByTag(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagAliases := []models.AnthroveTagAliasName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + expectedResult := []models.TagAlias{ + { + Name: string(validTagAliases[0]), + TagID: string(validTagID), + }, + { + Name: string(validTagAliases[1]), + TagID: string(validTagID), + }, + { + Name: string(validTagAliases[2]), + TagID: string(validTagID), + }, + } + + err = CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagAliasName := range validTagAliases { + err = CreateTagAlias(ctx, gormDB, tagAliasName, validTagID) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tagID models.AnthroveTagID + } + tests := []struct { + name string + args args + want []models.TagAlias + wantErr bool + }{ + { + name: "Test 1: Valid TagID", + args: args{ + ctx: ctx, + db: gormDB, + tagID: validTagID, + }, + want: expectedResult, + wantErr: false, + }, + { + name: "Test 2: No TagID", + args: args{ + ctx: ctx, + db: gormDB, + tagID: "", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: Invalid TagID", + args: args{ + ctx: ctx, + db: gormDB, + tagID: "adads", + }, + want: []models.TagAlias{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetAllTagAliasByTag(tt.args.ctx, tt.args.db, tt.args.tagID) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagAliasByTag() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTagAliasByTag() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDeleteTagAlias(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagAliases := []models.AnthroveTagAliasName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + err = CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagAliasName := range validTagAliases { + err = CreateTagAlias(ctx, gormDB, tagAliasName, validTagID) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tagAliasName models.AnthroveTagAliasName + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveTagAliasName", + args: args{ + ctx: ctx, + db: gormDB, + tagAliasName: validTagAliases[0], + }, + wantErr: false, + }, + { + name: "Test 2: Invalid AnthroveTagAliasName", + args: args{ + ctx: ctx, + db: gormDB, + tagAliasName: "asdad", + }, + wantErr: false, + }, + { + name: "Test 3: No AnthroveTagAliasName", + args: args{ + ctx: ctx, + db: gormDB, + tagAliasName: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := DeleteTagAlias(tt.args.ctx, tt.args.db, tt.args.tagAliasName); (err != nil) != tt.wantErr { + t.Errorf("DeleteTagAlias() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCreateTagGroup(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validTagGroupName01 := models.AnthroveTagGroupName("httyd") + validTagGroupName02 := models.AnthroveTagGroupName("dragon") + + validTagID := models.AnthroveTagID("toothless") + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + err = CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tagGroupName models.AnthroveTagGroupName + tagID models.AnthroveTagID + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + db: gormDB, + tagGroupName: validTagGroupName01, + tagID: validTagID, + }, + wantErr: false, + }, + { + name: "Test 2: No TagGroupName", + args: args{ + ctx: ctx, + db: gormDB, + tagGroupName: "", + tagID: validTagID, + }, + wantErr: true, + }, + { + name: "Test 4: No tagID", + args: args{ + ctx: ctx, + db: gormDB, + tagGroupName: validTagGroupName01, + tagID: "", + }, + wantErr: true, + }, + { + name: "Test 5: Duplicate tagID", + args: args{ + ctx: ctx, + db: gormDB, + tagGroupName: validTagGroupName01, + tagID: validTagID, + }, + wantErr: true, + }, + { + name: "Test 6: Invalide tagID", + args: args{ + ctx: ctx, + db: gormDB, + tagGroupName: validTagGroupName02, + tagID: "aaa", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateTagGroup(tt.args.ctx, tt.args.db, tt.args.tagGroupName, tt.args.tagID); (err != nil) != tt.wantErr { + t.Errorf("CreateTagGroup() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetAllTagGroup(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagGroupes := []models.AnthroveTagGroupName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + expectedResult := []models.TagGroup{ + { + Name: string(validTagGroupes[0]), + TagID: string(validTagID), + }, + { + Name: string(validTagGroupes[1]), + TagID: string(validTagID), + }, + { + Name: string(validTagGroupes[2]), + TagID: string(validTagID), + }, + } + + err = CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagGroupName := range validTagGroupes { + err = CreateTagGroup(ctx, gormDB, tagGroupName, validTagID) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + } + tests := []struct { + name string + args args + want []models.TagGroup + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ + ctx: ctx, + db: gormDB, + }, + want: expectedResult, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetAllTagGroup(tt.args.ctx, tt.args.db) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagGroup() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTagGroup() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetAllTagGroupByTag(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagGroupes := []models.AnthroveTagGroupName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + expectedResult := []models.TagGroup{ + { + Name: string(validTagGroupes[0]), + TagID: string(validTagID), + }, + { + Name: string(validTagGroupes[1]), + TagID: string(validTagID), + }, + { + Name: string(validTagGroupes[2]), + TagID: string(validTagID), + }, + } + + err = CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagGroupName := range validTagGroupes { + err = CreateTagGroup(ctx, gormDB, tagGroupName, validTagID) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tagID models.AnthroveTagID + } + tests := []struct { + name string + args args + want []models.TagGroup + wantErr bool + }{ + { + name: "Test 1: Valid TagID", + args: args{ + ctx: ctx, + db: gormDB, + tagID: validTagID, + }, + want: expectedResult, + wantErr: false, + }, + { + name: "Test 2: No TagID", + args: args{ + ctx: ctx, + db: gormDB, + tagID: "", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: Invalid TagID", + args: args{ + ctx: ctx, + db: gormDB, + tagID: "adads", + }, + want: []models.TagGroup{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetAllTagGroupByTag(tt.args.ctx, tt.args.db, tt.args.tagID) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagGroupByTag() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTagGroupByTag() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDeleteTagGroup(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagGroupes := []models.AnthroveTagGroupName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + err = CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagGroupName := range validTagGroupes { + err = CreateTagGroup(ctx, gormDB, tagGroupName, validTagID) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tagGroupName models.AnthroveTagGroupName + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveTagGroupName", + args: args{ + ctx: ctx, + db: gormDB, + tagGroupName: validTagGroupes[0], + }, + wantErr: false, + }, + { + name: "Test 2: Invalid AnthroveTagGroupName", + args: args{ + ctx: ctx, + db: gormDB, + tagGroupName: "asdad", + }, + wantErr: false, + }, + { + name: "Test 3: No AnthroveTagGroupName", + args: args{ + ctx: ctx, + db: gormDB, + tagGroupName: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := DeleteTagGroup(tt.args.ctx, tt.args.db, tt.args.tagGroupName); (err != nil) != tt.wantErr { + t.Errorf("DeleteTagAlias() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestDeleteTag(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + err = CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tagName models.AnthroveTagName + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid TagName", + args: args{ + ctx: ctx, + db: gormDB, + tagName: models.AnthroveTagName(validTagID), + }, + wantErr: false, + }, + { + name: "Test 2: Invalid TagName", + args: args{ + ctx: ctx, + db: gormDB, + tagName: models.AnthroveTagName("aaa"), + }, + wantErr: false, + }, + { + name: "Test 3: No TagName", + args: args{ + ctx: ctx, + db: gormDB, + tagName: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := DeleteTag(tt.args.ctx, tt.args.db, tt.args.tagName); (err != nil) != tt.wantErr { + t.Errorf("DeleteTag() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetAllTagByTagType(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validTags := []models.Tag{ + { + Name: "JayTheFerret", + Type: models.Character, + }, + { + Name: "SoXX", + Type: models.Character, + }, + { + Name: "Alphyron", + Type: models.Character, + }, + { + Name: "Dragon", + Type: models.Species, + }, + } + + expectetResult := []models.Tag{ + { + Name: "JayTheFerret", + Type: models.Character, + }, + { + Name: "SoXX", + Type: models.Character, + }, + { + Name: "Alphyron", + Type: models.Character, + }, + } + + for _, tag := range validTags { + err = CreateTag(ctx, gormDB, models.AnthroveTagName(tag.Name), tag.Type) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tagType models.TagType + } + tests := []struct { + name string + args args + want []models.Tag + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ + ctx: ctx, + db: gormDB, + tagType: models.Character, + }, + want: expectetResult, + wantErr: false, + }, + { + name: "Test 2: invalid Tag Type", + args: args{ + ctx: ctx, + db: gormDB, + tagType: "aa", + }, + want: validTags, + wantErr: false, + }, + { + name: "Test 3: No Tag Type", + args: args{ + ctx: ctx, + db: gormDB, + tagType: "", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetAllTagByTagsType(tt.args.ctx, tt.args.db, tt.args.tagType) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagByTagsType() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkTag(got, tt.want) { + t.Errorf("GetAllTagByTagsType() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/postgres/user.go b/internal/postgres/user.go new file mode 100644 index 0000000..5037a17 --- /dev/null +++ b/internal/postgres/user.go @@ -0,0 +1,421 @@ +package postgres + +import ( + "context" + "errors" + "time" + + otterError "git.dragse.it/anthrove/otter-space-sdk/pkg/error" + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + gonanoid "github.com/matoous/go-nanoid/v2" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +// Workaround, should be changed later maybe, but its not that bad right now +type selectFrequencyTag struct { + tagName string `gorm:"tag_name"` + count int64 `gorm:"count"` + tagType models.TagType `gorm:"tag_type"` +} + +func CreateUser(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID) error { + + if anthroveUserID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthroveUserID) != 25 { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + user := models.User{ + BaseModel: models.BaseModel[models.AnthroveUserID]{ + ID: anthroveUserID, + }, + } + + result := db.WithContext(ctx).FirstOrCreate(&user) + if result.Error != nil { + + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + return &otterError.EntityAlreadyExists{} + } + return result.Error + } + + return nil +} + +func CreateUserWithRelationToSource(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, accountId string, accountUsername string) error { + + if anthroveUserID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthroveUserID) != 25 { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + if accountId == "" { + return &otterError.EntityValidationFailed{Reason: "accountID cannot be empty"} + } + + if accountUsername == "" { + return &otterError.EntityValidationFailed{Reason: "accountUsername cannot be empty"} + } + + validationCode, err := gonanoid.New(25) + + if err != nil { + return err + } + + result := db.WithContext(ctx).Exec(`WITH userObj AS ( + INSERT INTO "User" (id) + VALUES ($1) + ON CONFLICT (id) DO NOTHING + ) + INSERT INTO "UserSource" (user_id, source_id, account_username, account_id, account_validate, account_validation_key) + SELECT $2, source.id, $3, $4, false, $5 + FROM "Source" AS source + WHERE source.id = $6;`, anthroveUserID, anthroveUserID, accountUsername, accountId, validationCode, sourceID) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + return &otterError.EntityAlreadyExists{} + } + return result.Error + } + + if result.RowsAffected == 0 { + return &otterError.NoDataWritten{} + } + + log.WithFields(log.Fields{ + "anthrove_user_id": anthroveUserID, + "source_id": sourceID, + "account_username": accountUsername, + "account_id": accountId, + }).Info("database: created user-source relationship") + + return nil +} + +func GetUserFavoritesCount(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID) (int64, error) { + var count int64 + + if anthroveUserID == "" { + return 0, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthroveUserID) != 25 { + return 0, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + result := db.WithContext(ctx).Model(&models.UserFavorites{}).Where("user_id = ?", string(anthroveUserID)).Count(&count) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return 0, &otterError.NoDataFound{} + } + return 0, result.Error + } + + log.WithFields(log.Fields{ + "anthrove_user_id": anthroveUserID, + "anthrove_user_fav_count": count, + }).Trace("database: got user favorite count") + + return count, nil +} + +func GetUserSourceLinks(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID) (map[string]models.UserSource, error) { + var userSources []models.UserSource + userSourceMap := make(map[string]models.UserSource) + + if anthroveUserID == "" { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthroveUserID) != 25 { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + result := db.WithContext(ctx).Model(&models.UserSource{}).Where("user_id = ?", string(anthroveUserID)).Find(&userSources) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + for _, userSource := range userSources { + var source models.Source + result = db.WithContext(ctx).Model(&models.Source{}).Where("id = ?", userSource.SourceID).First(&source) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + userSourceMap[source.DisplayName] = models.UserSource{ + UserID: userSource.AccountID, + AccountUsername: userSource.AccountUsername, + Source: models.Source{ + DisplayName: source.DisplayName, + Domain: source.Domain, + Icon: source.Icon, + }, + } + } + + log.WithFields(log.Fields{ + "anthrove_user_id": anthroveUserID, + }).Trace("database: got user source link") + + return userSourceMap, nil +} + +func GetUserSourceBySourceID(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID) (*models.UserSource, error) { + var userSource models.UserSource + + if anthroveUserID == "" { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthroveUserID) != 25 { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + if sourceID == "" { + return nil, &otterError.EntityValidationFailed{Reason: "sourceID cannot be empty"} + } + + if len(sourceID) != 25 { + return nil, &otterError.EntityValidationFailed{Reason: "sourceID needs to be 25 characters long"} + } + + result := db.WithContext(ctx).Model(&models.UserSource{}).InnerJoins("Source", db.Where("id = ?", sourceID)).Where("user_id = ?", string(anthroveUserID)).First(&userSource) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + log.WithFields(log.Fields{ + "anthrove_user_id": anthroveUserID, + "source_id": sourceID, + }).Trace("database: got specified user source link") + + return &userSource, nil +} + +func GetAllUsers(ctx context.Context, db *gorm.DB) ([]models.User, error) { + var users []models.User + + result := db.WithContext(ctx).Model(&models.User{}).Find(&users) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, result.Error + } + + log.WithFields(log.Fields{ + "anthrove_user_id_count": len(users), + }).Trace("database: got all anthrove user IDs") + + return users, nil +} + +func GetUserFavoriteWithPagination(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) { + var userFavorites []models.UserFavorites + var favoritePosts []models.Post + + if anthroveUserID == "" { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthroveUserID) != 25 { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + err := db.WithContext(ctx).Model(&models.UserFavorites{}).Where("user_id = ?", string(anthroveUserID)).Offset(skip).Limit(limit).Find(&userFavorites).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, err + } + + for _, userFavorite := range userFavorites { + var post models.Post + err = db.WithContext(ctx).Model(&models.Post{}).Where("id = ?", userFavorite.PostID).First(&post).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, err + } + + favoritePosts = append(favoritePosts, + models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: post.ID}, + Rating: post.Rating, + }) + } + + log.WithFields(log.Fields{ + "anthrove_user_id": anthroveUserID, + "anthrove_user_fav_count": len(favoritePosts), + }).Trace("database: got all anthrove user favorites") + + return &models.FavoriteList{Posts: favoritePosts}, nil +} + +func GetUserTagWitRelationToFavedPosts(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error) { + var queryUserFavorites []selectFrequencyTag + + if anthroveUserID == "" { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthroveUserID) != 25 { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + rows, err := db.WithContext(ctx).Raw( + `WITH user_posts AS ( + SELECT post_id FROM "UserFavorites" WHERE user_id = $1 + ) + SELECT post_tags.tag_name AS tag_name, count(*) AS count, (SELECT tag_type FROM "Tag" WHERE "Tag".name = post_tags.tag_name LIMIT 1) AS tag_type FROM post_tags, user_posts WHERE post_tags.post_id IN (user_posts.post_id) GROUP BY post_tags.tag_name ORDER BY tag_type DESC, tag_name DESC`, anthroveUserID).Rows() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, &otterError.NoDataFound{} + } + return nil, err + } + + var userFavoritesFrequency = make([]models.TagsWithFrequency, 0) + defer rows.Close() + for rows.Next() { + var tagName string + var count int64 + var tagType string + rows.Scan(&tagName, &count, &tagType) + userFavoritesFrequency = append(userFavoritesFrequency, models.TagsWithFrequency{ + Frequency: count, + Tags: models.Tag{ + Name: tagName, + Type: models.TagType(tagType), + }, + }) + } + + log.WithFields(log.Fields{ + "anthrove_user_id": anthroveUserID, + "tag_amount": len(queryUserFavorites), + }).Trace("database: got user tag node with relation to faved posts") + + return userFavoritesFrequency, nil +} + +func UpdateUserSourceScrapeTimeInterval(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, scrapeTime models.AnthroveScrapeTimeInterval) error { + + if anthroveUserID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthroveUserID) != 25 { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + if sourceID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDEmpty} + } + + if len(sourceID) != 25 { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDToShort} + } + + if scrapeTime == 0 { + return &otterError.EntityValidationFailed{Reason: "ScrapeTimeInterval cannot be empty"} + } + + userSource := &models.UserSource{ + UserID: string(anthroveUserID), + } + + result := db.WithContext(ctx).Model(&userSource).Update("scrape_time_interval", scrapeTime) + if result.Error != nil { + return result.Error + } + + return nil +} + +func UpdateUserSourceLastScrapeTime(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, lastScrapeTime models.AnthroveUserLastScrapeTime) error { + + if anthroveUserID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthroveUserID) != 25 { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + if sourceID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDEmpty} + } + + if len(sourceID) != 25 { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDToShort} + } + + if time.Time.IsZero(time.Time(lastScrapeTime)) { + return &otterError.EntityValidationFailed{Reason: "LastScrapeTime cannot be empty"} + } + + userSource := &models.UserSource{ + UserID: string(anthroveUserID), + } + + result := db.WithContext(ctx).Model(&userSource).Update("last_scrape_time", time.Time(lastScrapeTime)) + if result.Error != nil { + return result.Error + } + + return nil +} + +func UpdateUserSourceValidation(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, valid bool) error { + + if anthroveUserID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthroveUserID) != 25 { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + if sourceID == "" { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDEmpty} + } + + if len(sourceID) != 25 { + return &otterError.EntityValidationFailed{Reason: otterError.AnthroveSourceIDToShort} + } + + userSource := &models.UserSource{ + UserID: string(anthroveUserID), + } + + result := db.WithContext(ctx).Model(&userSource).Update("account_validate", valid) + if result.Error != nil { + return result.Error + } + + return nil +} diff --git a/internal/postgres/user_test.go b/internal/postgres/user_test.go new file mode 100644 index 0000000..c8be97a --- /dev/null +++ b/internal/postgres/user_test.go @@ -0,0 +1,1353 @@ +package postgres + +import ( + "context" + "fmt" + "reflect" + "testing" + "time" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/test" + "gorm.io/gorm" +) + +func TestCreateUser(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveUserID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + }, + wantErr: false, + }, + { + name: "Test 2: Invalid AnthroveUserID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + }, + wantErr: true, + }, + { + name: "Test 3: No anthroveUserID given", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateUser(tt.args.ctx, tt.args.db, tt.args.anthroveUserID); (err != nil) != tt.wantErr { + t.Errorf("CreateUser() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCreateUserNodeWithSourceRelation(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validSourceID := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + source := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: validSourceID, + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "icon.e621.net", + } + err = CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + sourceID models.AnthroveSourceID + userID string + username string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid anthroveUserID, sourceID, userID, username", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: source.ID, + userID: "e1", + username: "marius", + }, + wantErr: false, + }, + { + name: "Test 2: Invalid anthroveUserID, valid sourceID, userID, username", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + sourceID: source.ID, + userID: "e1", + username: "marius", + }, + wantErr: true, + }, + { + name: "Test 3: Empty anthroveUserID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + sourceID: source.ID, + userID: "e1", + username: "marius", + }, + wantErr: true, + }, + { + name: "Test 4: invalid sourceID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: "fa.net", + userID: "e1", + username: "marius", + }, + wantErr: true, + }, + { + name: "Test 5: no userID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: source.ID, + userID: "", + username: "marius", + }, + wantErr: true, + }, + { + name: "Test 6: no username", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: source.ID, + userID: "aa", + username: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateUserWithRelationToSource(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.sourceID, tt.args.userID, tt.args.username); (err != nil) != tt.wantErr { + t.Errorf("CreateUserWithRelationToSource() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetAllUsers(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validUserID01 := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + validUserID02 := models.AnthroveUserID(fmt.Sprintf("%025s", "User2")) + validUserID03 := models.AnthroveUserID(fmt.Sprintf("%025s", "User3")) + + users := []models.User{ + { + BaseModel: models.BaseModel[models.AnthroveUserID]{ID: validUserID01}, + }, + { + BaseModel: models.BaseModel[models.AnthroveUserID]{ID: validUserID02}, + }, + { + BaseModel: models.BaseModel[models.AnthroveUserID]{ID: validUserID03}, + }, + } + + for _, user := range users { + err = CreateUser(ctx, gormDB, user.ID) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + } + tests := []struct { + name string + args args + want []models.User + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ + ctx: ctx, + db: gormDB, + }, + want: users, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetAllUsers(tt.args.ctx, tt.args.db) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllUsers() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkUser(got, tt.want) { + t.Errorf("GetAllUsers() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetUserSourceBySourceID(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validSourceID := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + source := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: validSourceID, + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.icon", + } + + expectedResult := &models.UserSource{ + UserID: string(validUserID), + AccountUsername: "euser", + Source: models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: source.ID}, + DisplayName: source.DisplayName, + Domain: source.Domain, + Icon: source.Icon, + }, + } + + err = CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + err = CreateUserWithRelationToSource(ctx, gormDB, validUserID, validSourceID, expectedResult.UserID, expectedResult.AccountUsername) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + sourceID models.AnthroveSourceID + } + tests := []struct { + name string + args args + want *models.UserSource + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveUserID and sourceID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: source.ID, + }, + want: expectedResult, + wantErr: false, + }, + { + name: "Test 2: Invalid AnthroveUserID and valid sourceID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + sourceID: source.ID, + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: Valid AnthroveUserID and invalid sourceID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: "fa", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 4: No AnthroveUserID and Valid sourceID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + sourceID: source.ID, + }, + want: nil, + wantErr: true, + }, + { + name: "Test 5: Valid AnthroveUserID and No anthroveUserID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "1", + sourceID: "", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 6: No AnthroveUserID and No anthroveUserID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + sourceID: "", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 7: No anthroveUserID given", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: "", + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetUserSourceBySourceID(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.sourceID) + if (err != nil) != tt.wantErr { + t.Errorf("GetUserSourceBySourceID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkUserSource(got, tt.want) { + t.Errorf("GetUserSourceBySourceID() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetUserFavoriteNodeWithPagination(t *testing.T) { + // Setup trow away containert + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validAnthroveUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + validPostID2 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post2")) + validPostID3 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post3")) + validPostID4 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post4")) + validPostID5 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post5")) + validPostID6 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post6")) + + expectedResultPosts := []models.Post{ + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID1}, + Rating: "safe", + }, + { + + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID2}, + Rating: "safe", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID3}, + Rating: "explicit", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID4}, + Rating: "explicit", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID5}, + Rating: "questionable", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID6}, + Rating: "safe", + }, + } + expectedResult := &models.FavoriteList{ + Posts: expectedResultPosts, + } + expectedResult2 := &models.FavoriteList{ + Posts: expectedResultPosts[2:], + } + expectedResult3 := &models.FavoriteList{ + Posts: expectedResultPosts[:3], + } + + err = CreateUser(ctx, gormDB, validAnthroveUserID) + if err != nil { + t.Fatal(err) + } + + for _, expectedResultPost := range expectedResultPosts { + err = CreatePost(ctx, gormDB, &expectedResultPost) + if err != nil { + t.Fatal(err) + } + err = CreateReferenceBetweenUserAndPost(ctx, gormDB, validAnthroveUserID, expectedResultPost.ID) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + skip int + limit int + } + tests := []struct { + name string + args args + want *models.FavoriteList + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveUserID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validAnthroveUserID, + skip: 0, + limit: 2000, + }, + want: expectedResult, + wantErr: false, + }, + { + name: "Test 2: Skip first two", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validAnthroveUserID, + skip: 2, + limit: 2000, + }, + want: expectedResult2, + wantErr: false, + }, + { + name: "Test 3: Limit of 3", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validAnthroveUserID, + skip: 0, + limit: 3, + }, + want: expectedResult3, + wantErr: false, + }, + { + name: "Test 4: No anthroveUserID given", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + skip: 0, + limit: 3, + }, + want: nil, + wantErr: true, + }, + { + name: "Test 5: Short anthroveUserID given", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "aaa", + skip: 0, + limit: 3, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetUserFavoriteWithPagination(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.skip, tt.args.limit) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllUserFavoritesWithPagination() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllUserFavoritesWithPagination() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetUserFavoritesCount(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validAnthroveUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + validPostID2 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post2")) + validPostID3 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post3")) + validPostID4 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post4")) + validPostID5 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post5")) + validPostID6 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post6")) + + expectedResultPosts := []models.Post{ + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID1}, + Rating: "safe", + }, + { + + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID2}, + Rating: "safe", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID3}, + Rating: "explicit", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID4}, + Rating: "explicit", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID5}, + Rating: "questionable", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID6}, + Rating: "safe", + }, + } + + err = CreateUser(ctx, gormDB, validAnthroveUserID) + if err != nil { + t.Fatal(err) + } + + for _, post := range expectedResultPosts { + err = CreatePost(ctx, gormDB, &post) + if err != nil { + t.Fatal(err) + } + err = CreateReferenceBetweenUserAndPost(ctx, gormDB, validAnthroveUserID, post.ID) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + } + tests := []struct { + name string + args args + want int64 + wantErr bool + }{ + { + name: "Test 1: Valid anthroveUserID and 6 favorite posts", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validAnthroveUserID, + }, + want: 6, + wantErr: false, + }, + { + name: "Test 2: Invalid anthroveUserID and 6 favorite posts", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "2", + }, + want: 0, + wantErr: true, + }, + { + name: "Test 3: no anthroveUserID and 6 favorite posts", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + }, + want: 0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetUserFavoritesCount(tt.args.ctx, tt.args.db, tt.args.anthroveUserID) + if (err != nil) != tt.wantErr { + t.Errorf("GetUserFavoritesCount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetUserFavoritesCount() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetUserSourceLinks(t *testing.T) { + // Setup trow away containert + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validAnthroveUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + + validSourceID1 := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + validSourceID2 := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source2")) + + eSource := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validSourceID1}, + DisplayName: "e621", + Domain: "e621.net", + } + err = CreateSource(ctx, gormDB, eSource) + if err != nil { + t.Fatal("Create Source e621:", err) + } + + faSource := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validSourceID2}, + DisplayName: "fa", + Domain: "fa.net", + } + err = CreateSource(ctx, gormDB, faSource) + if err != nil { + t.Fatal("Create Source fa:", err) + } + + expectedResult := make(map[string]models.UserSource) + expectedResult["e621"] = models.UserSource{ + UserID: "e1", + AccountUsername: "e621-user", + Source: models.Source{ + DisplayName: eSource.DisplayName, + Domain: eSource.Domain, + }, + } + expectedResult["fa"] = models.UserSource{ + UserID: "fa1", + AccountUsername: "fa-user", + Source: models.Source{ + DisplayName: faSource.DisplayName, + Domain: faSource.Domain, + }, + } + + err = CreateUserWithRelationToSource(ctx, gormDB, validAnthroveUserID, eSource.ID, expectedResult["e621"].UserID, expectedResult["e621"].AccountUsername) + if err != nil { + t.Fatal("CreateUserWithRelationToSource e621:", err) + } + err = CreateUserWithRelationToSource(ctx, gormDB, validAnthroveUserID, faSource.ID, expectedResult["fa"].UserID, expectedResult["fa"].AccountUsername) + if err != nil { + t.Fatal("CreateUserWithRelationToSource fa:", err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + } + tests := []struct { + name string + args args + want map[string]models.UserSource + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validAnthroveUserID, + }, + want: expectedResult, + wantErr: false, + }, + { + name: "Test 3: No AnthroveID", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 1: AnthroveID to short", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "aaa", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetUserSourceLinks(tt.args.ctx, tt.args.db, tt.args.anthroveUserID) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllUserSources() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllUserSources() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetUserTagNodeWitRelationToFavedPosts(t *testing.T) { + // Setup trow away containert + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validAnthroveUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + validPostID2 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post2")) + validPostID3 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post3")) + + err = CreateUser(ctx, gormDB, validAnthroveUserID) + if err != nil { + t.Fatal(err) + } + + posts := []models.Post{ + {BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID1}, Rating: "safe"}, + {BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID2}, Rating: "safe"}, + {BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID3}, Rating: "explicit"}, + } + + for _, post := range posts { + err = CreatePost(ctx, gormDB, &post) + if err != nil { + t.Fatal(err) + } + err = CreateReferenceBetweenUserAndPost(ctx, gormDB, validAnthroveUserID, post.ID) + if err != nil { + t.Fatal(err) + } + } + + tags := []models.Tag{ + {Name: "JayTheFerret", Type: "artist"}, + {Name: "Ferret", Type: "species"}, + {Name: "Jay", Type: "character"}, + } + + for i, tag := range tags { + err = CreateTagAndReferenceToPost(ctx, gormDB, posts[i].ID, &tag) + if err != nil { + t.Fatal(err) + } + } + + expectedResult := []models.TagsWithFrequency{ + { + Frequency: 1, + Tags: models.Tag{ + Name: tags[0].Name, + Type: tags[0].Type, + }, + }, + { + Frequency: 1, + Tags: models.Tag{ + Name: tags[2].Name, + Type: tags[2].Type, + }, + }, + { + Frequency: 1, + Tags: models.Tag{ + Name: tags[1].Name, + Type: tags[1].Type, + }, + }, + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + } + tests := []struct { + name string + args args + want []models.TagsWithFrequency + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validAnthroveUserID, + }, + want: expectedResult, + wantErr: false, + }, + { + name: "Test 2: No anthroveUserID given", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: short anthroveUserID given", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "aaa", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetUserTagWitRelationToFavedPosts(tt.args.ctx, tt.args.db, tt.args.anthroveUserID) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagsFromUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTagsFromUser() got = %v, want %v", got, tt.want) + } + }) + } +} + +func checkUser(got []models.User, want []models.User) bool { + for i, user := range want { + if user.ID != got[i].ID { + return false + } + } + return true +} + +func checkUserSource(got *models.UserSource, want *models.UserSource) bool { + + if got == nil && want == nil { + return true + } else if got == nil || want == nil { + return false + } + + if got.UserID != want.UserID { + return false + } + if got.AccountUsername != want.AccountUsername { + return false + } + if got.Source.DisplayName != want.Source.DisplayName { + return false + } + if got.Source.Domain != want.Source.Domain { + return false + } + if got.Source.Icon != want.Source.Icon { + return false + } + + return true +} + +func TestUpdateUserSourceScrapeTimeInterval(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validSourceID := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + source := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: validSourceID, + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.icon", + } + + err = CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + err = CreateUserWithRelationToSource(ctx, gormDB, validUserID, validSourceID, "e66e6e6e6", "euser") + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + sourceID models.AnthroveSourceID + scrapeTime models.AnthroveScrapeTimeInterval + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: validSourceID, + scrapeTime: 10, + }, + wantErr: false, + }, + { + name: "Test 2: anthroveUserID to short", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + sourceID: validSourceID, + scrapeTime: 10, + }, + wantErr: true, + }, + { + name: "Test 3: anthroveUserID is empty", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + sourceID: validSourceID, + scrapeTime: 10, + }, + wantErr: true, + }, + { + name: "Test 4: anthroveUserID to short", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: "111", + scrapeTime: 10, + }, + wantErr: true, + }, + { + name: "Test 5: anthroveUserID is empty", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: "", + scrapeTime: 10, + }, + wantErr: true, + }, + { + name: "Test 5: scrapeTime is empty", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: validSourceID, + scrapeTime: 0, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := UpdateUserSourceScrapeTimeInterval(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.sourceID, tt.args.scrapeTime); (err != nil) != tt.wantErr { + t.Errorf("UpdateUserSourceScrapeTimeInterval() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestUpdateUserSourceLastScrapeTime(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validSourceID := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + validScrapeTime := models.AnthroveUserLastScrapeTime(time.Now()) + inValidScrapeTime := models.AnthroveUserLastScrapeTime{} + + source := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: validSourceID, + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.icon", + } + + err = CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + err = CreateUserWithRelationToSource(ctx, gormDB, validUserID, validSourceID, "e66e6e6e6", "euser") + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + sourceID models.AnthroveSourceID + lastScrapeTime models.AnthroveUserLastScrapeTime + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: validSourceID, + lastScrapeTime: validScrapeTime, + }, + wantErr: false, + }, + { + name: "Test 2: anthroveUserID to short", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + sourceID: validSourceID, + lastScrapeTime: validScrapeTime, + }, + wantErr: true, + }, + { + name: "Test 3: anthroveUserID is empty", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + sourceID: validSourceID, + lastScrapeTime: validScrapeTime, + }, + wantErr: true, + }, + { + name: "Test 4: anthroveUserID to short", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: "111", + lastScrapeTime: validScrapeTime, + }, + wantErr: true, + }, + { + name: "Test 5: anthroveUserID is empty", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: "", + lastScrapeTime: validScrapeTime, + }, + wantErr: true, + }, + { + name: "Test 5: scrapeTime is empty", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: validSourceID, + lastScrapeTime: inValidScrapeTime, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := UpdateUserSourceLastScrapeTime(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.sourceID, tt.args.lastScrapeTime); (err != nil) != tt.wantErr { + t.Errorf("UpdateUserSourceLastScrapeTime() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestUpdateUserSourceValidation(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validSourceID := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + source := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: validSourceID, + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.icon", + } + + err = CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + err = CreateUserWithRelationToSource(ctx, gormDB, validUserID, validSourceID, "e66e6e6e6", "euser") + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthroveUserID models.AnthroveUserID + sourceID models.AnthroveSourceID + valid bool + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: validSourceID, + valid: true, + }, + wantErr: false, + }, + { + name: "Test 2: anthroveUserID to short", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: invalidUserID, + sourceID: validSourceID, + valid: true, + }, + wantErr: true, + }, + { + name: "Test 3: anthroveUserID is empty", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: "", + sourceID: validSourceID, + valid: true, + }, + wantErr: true, + }, + { + name: "Test 4: anthroveUserID to short", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: "111", + valid: true, + }, + wantErr: true, + }, + { + name: "Test 5: anthroveUserID is empty", + args: args{ + ctx: ctx, + db: gormDB, + anthroveUserID: validUserID, + sourceID: "", + valid: true, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := UpdateUserSourceValidation(tt.args.ctx, tt.args.db, tt.args.anthroveUserID, tt.args.sourceID, tt.args.valid); (err != nil) != tt.wantErr { + t.Errorf("UpdateUserSourceValidation() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/relationships.go b/internal/relationships.go deleted file mode 100644 index 7e18bca..0000000 --- a/internal/relationships.go +++ /dev/null @@ -1,100 +0,0 @@ -package internal - -import ( - "context" - "fmt" - - "git.dragse.it/anthrove/otter-space-sdk/pkg/models" - "github.com/neo4j/neo4j-go-driver/v5/neo4j" - log "github.com/sirupsen/logrus" -) - -func EstablishAnthrovePostToSourceLink(ctx context.Context, driver neo4j.DriverWithContext, anthrovePostID models.AnthrovePostID, anthroveSourceDomain string, anthrovePostRelationship *models.AnthrovePostRelationship) error { - query := ` - MATCH (sourceNode:Source {domain: $source_url}) - MATCH (postNode:AnthrovePost {post_id: $anthrove_post_id}) - MERGE (sourceNode)-[:REFERENCE {url: $source_post_url, source_post_id: $source_post_id}]->(postNode) - ` - - params := map[string]any{ - "source_url": anthroveSourceDomain, - "anthrove_post_id": anthrovePostID, - "source_post_url": anthrovePostRelationship.Url, - "source_post_id": anthrovePostRelationship.PostID, - } - - _, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return err - } - - log.WithFields(log.Fields{ - "source_url": anthroveSourceDomain, - "anthrove_post_id": anthrovePostID, - "source_post_url": anthrovePostRelationship.Url, - "source_post_id": anthrovePostRelationship.PostID, - }).Trace("graph: creating anthrove post to source link") - - return nil -} - -func EstablishUserToPostLink(ctx context.Context, driver neo4j.DriverWithContext, anthroveUser *models.AnthroveUser, anthrovePost *models.AnthrovePost) error { - - query := ` - MATCH (user:User {user_id: $anthrove_user_id}) - MATCH (anthrovePost:AnthrovePost {post_id: $anthrove_post_id}) - MERGE (user)-[:FAV]->(anthrovePost) - ` - - params := map[string]any{ - "anthrove_post_id": anthrovePost.PostID, - "anthrove_user_id": anthroveUser.UserID, - } - - _, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return err - } - - log.WithFields(log.Fields{ - "anthrove_post_id": anthrovePost.PostID, - "anthrove_user_id": anthroveUser.UserID, - }).Trace("graph: created user to post link") - - return nil -} - -func CheckUserToPostLink(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID, sourcePostID string, sourceUrl string) (bool, error) { - query := ` - OPTIONAL MATCH (:User {user_id: $anthrove_user_id})-[f:FAV]->(:AnthrovePost)<-[:REFERENCE{source_post_id: $source_post_id}]-(:Source{domain: $source_domain}) - RETURN COUNT(f) > 0 AS hasRelationship - ` - - params := map[string]any{ - "anthrove_user_id": anthroveUserID, - "source_post_id": sourcePostID, - "source_domain": sourceUrl, - } - - result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return false, err - } - - if len(result.Records) == 0 { - return false, fmt.Errorf("no records found") - } - - exists, _, err := neo4j.GetRecordValue[bool](result.Records[0], "hasRelationship") - if err != nil { - return false, err - } - - log.WithFields(log.Fields{ - "relationship_exists": exists, - "relationship_anthrove_user_id": anthroveUserID, - "relationship_e621_post_id": "", - }).Trace("graph: checked user post relationship") - - return exists, nil -} diff --git a/internal/source.go b/internal/source.go deleted file mode 100644 index bb91fdf..0000000 --- a/internal/source.go +++ /dev/null @@ -1,113 +0,0 @@ -package internal - -import ( - "context" - "fmt" - - "git.dragse.it/anthrove/otter-space-sdk/pkg/models" - "github.com/neo4j/neo4j-go-driver/v5/neo4j" - log "github.com/sirupsen/logrus" -) - -func CreateSourceNode(ctx context.Context, driver neo4j.DriverWithContext, anthroveSource *models.AnthroveSource) error { - query := ` - MERGE (sourceNode:Source {domain: $source_url}) - ON CREATE SET sourceNode.domain = $source_url, sourceNode.display_name = $source_display_name, sourceNode.icon = $source_icon - ` - params := map[string]any{ - "source_url": anthroveSource.Domain, - "source_display_name": anthroveSource.DisplayName, - "source_icon": anthroveSource.Icon, - } - - _, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return fmt.Errorf("graph: %w", err) - } - - log.WithFields(log.Fields{ - "node_source_url": anthroveSource.Domain, - "node_source_displayName": anthroveSource.DisplayName, - "node_source_icon": anthroveSource.Icon, - }).Trace("graph: created source node") - - return nil -} - -func GetAllSourceNodes(ctx context.Context, driver neo4j.DriverWithContext) ([]models.AnthroveSource, error) { - var sources []models.AnthroveSource - - query := ` - MATCH (s:Source) - RETURN s as source - ` - params := map[string]any{} - - result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return nil, err - } - - if len(result.Records) == 0 { - return nil, nil - } - - for i := range result.Records { - record := result.Records[i] - - source, _, err := neo4j.GetRecordValue[neo4j.Node](record, "source") - if err != nil { - return nil, err - } - - sources = append(sources, models.AnthroveSource{ - DisplayName: source.Props["display_name"].(string), - Domain: source.Props["domain"].(string), - Icon: source.Props["icon"].(string), - }) - - } - - log.WithFields(log.Fields{ - "tag_amount": len(sources), - }).Trace("graph: created tag node") - - return sources, nil -} - -func GetSourceNodesByURL(ctx context.Context, driver neo4j.DriverWithContext, sourceUrl string) (*models.AnthroveSource, error) { - - var source models.AnthroveSource - - query := ` - MATCH (s:Source {domain: $source_url}) - RETURN s as source - ` - params := map[string]any{ - "source_url": sourceUrl, - } - - result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return nil, err - } - - if len(result.Records) == 0 { - return nil, fmt.Errorf("source not found") - } - - record, _, err := neo4j.GetRecordValue[neo4j.Node](result.Records[0], "source") - if err != nil { - return nil, err - } - - source.DisplayName = record.Props["display_name"].(string) - source.Domain = record.Props["domain"].(string) - source.Icon = record.Props["icon"].(string) - - log.WithFields(log.Fields{ - "source_url": sourceUrl, - }).Trace("graph: got source node") - - return &source, nil -} diff --git a/internal/tag.go b/internal/tag.go deleted file mode 100644 index 3e5a02d..0000000 --- a/internal/tag.go +++ /dev/null @@ -1,84 +0,0 @@ -package internal - -import ( - "context" - - "git.dragse.it/anthrove/otter-space-sdk/pkg/models" - "github.com/neo4j/neo4j-go-driver/v5/neo4j" - log "github.com/sirupsen/logrus" -) - -func CreateTagNodeWitRelation(ctx context.Context, driver neo4j.DriverWithContext, anthrovePostID models.AnthrovePostID, anthroveTag *models.AnthroveTag) error { - query := ` - MATCH (anthrovePost:AnthrovePost {post_id: $anthrove_post_id}) - MERGE (tagNode:Tag {name: $tag_name, type: $tag_type}) - MERGE (anthrovePost)-[:HAS]->(tagNode) - ` - params := map[string]interface{}{ - "tag_name": anthroveTag.Name, - "tag_type": anthroveTag.Type, - "anthrove_post_id": anthrovePostID, - } - - _, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return err - } - - log.WithFields(log.Fields{ - "anthrove_post_id": anthrovePostID, - "tag_name": anthroveTag.Name, - "tag_type": anthroveTag.Type, - }).Trace("graph: created tag node") - - return nil -} - -func GetTags(ctx context.Context, driver neo4j.DriverWithContext) ([]models.TagsWithFrequency, error) { - var userTags []models.TagsWithFrequency - - query := ` - MATCH (:AnthrovePost)-[:HAS]->(t:Tag) - RETURN t as tag, COUNT(t) AS frequency - ORDER BY frequency DESC - ` - params := map[string]any{} - - result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return nil, err - } - - if len(result.Records) == 0 { - return nil, nil - } - - for i := range result.Records { - record := result.Records[i] - - tag, _, err := neo4j.GetRecordValue[neo4j.Node](record, "tag") - if err != nil { - return nil, err - } - - frequency, _, err := neo4j.GetRecordValue[int64](record, "frequency") - if err != nil { - return nil, err - } - - userTags = append(userTags, models.TagsWithFrequency{ - Frequency: frequency, - Tags: models.AnthroveTag{ - Name: tag.Props["name"].(string), - Type: tag.Props["type"].(string), - }, - }) - - } - - log.WithFields(log.Fields{ - "tag_amount": len(userTags), - }).Trace("graph: created tag node") - - return userTags, nil -} diff --git a/internal/user.go b/internal/user.go deleted file mode 100644 index a1128ec..0000000 --- a/internal/user.go +++ /dev/null @@ -1,454 +0,0 @@ -package internal - -import ( - "context" - "fmt" - - "git.dragse.it/anthrove/otter-space-sdk/internal/utils" - "git.dragse.it/anthrove/otter-space-sdk/pkg/models" - "github.com/neo4j/neo4j-go-driver/v5/neo4j" - log "github.com/sirupsen/logrus" -) - -func CreateUserNodeWithSourceRelation(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID, sourceDomain string, userID string, username string) error { - query := ` - MATCH (userNode:User {user_id: $anthrove_user_id}) - MATCH (sourceNode:Source {domain: $source_domain}) - MERGE (userNode)-[r:HAS_ACCOUNT_AT {username: $source_user_name, user_id: $source_user_id}]->(sourceNode) - ` - params := map[string]any{ - "anthrove_user_id": anthroveUserID, - "source_user_id": userID, - "source_user_name": username, - "source_domain": sourceDomain, - } - - _, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return err - } - - var anthroveUserRelationship []models.AnthroveUserRelationship - - anthroveUserRelationship = append(anthroveUserRelationship, models.AnthroveUserRelationship{ - UserID: userID, - Username: username, - ScrapeTimeInterval: "", - Source: models.AnthroveSource{ - DisplayName: "", - Domain: sourceDomain, - Icon: "", - }, - }) - - log.WithFields(log.Fields{ - "anthrove_user_id": anthroveUserID, - "source_user_id": userID, - "source_user_name": username, - "source_domain": sourceDomain, - }).Trace("graph: crated user with relationship") - - return nil -} - -func GetUserFavoritesCount(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID) (int64, error) { - var userFavoriteCount int64 - - query := ` - MATCH (userNode:User {user_id: $anthrove_user_id}) - MATCH (userNode)-[:FAV]->(favPost:AnthrovePost) - MATCH (sourceNode)-[:REFERENCE]->(favPost) - RETURN count( DISTINCT favPost) AS FavoritePostsCount - ` - - params := map[string]any{ - "anthrove_user_id": anthroveUserID, - } - - result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return 0, err - } - - if len(result.Records) == 0 { - // no matches -> user does not exist, return count 0 - return userFavoriteCount, err - } - - record := result.Records[0] - - userFavoriteCount, _, err = neo4j.GetRecordValue[int64](record, "FavoritePostsCount") - if err != nil { - return userFavoriteCount, err - } - - log.WithFields(log.Fields{ - "anthrove_user_id": anthroveUserID, - "anthrove_user_fav_count": userFavoriteCount, - }).Trace("graph: got user favorite count") - - return userFavoriteCount, nil -} - -func GetUserSourceLink(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID) (map[string]models.AnthroveUserRelationship, error) { - - userSource := make(map[string]models.AnthroveUserRelationship) - - query := ` - MATCH (user:User{user_id: $anthrove_user_id})-[r:HAS_ACCOUNT_AT]->(s:Source) - RETURN toString(r.user_id) AS sourceUserID, toString(r.username) AS sourceUsername, s as source; - ` - params := map[string]any{ - "anthrove_user_id": anthroveUserID, - } - - result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return nil, err - } - - if len(result.Records) == 0 { - return nil, fmt.Errorf("user has no relations") - } - - for i := range result.Records { - record := result.Records[i] - source, _, err := neo4j.GetRecordValue[neo4j.Node](record, "source") - if err != nil { - return nil, err - } - sourceUserID, _, err := neo4j.GetRecordValue[string](record, "sourceUserID") - if err != nil { - return nil, err - } - sourceUsername, _, err := neo4j.GetRecordValue[string](record, "sourceUsername") - if err != nil { - return nil, err - } - - displayName := source.Props["display_name"].(string) - domain := source.Props["domain"].(string) - icon := source.Props["icon"].(string) - - anthroveSourceUser := models.AnthroveUserRelationship{ - UserID: sourceUserID, - Username: sourceUsername, - Source: models.AnthroveSource{ - DisplayName: displayName, - Domain: domain, - Icon: icon, - }, - } - userSource[displayName] = anthroveSourceUser - } - - log.WithFields(log.Fields{ - "anthrove_user_id": anthroveUserID, - "anthrove_data": userSource, - }).Trace("graph: got user favorite count") - - return userSource, nil -} - -func GetSpecifiedUserSourceLink(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID, sourceDisplayName string) (map[string]models.AnthroveUserRelationship, error) { - - userSource := make(map[string]models.AnthroveUserRelationship) - - query := ` - MATCH (user:User{user_id: $anthrove_user_id})-[r:HAS_ACCOUNT_AT]->(s:Source{display_name: $source_display_name}) - RETURN toString(r.user_id) AS sourceUserID, toString(r.username) AS sourceUsername, s as source; - ` - params := map[string]any{ - "anthrove_user_id": anthroveUserID, - "source_display_name": sourceDisplayName, - } - - result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return nil, err - } - - if len(result.Records) == 0 { - return nil, fmt.Errorf("user has no relations with the source %s", sourceDisplayName) - } - - for i := range result.Records { - record := result.Records[i] - source, _, err := neo4j.GetRecordValue[neo4j.Node](record, "source") - if err != nil { - return nil, err - } - sourceUserID, _, err := neo4j.GetRecordValue[string](record, "sourceUserID") - if err != nil { - return nil, err - } - sourceUsername, _, err := neo4j.GetRecordValue[string](record, "sourceUsername") - if err != nil { - return nil, err - } - - displayName := source.Props["display_name"].(string) - domain := source.Props["domain"].(string) - icon := source.Props["icon"].(string) - - anthroveSourceUser := models.AnthroveUserRelationship{ - UserID: sourceUserID, - Username: sourceUsername, - Source: models.AnthroveSource{ - DisplayName: displayName, - Domain: domain, - Icon: icon, - }, - } - userSource[displayName] = anthroveSourceUser - } - - log.WithFields(log.Fields{ - "anthrove_user_id": anthroveUserID, - "anthrove_data": userSource, - }).Trace("graph: got user favorite count") - - return userSource, nil -} - -func GetAnthroveUser(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID) (*models.AnthroveUser, error) { - var err error - var anthroveUser models.AnthroveUser - var userSources models.AnthroveSource - userRelationships := make([]models.AnthroveUserRelationship, 0) - - query := ` - MATCH (user:User{user_id: $anthrove_user_id})-[relation:HAS_ACCOUNT_AT]->(source:Source) - RETURN user as User, relation as Relation, source as Source; - ` - params := map[string]any{ - "anthrove_user_id": anthroveUserID, - } - - result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return nil, err - } - - if len(result.Records) == 0 { - return nil, fmt.Errorf("user has no relations") - } - - for i := range result.Records { - record := result.Records[i] - - user, _, err := neo4j.GetRecordValue[neo4j.Node](record, "User") - if err != nil { - return nil, err - } - relation, _, err := neo4j.GetRecordValue[neo4j.Relationship](record, "Relation") - if err != nil { - return nil, err - } - source, _, err := neo4j.GetRecordValue[neo4j.Node](record, "Source") - if err != nil { - return nil, err - } - - userRelationships = append(userRelationships, models.AnthroveUserRelationship{ - UserID: fmt.Sprintf("%v", utils.GetOrDefault(relation.Props, "user_id", "")), - Username: utils.GetOrDefault(relation.Props, "username", "").(string), - ScrapeTimeInterval: utils.GetOrDefault(relation.Props, "scrape_time_interval", "").(string), - }) - - userSources = models.AnthroveSource{ - DisplayName: utils.GetOrDefault(source.Props, "display_name", "").(string), - Domain: utils.GetOrDefault(source.Props, "domain", "").(string), - Icon: utils.GetOrDefault(source.Props, "icon", "").(string), - } - - anthroveUser.UserID = models.AnthroveUserID(utils.GetOrDefault(user.Props, "user_id", "").(string)) - anthroveUser.Relationship = userRelationships - - for j := range userRelationships { - anthroveUser.Relationship[j].Source = userSources - } - - } - - log.WithFields(log.Fields{ - "anthrove_user_id": anthroveUserID, - }).Trace("graph: got anthrove user") - - return &anthroveUser, nil - -} - -func GetAllAnthroveUserIDs(ctx context.Context, driver neo4j.DriverWithContext) ([]models.AnthroveUserID, error) { - var err error - var anthroveUsers []models.AnthroveUserID - - query := ` - MATCH (anthroveUser:User) - RETURN anthroveUser - ` - result, err := neo4j.ExecuteQuery(ctx, driver, query, nil, neo4j.EagerResultTransformer) - if err != nil { - return nil, err - } - - if len(result.Records) == 0 { - log.Warnf("No users found, this should not be happening!") - return []models.AnthroveUserID{}, nil - } - - for i := range result.Records { - record := result.Records[i] - - user, _, err := neo4j.GetRecordValue[neo4j.Node](record, "anthroveUser") - if err != nil { - return nil, err - } - - anthroveUsers = append(anthroveUsers, models.AnthroveUserID(fmt.Sprintf(user.Props["user_id"].(string)))) - } - - log.WithFields(log.Fields{ - "anthrove_user_id_count": len(anthroveUsers), - }).Trace("graph: got al anthrove user IDs") - - return anthroveUsers, nil - -} - -func GetUserFavoriteNodeWithPagination(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) { - var err error - var favoritePosts []models.FavoritePost - - query := ` - CALL { - MATCH (user:User{user_id: $anthrove_user_id})-[r:FAV]->(p:AnthrovePost) - RETURN p.post_id AS post_id - ORDER BY id(p) ASC - SKIP $skip - LIMIT $limit - } - WITH collect(post_id) AS faves - MATCH (a:AnthrovePost)<-[r:REFERENCE]-(s:Source) - WHERE a.post_id in faves - RETURN a AS anthrovePost, r AS postRelation, s AS Source - ORDER BY id(a) ASC - ` - params := map[string]any{ - "anthrove_user_id": anthroveUserID, - "limit": limit, - "skip": skip, - } - - result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return nil, err - } - - if len(result.Records) == 0 { - return nil, nil - } - - for i := range result.Records { - record := result.Records[i] - - anthrovePost, _, err := neo4j.GetRecordValue[neo4j.Node](record, "anthrovePost") - if err != nil { - return nil, err - } - - postRelation, _, err := neo4j.GetRecordValue[neo4j.Relationship](record, "postRelation") - if err != nil { - return nil, err - } - - source, _, err := neo4j.GetRecordValue[neo4j.Node](record, "Source") - if err != nil { - return nil, err - } - - if len(favoritePosts) != 0 && favoritePosts[len(favoritePosts)-1].AnthrovePost.PostID == models.AnthrovePostID(anthrovePost.Props["post_id"].(string)) { - favoritePosts[len(favoritePosts)-1].Relations = append(favoritePosts[len(favoritePosts)-1].Relations, models.FavoriteRelations{ - SourcesID: source.Props["display_name"].(string), - Relations: models.AnthrovePostRelationship{ - PostID: postRelation.Props["source_post_id"].(string), - Url: postRelation.Props["url"].(string), - }, - }) - } else { - favoritePosts = append(favoritePosts, models.FavoritePost{ - AnthrovePost: models.AnthrovePost{ - PostID: models.AnthrovePostID(anthrovePost.Props["post_id"].(string)), - Rating: models.AnthroveRating(anthrovePost.Props["rating"].(string)), - }, - Relations: []models.FavoriteRelations{{ - SourcesID: source.Props["display_name"].(string), - Relations: models.AnthrovePostRelationship{ - PostID: postRelation.Props["source_post_id"].(string), - Url: postRelation.Props["url"].(string), - }, - }}, - }) - - } - - } - - log.WithFields(log.Fields{ - "anthrove_user_fav_count": len(favoritePosts), - }).Trace("graph: got al anthrove user favorites") - - return &models.FavoriteList{Posts: favoritePosts}, nil - -} - -func GetUserTagNodeWitRelationToFavedPosts(ctx context.Context, driver neo4j.DriverWithContext, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error) { - var userTags []models.TagsWithFrequency - - query := ` - MATCH (u:User {user_id: $anthrove_user_id})-[:FAV]->(:AnthrovePost)-[:HAS]->(t:Tag) - RETURN t as tag, COUNT(t) AS frequency - ORDER BY frequency DESC - ` - params := map[string]any{ - "anthrove_user_id": anthroveUserID, - } - - result, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) - if err != nil { - return nil, err - } - - if len(result.Records) == 0 { - return nil, nil - } - - for i := range result.Records { - record := result.Records[i] - - tag, _, err := neo4j.GetRecordValue[neo4j.Node](record, "tag") - if err != nil { - return nil, err - } - - frequency, _, err := neo4j.GetRecordValue[int64](record, "frequency") - if err != nil { - return nil, err - } - - userTags = append(userTags, models.TagsWithFrequency{ - Frequency: frequency, - Tags: models.AnthroveTag{ - Name: tag.Props["name"].(string), - Type: tag.Props["type"].(string), - }, - }) - - } - - log.WithFields(log.Fields{ - "tag_amount": len(userTags), - }).Trace("graph: created tag node") - - return userTags, nil -} diff --git a/internal/utils/slices_test.go b/internal/utils/slices_test.go new file mode 100644 index 0000000..9f8cdc6 --- /dev/null +++ b/internal/utils/slices_test.go @@ -0,0 +1,60 @@ +package utils + +import ( + "reflect" + "testing" +) + +func TestGetOrDefault(t *testing.T) { + type args struct { + data map[string]any + key string + defaultVal any + } + tests := []struct { + name string + args args + want any + }{ + { + name: "Test 1: Nil map", + args: args{ + data: nil, + key: "key1", + defaultVal: "default", + }, + want: "default", + }, + { + name: "Test 2: Existing key", + args: args{ + data: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + key: "key1", + defaultVal: "default", + }, + want: "value1", + }, + { + name: "Test 3: Non-existing key", + args: args{ + data: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + key: "key3", + defaultVal: "default", + }, + want: "default", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetOrDefault(tt.args.data, tt.args.key, tt.args.defaultVal); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetOrDefault() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/database/database.go b/pkg/database/database.go new file mode 100644 index 0000000..bc19286 --- /dev/null +++ b/pkg/database/database.go @@ -0,0 +1,30 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" +) + +type OtterSpace interface { + // Connect establishes a connection to the database. + Connect(ctx context.Context, config models.DatabaseConfig) error + + // Post contains all function that are needed to manage Posts + Post + + // User contains all function that are needed to manage the AnthroveUser + User + + // Source contains all function that are needed to manage the Source + Source + + // Tag contains all functions that are used to manage Tag + Tag + + // TagAlias contains all function that are needed to manage the TagAlias + TagAlias + + // TagGroup contains all function that are needed to manage the TagGroup + TagGroup +} diff --git a/pkg/database/migrations/001_inital_database.sql b/pkg/database/migrations/001_inital_database.sql new file mode 100644 index 0000000..03932c9 --- /dev/null +++ b/pkg/database/migrations/001_inital_database.sql @@ -0,0 +1,109 @@ +-- +migrate Up +CREATE TYPE Rating AS ENUM ( + 'safe', + 'questionable', + 'explicit' + ); + +CREATE TYPE TagType AS ENUM ( + 'general', + 'species', + 'character', + 'artist', + 'lore', + 'meta', + 'invalid', + 'copyright' + ); + +CREATE TABLE "Post" +( + id CHAR(25) PRIMARY KEY, + rating Rating, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL NULL +); + +CREATE TABLE "Source" +( + id CHAR(25) PRIMARY KEY, + display_name TEXT NULL, + icon TEXT NULL, + domain TEXT NOT NULL UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL +); + +CREATE TABLE "Tag" +( + name TEXT PRIMARY KEY, + tag_type TagType, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL +); + +CREATE TABLE "User" +( + id TEXT PRIMARY KEY, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL +); + +CREATE TABLE "PostReference" +( + post_id TEXT REFERENCES "Post" (id), + source_id TEXT REFERENCES "Source" (id), + url TEXT NOT NULL, + full_file_url TEXT, + preview_file_url TEXT, + sample_file_url TEXT, + source_post_id TEXT, + PRIMARY KEY (post_id, source_id, url) +); + +CREATE TABLE "TagAlias" +( + name TEXT PRIMARY KEY, + tag_id TEXT REFERENCES "Tag" (name), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE "TagGroup" +( + name TEXT PRIMARY KEY, + tag_id TEXT REFERENCES "Tag" (name), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE "UserFavorites" +( + user_id TEXT REFERENCES "User" (id), + post_id TEXT REFERENCES "Post" (id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, post_id) +); + +CREATE TABLE "UserSource" +( + user_id TEXT REFERENCES "User" (id), + source_id TEXT REFERENCES "Source" (id), + scrape_time_interval INT, + account_username TEXT, + account_id TEXT, + last_scrape_time TIMESTAMP, + account_validate BOOL DEFAULT FALSE, + account_validation_key CHAR(25), + PRIMARY KEY (user_id, source_id), + UNIQUE (source_id, account_username, account_id) +); + +CREATE TABLE "post_tags" +( + post_id TEXT REFERENCES "Post" (id), + tag_name TEXT REFERENCES "Tag" (name), + PRIMARY KEY (post_id, tag_name) +); \ No newline at end of file diff --git a/pkg/database/post.go b/pkg/database/post.go new file mode 100644 index 0000000..0111170 --- /dev/null +++ b/pkg/database/post.go @@ -0,0 +1,28 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" +) + +type Post interface { + + // CreatePost adds a new post to the database. + CreatePost(ctx context.Context, anthrovePost *models.Post) error + + // GetPostByAnthroveID retrieves a post by its Anthrove ID. + GetPostByAnthroveID(ctx context.Context, anthrovePostID models.AnthrovePostID) (*models.Post, error) + + // GetPostByURL retrieves a post by its source URL. + GetPostByURL(ctx context.Context, postURL string) (*models.Post, error) + + // GetPostBySourceID retrieves a post by its source ID. + GetPostBySourceID(ctx context.Context, sourceID models.AnthroveSourceID) (*models.Post, error) + + // CreatePostWithReferenceToTagAnd adds a tag with a relation to a post. + CreatePostWithReferenceToTagAnd(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveTag *models.Tag) error + + // CreatePostReference links a post with a source. + CreatePostReference(ctx context.Context, anthrovePostID models.AnthrovePostID, sourceDomain models.AnthroveSourceDomain, postURL models.AnthrovePostURL, config models.PostReferenceConfig) error +} diff --git a/pkg/database/postgres.go b/pkg/database/postgres.go new file mode 100644 index 0000000..2692907 --- /dev/null +++ b/pkg/database/postgres.go @@ -0,0 +1,244 @@ +package database + +import ( + "context" + "embed" + "fmt" + log2 "log" + "os" + "time" + + "git.dragse.it/anthrove/otter-space-sdk/internal/postgres" + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + _ "github.com/lib/pq" + migrate "github.com/rubenv/sql-migrate" + log "github.com/sirupsen/logrus" + gormPostgres "gorm.io/driver/postgres" + "gorm.io/gorm" + gormLogger "gorm.io/gorm/logger" +) + +//go:embed migrations/*.sql +var embedMigrations embed.FS + +type postgresqlConnection struct { + db *gorm.DB + debug bool +} + +func NewPostgresqlConnection() OtterSpace { + return &postgresqlConnection{ + db: nil, + } +} + +func (p *postgresqlConnection) Connect(_ context.Context, config models.DatabaseConfig) error { + var localSSL string + var logLevel gormLogger.LogLevel + + if config.SSL { + localSSL = "require" + } else { + localSSL = "disable" + } + + p.debug = config.Debug + + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=%s", config.Endpoint, config.Username, config.Password, config.Database, config.Port, localSSL, config.Timezone) + var err error + + if p.debug { + logLevel = gormLogger.Info + } else { + logLevel = gormLogger.Silent + } + + dbLogger := gormLogger.New(log2.New(os.Stdout, "\r\n", log2.LstdFlags), gormLogger.Config{ + SlowThreshold: 200 * time.Millisecond, + LogLevel: logLevel, + IgnoreRecordNotFoundError: true, + Colorful: true, + }) + + db, err := gorm.Open(gormPostgres.Open(dsn), &gorm.Config{ + Logger: dbLogger, + }) + p.db = db + if err != nil { + return err + } + + log.Infof("OtterSpace: database connection established") + + err = p.migrateDatabase(db) + if err != nil { + return err + } + + log.Infof("OtterSpace: migration compleate") + + return nil +} + +func (p *postgresqlConnection) CreateUserWithRelationToSource(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, accountId string, accountUsername string) error { + return postgres.CreateUserWithRelationToSource(ctx, p.db, anthroveUserID, sourceID, accountId, accountUsername) +} + +func (p *postgresqlConnection) CreateSource(ctx context.Context, anthroveSource *models.Source) error { + return postgres.CreateSource(ctx, p.db, anthroveSource) +} + +func (p *postgresqlConnection) CreatePost(ctx context.Context, anthrovePost *models.Post) error { + return postgres.CreatePost(ctx, p.db, anthrovePost) +} + +func (p *postgresqlConnection) CreatePostWithReferenceToTagAnd(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveTag *models.Tag) error { + return postgres.CreateTagAndReferenceToPost(ctx, p.db, anthrovePostID, anthroveTag) +} + +func (p *postgresqlConnection) CreatePostReference(ctx context.Context, anthrovePostID models.AnthrovePostID, sourceDomain models.AnthroveSourceDomain, postURL models.AnthrovePostURL, config models.PostReferenceConfig) error { + return postgres.CreateReferenceBetweenPostAndSource(ctx, p.db, anthrovePostID, sourceDomain, postURL, config) +} + +func (p *postgresqlConnection) CreateReferenceBetweenUserAndPost(ctx context.Context, anthroveUserID models.AnthroveUserID, anthrovePostID models.AnthrovePostID) error { + return postgres.CreateReferenceBetweenUserAndPost(ctx, p.db, anthroveUserID, anthrovePostID) +} + +func (p *postgresqlConnection) CheckIfUserHasPostAsFavorite(ctx context.Context, anthroveUserID models.AnthroveUserID, anthrovePostID models.AnthrovePostID) (bool, error) { + return postgres.CheckReferenceBetweenUserAndPost(ctx, p.db, anthroveUserID, anthrovePostID) +} + +func (p *postgresqlConnection) GetPostByAnthroveID(ctx context.Context, anthrovePostID models.AnthrovePostID) (*models.Post, error) { + return postgres.GetPostByAnthroveID(ctx, p.db, anthrovePostID) +} + +func (p *postgresqlConnection) GetPostByURL(ctx context.Context, sourceUrl string) (*models.Post, error) { + return postgres.GetPostBySourceURL(ctx, p.db, sourceUrl) +} + +func (p *postgresqlConnection) GetPostBySourceID(ctx context.Context, sourceID models.AnthroveSourceID) (*models.Post, error) { + return postgres.GetPostBySourceID(ctx, p.db, sourceID) +} + +func (p *postgresqlConnection) GetUserFavoritesCount(ctx context.Context, anthroveUserID models.AnthroveUserID) (int64, error) { + return postgres.GetUserFavoritesCount(ctx, p.db, anthroveUserID) +} + +func (p *postgresqlConnection) GetAllUserSources(ctx context.Context, anthroveUserID models.AnthroveUserID) (map[string]models.UserSource, error) { + return postgres.GetUserSourceLinks(ctx, p.db, anthroveUserID) +} + +func (p *postgresqlConnection) GetUserSourceBySourceID(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID) (*models.UserSource, error) { + return postgres.GetUserSourceBySourceID(ctx, p.db, anthroveUserID, sourceID) +} + +func (p *postgresqlConnection) GetAllUsers(ctx context.Context) ([]models.User, error) { + return postgres.GetAllUsers(ctx, p.db) +} + +func (p *postgresqlConnection) GetAllUserFavoritesWithPagination(ctx context.Context, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) { + return postgres.GetUserFavoriteWithPagination(ctx, p.db, anthroveUserID, skip, limit) +} + +func (p *postgresqlConnection) GetAllTagsFromUser(ctx context.Context, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error) { + return postgres.GetUserTagWitRelationToFavedPosts(ctx, p.db, anthroveUserID) +} + +func (p *postgresqlConnection) GetAllTags(ctx context.Context) ([]models.Tag, error) { + return postgres.GetTags(ctx, p.db) +} + +func (p *postgresqlConnection) GetAllSources(ctx context.Context) ([]models.Source, error) { + return postgres.GetAllSource(ctx, p.db) +} + +func (p *postgresqlConnection) GetSourceByDomain(ctx context.Context, sourceDomain models.AnthroveSourceDomain) (*models.Source, error) { + return postgres.GetSourceByDomain(ctx, p.db, sourceDomain) +} + +// NEW FUNCTIONS + +func (p *postgresqlConnection) UpdateUserSourceScrapeTimeInterval(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, scrapeTime models.AnthroveScrapeTimeInterval) error { + return postgres.UpdateUserSourceScrapeTimeInterval(ctx, p.db, anthroveUserID, sourceID, scrapeTime) +} + +func (p *postgresqlConnection) UpdateUserSourceLastScrapeTime(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, lastScrapeTime models.AnthroveUserLastScrapeTime) error { + return postgres.UpdateUserSourceLastScrapeTime(ctx, p.db, anthroveUserID, sourceID, lastScrapeTime) +} + +func (p *postgresqlConnection) UpdateUserSourceValidation(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, valid bool) error { + return postgres.UpdateUserSourceValidation(ctx, p.db, anthroveUserID, sourceID, valid) +} + +func (p *postgresqlConnection) CreateTagAlias(ctx context.Context, tagAliasName models.AnthroveTagAliasName, tagID models.AnthroveTagID) error { + return postgres.CreateTagAlias(ctx, p.db, tagAliasName, tagID) +} + +func (p *postgresqlConnection) GetAllTagAlias(ctx context.Context) ([]models.TagAlias, error) { + return postgres.GetAllTagAlias(ctx, p.db) +} + +func (p *postgresqlConnection) GetAllTagAliasByTag(ctx context.Context, tagID models.AnthroveTagID) ([]models.TagAlias, error) { + return postgres.GetAllTagAliasByTag(ctx, p.db, tagID) +} + +func (p *postgresqlConnection) DeleteTagAlias(ctx context.Context, tagAliasName models.AnthroveTagAliasName) error { + return postgres.DeleteTagAlias(ctx, p.db, tagAliasName) +} + +func (p *postgresqlConnection) CreateTagGroup(ctx context.Context, tagGroupName models.AnthroveTagGroupName, tagID models.AnthroveTagID) error { + return postgres.CreateTagGroup(ctx, p.db, tagGroupName, tagID) +} + +func (p *postgresqlConnection) GetAllTagGroup(ctx context.Context) ([]models.TagGroup, error) { + return postgres.GetAllTagGroup(ctx, p.db) +} + +func (p *postgresqlConnection) GetAllTagGroupByTag(ctx context.Context, tagID models.AnthroveTagID) ([]models.TagGroup, error) { + return postgres.GetAllTagGroupByTag(ctx, p.db, tagID) +} + +func (p *postgresqlConnection) DeleteTagGroup(ctx context.Context, tagGroupName models.AnthroveTagGroupName) error { + return postgres.DeleteTagGroup(ctx, p.db, tagGroupName) +} + +func (p *postgresqlConnection) CreateTag(ctx context.Context, tagName models.AnthroveTagName, tagType models.TagType) error { + return postgres.CreateTag(ctx, p.db, tagName, tagType) +} + +func (p *postgresqlConnection) GetAllTagsByTagType(ctx context.Context, tagType models.TagType) ([]models.Tag, error) { + return postgres.GetAllTagByTagsType(ctx, p.db, tagType) +} + +func (p *postgresqlConnection) DeleteTag(ctx context.Context, tagName models.AnthroveTagName) error { + return postgres.DeleteTag(ctx, p.db, tagName) +} + +// HELPER + +func (p *postgresqlConnection) migrateDatabase(dbPool *gorm.DB) error { + dialect := "postgres" + migrations := &migrate.EmbedFileSystemMigrationSource{FileSystem: embedMigrations, Root: "migrations"} + + db, err := dbPool.DB() + if err != nil { + return fmt.Errorf("postgres migration: %v", err) + } + + n, err := migrate.Exec(db, dialect, migrations, migrate.Up) + if err != nil { + return fmt.Errorf("postgres migration: %v", err) + } + + if p.debug { + if n != 0 { + log.Infof("postgres migration: applied %d migrations!", n) + + } else { + log.Info("postgres migration: nothing to migrate") + + } + } + + return nil +} diff --git a/pkg/database/postgres_test.go b/pkg/database/postgres_test.go new file mode 100644 index 0000000..56e2036 --- /dev/null +++ b/pkg/database/postgres_test.go @@ -0,0 +1,3254 @@ +package database + +import ( + "context" + "fmt" + "reflect" + "testing" + "time" + + "git.dragse.it/anthrove/otter-space-sdk/internal/postgres" + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/test" + "gorm.io/gorm" +) + +func TestNewPostgresqlConnection(t *testing.T) { + // Test + tests := []struct { + name string + want OtterSpace + }{ + { + name: "Test 1: Create new postgresql connection", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.want != NewPostgresqlConnection() { + } else { + t.Errorf("NewPostgresqlConnection() = %s", tt.want) + } + }) + } +} + +func Test_postgresqlConnection_Connect(t *testing.T) { + + // Setup trow away container + ctx := context.Background() + container, _, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Tests + + dbConfig, err := test.DatabaseModesFromConnectionString(ctx, container) + if err != nil { + t.Fatalf("Could not parse database config: %v", err) + } + + // Test + type args struct { + in0 context.Context + config models.DatabaseConfig + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Connect to postgresql connection", + args: args{ + in0: ctx, + config: *dbConfig, + }, + wantErr: false, + }, + { + name: "Test 1: Empty connection config", + args: args{ + in0: ctx, + config: models.DatabaseConfig{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{} + if err := p.Connect(tt.args.in0, tt.args.config); (err != nil) != tt.wantErr { + t.Errorf("Connect() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_CreateUserWithRelationToSource(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + validSourceID1 := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + source := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validSourceID1}, + DisplayName: "e621", + Domain: "e621.net", + Icon: "icon.e621.net", + } + err = postgres.CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + anthroveUserID models.AnthroveUserID + sourceID models.AnthroveSourceID + accountId string + accountUsername string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid anthroveUserID, sourceID, accountId, accountUsername", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: source.ID, + accountId: "e1", + accountUsername: "marius", + }, + wantErr: false, + }, + { + name: "Test 2: Invalid anthroveUserID, valid sourceID, accountId, accountUsername", + args: args{ + ctx: ctx, + anthroveUserID: "2", + sourceID: source.ID, + accountId: "e1", + accountUsername: "marius", + }, + wantErr: true, + }, + { + name: "Test 3: Empty anthroveUserID", + args: args{ + ctx: ctx, + anthroveUserID: "", + sourceID: source.ID, + accountId: "e1", + accountUsername: "marius", + }, + wantErr: true, + }, + { + name: "Test 4: invalid sourceID", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: "fa.net", + accountId: "e1", + accountUsername: "marius", + }, + wantErr: true, + }, + { + name: "Test 5: no accountId", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: source.ID, + accountId: "", + accountUsername: "marius", + }, + wantErr: true, + }, + { + name: "Test 6: no accountUsername", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: source.ID, + accountId: "aa", + accountUsername: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreateUserWithRelationToSource(tt.args.ctx, tt.args.anthroveUserID, tt.args.sourceID, tt.args.accountId, tt.args.accountUsername); (err != nil) != tt.wantErr { + t.Errorf("CreateUserWithRelationToSource() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_CreateSource(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validSource := &models.Source{ + DisplayName: "e621", + Domain: "e621.net", + Icon: "icon.e621.net", + } + + invalidSource := &models.Source{ + Domain: "", + } + + // Test + type args struct { + ctx context.Context + anthroveSource *models.Source + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid anthroveSource", + args: args{ + ctx: ctx, + anthroveSource: validSource, + }, + wantErr: false, + }, + { + name: "Test 2: inValid anthroveSource", + args: args{ + ctx: ctx, + anthroveSource: invalidSource, + }, + wantErr: true, + }, + { + name: "Test 3: unique anthroveSource", + args: args{ + ctx: ctx, + anthroveSource: validSource, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreateSource(tt.args.ctx, tt.args.anthroveSource); (err != nil) != tt.wantErr { + t.Errorf("CreateSource() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_CreatePost(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Tests + + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + + validPost := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: validPostID1, + }, + Rating: "safe", + } + + invalidPost := &models.Post{ + Rating: "error", + } + + // Test + type args struct { + ctx context.Context + anthrovePost *models.Post + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid AnthrovePostID and Rating", + args: args{ + ctx: context.Background(), + anthrovePost: validPost, + }, + wantErr: false, + }, + { + name: "Test 2: Invalid Rating", + args: args{ + ctx: context.Background(), + anthrovePost: invalidPost, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreatePost(tt.args.ctx, tt.args.anthrovePost); (err != nil) != tt.wantErr { + t.Errorf("CreatePost() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_CreateTagAndReferenceToPost(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: validPostID1, + }, + Rating: "safe", + } + + err = postgres.CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal(err) + } + + tag := &models.Tag{ + Name: "JayTheFerret", + Type: "artist", + } + + // Test + type args struct { + ctx context.Context + anthrovePostID models.AnthrovePostID + anthroveTag *models.Tag + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid PostID and Tag", + args: args{ + ctx: ctx, + anthrovePostID: post.ID, + anthroveTag: tag, + }, + wantErr: false, + }, + { + name: "Test 2: Valid PostID and no Tag", + args: args{ + ctx: ctx, + anthrovePostID: post.ID, + anthroveTag: nil, + }, + wantErr: true, + }, + { + name: "Test 3: Invalid PostID and valid Tag", + args: args{ + ctx: ctx, + anthrovePostID: "123456", + anthroveTag: tag, + }, + wantErr: true, + }, + { + name: "Test 4: No PostID and valid Tag", + args: args{ + ctx: ctx, + anthrovePostID: "", + anthroveTag: tag, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreatePostWithReferenceToTagAnd(tt.args.ctx, tt.args.anthrovePostID, tt.args.anthroveTag); (err != nil) != tt.wantErr { + t.Errorf("CreatePostWithReferenceToTagAnd() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_CreateReferenceBetweenPostAndSource(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + validSourceID1 := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: validPostID1, + }, + Rating: "safe", + } + + source := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validSourceID1}, + DisplayName: "e621", + Domain: "e621.net", + Icon: "icon.e621.net", + } + + err = postgres.CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal(err) + } + + err = postgres.CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + anthrovePostID models.AnthrovePostID + sourceDomain models.AnthroveSourceDomain + postURl models.AnthrovePostURL + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid AnthrovePostID and anthroveSourceDomain", + args: args{ + ctx: ctx, + anthrovePostID: post.ID, + sourceDomain: "e621.net", + postURl: "http://e621.net/post/eeasd", + }, + wantErr: false, + }, + { + name: "Test 2: Invalid AnthrovePostID and Valid anthroveSourceDomain", + args: args{ + ctx: ctx, + anthrovePostID: "123456", + sourceDomain: "e621.net", + postURl: "", + }, + wantErr: true, + }, + { + name: "Test 3: Invalid anthroveSourceDomain and Valid AnthrovePostID", + args: args{ + ctx: ctx, + anthrovePostID: "1234", + sourceDomain: "fa.banana", + postURl: "", + }, + wantErr: true, + }, + { + name: "Test 4: Invalid anthroveSourceDomain and Invalid AnthrovePostID", + args: args{ + ctx: ctx, + anthrovePostID: "696969", + postURl: "", + }, + wantErr: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreatePostReference(tt.args.ctx, tt.args.anthrovePostID, tt.args.sourceDomain, tt.args.postURl, models.PostReferenceConfig{}); (err != nil) != tt.wantErr { + t.Errorf("CreatePostReference() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_CreateReferenceBetweenUserAndPost(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + + err = postgres.CreateUser(ctx, gormDB, validUserID) + if err != nil { + t.Fatal(err) + } + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: validPostID1, + }, + Rating: "safe", + } + + err = postgres.CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + anthroveUserID models.AnthroveUserID + anthrovePostID models.AnthrovePostID + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveUserID and AnthrovePostID", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + anthrovePostID: post.ID, + }, + wantErr: false, + }, + { + name: "Test 2: Valid AnthroveUserID and invalid AnthrovePostID", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + anthrovePostID: "123456", + }, + wantErr: true, + }, + { + name: "Test 3: Valid AnthrovePostID and invalid AnthroveUserID", + args: args{ + ctx: ctx, + anthroveUserID: "123", + anthrovePostID: "1234", + }, + wantErr: true, + }, + { + name: "Test 4: Invalid AnthrovePostID and invalid AnthroveUserID", + args: args{ + ctx: ctx, + anthroveUserID: "123", + anthrovePostID: "123456", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreateReferenceBetweenUserAndPost(tt.args.ctx, tt.args.anthroveUserID, tt.args.anthrovePostID); (err != nil) != tt.wantErr { + t.Errorf("CreateReferenceBetweenUserAndPost() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_CheckReferenceBetweenUserAndPost(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + + err = postgres.CreateUser(ctx, gormDB, validUserID) + if err != nil { + t.Fatal(err) + } + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: validPostID1, + }, + Rating: "safe", + } + + err = postgres.CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal(err) + } + + err = postgres.CreateReferenceBetweenUserAndPost(ctx, gormDB, validUserID, post.ID) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + anthroveUserID models.AnthroveUserID + anthrovePostID models.AnthrovePostID + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveUserID and AnthrovePostID", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + anthrovePostID: post.ID, + }, + want: true, + wantErr: false, + }, + { + name: "Test 2: Valid AnthroveUserID and invalid AnthrovePostID", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + anthrovePostID: "qadw", + }, + want: false, + wantErr: true, + }, + { + name: "Test 3: Valid AnthrovePostID and invalid AnthroveUserID", + args: args{ + ctx: ctx, + anthroveUserID: "123", + anthrovePostID: post.ID, + }, + want: false, + wantErr: true, + }, + { + name: "Test 4: Invalid AnthrovePostID and invalid AnthroveUserID", + args: args{ + ctx: ctx, + anthroveUserID: "123", + anthrovePostID: "123456", + }, + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.CheckIfUserHasPostAsFavorite(tt.args.ctx, tt.args.anthroveUserID, tt.args.anthrovePostID) + if (err != nil) != tt.wantErr { + t.Errorf("CheckIfUserHasPostAsFavorite() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("CheckIfUserHasPostAsFavorite() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetPostByAnthroveID(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Tests + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: validPostID1, + }, + Rating: "safe", + } + + err = postgres.CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal("Could not create post", err) + } + + // Test + type args struct { + ctx context.Context + anthrovePost models.AnthrovePostID + } + tests := []struct { + name string + args args + want *models.Post + wantErr bool + }{ + { + name: "Test 1: Valid anthrovePostID", + args: args{ + ctx: ctx, + anthrovePost: post.ID, + }, + want: post, + wantErr: false, + }, + { + name: "Test 2: No anthrovePostID", + args: args{ + ctx: ctx, + anthrovePost: "nil", + }, + want: nil, + wantErr: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetPostByAnthroveID(tt.args.ctx, tt.args.anthrovePost) + if (err != nil) != tt.wantErr { + t.Errorf("GetPostByAnthroveID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkPost(got, tt.want) { + t.Errorf("GetPostByAnthroveID() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetPostByURL(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Tests + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: validPostID1, + }, + Rating: "safe", + } + + err = postgres.CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal("Could not create post", err) + } + + source := models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: models.AnthroveSourceID(fmt.Sprintf("%025s", "1")), + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.net/icon.ico", + } + + err = postgres.CreateSource(ctx, gormDB, &source) + if err != nil { + t.Fatal("Could not create source", err) + } + + err = postgres.CreateReferenceBetweenPostAndSource(ctx, gormDB, post.ID, models.AnthroveSourceDomain(source.Domain), "https://e62asdwad.com/asdas", models.PostReferenceConfig{}) + if err != nil { + t.Fatal("Could not create source reference", err) + } + + // Test + type args struct { + ctx context.Context + sourceUrl string + } + tests := []struct { + name string + args args + want *models.Post + wantErr bool + }{ + { + name: "Test 1: Valid sourceUrl", + args: args{ + ctx: ctx, + sourceUrl: "https://e62asdwad.com/asdas", + }, + want: post, + wantErr: false, + }, + { + name: "Test 2: Invalid sourceUrl", + args: args{ + ctx: ctx, + sourceUrl: "1234", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: No sourceUrl", + args: args{ + ctx: ctx, + sourceUrl: "", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetPostByURL(tt.args.ctx, tt.args.sourceUrl) + if (err != nil) != tt.wantErr { + t.Errorf("GetPostBySourceURL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkPost(got, tt.want) { + t.Errorf("GetPostBySourceURL() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetPostBySourceID(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Tests + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + validSourceID1 := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + post := &models.Post{ + BaseModel: models.BaseModel[models.AnthrovePostID]{ + ID: validPostID1, + }, + Rating: "safe", + } + + source := models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: validSourceID1, + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.net/icon.ico", + } + + err = postgres.CreatePost(ctx, gormDB, post) + if err != nil { + t.Fatal("Could not create post", err) + } + + err = postgres.CreateSource(ctx, gormDB, &source) + if err != nil { + t.Fatal("Could not create source", err) + } + + err = postgres.CreateReferenceBetweenPostAndSource(ctx, gormDB, post.ID, models.AnthroveSourceDomain(source.Domain), "https://easd15aed.de/asd", models.PostReferenceConfig{}) + if err != nil { + t.Fatal("Could not create source reference", err) + } + + // Test + type args struct { + ctx context.Context + sourceID models.AnthroveSourceID + } + tests := []struct { + name string + args args + want *models.Post + wantErr bool + }{ + { + name: "Test 1: Valid sourceID", + args: args{ + ctx: ctx, + sourceID: source.ID, + }, + want: post, + wantErr: false, + }, + { + name: "Test 2: Invalid sourceID", + args: args{ + ctx: ctx, + sourceID: "1234", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: No sourceID", + args: args{ + ctx: ctx, + sourceID: "", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetPostBySourceID(tt.args.ctx, tt.args.sourceID) + if (err != nil) != tt.wantErr { + t.Errorf("GetPostBySourceID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkPost(got, tt.want) { + t.Errorf("GetPostBySourceID() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetUserFavoritesCount(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validAnthroveUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + validPostID2 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post2")) + validPostID3 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post3")) + validPostID4 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post4")) + validPostID5 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post5")) + validPostID6 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post6")) + + expectedResultPosts := []models.Post{ + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID1}, + Rating: "safe", + }, + { + + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID2}, + Rating: "safe", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID3}, + Rating: "explicit", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID4}, + Rating: "explicit", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID5}, + Rating: "questionable", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID6}, + Rating: "safe", + }, + } + + err = postgres.CreateUser(ctx, gormDB, validAnthroveUserID) + if err != nil { + t.Fatal(err) + } + + for _, post := range expectedResultPosts { + err = postgres.CreatePost(ctx, gormDB, &post) + if err != nil { + t.Fatal(err) + } + err = postgres.CreateReferenceBetweenUserAndPost(ctx, gormDB, validAnthroveUserID, post.ID) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + anthroveUserID models.AnthroveUserID + } + tests := []struct { + name string + args args + want int64 + wantErr bool + }{ + { + name: "Test 1: Valid anthroveUserID and 6 favorite posts", + args: args{ + ctx: ctx, + anthroveUserID: validAnthroveUserID, + }, + want: 6, + wantErr: false, + }, + { + name: "Test 2: Invalid anthroveUserID and 6 favorite posts", + args: args{ + ctx: ctx, + anthroveUserID: "2", + }, + want: 0, + wantErr: true, + }, + { + name: "Test 3: no anthroveUserID and 6 favorite posts", + args: args{ + ctx: ctx, + anthroveUserID: "", + }, + want: 0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetUserFavoritesCount(tt.args.ctx, tt.args.anthroveUserID) + if (err != nil) != tt.wantErr { + t.Errorf("GetUserFavoritesCount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetUserFavoritesCount() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetUserSourceLinks(t *testing.T) { + // Setup trow away containert + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validAnthroveUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + + validSourceID1 := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + validSourceID2 := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source2")) + + eSource := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validSourceID1}, + DisplayName: "e621", + Domain: "e621.net", + } + err = postgres.CreateSource(ctx, gormDB, eSource) + if err != nil { + t.Fatal(err) + } + + faSource := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: validSourceID2}, + DisplayName: "fa", + Domain: "fa.net", + } + err = postgres.CreateSource(ctx, gormDB, faSource) + if err != nil { + t.Fatal(err) + } + + expectedResult := make(map[string]models.UserSource) + expectedResult["e621"] = models.UserSource{ + UserID: "e1", + AccountUsername: "e621-user", + Source: models.Source{ + DisplayName: eSource.DisplayName, + Domain: eSource.Domain, + }, + } + expectedResult["fa"] = models.UserSource{ + UserID: "fa1", + AccountUsername: "fa-user", + Source: models.Source{ + DisplayName: faSource.DisplayName, + Domain: faSource.Domain, + }, + } + + err = postgres.CreateUserWithRelationToSource(ctx, gormDB, validAnthroveUserID, eSource.ID, expectedResult["e621"].UserID, expectedResult["e621"].AccountUsername) + if err != nil { + t.Fatal(err) + } + err = postgres.CreateUserWithRelationToSource(ctx, gormDB, validAnthroveUserID, faSource.ID, expectedResult["fa"].UserID, expectedResult["fa"].AccountUsername) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + anthroveUserID models.AnthroveUserID + } + tests := []struct { + name string + args args + want map[string]models.UserSource + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ + ctx: ctx, + anthroveUserID: validAnthroveUserID, + }, + want: expectedResult, + wantErr: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetAllUserSources(tt.args.ctx, tt.args.anthroveUserID) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllUserSources() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllUserSources() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetUserSourceBySourceID(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validSourceID := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + source := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: validSourceID, + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.icon", + } + + expectedResult := &models.UserSource{ + UserID: string(validUserID), + AccountUsername: "euser", + Source: models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ID: source.ID}, + DisplayName: source.DisplayName, + Domain: source.Domain, + Icon: source.Icon, + }, + } + + err = postgres.CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + err = postgres.CreateUserWithRelationToSource(ctx, gormDB, validUserID, validSourceID, expectedResult.UserID, expectedResult.AccountUsername) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + anthroveUserID models.AnthroveUserID + sourceID models.AnthroveSourceID + } + + tests := []struct { + name string + args args + want *models.UserSource + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveUserID and sourceID", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: source.ID, + }, + want: expectedResult, + wantErr: false, + }, + { + name: "Test 2: Invalid AnthroveUserID and valid sourceID", + args: args{ + ctx: ctx, + anthroveUserID: invalidUserID, + sourceID: source.ID, + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: Valid AnthroveUserID and invalid sourceID", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: "fa", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 4: No AnthroveUserID and Valid sourceID", + args: args{ + ctx: ctx, + anthroveUserID: "", + sourceID: source.ID, + }, + want: nil, + wantErr: true, + }, + { + name: "Test 5: Valid AnthroveUserID and No SourceDisplayName", + args: args{ + ctx: ctx, + anthroveUserID: "1", + sourceID: "", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 6: No AnthroveUserID and No SourceDisplayName", + args: args{ + ctx: ctx, + anthroveUserID: "", + sourceID: "", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetUserSourceBySourceID(tt.args.ctx, tt.args.anthroveUserID, tt.args.sourceID) + if (err != nil) != tt.wantErr { + t.Errorf("GetUserSourceBySourceID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkUserSource(got, tt.want) { + t.Errorf("GetUserSourceBySourceID() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetAllUsers(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validUserID01 := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + validUserID02 := models.AnthroveUserID(fmt.Sprintf("%025s", "User2")) + validUserID03 := models.AnthroveUserID(fmt.Sprintf("%025s", "User3")) + + users := []models.User{ + { + BaseModel: models.BaseModel[models.AnthroveUserID]{ID: validUserID01}, + }, + { + BaseModel: models.BaseModel[models.AnthroveUserID]{ID: validUserID02}, + }, + { + BaseModel: models.BaseModel[models.AnthroveUserID]{ID: validUserID03}, + }, + } + + for _, user := range users { + err = postgres.CreateUser(ctx, gormDB, user.ID) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + } + tests := []struct { + name string + args args + want []models.User + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ + ctx: ctx, + }, + want: users, + wantErr: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetAllUsers(tt.args.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllUsers() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkUser(got, tt.want) { + t.Errorf("GetAllUsers() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetUserFavoriteWithPagination(t *testing.T) { + // Setup trow away containert + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validAnthroveUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + validPostID2 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post2")) + validPostID3 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post3")) + validPostID4 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post4")) + validPostID5 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post5")) + validPostID6 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post6")) + + expectedResultPosts := []models.Post{ + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID1}, + Rating: "safe", + }, + { + + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID2}, + Rating: "safe", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID3}, + Rating: "explicit", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID4}, + Rating: "explicit", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID5}, + Rating: "questionable", + }, + { + BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID6}, + Rating: "safe", + }, + } + expectedResult := &models.FavoriteList{ + Posts: expectedResultPosts, + } + expectedResult2 := &models.FavoriteList{ + Posts: expectedResultPosts[2:], + } + expectedResult3 := &models.FavoriteList{ + Posts: expectedResultPosts[:3], + } + + err = postgres.CreateUser(ctx, gormDB, validAnthroveUserID) + if err != nil { + t.Fatal(err) + } + + for _, expectedResultPost := range expectedResultPosts { + err = postgres.CreatePost(ctx, gormDB, &expectedResultPost) + if err != nil { + t.Fatal(err) + } + err = postgres.CreateReferenceBetweenUserAndPost(ctx, gormDB, validAnthroveUserID, expectedResultPost.ID) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + anthroveUserID models.AnthroveUserID + skip int + limit int + } + tests := []struct { + name string + args args + want *models.FavoriteList + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveUserID", + args: args{ + ctx: ctx, + anthroveUserID: validAnthroveUserID, + skip: 0, + limit: 2000, + }, + want: expectedResult, + wantErr: false, + }, + { + name: "Test 2: Skip first two", + args: args{ + ctx: ctx, + anthroveUserID: validAnthroveUserID, + skip: 2, + limit: 2000, + }, + want: expectedResult2, + wantErr: false, + }, + { + name: "Test 3: Limit of 3", + args: args{ + ctx: ctx, + anthroveUserID: validAnthroveUserID, + skip: 0, + limit: 3, + }, + want: expectedResult3, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetAllUserFavoritesWithPagination(tt.args.ctx, tt.args.anthroveUserID, tt.args.skip, tt.args.limit) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllUserFavoritesWithPagination() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllUserFavoritesWithPagination() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetUserTagWitRelationToFavedPosts(t *testing.T) { + // Setup trow away containert + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validAnthroveUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + + validPostID1 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post1")) + validPostID2 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post2")) + validPostID3 := models.AnthrovePostID(fmt.Sprintf("%025s", "Post3")) + + err = postgres.CreateUser(ctx, gormDB, validAnthroveUserID) + if err != nil { + t.Fatal(err) + } + + posts := []models.Post{ + {BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID1}, Rating: "safe"}, + {BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID2}, Rating: "safe"}, + {BaseModel: models.BaseModel[models.AnthrovePostID]{ID: validPostID3}, Rating: "explicit"}, + } + + for _, post := range posts { + err = postgres.CreatePost(ctx, gormDB, &post) + if err != nil { + t.Fatal(err) + } + err = postgres.CreateReferenceBetweenUserAndPost(ctx, gormDB, validAnthroveUserID, models.AnthrovePostID(post.ID)) + if err != nil { + t.Fatal(err) + } + } + + tags := []models.Tag{ + {Name: "JayTheFerret", Type: "artist"}, + {Name: "Ferret", Type: "species"}, + {Name: "Jay", Type: "character"}, + } + + for i, tag := range tags { + err = postgres.CreateTagAndReferenceToPost(ctx, gormDB, posts[i].ID, &tag) + if err != nil { + t.Fatal(err) + } + } + + expectedResult := []models.TagsWithFrequency{ + { + Frequency: 1, + Tags: models.Tag{ + Name: tags[0].Name, + Type: tags[0].Type, + }, + }, + { + Frequency: 1, + Tags: models.Tag{ + Name: tags[2].Name, + Type: tags[2].Type, + }, + }, + { + Frequency: 1, + Tags: models.Tag{ + Name: tags[1].Name, + Type: tags[1].Type, + }, + }, + } + + // Test + type args struct { + ctx context.Context + anthroveUserID models.AnthroveUserID + } + tests := []struct { + name string + args args + want []models.TagsWithFrequency + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ + ctx: ctx, + anthroveUserID: validAnthroveUserID, + }, + want: expectedResult, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetAllTagsFromUser(tt.args.ctx, tt.args.anthroveUserID) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagsFromUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTagsFromUser() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetAllTags(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + tags := []models.Tag{ + { + Name: "JayTheFerret", + Type: "artist", + }, + { + Name: "anthro", + Type: "general", + }, + { + Name: "soxx", + Type: "character", + }, + } + + for _, tag := range tags { + err = postgres.CreateTag(ctx, gormDB, models.AnthroveTagName(tag.Name), tag.Type) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + } + tests := []struct { + name string + args args + want []models.Tag + wantErr bool + }{ + { + name: "Test 1: Get Tags", + args: args{ + ctx: ctx, + }, + want: tags, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetAllTags(tt.args.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTags() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTags() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetAllSources(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + sources := []models.Source{ + { + DisplayName: "e621", + Domain: "e621.net", + Icon: "icon.e621.net", + }, + { + DisplayName: "furaffinity", + Domain: "furaffinity.net", + Icon: "icon.furaffinity.net", + }, + { + DisplayName: "fenpaws", + Domain: "fenpa.ws", + Icon: "icon.fenpa.ws", + }, + } + + for _, source := range sources { + err = postgres.CreateSource(ctx, gormDB, &source) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + } + tests := []struct { + name string + args args + want []models.Source + wantErr bool + }{ + { + name: "Test 1: Get all entries", + args: args{ + ctx: ctx, + }, + want: sources, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetAllSources(tt.args.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllSources() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkSources(got, tt.want) { + t.Errorf("GetAllSources() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetSourceByDomain(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + source := &models.Source{ + DisplayName: "e621", + Domain: "e621.net", + Icon: "icon.e621.net", + } + + err = postgres.CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + sourceDomain models.AnthroveSourceDomain + } + tests := []struct { + name string + args args + want *models.Source + wantErr bool + }{ + { + name: "Test 1: Valid URL", + args: args{ + ctx: ctx, + sourceDomain: "e621.net", + }, + want: source, + wantErr: false, + }, + { + name: "Test 2: Invalid URL", + args: args{ + ctx: ctx, + sourceDomain: "eeeee.net", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 2: No URL", + args: args{ + ctx: ctx, + sourceDomain: "", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetSourceByDomain(tt.args.ctx, tt.args.sourceDomain) + if (err != nil) != tt.wantErr { + t.Errorf("GetSourceByDomain() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !checkSource(got, tt.want) { + t.Errorf("GetSourceByDomain() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_migrateDatabase(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + // Test + type args struct { + dbPool *gorm.DB + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Migrate Databases", + args: args{dbPool: gormDB}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.migrateDatabase(tt.args.dbPool); (err != nil) != tt.wantErr { + t.Errorf("migrateDatabase() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +// Helper functions for validation purposes +func checkSource(got *models.Source, want *models.Source) bool { + + if want == nil && got == nil { + return true + } + + if got.Domain != want.Domain { + return false + } + + return true + +} +func checkPost(got *models.Post, want *models.Post) bool { + + if got == nil && want == nil { + return true + } else if got == nil || want == nil { + return false + } + + if got.ID != want.ID { + return false + } + + if got.Rating != want.Rating { + return false + } + + return true +} +func checkSources(got []models.Source, want []models.Source) bool { + for i, source := range want { + if source.DisplayName != got[i].DisplayName { + return false + } + if source.Domain != got[i].Domain { + return false + } + if source.Icon != got[i].Icon { + return false + } + } + + return true +} +func checkUser(got []models.User, want []models.User) bool { + for i, user := range want { + if user.ID != got[i].ID { + return false + } + } + return true +} +func checkUserSource(got *models.UserSource, want *models.UserSource) bool { + + if got == nil && want == nil { + return true + } else if got == nil || want == nil { + return false + } + + if got.UserID != want.UserID { + return false + } + if got.AccountUsername != want.AccountUsername { + return false + } + if got.Source.DisplayName != want.Source.DisplayName { + return false + } + if got.Source.Domain != want.Source.Domain { + return false + } + if got.Source.Icon != want.Source.Icon { + return false + } + + return true +} + +//------------------------- + +func Test_postgresqlConnection_CreateTagAlias(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validTagAliasName01 := models.AnthroveTagAliasName("httyd") + validTagAliasName02 := models.AnthroveTagAliasName("dragon") + + validTagID := models.AnthroveTagID("toothless") + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + err = postgres.CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + tagAliasName models.AnthroveTagAliasName + tagID models.AnthroveTagID + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + tagAliasName: validTagAliasName01, + tagID: validTagID, + }, + wantErr: false, + }, + { + name: "Test 2: No TagAliasName", + args: args{ + ctx: ctx, + tagAliasName: "", + tagID: validTagID, + }, + wantErr: true, + }, + { + name: "Test 4: No tagID", + args: args{ + ctx: ctx, + tagAliasName: validTagAliasName01, + tagID: "", + }, + wantErr: true, + }, + { + name: "Test 5: Duplicate tagID", + args: args{ + ctx: ctx, + tagAliasName: validTagAliasName01, + tagID: validTagID, + }, + wantErr: true, + }, + { + name: "Test 6: Invalide tagID", + args: args{ + ctx: ctx, + tagAliasName: validTagAliasName02, + tagID: "aaa", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreateTagAlias(tt.args.ctx, tt.args.tagAliasName, tt.args.tagID); (err != nil) != tt.wantErr { + t.Errorf("CreateTagAlias() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_GetAllTagAlias(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagAliases := []models.AnthroveTagAliasName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + expectedResult := []models.TagAlias{ + { + Name: string(validTagAliases[0]), + TagID: string(validTagID), + }, + { + Name: string(validTagAliases[1]), + TagID: string(validTagID), + }, + { + Name: string(validTagAliases[2]), + TagID: string(validTagID), + }, + } + + err = postgres.CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagAliasName := range validTagAliases { + err = postgres.CreateTagAlias(ctx, gormDB, tagAliasName, validTagID) + } + + // Test + type args struct { + ctx context.Context + } + tests := []struct { + name string + args args + want []models.TagAlias + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ctx: ctx}, + want: expectedResult, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetAllTagAlias(tt.args.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagAlias() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTagAlias() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetAllTagAliasByTag(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagAliases := []models.AnthroveTagAliasName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + expectedResult := []models.TagAlias{ + { + Name: string(validTagAliases[0]), + TagID: string(validTagID), + }, + { + Name: string(validTagAliases[1]), + TagID: string(validTagID), + }, + { + Name: string(validTagAliases[2]), + TagID: string(validTagID), + }, + } + + err = postgres.CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagAliasName := range validTagAliases { + err = postgres.CreateTagAlias(ctx, gormDB, tagAliasName, validTagID) + } + + // Test + type args struct { + ctx context.Context + tagID models.AnthroveTagID + } + tests := []struct { + name string + args args + want []models.TagAlias + wantErr bool + }{ + { + name: "Test 1: Valid TagID", + args: args{ + ctx: ctx, + tagID: validTagID, + }, + want: expectedResult, + wantErr: false, + }, + { + name: "Test 2: No TagID", + args: args{ + ctx: ctx, + tagID: "", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: Invalid TagID", + args: args{ + ctx: ctx, + tagID: "adads", + }, + want: []models.TagAlias{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetAllTagAliasByTag(tt.args.ctx, tt.args.tagID) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagAliasByTag() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTagAliasByTag() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_DeleteTagAlias(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagAliases := []models.AnthroveTagAliasName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + err = postgres.CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagAliasName := range validTagAliases { + err = postgres.CreateTagAlias(ctx, gormDB, tagAliasName, validTagID) + } + + // Test + type args struct { + ctx context.Context + tagAliasName models.AnthroveTagAliasName + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveTagAliasName", + args: args{ + ctx: ctx, + tagAliasName: validTagAliases[0], + }, + wantErr: false, + }, + { + name: "Test 2: Invalid AnthroveTagAliasName", + args: args{ + ctx: ctx, + tagAliasName: "asdad", + }, + wantErr: false, + }, + { + name: "Test 3: No AnthroveTagAliasName", + args: args{ + ctx: ctx, + tagAliasName: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.DeleteTagAlias(tt.args.ctx, tt.args.tagAliasName); (err != nil) != tt.wantErr { + t.Errorf("DeleteTagAlias() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +//-------------------------- + +func Test_postgresqlConnection_CreateTagGroup(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validTagGroupName01 := models.AnthroveTagGroupName("httyd") + validTagGroupName02 := models.AnthroveTagGroupName("dragon") + + validTagID := models.AnthroveTagID("toothless") + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + err = postgres.CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + tagGroupName models.AnthroveTagGroupName + tagID models.AnthroveTagID + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + tagGroupName: validTagGroupName01, + tagID: validTagID, + }, + wantErr: false, + }, + { + name: "Test 2: No TagGroupName", + args: args{ + ctx: ctx, + tagGroupName: "", + tagID: validTagID, + }, + wantErr: true, + }, + { + name: "Test 4: No tagID", + args: args{ + ctx: ctx, + tagGroupName: validTagGroupName01, + tagID: "", + }, + wantErr: true, + }, + { + name: "Test 5: Duplicate tagID", + args: args{ + ctx: ctx, + tagGroupName: validTagGroupName01, + tagID: validTagID, + }, + wantErr: true, + }, + { + name: "Test 6: Invalide tagID", + args: args{ + ctx: ctx, + tagGroupName: validTagGroupName02, + tagID: "aaa", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreateTagGroup(tt.args.ctx, tt.args.tagGroupName, tt.args.tagID); (err != nil) != tt.wantErr { + t.Errorf("CreateTagGroup() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_GetAllTagGroup(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagGroupes := []models.AnthroveTagGroupName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + expectedResult := []models.TagGroup{ + { + Name: string(validTagGroupes[0]), + TagID: string(validTagID), + }, + { + Name: string(validTagGroupes[1]), + TagID: string(validTagID), + }, + { + Name: string(validTagGroupes[2]), + TagID: string(validTagID), + }, + } + + err = postgres.CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagGroupName := range validTagGroupes { + err = postgres.CreateTagGroup(ctx, gormDB, tagGroupName, validTagID) + } + + // Test + type args struct { + ctx context.Context + } + tests := []struct { + name string + args args + want []models.TagGroup + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ctx: ctx}, + want: expectedResult, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetAllTagGroup(tt.args.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagGroup() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTagGroup() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_GetAllTagGroupByTag(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagGroupes := []models.AnthroveTagGroupName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + expectedResult := []models.TagGroup{ + { + Name: string(validTagGroupes[0]), + TagID: string(validTagID), + }, + { + Name: string(validTagGroupes[1]), + TagID: string(validTagID), + }, + { + Name: string(validTagGroupes[2]), + TagID: string(validTagID), + }, + } + + err = postgres.CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagGroupName := range validTagGroupes { + err = postgres.CreateTagGroup(ctx, gormDB, tagGroupName, validTagID) + } + + // Test + type args struct { + ctx context.Context + tagID models.AnthroveTagID + } + tests := []struct { + name string + args args + want []models.TagGroup + wantErr bool + }{ + { + name: "Test 1: Valid TagID", + args: args{ + ctx: ctx, + tagID: validTagID, + }, + want: expectedResult, + wantErr: false, + }, + { + name: "Test 2: No TagID", + args: args{ + ctx: ctx, + tagID: "", + }, + want: nil, + wantErr: true, + }, + { + name: "Test 3: Invalid TagID", + args: args{ + ctx: ctx, + tagID: "adads", + }, + want: []models.TagGroup{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetAllTagGroupByTag(tt.args.ctx, tt.args.tagID) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagGroupByTag() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTagGroupByTag() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_postgresqlConnection_DeleteTagGroup(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + validTagGroupes := []models.AnthroveTagGroupName{"httyd", "dragon", "scaly"} + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + err = postgres.CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + for _, tagGroupName := range validTagGroupes { + err = postgres.CreateTagGroup(ctx, gormDB, tagGroupName, validTagID) + } + + // Test + type args struct { + ctx context.Context + tagGroupName models.AnthroveTagGroupName + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid AnthroveTagGroupName", + args: args{ + ctx: ctx, + tagGroupName: validTagGroupes[0], + }, + wantErr: false, + }, + { + name: "Test 2: Invalid AnthroveTagGroupName", + args: args{ + ctx: ctx, + tagGroupName: "asdad", + }, + wantErr: false, + }, + { + name: "Test 3: No AnthroveTagGroupName", + args: args{ + ctx: ctx, + tagGroupName: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.DeleteTagGroup(tt.args.ctx, tt.args.tagGroupName); (err != nil) != tt.wantErr { + t.Errorf("DeleteTagAlias() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_UpdateUserSourceScrapeTimeInterval(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validSourceID := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + source := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: validSourceID, + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.icon", + } + + err = postgres.CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + err = postgres.CreateUserWithRelationToSource(ctx, gormDB, validUserID, validSourceID, "e66e6e6e6", "euser") + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + anthroveUserID models.AnthroveUserID + sourceID models.AnthroveSourceID + scrapeTime models.AnthroveScrapeTimeInterval + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: validSourceID, + scrapeTime: 10, + }, + wantErr: false, + }, + { + name: "Test 2: anthroveUserID to short", + args: args{ + ctx: ctx, + anthroveUserID: invalidUserID, + sourceID: validSourceID, + scrapeTime: 10, + }, + wantErr: true, + }, + { + name: "Test 3: anthroveUserID is empty", + args: args{ + ctx: ctx, + anthroveUserID: "", + sourceID: validSourceID, + scrapeTime: 10, + }, + wantErr: true, + }, + { + name: "Test 4: anthroveUserID to short", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: "111", + scrapeTime: 10, + }, + wantErr: true, + }, + { + name: "Test 5: anthroveUserID is empty", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: "", + scrapeTime: 10, + }, + wantErr: true, + }, + { + name: "Test 5: scrapeTime is empty", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: validSourceID, + scrapeTime: 0, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.UpdateUserSourceScrapeTimeInterval(tt.args.ctx, tt.args.anthroveUserID, tt.args.sourceID, tt.args.scrapeTime); (err != nil) != tt.wantErr { + t.Errorf("UpdateUserSourceScrapeTimeInterval() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_UpdateUserSourceLastScrapeTime(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validSourceID := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + validScrapeTime := models.AnthroveUserLastScrapeTime(time.Now()) + inValidScrapeTime := models.AnthroveUserLastScrapeTime{} + + source := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: validSourceID, + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.icon", + } + + err = postgres.CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + err = postgres.CreateUserWithRelationToSource(ctx, gormDB, validUserID, validSourceID, "e66e6e6e6", "euser") + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + anthroveUserID models.AnthroveUserID + sourceID models.AnthroveSourceID + lastScrapeTime models.AnthroveUserLastScrapeTime + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: validSourceID, + lastScrapeTime: validScrapeTime, + }, + wantErr: false, + }, + { + name: "Test 2: anthroveUserID to short", + args: args{ + ctx: ctx, + anthroveUserID: invalidUserID, + sourceID: validSourceID, + lastScrapeTime: validScrapeTime, + }, + wantErr: true, + }, + { + name: "Test 3: anthroveUserID is empty", + args: args{ + ctx: ctx, + anthroveUserID: "", + sourceID: validSourceID, + lastScrapeTime: validScrapeTime, + }, + wantErr: true, + }, + { + name: "Test 4: anthroveUserID to short", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: "111", + lastScrapeTime: validScrapeTime, + }, + wantErr: true, + }, + { + name: "Test 5: anthroveUserID is empty", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: "", + lastScrapeTime: validScrapeTime, + }, + wantErr: true, + }, + { + name: "Test 5: scrapeTime is empty", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: validSourceID, + lastScrapeTime: inValidScrapeTime, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.UpdateUserSourceLastScrapeTime(tt.args.ctx, tt.args.anthroveUserID, tt.args.sourceID, tt.args.lastScrapeTime); (err != nil) != tt.wantErr { + t.Errorf("UpdateUserSourceLastScrapeTime() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_UpdateUserSourceValidation(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validUserID := models.AnthroveUserID(fmt.Sprintf("%025s", "User1")) + invalidUserID := models.AnthroveUserID("XXX") + + validSourceID := models.AnthroveSourceID(fmt.Sprintf("%025s", "Source1")) + + source := &models.Source{ + BaseModel: models.BaseModel[models.AnthroveSourceID]{ + ID: validSourceID, + }, + DisplayName: "e621", + Domain: "e621.net", + Icon: "https://e621.icon", + } + + err = postgres.CreateSource(ctx, gormDB, source) + if err != nil { + t.Fatal(err) + } + + err = postgres.CreateUserWithRelationToSource(ctx, gormDB, validUserID, validSourceID, "e66e6e6e6", "euser") + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + anthroveUserID models.AnthroveUserID + sourceID models.AnthroveSourceID + valid bool + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: validSourceID, + valid: true, + }, + wantErr: false, + }, + { + name: "Test 2: anthroveUserID to short", + args: args{ + ctx: ctx, + anthroveUserID: invalidUserID, + sourceID: validSourceID, + valid: true, + }, + wantErr: true, + }, + { + name: "Test 3: anthroveUserID is empty", + args: args{ + ctx: ctx, + anthroveUserID: "", + sourceID: validSourceID, + valid: true, + }, + wantErr: true, + }, + { + name: "Test 4: anthroveUserID to short", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: "111", + valid: true, + }, + wantErr: true, + }, + { + name: "Test 5: anthroveUserID is empty", + args: args{ + ctx: ctx, + anthroveUserID: validUserID, + sourceID: "", + valid: true, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.UpdateUserSourceValidation(tt.args.ctx, tt.args.anthroveUserID, tt.args.sourceID, tt.args.valid); (err != nil) != tt.wantErr { + t.Errorf("UpdateUserSourceValidation() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_DeleteTag(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + validTagID := models.AnthroveTagID("toothless") + + validTag := &models.Tag{ + Name: string(validTagID), + Type: models.Character, + } + + err = postgres.CreateTag(ctx, gormDB, models.AnthroveTagName(validTag.Name), validTag.Type) + if err != nil { + t.Fatal(err) + } + + // Test + type args struct { + ctx context.Context + tagName models.AnthroveTagName + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid TagName", + args: args{ + ctx: ctx, + tagName: models.AnthroveTagName(validTagID), + }, + wantErr: false, + }, + { + name: "Test 2: Invalid TagName", + args: args{ + ctx: ctx, + tagName: models.AnthroveTagName("aaa"), + }, + wantErr: false, + }, + { + name: "Test 3: No TagName", + args: args{ + ctx: ctx, + tagName: "", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.DeleteTag(tt.args.ctx, tt.args.tagName); (err != nil) != tt.wantErr { + t.Errorf("DeleteTag() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_GetAllTagsByTagType(t *testing.T) { + // Setup trow away container + ctx := context.Background() + container, gormDB, err := test.StartPostgresContainer(ctx) + if err != nil { + t.Fatalf("Could not start PostgreSQL container: %v", err) + } + defer container.Terminate(ctx) + + // Setup Test + + validTags := []models.Tag{ + { + Name: "JayTheFerret", + Type: models.Character, + }, + { + Name: "SoXX", + Type: models.Character, + }, + { + Name: "Alphyron", + Type: models.Character, + }, + { + Name: "Dragon", + Type: models.Species, + }, + } + + expectetResult := []models.Tag{ + { + Name: "JayTheFerret", + Type: models.Character, + }, + { + Name: "SoXX", + Type: models.Character, + }, + { + Name: "Alphyron", + Type: models.Character, + }, + } + + for _, tag := range validTags { + err = postgres.CreateTag(ctx, gormDB, models.AnthroveTagName(tag.Name), tag.Type) + if err != nil { + t.Fatal(err) + } + } + + // Test + type args struct { + ctx context.Context + tagType models.TagType + } + tests := []struct { + name string + args args + want []models.Tag + wantErr bool + }{ + { + name: "Test 1: Get Data", + args: args{ + ctx: ctx, + tagType: models.Character, + }, + want: expectetResult, + wantErr: false, + }, + { + name: "Test 2: invalid Tag Type", + args: args{ + ctx: ctx, + tagType: "aa", + }, + want: validTags, + wantErr: false, + }, + { + name: "Test 3: No Tag Type", + args: args{ + ctx: ctx, + tagType: "", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + got, err := p.GetAllTagsByTagType(tt.args.ctx, tt.args.tagType) + if (err != nil) != tt.wantErr { + t.Errorf("GetAllTagsByTagType() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetAllTagsByTagType() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/database/source.go b/pkg/database/source.go new file mode 100644 index 0000000..776c1d1 --- /dev/null +++ b/pkg/database/source.go @@ -0,0 +1,19 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" +) + +type Source interface { + + // CreateSource adds a new source to the database. + CreateSource(ctx context.Context, anthroveSource *models.Source) error + + // GetAllSources retrieves all sources. + GetAllSources(ctx context.Context) ([]models.Source, error) + + // GetSourceByDomain retrieves a source by its URL. + GetSourceByDomain(ctx context.Context, sourceDomain models.AnthroveSourceDomain) (*models.Source, error) +} diff --git a/pkg/database/tag.go b/pkg/database/tag.go new file mode 100644 index 0000000..44fa553 --- /dev/null +++ b/pkg/database/tag.go @@ -0,0 +1,18 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" +) + +type Tag interface { + CreateTag(ctx context.Context, tagName models.AnthroveTagName, tagType models.TagType) error + + // GetAllTags retrieves all tags. + GetAllTags(ctx context.Context) ([]models.Tag, error) + + GetAllTagsByTagType(ctx context.Context, tagType models.TagType) ([]models.Tag, error) + + DeleteTag(ctx context.Context, tagName models.AnthroveTagName) error +} diff --git a/pkg/database/tagalias.go b/pkg/database/tagalias.go new file mode 100644 index 0000000..30171d4 --- /dev/null +++ b/pkg/database/tagalias.go @@ -0,0 +1,17 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" +) + +type TagAlias interface { + CreateTagAlias(ctx context.Context, tagAliasName models.AnthroveTagAliasName, tagID models.AnthroveTagID) error + + GetAllTagAlias(ctx context.Context) ([]models.TagAlias, error) + + GetAllTagAliasByTag(ctx context.Context, tagID models.AnthroveTagID) ([]models.TagAlias, error) + + DeleteTagAlias(ctx context.Context, tagAliasName models.AnthroveTagAliasName) error +} diff --git a/pkg/database/taggroup.go b/pkg/database/taggroup.go new file mode 100644 index 0000000..f25bd68 --- /dev/null +++ b/pkg/database/taggroup.go @@ -0,0 +1,17 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" +) + +type TagGroup interface { + CreateTagGroup(ctx context.Context, tagGroupName models.AnthroveTagGroupName, tagID models.AnthroveTagID) error + + GetAllTagGroup(ctx context.Context) ([]models.TagGroup, error) + + GetAllTagGroupByTag(ctx context.Context, tagID models.AnthroveTagID) ([]models.TagGroup, error) + + DeleteTagGroup(ctx context.Context, tagGroupName models.AnthroveTagGroupName) error +} diff --git a/pkg/database/user.go b/pkg/database/user.go new file mode 100644 index 0000000..b74d672 --- /dev/null +++ b/pkg/database/user.go @@ -0,0 +1,42 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" +) + +type User interface { + + // CreateUserWithRelationToSource adds a user with a relation to a source. + CreateUserWithRelationToSource(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, accountId string, accountUsername string) error + + // CreateReferenceBetweenUserAndPost links a user with a post. + CreateReferenceBetweenUserAndPost(ctx context.Context, anthroveUserID models.AnthroveUserID, anthrovePostID models.AnthrovePostID) error + + UpdateUserSourceScrapeTimeInterval(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, scrapeTime models.AnthroveScrapeTimeInterval) error + + UpdateUserSourceLastScrapeTime(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, lastScrapeTime models.AnthroveUserLastScrapeTime) error + + UpdateUserSourceValidation(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID, valid bool) error + + GetAllUsers(ctx context.Context) ([]models.User, error) + + // GetUserFavoritesCount retrieves the count of a user's favorites. + GetUserFavoritesCount(ctx context.Context, anthroveUserID models.AnthroveUserID) (int64, error) + + // GetAllUserSources retrieves the source links of a user. + GetAllUserSources(ctx context.Context, anthroveUserID models.AnthroveUserID) (map[string]models.UserSource, error) + + // GetUserSourceBySourceID retrieves a specified source link of a user. + GetUserSourceBySourceID(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceID models.AnthroveSourceID) (*models.UserSource, error) + + // GetAllUserFavoritesWithPagination retrieves a user's favorite posts with pagination. + GetAllUserFavoritesWithPagination(ctx context.Context, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) + + // GetAllTagsFromUser retrieves a user's tags through their favorite posts. + GetAllTagsFromUser(ctx context.Context, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error) + + // CheckIfUserHasPostAsFavorite checks if a user-post link exists. + CheckIfUserHasPostAsFavorite(ctx context.Context, anthroveUserID models.AnthroveUserID, sourcePostID models.AnthrovePostID) (bool, error) +} diff --git a/pkg/error/database.go b/pkg/error/database.go new file mode 100644 index 0000000..278a043 --- /dev/null +++ b/pkg/error/database.go @@ -0,0 +1,25 @@ +package error + +type EntityAlreadyExists struct{} + +func (e *EntityAlreadyExists) Error() string { + return "EntityAlreadyExists error" +} + +type NoDataWritten struct{} + +func (e *NoDataWritten) Error() string { + return "NoDataWritten error" +} + +type NoDataFound struct{} + +func (e *NoDataFound) Error() string { + return "NoDataFound error" +} + +type NoRelationCreated struct{} + +func (e *NoRelationCreated) Error() string { + return "relationship creation error" +} diff --git a/pkg/error/database_test.go b/pkg/error/database_test.go new file mode 100644 index 0000000..5fd2e1f --- /dev/null +++ b/pkg/error/database_test.go @@ -0,0 +1,83 @@ +package error + +import "testing" + +func TestEntityAlreadyExists_Error(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "Test : Valid error String", + want: "EntityAlreadyExists error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &EntityAlreadyExists{} + if got := e.Error(); got != tt.want { + t.Errorf("Error() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNoDataFound_Error(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "Test : Valid error String", + want: "NoDataFound error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &NoDataFound{} + if got := e.Error(); got != tt.want { + t.Errorf("Error() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNoDataWritten_Error(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "Test : Valid error String", + want: "NoDataWritten error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &NoDataWritten{} + if got := e.Error(); got != tt.want { + t.Errorf("Error() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNoRelationCreated_Error(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "Test : Valid error String", + want: "relationship creation error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &NoRelationCreated{} + if got := e.Error(); got != tt.want { + t.Errorf("Error() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/error/validation.go b/pkg/error/validation.go new file mode 100644 index 0000000..27ec3c4 --- /dev/null +++ b/pkg/error/validation.go @@ -0,0 +1,19 @@ +package error + +import "fmt" + +const ( + AnthroveUserIDIsEmpty = "anthrovePostID cannot be empty" + AnthroveUserIDToShort = "anthrovePostID needs to be 25 characters long" + AnthroveSourceIDEmpty = "anthroveSourceID cannot be empty" + AnthroveSourceIDToShort = "anthroveSourceID needs to be 25 characters long" + AnthroveTagIDEmpty = "tagID cannot be empty" +) + +type EntityValidationFailed struct { + Reason string +} + +func (e EntityValidationFailed) Error() string { + return fmt.Sprintf("Entity validation failed: %s", e.Reason) +} diff --git a/pkg/error/validation_test.go b/pkg/error/validation_test.go new file mode 100644 index 0000000..88e9d3c --- /dev/null +++ b/pkg/error/validation_test.go @@ -0,0 +1,30 @@ +package error + +import "testing" + +func TestEntityValidationFailed_Error(t *testing.T) { + type fields struct { + Reason string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1: Reason", + fields: fields{Reason: "TEST ERROR"}, + want: "Entity validation failed: TEST ERROR", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := EntityValidationFailed{ + Reason: tt.fields.Reason, + } + if got := e.Error(); got != tt.want { + t.Errorf("Error() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/graph/graph.go b/pkg/graph/graph.go deleted file mode 100644 index 3cd7630..0000000 --- a/pkg/graph/graph.go +++ /dev/null @@ -1,116 +0,0 @@ -// Package graph provides a client for using the OtterSpace API. -// -// This package provides a client to interact with the OtterSpace API. It includes -// methods for all API endpoints, and convenience methods for common tasks. -// -// This is a simple usage example: -// -// package main -// -// import ( -// "context" -// "fmt" -// "git.dragse.it/anthrove/otter-space-sdk/pkg/models" -// "git.dragse.it/anthrove/otter-space-sdk/pkg/graph" -// ) -// -// func main() { -// client := graph.NewGraphConnection() -// err := client.Connect(context.Background(), "your-endpoint", "your-username", "your-password") -// if err != nil { -// fmt.Println(err) -// return -// } -// // further usage of the client... -// } -package graph - -import ( - "context" - - "git.dragse.it/anthrove/otter-space-sdk/pkg/models" -) - -// OtterSpace provides an interface for interacting with the OtterSpace API. -// It includes methods for connecting to the API, adding and linking users, posts, and sources, -// and retrieving information about users and posts. -type OtterSpace interface { - // Connect sets up a connection to the OtterSpace API endpoint using the provided username and password. - // It returns an error if the connection cannot be established. - Connect(ctx context.Context, endpoint string, username string, password string) error - - // AddUserWithRelationToSource adds a new user to the OtterSpace graph and associates them with a source. - // It returns the newly created user and an error if the operation fails. - AddUserWithRelationToSource(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceDomain string, userID string, username string) error - - // AddSource adds a new source to the OtterSpace graph. - // It returns an error if the operation fails. - AddSource(ctx context.Context, anthroveSource *models.AnthroveSource) error - - // AddPost adds a new post to the OtterSpace graph. - // It returns an error if the operation fails. - AddPost(ctx context.Context, anthrovePost *models.AnthrovePost) error - - // AddTagWithRelationToPost adds a new tag to the OtterSpace graph and associates it with a post. - // It returns an error if the operation fails. - AddTagWithRelationToPost(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveTag *models.AnthroveTag) error - - // LinkPostWithSource establishes a link between a post and a source in the OtterSpace graph. - // It returns an error if the operation fails. - LinkPostWithSource(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveSourceDomain string, anthrovePostRelationship *models.AnthrovePostRelationship) error - - // LinkUserWithPost establishes a link between a user and a post in the OtterSpace graph. - // It returns an error if the operation fails. - LinkUserWithPost(ctx context.Context, anthroveUser *models.AnthroveUser, anthrovePost *models.AnthrovePost) error - - // CheckUserPostLink checks if a link between a user and a post exists in the OtterSpace graph. - // It returns true if the link exists, false otherwise, and an error if the operation fails. - CheckUserPostLink(ctx context.Context, anthroveUserID models.AnthroveUserID, sourcePostID string, sourceUrl string) (bool, error) - - // CheckPostNodeExistsByAnthroveID checks if a post node exists in the OtterSpace graph by its Anthrove ID. - // It returns the post if it exists, a boolean indicating whether the post was found, and an error if the operation fails. - CheckPostNodeExistsByAnthroveID(ctx context.Context, anthrovePost *models.AnthrovePost) (*models.AnthrovePost, bool, error) - - // CheckPostNodeExistsBySourceURL checks if a post node exists in the OtterSpace graph by its source URL. - // It returns the post if it exists, a boolean indicating whether the post was found, and an error if the operation fails. - CheckPostNodeExistsBySourceURL(ctx context.Context, sourceUrl string) (*models.AnthrovePost, bool, error) - - // CheckPostNodeExistsBySourceID checks if a post node exists in the OtterSpace graph by its source ID. - // It returns the post if it exists, a boolean indicating whether the post was found, and an error if the operation fails. - CheckPostNodeExistsBySourceID(ctx context.Context, sourcePostID string) (*models.AnthrovePost, bool, error) - - // GetUserFavoriteCount retrieves the count of a user's favorite posts from the OtterSpace graph. - // It returns the count and an error if the operation fails. - GetUserFavoriteCount(ctx context.Context, anthroveUserID models.AnthroveUserID) (int64, error) - - // GetUserSourceLinks retrieves the links between a user and sources in the OtterSpace graph. - // It returns a map of source domains to user-source relationships, and an error if the operation fails. - GetUserSourceLinks(ctx context.Context, anthroveUserID models.AnthroveUserID) (map[string]models.AnthroveUserRelationship, error) - - // GetSpecifiedUserSourceLink GetUserSourceLinks retrieves the links between a user and a specific source in the OtterSpace graph. - // It returns a map of source domains to user-source relationships, and an error if the operation fails. - GetSpecifiedUserSourceLink(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceDisplayName string) (map[string]models.AnthroveUserRelationship, error) - - // GetAnthroveUser retrieves a user from the OtterSpace graph by their ID. - // It returns the user and an error if the operation fails. - GetAnthroveUser(ctx context.Context, anthroveUserID models.AnthroveUserID) (*models.AnthroveUser, error) - - // GetAllAnthroveUserIDs retrieves all user IDs from the OtterSpace graph. - // It returns a slice of user IDs and an error if the operation fails. - GetAllAnthroveUserIDs(ctx context.Context) ([]models.AnthroveUserID, error) - - // GetUserFavoritePostsWithPagination gets all user favorites with relation and sources for the given user - GetUserFavoritePostsWithPagination(ctx context.Context, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) - - // GetUserTagsTroughFavedPosts returns a list of Tags that the user hs favorites through a post - GetUserTagsTroughFavedPosts(ctx context.Context, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error) - - // GetAllTags returns a list of Tags that the user hs favorites through a post - GetAllTags(ctx context.Context) ([]models.TagsWithFrequency, error) - - // GetAllSources returns a list of Sources in the database - GetAllSources(ctx context.Context) ([]models.AnthroveSource, error) - - // GetSourceByURL returns the Source Node based on the URL - GetSourceByURL(ctx context.Context, sourceUrl string) (*models.AnthroveSource, error) -} diff --git a/pkg/graph/impl.go b/pkg/graph/impl.go deleted file mode 100644 index 6b1b3bb..0000000 --- a/pkg/graph/impl.go +++ /dev/null @@ -1,123 +0,0 @@ -package graph - -import ( - "context" - - "git.dragse.it/anthrove/otter-space-sdk/internal" - "git.dragse.it/anthrove/otter-space-sdk/pkg/models" - "github.com/neo4j/neo4j-go-driver/v5/neo4j" - "github.com/neo4j/neo4j-go-driver/v5/neo4j/config" -) - -type graphConnection struct { - driver neo4j.DriverWithContext - graphDebug bool -} - -func NewGraphConnection(graphDebug bool) OtterSpace { - return &graphConnection{ - driver: nil, - graphDebug: graphDebug, - } -} - -func (g *graphConnection) Connect(ctx context.Context, endpoint string, username string, password string) error { - driver, err := neo4j.NewDriverWithContext(endpoint, neo4j.BasicAuth(username, password, ""), - logger(g.graphDebug)) - - if err != nil { - return err - } - err = driver.VerifyAuthentication(ctx, nil) - if err != nil { - return err - } - g.driver = driver - return nil -} - -func (g *graphConnection) AddUserWithRelationToSource(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceDomain string, userID string, username string) error { - return internal.CreateUserNodeWithSourceRelation(ctx, g.driver, anthroveUserID, sourceDomain, userID, username) -} - -func (g *graphConnection) AddSource(ctx context.Context, anthroveSource *models.AnthroveSource) error { - return internal.CreateSourceNode(ctx, g.driver, anthroveSource) -} - -func (g *graphConnection) AddPost(ctx context.Context, anthrovePost *models.AnthrovePost) error { - return internal.CreateAnthrovePostNode(ctx, g.driver, anthrovePost) -} - -func (g *graphConnection) AddTagWithRelationToPost(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveTag *models.AnthroveTag) error { - return internal.CreateTagNodeWitRelation(ctx, g.driver, anthrovePostID, anthroveTag) -} - -func (g *graphConnection) LinkPostWithSource(ctx context.Context, anthrovePostID models.AnthrovePostID, anthroveSourceDomain string, anthrovePostRelationship *models.AnthrovePostRelationship) error { - return internal.EstablishAnthrovePostToSourceLink(ctx, g.driver, anthrovePostID, anthroveSourceDomain, anthrovePostRelationship) -} - -func (g *graphConnection) LinkUserWithPost(ctx context.Context, anthroveUser *models.AnthroveUser, anthrovePost *models.AnthrovePost) error { - return internal.EstablishUserToPostLink(ctx, g.driver, anthroveUser, anthrovePost) -} - -func (g *graphConnection) CheckUserPostLink(ctx context.Context, anthroveUserID models.AnthroveUserID, sourcePostID string, sourceUrl string) (bool, error) { - return internal.CheckUserToPostLink(ctx, g.driver, anthroveUserID, sourcePostID, sourceUrl) -} - -func (g *graphConnection) CheckPostNodeExistsByAnthroveID(ctx context.Context, anthrovePost *models.AnthrovePost) (*models.AnthrovePost, bool, error) { - return internal.CheckIfAnthrovePostNodeExistsByAnthroveID(ctx, g.driver, anthrovePost) -} - -func (g *graphConnection) CheckPostNodeExistsBySourceURL(ctx context.Context, sourceUrl string) (*models.AnthrovePost, bool, error) { - return internal.CheckIfAnthrovePostNodeExistsBySourceURl(ctx, g.driver, sourceUrl) -} - -func (g *graphConnection) CheckPostNodeExistsBySourceID(ctx context.Context, sourcePostID string) (*models.AnthrovePost, bool, error) { - return internal.CheckIfAnthrovePostNodeExistsBySourceID(ctx, g.driver, sourcePostID) -} - -func (g *graphConnection) GetUserFavoriteCount(ctx context.Context, anthroveUserID models.AnthroveUserID) (int64, error) { - return internal.GetUserFavoritesCount(ctx, g.driver, anthroveUserID) -} - -func (g *graphConnection) GetUserSourceLinks(ctx context.Context, anthroveUserID models.AnthroveUserID) (map[string]models.AnthroveUserRelationship, error) { - return internal.GetUserSourceLink(ctx, g.driver, anthroveUserID) -} - -func (g *graphConnection) GetSpecifiedUserSourceLink(ctx context.Context, anthroveUserID models.AnthroveUserID, sourceDisplayName string) (map[string]models.AnthroveUserRelationship, error) { - return internal.GetSpecifiedUserSourceLink(ctx, g.driver, anthroveUserID, sourceDisplayName) -} - -func (g *graphConnection) GetAnthroveUser(ctx context.Context, anthroveUserID models.AnthroveUserID) (*models.AnthroveUser, error) { - return internal.GetAnthroveUser(ctx, g.driver, anthroveUserID) -} - -func (g *graphConnection) GetAllAnthroveUserIDs(ctx context.Context) ([]models.AnthroveUserID, error) { - return internal.GetAllAnthroveUserIDs(ctx, g.driver) -} - -func (g *graphConnection) GetUserFavoritePostsWithPagination(ctx context.Context, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) { - return internal.GetUserFavoriteNodeWithPagination(ctx, g.driver, anthroveUserID, skip, limit) -} - -func (g *graphConnection) GetUserTagsTroughFavedPosts(ctx context.Context, anthroveUserID models.AnthroveUserID) ([]models.TagsWithFrequency, error) { - return internal.GetUserTagNodeWitRelationToFavedPosts(ctx, g.driver, anthroveUserID) -} - -func (g *graphConnection) GetAllTags(ctx context.Context) ([]models.TagsWithFrequency, error) { - return internal.GetTags(ctx, g.driver) -} - -func (g *graphConnection) GetAllSources(ctx context.Context) ([]models.AnthroveSource, error) { - return internal.GetAllSourceNodes(ctx, g.driver) -} - -func (g *graphConnection) GetSourceByURL(ctx context.Context, sourceUrl string) (*models.AnthroveSource, error) { - return internal.GetSourceNodesByURL(ctx, g.driver, sourceUrl) -} - -func logger(graphDebug bool) func(config *config.Config) { - return func(config *config.Config) { - config.Log = internal.NewGraphLogger(graphDebug) - } -} diff --git a/pkg/models/api.go b/pkg/models/api.go index a30b955..37adbd0 100644 --- a/pkg/models/api.go +++ b/pkg/models/api.go @@ -1,20 +1,5 @@ package models -type FavoriteRelations struct { - SourcesID string `json:"sources_id"` - Relations AnthrovePostRelationship `json:"relations"` -} - -type FavoritePost struct { - AnthrovePost AnthrovePost `json:"anthrove_post"` - Relations []FavoriteRelations `json:"relations"` -} - type FavoriteList struct { - Posts []FavoritePost `json:"posts,omitempty"` -} - -type TagsWithFrequency struct { - Frequency int64 `json:"frequency"` - Tags AnthroveTag `json:"tags"` + Posts []Post `json:"posts,omitempty"` } diff --git a/pkg/models/config.go b/pkg/models/config.go new file mode 100644 index 0000000..2013b95 --- /dev/null +++ b/pkg/models/config.go @@ -0,0 +1,12 @@ +package models + +type DatabaseConfig struct { + Endpoint string `env:"DB_ENDPOINT,required"` + Username string `env:"DB_USERNAME,required"` + Password string `env:"DB_PASSWORD,required,unset"` + Database string `env:"DB_DATABASE,required"` + Port int `env:"DB_PORT,required" envDefault:"5432"` + SSL bool `env:"DB_SSL,required" envDefault:"true"` + Timezone string `env:"DB_TIMEZONE,required" envDefault:"Europe/Berlin"` + Debug bool `env:"DB_DEBUG" envDefault:"false"` +} diff --git a/pkg/models/const.go b/pkg/models/const.go index 9dd312e..7e84b0c 100644 --- a/pkg/models/const.go +++ b/pkg/models/const.go @@ -1,18 +1,41 @@ package models +import "time" + type AnthroveUserID string type AnthrovePostID string -type AnthroveRating string +type AnthroveSourceID string +type AnthroveSourceDomain string +type AnthrovePostURL string +type AnthroveTagGroupName string +type AnthroveTagAliasName string +type AnthroveTagID string +type AnthroveScrapeTimeInterval int +type AnthroveUserLastScrapeTime time.Time +type AnthroveTagName string + +type Rating string +type TagType string const ( - SFW AnthroveRating = "s" - NSFW AnthroveRating = "e" - Questionable AnthroveRating = "q" - Unknown AnthroveRating = "unknown" + SFW Rating = "safe" + NSFW Rating = "explicit" + Questionable Rating = "questionable" + Unknown Rating = "unknown" ) -func (r *AnthroveRating) Convert(e621Rating string) { +const ( + General TagType = "general" + Species TagType = "species" + Character TagType = "character" + Artist TagType = "artist" + Lore TagType = "lore" + Meta TagType = "meta" + Invalid TagType = "invalid" + Copyright TagType = "copyright" +) +func (r *Rating) Convert(e621Rating string) { switch e621Rating { case "e": *r = NSFW @@ -23,5 +46,4 @@ func (r *AnthroveRating) Convert(e621Rating string) { default: *r = Unknown } - } diff --git a/pkg/models/const_test.go b/pkg/models/const_test.go new file mode 100644 index 0000000..cba0f75 --- /dev/null +++ b/pkg/models/const_test.go @@ -0,0 +1,59 @@ +package models + +import ( + "reflect" + "testing" +) + +func TestRating_Convert(t *testing.T) { + type args struct { + e621Rating string + } + tests := []struct { + name string + r *Rating + args args + want Rating + }{ + { + name: "Test 1: NSFW Rating", + r: new(Rating), + args: args{ + e621Rating: "e", + }, + want: NSFW, + }, + { + name: "Test 2: Questionable Rating", + r: new(Rating), + args: args{ + e621Rating: "q", + }, + want: Questionable, + }, + { + name: "Test 3: SFW Rating", + r: new(Rating), + args: args{ + e621Rating: "s", + }, + want: SFW, + }, + { + name: "Test 4: Unknown Rating", + r: new(Rating), + args: args{ + e621Rating: "x", + }, + want: Unknown, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.r.Convert(tt.args.e621Rating) + if !reflect.DeepEqual(*tt.r, tt.want) { + t.Errorf("Convert() = %v, want %v", *tt.r, tt.want) + } + }) + } +} diff --git a/pkg/models/orm.go b/pkg/models/orm.go new file mode 100644 index 0000000..7235e57 --- /dev/null +++ b/pkg/models/orm.go @@ -0,0 +1,33 @@ +package models + +import ( + gonanoid "github.com/matoous/go-nanoid/v2" + "gorm.io/gorm" + "time" +) + +type ID interface { + AnthroveUserID | AnthroveSourceID | AnthrovePostID +} + +type BaseModel[T ID] struct { + ID T `gorm:"primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (base *BaseModel[T]) BeforeCreate(db *gorm.DB) error { + var defaultVar T + + if base.ID == defaultVar { + id, err := gonanoid.New(25) + if err != nil { + return err + } + + base.ID = T(id) + } + + return nil +} diff --git a/pkg/models/orm_test.go b/pkg/models/orm_test.go new file mode 100644 index 0000000..cf9e1a4 --- /dev/null +++ b/pkg/models/orm_test.go @@ -0,0 +1,56 @@ +package models + +import ( + "testing" + "time" + + "gorm.io/gorm" +) + +func TestBaseModel_BeforeCreate(t *testing.T) { + type fields struct { + ID string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt + } + type args struct { + db *gorm.DB + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "Test 1: Prefilled ID", + fields: fields{ + ID: "1", + }, + args: args{db: nil}, + wantErr: false, + }, + { + name: "Test 1: Autogenerate ID", + fields: fields{ + ID: "", + }, + args: args{db: nil}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + base := &BaseModel[AnthrovePostID]{ + ID: AnthrovePostID(tt.fields.ID), + CreatedAt: tt.fields.CreatedAt, + UpdatedAt: tt.fields.UpdatedAt, + DeletedAt: tt.fields.DeletedAt, + } + if err := base.BeforeCreate(tt.args.db); (err != nil) != tt.wantErr { + t.Errorf("BeforeCreate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/models/post.go b/pkg/models/post.go index e2d4b9b..bfe9946 100644 --- a/pkg/models/post.go +++ b/pkg/models/post.go @@ -1,6 +1,14 @@ package models -type AnthrovePost struct { - PostID AnthrovePostID `json:"post_id"` - Rating AnthroveRating `json:"rating"` +// Post model +type Post struct { + BaseModel[AnthrovePostID] + Rating Rating `gorm:"type:enum('safe','questionable','explicit')"` + Tags []Tag `gorm:"many2many:post_tags;"` + Favorites []UserFavorites `gorm:"foreignKey:PostID"` + References []PostReference `gorm:"foreignKey:PostID"` +} + +func (Post) TableName() string { + return "Post" } diff --git a/pkg/models/postReference.go b/pkg/models/postReference.go new file mode 100644 index 0000000..df1267c --- /dev/null +++ b/pkg/models/postReference.go @@ -0,0 +1,19 @@ +package models + +type PostReference struct { + PostID string `gorm:"primaryKey"` + SourceID string `gorm:"primaryKey"` + URL string `gorm:"not null;unique"` + PostReferenceConfig +} + +type PostReferenceConfig struct { + SourcePostID string + FullFileURL string + PreviewFileURL string + SampleFileURL string +} + +func (PostReference) TableName() string { + return "PostReference" +} diff --git a/pkg/models/postReference_test.go b/pkg/models/postReference_test.go new file mode 100644 index 0000000..d203024 --- /dev/null +++ b/pkg/models/postReference_test.go @@ -0,0 +1,44 @@ +package models + +import "testing" + +func TestPostReference_TableName(t *testing.T) { + type fields struct { + PostID string + SourceID string + URL string + SourcePostID string + FullFileURL string + PreviewFileURL string + SampleFileURL string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1: PostReference", + fields: fields{}, + want: "PostReference", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + po := PostReference{ + PostID: tt.fields.PostID, + SourceID: tt.fields.SourceID, + URL: tt.fields.URL, + PostReferenceConfig: PostReferenceConfig{ + SourcePostID: tt.fields.SourcePostID, + FullFileURL: tt.fields.FullFileURL, + PreviewFileURL: tt.fields.PreviewFileURL, + SampleFileURL: tt.fields.SampleFileURL, + }, + } + if got := po.TableName(); got != tt.want { + t.Errorf("TableName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/models/post_test.go b/pkg/models/post_test.go new file mode 100644 index 0000000..2e7228d --- /dev/null +++ b/pkg/models/post_test.go @@ -0,0 +1,38 @@ +package models + +import "testing" + +func TestPost_TableName(t *testing.T) { + type fields struct { + BaseModel BaseModel[AnthrovePostID] + Rating Rating + Tags []Tag + Favorites []UserFavorites + References []PostReference + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1: Is name Post", + fields: fields{}, + want: "Post", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + po := Post{ + BaseModel: tt.fields.BaseModel, + Rating: tt.fields.Rating, + Tags: tt.fields.Tags, + Favorites: tt.fields.Favorites, + References: tt.fields.References, + } + if got := po.TableName(); got != tt.want { + t.Errorf("TableName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/models/relationships.go b/pkg/models/relationships.go deleted file mode 100644 index f62e134..0000000 --- a/pkg/models/relationships.go +++ /dev/null @@ -1,12 +0,0 @@ -package models - -type AnthroveUserRelationship struct { - UserID string `json:"user_id"` - Username string `json:"username"` - ScrapeTimeInterval string `json:"scrape_time_interval"` - Source AnthroveSource `json:"source"` -} -type AnthrovePostRelationship struct { - PostID string `json:"post_id"` - Url string `json:"url"` -} diff --git a/pkg/models/source.go b/pkg/models/source.go index 00040ae..200b48e 100644 --- a/pkg/models/source.go +++ b/pkg/models/source.go @@ -1,7 +1,15 @@ package models -type AnthroveSource struct { - DisplayName string `json:"display_name"` - Domain string `json:"domain"` - Icon string `json:"icon"` +// Source model +type Source struct { + BaseModel[AnthroveSourceID] + DisplayName string + Domain string `gorm:"not null;unique"` + Icon string `gorm:"not null"` + UserSources []UserSource `gorm:"foreignKey:SourceID"` + References []PostReference `gorm:"foreignKey:SourceID"` +} + +func (Source) TableName() string { + return "Source" } diff --git a/pkg/models/source_test.go b/pkg/models/source_test.go new file mode 100644 index 0000000..e65b2c8 --- /dev/null +++ b/pkg/models/source_test.go @@ -0,0 +1,40 @@ +package models + +import "testing" + +func TestSource_TableName(t *testing.T) { + type fields struct { + BaseModel BaseModel[AnthroveSourceID] + DisplayName string + Domain string + Icon string + UserSources []UserSource + References []PostReference + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1: Is name Source", + fields: fields{}, + want: "Source", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + so := Source{ + BaseModel: tt.fields.BaseModel, + DisplayName: tt.fields.DisplayName, + Domain: tt.fields.Domain, + Icon: tt.fields.Icon, + UserSources: tt.fields.UserSources, + References: tt.fields.References, + } + if got := so.TableName(); got != tt.want { + t.Errorf("TableName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/models/tag.go b/pkg/models/tag.go index 8d0d1e6..583cd78 100644 --- a/pkg/models/tag.go +++ b/pkg/models/tag.go @@ -1,6 +1,39 @@ package models -type AnthroveTag struct { - Name string `json:"name"` - Type string `json:"type"` +// Tag models +type Tag struct { + Name string `gorm:"primaryKey"` + Type TagType `gorm:"column:tag_type"` + Aliases []TagAlias `gorm:"foreignKey:TagID"` + Groups []TagGroup `gorm:"foreignKey:TagID"` + Posts []Post `gorm:"many2many:post_tags;"` +} + +func (Tag) TableName() string { + return "Tag" +} + +// TagAlias model +type TagAlias struct { + Name string `gorm:"primaryKey"` + TagID string +} + +func (TagAlias) TableName() string { + return "TagAlias" +} + +// TagGroup model +type TagGroup struct { + Name string `gorm:"primaryKey"` + TagID string +} + +func (TagGroup) TableName() string { + return "TagGroup" +} + +type TagsWithFrequency struct { + Frequency int64 `json:"frequency"` + Tags Tag `json:"tags"` } diff --git a/pkg/models/tag_test.go b/pkg/models/tag_test.go new file mode 100644 index 0000000..6f66b18 --- /dev/null +++ b/pkg/models/tag_test.go @@ -0,0 +1,96 @@ +package models + +import "testing" + +func TestTagAlias_TableName(t *testing.T) { + type fields struct { + Name string + TagID string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1: Is Name TagAlias", + fields: fields{}, + want: "TagAlias", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ta := TagAlias{ + Name: tt.fields.Name, + TagID: tt.fields.TagID, + } + if got := ta.TableName(); got != tt.want { + t.Errorf("TableName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTagGroup_TableName(t *testing.T) { + type fields struct { + Name string + TagID string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1: Is name TagGroup", + fields: fields{}, + want: "TagGroup", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ta := TagGroup{ + Name: tt.fields.Name, + TagID: tt.fields.TagID, + } + if got := ta.TableName(); got != tt.want { + t.Errorf("TableName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTag_TableName(t *testing.T) { + type fields struct { + Name string + Type TagType + Aliases []TagAlias + Groups []TagGroup + Posts []Post + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1: Is name Tag", + fields: fields{}, + want: "Tag", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ta := Tag{ + Name: tt.fields.Name, + Type: tt.fields.Type, + Aliases: tt.fields.Aliases, + Groups: tt.fields.Groups, + Posts: tt.fields.Posts, + } + if got := ta.TableName(); got != tt.want { + t.Errorf("TableName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/models/user.go b/pkg/models/user.go index af6e36c..95ba75d 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -1,6 +1,12 @@ package models -type AnthroveUser struct { - UserID AnthroveUserID `json:"user_id"` - Relationship []AnthroveUserRelationship `json:"relationship"` +// User model +type User struct { + BaseModel[AnthroveUserID] + Favorites []UserFavorites `gorm:"foreignKey:UserID"` + Sources []UserSource `gorm:"foreignKey:UserID"` +} + +func (User) TableName() string { + return "User" } diff --git a/pkg/models/userFavorite.go b/pkg/models/userFavorite.go new file mode 100644 index 0000000..028b907 --- /dev/null +++ b/pkg/models/userFavorite.go @@ -0,0 +1,13 @@ +package models + +import "time" + +type UserFavorites struct { + UserID string `gorm:"primaryKey"` + PostID string `gorm:"primaryKey"` + CreatedAt time.Time +} + +func (UserFavorites) TableName() string { + return "UserFavorites" +} diff --git a/pkg/models/userFavorite_test.go b/pkg/models/userFavorite_test.go new file mode 100644 index 0000000..3b69143 --- /dev/null +++ b/pkg/models/userFavorite_test.go @@ -0,0 +1,37 @@ +package models + +import ( + "testing" + "time" +) + +func TestUserFavorite_TableName(t *testing.T) { + type fields struct { + UserID string + PostID string + CreatedAt time.Time + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1: Is name UserFavorites", + fields: fields{}, + want: "UserFavorites", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + us := UserFavorites{ + UserID: tt.fields.UserID, + PostID: tt.fields.PostID, + CreatedAt: tt.fields.CreatedAt, + } + if got := us.TableName(); got != tt.want { + t.Errorf("TableName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/models/userSource.go b/pkg/models/userSource.go new file mode 100644 index 0000000..8ac124e --- /dev/null +++ b/pkg/models/userSource.go @@ -0,0 +1,20 @@ +package models + +import "time" + +type UserSource struct { + User User `gorm:"foreignKey:ID;references:UserID"` + UserID string `gorm:"primaryKey"` + Source Source `gorm:"foreignKey:ID;references:SourceID"` + SourceID string `gorm:"primaryKey"` + ScrapeTimeInterval string + AccountUsername string + AccountID string + LastScrapeTime time.Time + AccountValidate bool + AccountValidationKey string +} + +func (UserSource) TableName() string { + return "UserSource" +} diff --git a/pkg/models/userSource_test.go b/pkg/models/userSource_test.go new file mode 100644 index 0000000..ff8cd95 --- /dev/null +++ b/pkg/models/userSource_test.go @@ -0,0 +1,42 @@ +package models + +import "testing" + +func TestUserSource_TableName(t *testing.T) { + type fields struct { + User User + UserID string + Source Source + SourceID string + ScrapeTimeInterval string + AccountUsername string + AccountID string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1: Is name UserSource", + fields: fields{}, + want: "UserSource", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + us := UserSource{ + User: tt.fields.User, + UserID: tt.fields.UserID, + Source: tt.fields.Source, + SourceID: tt.fields.SourceID, + ScrapeTimeInterval: tt.fields.ScrapeTimeInterval, + AccountUsername: tt.fields.AccountUsername, + AccountID: tt.fields.AccountID, + } + if got := us.TableName(); got != tt.want { + t.Errorf("TableName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/models/user_test.go b/pkg/models/user_test.go new file mode 100644 index 0000000..c31745b --- /dev/null +++ b/pkg/models/user_test.go @@ -0,0 +1,34 @@ +package models + +import "testing" + +func TestUser_TableName(t *testing.T) { + type fields struct { + BaseModel BaseModel[AnthroveUserID] + Favorites []UserFavorites + Sources []UserSource + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Test 1: Is name User", + fields: fields{}, + want: "User", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + us := User{ + BaseModel: tt.fields.BaseModel, + Favorites: tt.fields.Favorites, + Sources: tt.fields.Sources, + } + if got := us.TableName(); got != tt.want { + t.Errorf("TableName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/test/helper.go b/test/helper.go new file mode 100644 index 0000000..b16d9d1 --- /dev/null +++ b/test/helper.go @@ -0,0 +1,123 @@ +package test + +import ( + "context" + "database/sql" + "net/url" + "strconv" + "strings" + "time" + + "git.dragse.it/anthrove/otter-space-sdk/pkg/models" + migrate "github.com/rubenv/sql-migrate" + postgrescontainer "github.com/testcontainers/testcontainers-go/modules/postgres" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + databaseName = "anthrove" + databaseUser = "anthrove" + databasePassword = "anthrove" + migrationSource = "../../pkg/database/migrations/" +) + +func StartPostgresContainer(ctx context.Context) (*postgrescontainer.PostgresContainer, *gorm.DB, error) { + + pgContainer, err := postgrescontainer.RunContainer(ctx, + testcontainers.WithImage("postgres:alpine"), + postgrescontainer.WithDatabase(databaseName), + postgrescontainer.WithUsername(databaseUser), + postgrescontainer.WithPassword(databasePassword), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2).WithStartupTimeout(60*time.Second)), + ) + if err != nil { + return nil, nil, err + } + + connectionString, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + if err != nil { + return nil, nil, err + } + + err = migrateDatabase(connectionString) + if err != nil { + return nil, nil, err + + } + + gormDB, err := getGormDB(connectionString) + if err != nil { + return nil, nil, err + } + return pgContainer, gormDB, nil +} + +func migrateDatabase(connectionString string) error { + db, err := sql.Open("postgres", connectionString) + if err != nil { + return err + } + + migrations := &migrate.FileMigrationSource{ + Dir: migrationSource, + } + + _, err = migrate.Exec(db, "postgres", migrations, migrate.Up) + if err != nil { + return err + } + + return nil + +} + +func getGormDB(connectionString string) (*gorm.DB, error) { + return gorm.Open(postgres.Open(connectionString), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) +} + +func DatabaseModesFromConnectionString(ctx context.Context, pgContainer *postgrescontainer.PostgresContainer) (*models.DatabaseConfig, error) { + var err error + + connectionString, err := pgContainer.ConnectionString(ctx) + if err != nil { + return nil, err + } + + connectionStringUrl, err := url.Parse(connectionString) + if err != nil { + return nil, err + } + + split := strings.Split(connectionStringUrl.Host, ":") + host := split[0] + + port, err := strconv.Atoi(split[1]) + if err != nil { + return nil, err + } + + database := strings.TrimPrefix(connectionStringUrl.Path, "/") + + username := connectionStringUrl.User.Username() + password, _ := connectionStringUrl.User.Password() + + return &models.DatabaseConfig{ + Endpoint: host, + Username: username, + Password: password, + Database: database, + Port: port, + SSL: false, + Timezone: "Europe/Berlin", + Debug: true, + }, nil +}