From 34c001473b4d1aee3012fb7cfb1287af1af1fb05 Mon Sep 17 00:00:00 2001 From: SoXX Date: Fri, 19 Jul 2024 10:03:35 +0200 Subject: [PATCH] migration from old git (no git history) --- .gitea/workflows/build_check.yaml | 39 + .gitignore | 195 + README.md | 57 + go.mod | 73 + go.sum | 229 + internal/postgres/post.go | 131 + internal/postgres/post_test.go | 468 +++ internal/postgres/relationships.go | 126 + internal/postgres/relationships_test.go | 392 ++ internal/postgres/source.go | 85 + internal/postgres/source_test.go | 270 ++ internal/postgres/tag.go | 440 ++ internal/postgres/tag_test.go | 1452 +++++++ internal/postgres/user.go | 398 ++ internal/postgres/user_test.go | 1370 ++++++ internal/utils/slices.go | 11 + internal/utils/slices_test.go | 60 + pkg/database/database.go | 30 + .../migrations/001_inital_database.sql | 109 + pkg/database/post.go | 31 + pkg/database/postgres.go | 261 ++ pkg/database/postgres_test.go | 3673 +++++++++++++++++ pkg/database/source.go | 19 + pkg/database/tag.go | 20 + pkg/database/tagalias.go | 19 + pkg/database/taggroup.go | 19 + pkg/database/user.go | 42 + pkg/error/database.go | 25 + pkg/error/database_test.go | 83 + pkg/error/validation.go | 19 + pkg/error/validation_test.go | 30 + pkg/models/api.go | 5 + pkg/models/config.go | 12 + pkg/models/const.go | 49 + pkg/models/const_test.go | 59 + pkg/models/orm.go | 34 + pkg/models/orm_test.go | 56 + pkg/models/post.go | 14 + pkg/models/postReference.go | 19 + pkg/models/postReference_test.go | 44 + pkg/models/post_test.go | 38 + pkg/models/source.go | 15 + pkg/models/source_test.go | 40 + pkg/models/tag.go | 39 + pkg/models/tag_test.go | 96 + pkg/models/user.go | 12 + pkg/models/userFavorite.go | 13 + pkg/models/userFavorite_test.go | 37 + pkg/models/userSource.go | 20 + pkg/models/userSource_test.go | 42 + pkg/models/user_test.go | 34 + test/helper.go | 123 + 52 files changed, 10977 insertions(+) create mode 100644 .gitea/workflows/build_check.yaml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/postgres/post.go create mode 100644 internal/postgres/post_test.go create mode 100644 internal/postgres/relationships.go create mode 100644 internal/postgres/relationships_test.go create mode 100644 internal/postgres/source.go create mode 100644 internal/postgres/source_test.go create mode 100644 internal/postgres/tag.go create mode 100644 internal/postgres/tag_test.go create mode 100644 internal/postgres/user.go create mode 100644 internal/postgres/user_test.go create mode 100644 internal/utils/slices.go create mode 100644 internal/utils/slices_test.go create mode 100644 pkg/database/database.go create mode 100644 pkg/database/migrations/001_inital_database.sql create mode 100644 pkg/database/post.go create mode 100644 pkg/database/postgres.go create mode 100644 pkg/database/postgres_test.go create mode 100644 pkg/database/source.go create mode 100644 pkg/database/tag.go create mode 100644 pkg/database/tagalias.go create mode 100644 pkg/database/taggroup.go create mode 100644 pkg/database/user.go create mode 100644 pkg/error/database.go create mode 100644 pkg/error/database_test.go create mode 100644 pkg/error/validation.go create mode 100644 pkg/error/validation_test.go create mode 100644 pkg/models/api.go create mode 100644 pkg/models/config.go create mode 100644 pkg/models/const.go create mode 100644 pkg/models/const_test.go create mode 100644 pkg/models/orm.go create mode 100644 pkg/models/orm_test.go create mode 100644 pkg/models/post.go create mode 100644 pkg/models/postReference.go create mode 100644 pkg/models/postReference_test.go create mode 100644 pkg/models/post_test.go create mode 100644 pkg/models/source.go create mode 100644 pkg/models/source_test.go create mode 100644 pkg/models/tag.go create mode 100644 pkg/models/tag_test.go create mode 100644 pkg/models/user.go create mode 100644 pkg/models/userFavorite.go create mode 100644 pkg/models/userFavorite_test.go create mode 100644 pkg/models/userSource.go create mode 100644 pkg/models/userSource_test.go create mode 100644 pkg/models/user_test.go create mode 100644 test/helper.go diff --git a/.gitea/workflows/build_check.yaml b/.gitea/workflows/build_check.yaml new file mode 100644 index 0000000..7120eae --- /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: + - main + 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/.gitignore b/.gitignore new file mode 100644 index 0000000..9adc1a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,195 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig +# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,goland,go +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,goland,go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### GoLand ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,goland,go + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + +.idea/* +/.run/* + + +.env +main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0828e0 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +![Build Check Runner](https://git.dragse.it/anthrove/otter-space-sdk/v2/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. + +## Installation + +To install the OtterSpace SDK, you can use `go get`: + +```shell +go get git.dragse.it/anthrove/otter-space-sdk/v2 +```` +## Usage + +Here's a simple usage example: + +```go +package main + +import ( + "context" + "fmt" + + "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/database" + "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models" +) + +func main() { + 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... +} +``` + +This example creates a new client, connects to the OtterSpace API, and then the client can be used to interact with the API. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a4ebeeb --- /dev/null +++ b/go.mod @@ -0,0 +1,73 @@ +module git.dragse.it/anthrove/otter-space-sdk/v2 + +go 1.22.0 + +require ( + 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.9 + gorm.io/gorm v1.25.10 +) + +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 new file mode 100644 index 0000000..3b44b4c --- /dev/null +++ b/go.sum @@ -0,0 +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/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/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= +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/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.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= +gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +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/postgres/post.go b/internal/postgres/post.go new file mode 100644 index 0000000..d22d286 --- /dev/null +++ b/internal/postgres/post.go @@ -0,0 +1,131 @@ +package postgres + +import ( + "context" + "errors" + + otterError "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/error" + + "git.dragse.it/anthrove/otter-space-sdk/v2/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 CreatePostInBatch(ctx context.Context, db *gorm.DB, anthrovePost []models.Post, batchSize int) error { + if anthrovePost == nil { + return &otterError.EntityValidationFailed{Reason: "anthrovePost cannot be nil"} + } + + if len(anthrovePost) == 0 { + return &otterError.EntityValidationFailed{Reason: "anthrovePost cannot be empty"} + } + + if batchSize == 0 { + return &otterError.EntityValidationFailed{Reason: "batch size cannot be zero"} + } + + result := db.WithContext(ctx).CreateInBatches(anthrovePost, batchSize) + 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_size": len(anthrovePost), + "batch_size": batchSize, + }).Trace("database: created tag node") + + 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..1886de6 --- /dev/null +++ b/internal/postgres/post_test.go @@ -0,0 +1,468 @@ +package postgres + +import ( + "context" + "fmt" + _ "github.com/lib/pq" + "testing" + + "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/v2/test" + "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 TestCreatePostInBatch(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 + + validPosts := []models.Post{ + { + Rating: models.SFW, + }, + { + Rating: models.NSFW, + }, + { + Rating: models.Questionable, + }, + } + + emptyPost := []models.Post{} + + // Test + type args struct { + ctx context.Context + db *gorm.DB + anthrovePost []models.Post + batchSize int + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + db: gormDB, + anthrovePost: validPosts, + batchSize: len(validPosts), + }, + wantErr: false, + }, + { + name: "Test 2: Emtpy Data", + args: args{ + ctx: ctx, + db: gormDB, + anthrovePost: emptyPost, + batchSize: 0, + }, + wantErr: true, + }, + { + name: "Test 3: Nil Data", + args: args{ + ctx: ctx, + db: gormDB, + anthrovePost: nil, + batchSize: 0, + }, + wantErr: true, + }, + { + name: "Test 4: batchSize 0", + args: args{ + ctx: ctx, + db: gormDB, + anthrovePost: validPosts, + batchSize: 0, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreatePostInBatch(tt.args.ctx, tt.args.db, tt.args.anthrovePost, tt.args.batchSize); (err != nil) != tt.wantErr { + t.Errorf("CreatePostInBatch() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +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..86ab670 --- /dev/null +++ b/internal/postgres/relationships.go @@ -0,0 +1,126 @@ +package postgres + +import ( + "context" + "errors" + + otterError "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/error" + "git.dragse.it/anthrove/otter-space-sdk/v2/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..456ce69 --- /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/v2/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/v2/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..ab3ca52 --- /dev/null +++ b/internal/postgres/source.go @@ -0,0 +1,85 @@ +package postgres + +import ( + "context" + "errors" + + otterError "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/error" + "git.dragse.it/anthrove/otter-space-sdk/v2/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..38a60d9 --- /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/v2/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/v2/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..0270085 --- /dev/null +++ b/internal/postgres/tag.go @@ -0,0 +1,440 @@ +package postgres + +import ( + "context" + "errors" + + "gorm.io/gorm/clause" + + otterError "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/error" + "git.dragse.it/anthrove/otter-space-sdk/v2/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 CreateTagInBatchAndUpdate(ctx context.Context, db *gorm.DB, tags []models.Tag, batchSize int) error { + if len(tags) == 0 { + return &otterError.EntityValidationFailed{Reason: "tags cannot be empty"} + } + + if tags == nil { + return &otterError.EntityValidationFailed{Reason: "tags cannot be nil"} + } + + if batchSize == 0 { + return &otterError.EntityValidationFailed{Reason: "batch size cannot be zero"} + } + + result := db.WithContext(ctx). + Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "name"}}, + DoUpdates: clause.AssignmentColumns([]string{"tag_type"}), + }).CreateInBatches(tags, batchSize) + 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_size": len(tags), + "batch_size": batchSize, + }).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).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "name"}}, + DoNothing: true, + }).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 CreateTagAliasInBatch(ctx context.Context, db *gorm.DB, tagAliases []models.TagAlias, batchSize int) error { + if len(tagAliases) == 0 { + return &otterError.EntityValidationFailed{Reason: "tagAliases cannot be empty"} + } + + if tagAliases == nil { + return &otterError.EntityValidationFailed{Reason: "tagAliases cannot be nil"} + } + + if batchSize == 0 { + return &otterError.EntityValidationFailed{Reason: "batch size cannot be zero"} + } + + result := db.WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "name"}}, + DoNothing: true, + }).CreateInBatches(tagAliases, batchSize) + 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_size": len(tagAliases), + "batch_size": batchSize, + }).Trace("database: created tag node") + + 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 CreateTagGroupInBatch(ctx context.Context, db *gorm.DB, tagGroups []models.TagGroup, batchSize int) error { + if len(tagGroups) == 0 { + return &otterError.EntityValidationFailed{Reason: "tagAliases cannot be empty"} + } + + if tagGroups == nil { + return &otterError.EntityValidationFailed{Reason: "tagAliases cannot be nil"} + } + + if batchSize == 0 { + return &otterError.EntityValidationFailed{Reason: "batch size cannot be zero"} + } + + result := db.WithContext(ctx).CreateInBatches(tagGroups, batchSize) + 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_size": len(tagGroups), + "batch_size": batchSize, + }).Trace("database: created tag node") + + 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..21cd455 --- /dev/null +++ b/internal/postgres/tag_test.go @@ -0,0 +1,1452 @@ +package postgres + +import ( + "context" + "fmt" + "reflect" + "testing" + + "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/v2/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: false, + }, + { + 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: nil, + wantErr: true, + }, + { + 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) + } + }) + } +} + +func TestCreateTagInBatchAndUpdate(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: models.Artist, + }, + { + Name: "SoXX", + Type: models.Character, + }, + { + Name: "Dragon", + Type: models.Species, + }, + { + Name: "Fennec", + Type: models.Species, + }, + } + emptyTags := []models.Tag{} + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tags []models.Tag + batchSize int + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Tags", + args: args{ + ctx: ctx, + db: gormDB, + tags: tags, + batchSize: 10, + }, + wantErr: false, + }, + { + name: "Test 2: Empty Tags", + args: args{ + ctx: ctx, + db: gormDB, + tags: emptyTags, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 3: Nil Tags", + args: args{ + ctx: ctx, + db: gormDB, + tags: nil, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 4: No batchSize", + args: args{ + ctx: ctx, + db: gormDB, + tags: nil, + batchSize: 0, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateTagInBatchAndUpdate(tt.args.ctx, tt.args.db, tt.args.tags, tt.args.batchSize); (err != nil) != tt.wantErr { + t.Errorf("CreateTagInBatchAndUpdate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCreateTagAliasInBatch(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: models.Artist, + }, + { + Name: "SoXX", + Type: models.Character, + }, + { + Name: "Dragon", + Type: models.Species, + }, + { + Name: "Fennec", + Type: models.Species, + }, + } + err = CreateTagInBatchAndUpdate(ctx, gormDB, tags, len(tags)) + if err != nil { + t.Fatal(err) + } + + tagAlias := []models.TagAlias{ + { + Name: "test1", + TagID: tags[0].Name, + }, + { + Name: "test2", + TagID: tags[1].Name, + }, + { + Name: "test3", + TagID: tags[2].Name, + }, + { + Name: "test4", + TagID: tags[3].Name, + }, + } + emptyTagAlias := []models.TagAlias{} + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tagAliases []models.TagAlias + batchSize int + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Tags", + args: args{ + ctx: ctx, + db: gormDB, + tagAliases: tagAlias, + batchSize: 10, + }, + wantErr: false, + }, + { + name: "Test 2: Empty Tags", + args: args{ + ctx: ctx, + db: gormDB, + tagAliases: emptyTagAlias, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 3: Nil Tags", + args: args{ + ctx: ctx, + db: gormDB, + tagAliases: nil, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 4: No batchSize", + args: args{ + ctx: ctx, + db: gormDB, + tagAliases: tagAlias, + batchSize: 0, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateTagAliasInBatch(tt.args.ctx, tt.args.db, tt.args.tagAliases, tt.args.batchSize); (err != nil) != tt.wantErr { + t.Errorf("CreateTagAliasInBatch() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCreateTagGroupInBatch(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: models.Artist, + }, + { + Name: "SoXX", + Type: models.Character, + }, + { + Name: "Dragon", + Type: models.Species, + }, + { + Name: "Fennec", + Type: models.Species, + }, + } + err = CreateTagInBatchAndUpdate(ctx, gormDB, tags, len(tags)) + if err != nil { + t.Fatal(err) + } + + tagGroup := []models.TagGroup{ + { + Name: "test1", + TagID: tags[0].Name, + }, + { + Name: "test2", + TagID: tags[1].Name, + }, + { + Name: "test3", + TagID: tags[2].Name, + }, + { + Name: "test4", + TagID: tags[3].Name, + }, + } + emptyTagGroup := []models.TagGroup{} + + // Test + type args struct { + ctx context.Context + db *gorm.DB + tagGroups []models.TagGroup + batchSize int + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Tags", + args: args{ + ctx: ctx, + db: gormDB, + tagGroups: tagGroup, + batchSize: 10, + }, + wantErr: false, + }, + { + name: "Test 2: Empty Tags", + args: args{ + ctx: ctx, + db: gormDB, + tagGroups: emptyTagGroup, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 3: Nil Tags", + args: args{ + ctx: ctx, + db: gormDB, + tagGroups: nil, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 4: No batchSize", + args: args{ + ctx: ctx, + db: gormDB, + tagGroups: tagGroup, + batchSize: 0, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateTagGroupInBatch(tt.args.ctx, tt.args.db, tt.args.tagGroups, tt.args.batchSize); (err != nil) != tt.wantErr { + t.Errorf("CreateTagGroupInBatch() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/postgres/user.go b/internal/postgres/user.go new file mode 100644 index 0000000..8445086 --- /dev/null +++ b/internal/postgres/user.go @@ -0,0 +1,398 @@ +package postgres + +import ( + "context" + "errors" + "time" + + otterError "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/error" + "git.dragse.it/anthrove/otter-space-sdk/v2/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 +} + +// TODO: FIX THE TEST +func GetUserFavoriteWithPagination(ctx context.Context, db *gorm.DB, anthroveUserID models.AnthroveUserID, skip int, limit int) (*models.FavoriteList, error) { + var favoritePosts []models.Post + + if anthroveUserID == "" { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDIsEmpty} + } + + if len(anthroveUserID) != 25 { + return nil, &otterError.EntityValidationFailed{Reason: otterError.AnthroveUserIDToShort} + } + + db.WithContext(ctx).Joins("RIGHT JOIN \"UserFavorites\" AS of ON \"Post\".id = of.post_id AND of.user_id = ?", anthroveUserID).Preload("References").Offset(skip).Limit(limit).Find(&favoritePosts) + + 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..006dc9c --- /dev/null +++ b/internal/postgres/user_test.go @@ -0,0 +1,1370 @@ +package postgres + +import ( + "context" + "fmt" + "reflect" + "testing" + "time" + + "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/v2/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 !checkFavoritePosts(got, tt.want) { + t.Errorf("GetAllUserFavoritesWithPagination() got = %v, want %v", got, tt.want) + } + }) + } +} + +func checkFavoritePosts(got *models.FavoriteList, want *models.FavoriteList) bool { + if got == nil && want == nil { + return true + } else if got == nil || want == nil { + return false + } + + for i, post := range got.Posts { + if post.ID == want.Posts[i].ID { + } else { + return false + } + } + + return true +} + +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/utils/slices.go b/internal/utils/slices.go new file mode 100644 index 0000000..dfb5aed --- /dev/null +++ b/internal/utils/slices.go @@ -0,0 +1,11 @@ +package utils + +func GetOrDefault(data map[string]any, key string, defaultVal any) any { + val, ok := data[key] + + if !ok { + return defaultVal + } + + return val +} 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..8fbd8da --- /dev/null +++ b/pkg/database/database.go @@ -0,0 +1,30 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/v2/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..5336c8a --- /dev/null +++ b/pkg/database/post.go @@ -0,0 +1,31 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models" +) + +type Post interface { + + // CreatePost adds a new post to the database. + CreatePost(ctx context.Context, anthrovePost *models.Post) error + + // TODO: Everything + CreatePostInBatch(ctx context.Context, anthrovePost []models.Post, batchSize int) 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..039e8e3 --- /dev/null +++ b/pkg/database/postgres.go @@ -0,0 +1,261 @@ +package database + +import ( + "context" + "embed" + "fmt" + log2 "log" + "os" + "time" + + "git.dragse.it/anthrove/otter-space-sdk/v2/internal/postgres" + "git.dragse.it/anthrove/otter-space-sdk/v2/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) CreatePostInBatch(ctx context.Context, anthrovePost []models.Post, batchSize int) error { + return postgres.CreatePostInBatch(ctx, p.db, anthrovePost, batchSize) +} + +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) +} + +func (p *postgresqlConnection) CreateTagInBatchAndUpdate(ctx context.Context, tags []models.Tag, batchSize int) error { + return postgres.CreateTagInBatchAndUpdate(ctx, p.db, tags, batchSize) +} + +func (p *postgresqlConnection) CreateTagAliasInBatch(ctx context.Context, tagAliases []models.TagAlias, batchSize int) error { + return postgres.CreateTagAliasInBatch(ctx, p.db, tagAliases, batchSize) +} + +func (p *postgresqlConnection) CreateTagGroupInBatch(ctx context.Context, tagGroups []models.TagGroup, batchSize int) error { + return postgres.CreateTagGroupInBatch(ctx, p.db, tagGroups, batchSize) + +} + +// 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..9de1078 --- /dev/null +++ b/pkg/database/postgres_test.go @@ -0,0 +1,3673 @@ +package database + +import ( + "context" + "fmt" + "reflect" + "testing" + "time" + + "git.dragse.it/anthrove/otter-space-sdk/v2/internal/postgres" + "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models" + "git.dragse.it/anthrove/otter-space-sdk/v2/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 !checkFavoritePosts(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 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_CreateTagAliasInBatch(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: models.Artist, + }, + { + Name: "SoXX", + Type: models.Character, + }, + { + Name: "Dragon", + Type: models.Species, + }, + { + Name: "Fennec", + Type: models.Species, + }, + } + err = postgres.CreateTagInBatchAndUpdate(ctx, gormDB, tags, len(tags)) + if err != nil { + t.Fatal(err) + } + + tagAlias := []models.TagAlias{ + { + Name: "test1", + TagID: tags[0].Name, + }, + { + Name: "test2", + TagID: tags[1].Name, + }, + { + Name: "test3", + TagID: tags[2].Name, + }, + { + Name: "test4", + TagID: tags[3].Name, + }, + } + emptyTagAlias := []models.TagAlias{} + + // Test + type args struct { + ctx context.Context + tagAliases []models.TagAlias + batchSize int + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Tags", + args: args{ + ctx: ctx, + tagAliases: tagAlias, + batchSize: 10, + }, + wantErr: false, + }, + { + name: "Test 2: Empty Tags", + args: args{ + ctx: ctx, + tagAliases: emptyTagAlias, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 3: Nil Tags", + args: args{ + ctx: ctx, + tagAliases: nil, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 4: No batchSize", + args: args{ + ctx: ctx, + tagAliases: tagAlias, + batchSize: 0, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreateTagAliasInBatch(tt.args.ctx, tt.args.tagAliases, tt.args.batchSize); (err != nil) != tt.wantErr { + t.Errorf("CreateTagAliasInBatch() 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_CreateTagGroupInBatch(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: models.Artist, + }, + { + Name: "SoXX", + Type: models.Character, + }, + { + Name: "Dragon", + Type: models.Species, + }, + { + Name: "Fennec", + Type: models.Species, + }, + } + err = postgres.CreateTagInBatchAndUpdate(ctx, gormDB, tags, len(tags)) + if err != nil { + t.Fatal(err) + } + + tagGroup := []models.TagGroup{ + { + Name: "test1", + TagID: tags[0].Name, + }, + { + Name: "test2", + TagID: tags[1].Name, + }, + { + Name: "test3", + TagID: tags[2].Name, + }, + { + Name: "test4", + TagID: tags[3].Name, + }, + } + emptyTagGroup := []models.TagGroup{} + + // Test + type args struct { + ctx context.Context + tagGroups []models.TagGroup + batchSize int + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Tags", + args: args{ + ctx: ctx, + tagGroups: tagGroup, + batchSize: 10, + }, + wantErr: false, + }, + { + name: "Test 2: Empty Tags", + args: args{ + ctx: ctx, + tagGroups: emptyTagGroup, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 3: Nil Tags", + args: args{ + ctx: ctx, + tagGroups: nil, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 4: No batchSize", + args: args{ + ctx: ctx, + tagGroups: tagGroup, + batchSize: 0, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreateTagGroupInBatch(tt.args.ctx, tt.args.tagGroups, tt.args.batchSize); (err != nil) != tt.wantErr { + t.Errorf("CreateTagGroupInBatch() 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: nil, + wantErr: true, + }, + { + 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) + } + }) + } +} + +func Test_postgresqlConnection_CreateTagInBatchAndUpdate(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: models.Artist, + }, + { + Name: "SoXX", + Type: models.Character, + }, + { + Name: "Dragon", + Type: models.Species, + }, + { + Name: "Fennec", + Type: models.Species, + }, + } + emptyTags := []models.Tag{} + + // Test + type args struct { + ctx context.Context + tags []models.Tag + batchSize int + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Tags", + args: args{ + ctx: ctx, + tags: tags, + batchSize: 10, + }, + wantErr: false, + }, + { + name: "Test 2: Empty Tags", + args: args{ + ctx: ctx, + tags: emptyTags, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 3: Nil Tags", + args: args{ + ctx: ctx, + tags: nil, + batchSize: 10, + }, + wantErr: true, + }, + { + name: "Test 4: No batchSize", + args: args{ + ctx: ctx, + tags: nil, + batchSize: 0, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreateTagInBatchAndUpdate(tt.args.ctx, tt.args.tags, tt.args.batchSize); (err != nil) != tt.wantErr { + t.Errorf("CreateTagInBatchAndUpdate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_postgresqlConnection_CreatePostInBatch(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 + + validPosts := []models.Post{ + { + Rating: models.SFW, + }, + { + Rating: models.NSFW, + }, + { + Rating: models.Questionable, + }, + } + + emptyPost := []models.Post{} + + // Test + type args struct { + ctx context.Context + anthrovePost []models.Post + batchSize int + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test 1: Valid Data", + args: args{ + ctx: ctx, + anthrovePost: validPosts, + batchSize: len(validPosts), + }, + wantErr: false, + }, + { + name: "Test 2: Emtpy Data", + args: args{ + ctx: ctx, + anthrovePost: emptyPost, + batchSize: 0, + }, + wantErr: true, + }, + { + name: "Test 3: Nil Data", + args: args{ + ctx: ctx, + anthrovePost: nil, + batchSize: 0, + }, + wantErr: true, + }, + { + name: "Test 4: batchSize 0", + args: args{ + ctx: ctx, + anthrovePost: validPosts, + batchSize: 0, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &postgresqlConnection{ + db: gormDB, + debug: true, + } + if err := p.CreatePostInBatch(tt.args.ctx, tt.args.anthrovePost, tt.args.batchSize); (err != nil) != tt.wantErr { + t.Errorf("CreatePostInBatch() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func checkFavoritePosts(got *models.FavoriteList, want *models.FavoriteList) bool { + if got == nil && want == nil { + return true + } else if got == nil || want == nil { + return false + } + + for i, post := range got.Posts { + if post.ID == want.Posts[i].ID { + } else { + return false + } + } + + return true +} diff --git a/pkg/database/source.go b/pkg/database/source.go new file mode 100644 index 0000000..6c431af --- /dev/null +++ b/pkg/database/source.go @@ -0,0 +1,19 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/v2/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..b9b3add --- /dev/null +++ b/pkg/database/tag.go @@ -0,0 +1,20 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models" +) + +type Tag interface { + CreateTag(ctx context.Context, tagName models.AnthroveTagName, tagType models.TagType) error + + CreateTagInBatchAndUpdate(ctx context.Context, tags []models.Tag, batchSize int) 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..aeaca57 --- /dev/null +++ b/pkg/database/tagalias.go @@ -0,0 +1,19 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models" +) + +type TagAlias interface { + CreateTagAlias(ctx context.Context, tagAliasName models.AnthroveTagAliasName, tagID models.AnthroveTagID) error + + CreateTagAliasInBatch(ctx context.Context, tagsAliases []models.TagAlias, batchSize int) 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..baef4a1 --- /dev/null +++ b/pkg/database/taggroup.go @@ -0,0 +1,19 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/v2/pkg/models" +) + +type TagGroup interface { + CreateTagGroup(ctx context.Context, tagGroupName models.AnthroveTagGroupName, tagID models.AnthroveTagID) error + + CreateTagGroupInBatch(ctx context.Context, tagsGroups []models.TagGroup, batchSize int) 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..b8936bb --- /dev/null +++ b/pkg/database/user.go @@ -0,0 +1,42 @@ +package database + +import ( + "context" + + "git.dragse.it/anthrove/otter-space-sdk/v2/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/models/api.go b/pkg/models/api.go new file mode 100644 index 0000000..37adbd0 --- /dev/null +++ b/pkg/models/api.go @@ -0,0 +1,5 @@ +package models + +type FavoriteList struct { + 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 new file mode 100644 index 0000000..7e84b0c --- /dev/null +++ b/pkg/models/const.go @@ -0,0 +1,49 @@ +package models + +import "time" + +type AnthroveUserID string +type AnthrovePostID 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 Rating = "safe" + NSFW Rating = "explicit" + Questionable Rating = "questionable" + Unknown Rating = "unknown" +) + +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 + case "q": + *r = Questionable + case "s": + *r = SFW + 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..f9209d9 --- /dev/null +++ b/pkg/models/orm.go @@ -0,0 +1,34 @@ +package models + +import ( + "time" + + gonanoid "github.com/matoous/go-nanoid/v2" + "gorm.io/gorm" +) + +type ID interface { + AnthroveUserID | AnthroveSourceID | AnthrovePostID +} + +type BaseModel[T ID] struct { + ID T `json:"id" gorm:"primaryKey"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt gorm.DeletedAt `json:"-" 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 new file mode 100644 index 0000000..d272387 --- /dev/null +++ b/pkg/models/post.go @@ -0,0 +1,14 @@ +package models + +// Post model +type Post struct { + BaseModel[AnthrovePostID] + Rating Rating `json:"rating" gorm:"type:enum('safe','questionable','explicit')"` + Tags []Tag `json:"-" gorm:"many2many:post_tags;"` + Favorites []UserFavorites `json:"-" gorm:"foreignKey:PostID"` + References []PostReference `json:"references" 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..615244a --- /dev/null +++ b/pkg/models/postReference.go @@ -0,0 +1,19 @@ +package models + +type PostReference struct { + PostID string `json:"post_id" gorm:"primaryKey"` + SourceID string `json:"source_id" gorm:"primaryKey"` + URL string `json:"url" gorm:"primaryKey"` + PostReferenceConfig +} + +type PostReferenceConfig struct { + SourcePostID string `json:"source_post_id"` + FullFileURL string `json:"full_file_url"` + PreviewFileURL string `json:"preview_file_url"` + SampleFileURL string `json:"sample_file_url"` +} + +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/source.go b/pkg/models/source.go new file mode 100644 index 0000000..9027b55 --- /dev/null +++ b/pkg/models/source.go @@ -0,0 +1,15 @@ +package models + +// Source model +type Source struct { + BaseModel[AnthroveSourceID] + DisplayName string `json:"display_name" ` + Domain string `json:"domain" gorm:"not null;unique"` + Icon string `json:"icon" gorm:"not null"` + UserSources []UserSource `json:"-" gorm:"foreignKey:SourceID"` + References []PostReference `json:"references" 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 new file mode 100644 index 0000000..09fafb9 --- /dev/null +++ b/pkg/models/tag.go @@ -0,0 +1,39 @@ +package models + +// Tag models +type Tag struct { + Name string `json:"name" gorm:"primaryKey"` + Type TagType `json:"type" gorm:"column:tag_type"` + Aliases []TagAlias `json:"aliases" gorm:"foreignKey:TagID"` + Groups []TagGroup `json:"groups" gorm:"foreignKey:TagID"` + Posts []Post `json:"posts" gorm:"many2many:post_tags;"` +} + +func (Tag) TableName() string { + return "Tag" +} + +// TagAlias model +type TagAlias struct { + Name string `json:"name" gorm:"primaryKey"` + TagID string `json:"tag_id"` +} + +func (TagAlias) TableName() string { + return "TagAlias" +} + +// TagGroup model +type TagGroup struct { + Name string `json:"name" gorm:"primaryKey"` + TagID string `json:"tag_id"` +} + +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 new file mode 100644 index 0000000..bc2d575 --- /dev/null +++ b/pkg/models/user.go @@ -0,0 +1,12 @@ +package models + +// User model +type User struct { + BaseModel[AnthroveUserID] + Favorites []UserFavorites `json:"-" gorm:"foreignKey:UserID"` + Sources []UserSource `json:"-" 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..182af8f --- /dev/null +++ b/pkg/models/userFavorite.go @@ -0,0 +1,13 @@ +package models + +import "time" + +type UserFavorites struct { + UserID string `json:"user_id" gorm:"primaryKey"` + PostID string `json:"post_id" gorm:"primaryKey"` + CreatedAt time.Time `json:"-"` +} + +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..9ee0f87 --- /dev/null +++ b/pkg/models/userSource.go @@ -0,0 +1,20 @@ +package models + +import "time" + +type UserSource struct { + User User `json:"user" gorm:"foreignKey:ID;references:UserID"` + UserID string `json:"user_id" gorm:"primaryKey"` + Source Source `json:"source" gorm:"foreignKey:ID;references:SourceID"` + SourceID string `json:"source_id" gorm:"primaryKey"` + ScrapeTimeInterval string `json:"scrape_time_interval"` + AccountUsername string `json:"account_username"` + AccountID string `json:"account_id"` + LastScrapeTime time.Time `json:"last_scrape_time"` + AccountValidate bool `json:"account_validate"` + AccountValidationKey string `json:"-"` +} + +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..6c8ac2b --- /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/v2/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 +}