Initial commit
This commit is contained in:
commit
cb6c40728b
|
@ -0,0 +1,16 @@
|
||||||
|
# Ignore everything and explicitly include the things we need
|
||||||
|
*
|
||||||
|
.*
|
||||||
|
|
||||||
|
!cmd
|
||||||
|
!config
|
||||||
|
!db
|
||||||
|
!handler
|
||||||
|
!httpserver
|
||||||
|
!lib
|
||||||
|
!mocks
|
||||||
|
!models
|
||||||
|
|
||||||
|
!go.mod
|
||||||
|
!go.sum
|
||||||
|
!Makefile
|
|
@ -0,0 +1,31 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
debug
|
||||||
|
# editor temp files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.orig
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# Build artifacts and cache
|
||||||
|
**/bin
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Visual studio code configuration
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# secrets for local use that cannot be encrypted
|
||||||
|
k8s/local/patches/core-http-config.yaml
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- goimports
|
||||||
|
- stylecheck
|
||||||
|
- lll
|
||||||
|
disable:
|
||||||
|
- errcheck
|
||||||
|
|
||||||
|
run:
|
||||||
|
go: '1.17'
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
- linters:
|
||||||
|
- lll
|
||||||
|
source: "// nolint:lll"
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
extends: default
|
||||||
|
# files managed by flux won't pass linting
|
||||||
|
ignore: |
|
||||||
|
k8s/*/patches/deployment*
|
||||||
|
k8s/*/secrets/*
|
||||||
|
rules:
|
||||||
|
line-length: disable
|
|
@ -0,0 +1,20 @@
|
||||||
|
FROM golang:1.19-bullseye AS builder
|
||||||
|
RUN apt update && apt install -y make
|
||||||
|
|
||||||
|
# first copy just enough to pull all dependencies, to cache this layer
|
||||||
|
COPY go.mod go.sum Makefile /app/
|
||||||
|
WORKDIR /app/
|
||||||
|
RUN make setup
|
||||||
|
|
||||||
|
# lint, build, etc..
|
||||||
|
COPY . /app/
|
||||||
|
RUN make build
|
||||||
|
|
||||||
|
FROM debian:bullseye-slim
|
||||||
|
RUN apt update \
|
||||||
|
&& apt install -y ca-certificates \
|
||||||
|
&& apt clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist/ /app/
|
||||||
|
ENTRYPOINT ["/app/ytrssil-api"]
|
|
@ -0,0 +1,40 @@
|
||||||
|
.PHONY: all setup ytrssil-api build lint k8s-lint yamllint test test-initdb image-build
|
||||||
|
|
||||||
|
DB_URI ?= postgres://ytrssil:ytrssil@localhost:5431/ytrssil?sslmode=disable
|
||||||
|
|
||||||
|
all: lint test build
|
||||||
|
|
||||||
|
setup: bin/golangci-lint
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
ytrssil-api:
|
||||||
|
go build -o dist/ytrssil-api cmd/main.go
|
||||||
|
|
||||||
|
build: ytrssil-api
|
||||||
|
|
||||||
|
bin/moq:
|
||||||
|
GOBIN=$(PWD)/bin go install github.com/matryer/moq@v0.2.7
|
||||||
|
|
||||||
|
gen-mocks: bin/moq
|
||||||
|
./bin/moq -pkg db_mock -out ./mocks/db/db.go ./db DB
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
bin/golangci-lint:
|
||||||
|
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.48.0
|
||||||
|
|
||||||
|
lint: bin/golangci-lint
|
||||||
|
go fmt ./...
|
||||||
|
go vet ./...
|
||||||
|
bin/golangci-lint -c .golangci.yml run ./...
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
test:
|
||||||
|
go mod tidy
|
||||||
|
go test -timeout=10s -race -benchmem ./...
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
migrate -database "$(DB_URI)" -path migrations up
|
||||||
|
|
||||||
|
image-build:
|
||||||
|
@echo "# Building docker image..."
|
||||||
|
docker build -t ytrssil-api -f Dockerfile .
|
|
@ -0,0 +1,97 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/config"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/db"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/handler"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/auth"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/ytrssil"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// always use UTC
|
||||||
|
time.Local = time.UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log := log.NewLogger()
|
||||||
|
|
||||||
|
config, err := config.Parse()
|
||||||
|
if err != nil {
|
||||||
|
log.Log("level", "FATAL", "call", "config.Parse", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := db.NewPSQLDB(log, config.DB)
|
||||||
|
if err != nil {
|
||||||
|
log.Log("level", "FATAL", "call", "db.NewPSQLDB", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := handler.New(log, db)
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
router, err := ytrssil.SetupGinRouter(
|
||||||
|
log,
|
||||||
|
handler,
|
||||||
|
auth.AuthMiddleware(db),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Log("level", "FATAL", "call", "ytrssil.SetupGinServer", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%v", config.Gin.Port),
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
server.RegisterOnShutdown(func() {
|
||||||
|
log.Log("level", "INFO", "msg", "shutdown server.Close()")
|
||||||
|
})
|
||||||
|
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
// handle Interrupt (ctrl-c) Term, used by `kill` et al, HUP which is commonly used to reload configs
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s := <-quit
|
||||||
|
log.Log(
|
||||||
|
"level", "INFO",
|
||||||
|
"msg", "signalRecv, for quitting",
|
||||||
|
"signal", s,
|
||||||
|
)
|
||||||
|
if err := server.Shutdown(context.Background()); err != nil {
|
||||||
|
log.Log(
|
||||||
|
"level", "ERROR",
|
||||||
|
"call", "server.Shutdown",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Log(
|
||||||
|
"level", "INFO",
|
||||||
|
"msg", "ytrssil API is starting up",
|
||||||
|
"port", config.Gin.Port,
|
||||||
|
)
|
||||||
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
if err != http.ErrServerClosed {
|
||||||
|
log.Log(
|
||||||
|
"level", "ERROR",
|
||||||
|
"call", "server.ListenAndServe",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Log("level", "INFO", "msg", "exit complete")
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/config"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/db"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/handler"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/auth"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/ytrssil"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testConfig config.Config
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
testConfig = config.TestConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestServer(t *testing.T, authEnabled bool) *http.Server {
|
||||||
|
l := log.NewNopLogger()
|
||||||
|
|
||||||
|
db, err := db.NewPSQLDB(l, testConfig.DB)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := handler.New(l, db)
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router, err := ytrssil.SetupGinRouter(
|
||||||
|
l,
|
||||||
|
handler,
|
||||||
|
auth.AuthMiddleware(db),
|
||||||
|
)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%v", testConfig.Gin.Port),
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHealthz(t *testing.T) {
|
||||||
|
server := setupTestServer(t, false)
|
||||||
|
if !assert.NotNil(t, server) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/healthz", nil)
|
||||||
|
server.Handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.Equal(t, "healthy", w.Body.String())
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
flags "github.com/jessevdk/go-flags"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
DBURI string `long:"db-uri" env:"DB_URI"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gin contains configuration for the gin framework
|
||||||
|
type Gin struct {
|
||||||
|
Port int `long:"port" env:"PORT" default:"8080"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config ties together all configs
|
||||||
|
type Config struct {
|
||||||
|
DB DB
|
||||||
|
Gin Gin
|
||||||
|
}
|
||||||
|
|
||||||
|
func getenvOrDefault(key string, defaultValue string) string {
|
||||||
|
value, found := os.LookupEnv(key)
|
||||||
|
if found {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses all the supplied configurations and returns
|
||||||
|
func Parse() (Config, error) {
|
||||||
|
var config Config
|
||||||
|
parser := flags.NewParser(&config, flags.Default)
|
||||||
|
_, err := parser.Parse()
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfig returns a mostly hardcoded configuration used for running tests
|
||||||
|
func TestConfig() Config {
|
||||||
|
dbURI := getenvOrDefault("DB_URI", "postgres://ytrssil:ytrssil@postgres:5432/ytrssil")
|
||||||
|
|
||||||
|
gin := Gin{
|
||||||
|
Port: 8080,
|
||||||
|
}
|
||||||
|
db := DB{
|
||||||
|
DBURI: dbURI,
|
||||||
|
}
|
||||||
|
config := Config{
|
||||||
|
Gin: gin,
|
||||||
|
DB: db,
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB represents a database layer for getting video and channel data
|
||||||
|
type DB interface {
|
||||||
|
// GetNewVideos returns unwatched videos from all channels
|
||||||
|
GetNewVideos(ctx context.Context, username string) ([]*models.Video, error)
|
||||||
|
AuthenticateUser(ctx context.Context, username string, password string) (bool, error)
|
||||||
|
CreateUser(ctx context.Context, user models.User) error
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
|
||||||
|
ytrssilConfig "gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/config"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type psqlDB struct {
|
||||||
|
l log.Logger
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPSQLDB(log log.Logger, dbCfg ytrssilConfig.DB) (*psqlDB, error) {
|
||||||
|
db, err := sql.Open("postgres", dbCfg.DBURI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &psqlDB{
|
||||||
|
l: log,
|
||||||
|
db: db,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alexedwards/argon2id"
|
||||||
|
"github.com/georgysavva/scany/v2/sqlscan"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var authenticateUserQuery = `SELECT username, password FROM users WHERE username = $1`
|
||||||
|
|
||||||
|
func (d *psqlDB) AuthenticateUser(ctx context.Context, username string, password string) (bool, error) {
|
||||||
|
var user []*models.User
|
||||||
|
err := sqlscan.Select(ctx, d.db, &user, authenticateUserQuery, username)
|
||||||
|
if err != nil {
|
||||||
|
d.l.Log("level", "ERROR", "function", "db.AuthenticateUser", "msg", "failed to query user", "error", err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
match, err := argon2id.ComparePasswordAndHash(password, user[0].Password)
|
||||||
|
if err != nil {
|
||||||
|
d.l.Log("level", "ERROR", "function", "db.AuthenticateUser", "msg", "failed to check hashed passsword", "error", err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return match, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var createUserQuery = `INSERT INTO users (username, password) VALUES ($1, $2)`
|
||||||
|
|
||||||
|
func (d *psqlDB) CreateUser(ctx context.Context, user models.User) error {
|
||||||
|
res, err := d.db.Exec(createUserQuery, user.Username, user.Password)
|
||||||
|
if err != nil {
|
||||||
|
d.l.Log("level", "ERROR", "function", "db.CreateUser", "msg", "failed to create user", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
d.l.Log("level", "ERROR", "function", "db.CreateUser", "msg", "failed to get affected row count", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if affected != 1 {
|
||||||
|
d.l.Log("level", "ERROR", "function", "db.CreateUser", "msg", "failed to get affected row count", "error", err)
|
||||||
|
return fmt.Errorf("expected to insert one row, but %d were inserted", affected)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/georgysavva/scany/v2/sqlscan"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var getNewVideosQuery = `
|
||||||
|
SELECT
|
||||||
|
video_id
|
||||||
|
, title
|
||||||
|
, published_timestamp
|
||||||
|
, watch_timestamp
|
||||||
|
, name as channel_name
|
||||||
|
FROM user_videos
|
||||||
|
LEFT JOIN videos ON video_id=videos.id
|
||||||
|
LEFT JOIN channels ON channel_id=channels.id
|
||||||
|
WHERE
|
||||||
|
1=1
|
||||||
|
AND watch_timestamp IS NULL
|
||||||
|
AND username=$1
|
||||||
|
ORDER BY published_timestamp
|
||||||
|
`
|
||||||
|
|
||||||
|
func (d *psqlDB) GetNewVideos(ctx context.Context, username string) ([]*models.Video, error) {
|
||||||
|
var videos []*models.Video
|
||||||
|
err := sqlscan.Select(ctx, d.db, &videos, getNewVideosQuery, username)
|
||||||
|
if err != nil {
|
||||||
|
d.l.Log("level", "ERROR", "function", "db.GetNewVideos", "msg", "failed to query new videos", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return videos, nil
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:14-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5431:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ytrssil
|
||||||
|
POSTGRES_USER: ytrssil
|
||||||
|
POSTGRES_DB: ytrssil
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready
|
||||||
|
interval: 5s
|
||||||
|
timeout: 2s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
DB_URI: "postgresql://ytrssil:ytrssil@postgres/ytrssil?sslmode=disable"
|
||||||
|
PORT: "80"
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
healthcheck:
|
||||||
|
test: "curl http://localhost:80/healthz"
|
||||||
|
interval: 5s
|
||||||
|
retries: 5
|
||||||
|
timeout: 2s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
|
@ -0,0 +1,40 @@
|
||||||
|
module gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||||
|
github.com/georgysavva/scany/v2 v2.0.0-alpha.3
|
||||||
|
github.com/gin-gonic/gin v1.8.1
|
||||||
|
github.com/go-kit/log v0.2.1
|
||||||
|
github.com/jessevdk/go-flags v1.5.0
|
||||||
|
github.com/lib/pq v1.10.7
|
||||||
|
github.com/stretchr/testify v1.8.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.9.11 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.8 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.0.3 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||||
|
golang.org/x/crypto v0.1.0 // indirect
|
||||||
|
golang.org/x/net v0.1.0 // indirect
|
||||||
|
golang.org/x/sys v0.1.0 // indirect
|
||||||
|
golang.org/x/text v0.4.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.28.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,118 @@
|
||||||
|
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 h1:loy0fjI90vF44BPW4ZYOkE3tDkGTy7yHURusOJimt+I=
|
||||||
|
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387/go.mod h1:GuR5j/NW7AU7tDAQUDGCtpiPxWIOy/c3kiRDnlwiCHc=
|
||||||
|
github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
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/georgysavva/scany/v2 v2.0.0-alpha.3 h1:+n7kJr/xyVQJcsE2ICn0jd/a4QaxaWnx+a+GJQTGRrQ=
|
||||||
|
github.com/georgysavva/scany/v2 v2.0.0-alpha.3/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||||
|
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||||
|
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
|
||||||
|
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||||
|
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||||
|
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||||
|
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||||
|
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
||||||
|
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||||
|
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||||
|
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||||
|
github.com/jackc/pgx/v5 v5.0.3 h1:4flM5ecR/555F0EcnjdaZa6MhBU+nr0QbZIo5vaKjuM=
|
||||||
|
github.com/jackc/pgx/v5 v5.0.3/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o=
|
||||||
|
github.com/jackc/puddle/v2 v2.0.0 h1:Kwk/AlLigcnZsDssc3Zun1dk1tAtQNPaBBxBHWn0Mjc=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
|
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||||
|
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
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/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
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 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||||
|
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||||
|
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||||
|
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||||
|
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||||
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||||
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=
|
|
@ -0,0 +1,39 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/alexedwards/argon2id"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/db"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
CreateUser(ctx context.Context, user models.User) error
|
||||||
|
GetNewVideos(ctx context.Context, username string) ([]*models.Video, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
log log.Logger
|
||||||
|
db db.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(log log.Logger, db db.DB) *handler {
|
||||||
|
return &handler{log: log, db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) CreateUser(ctx context.Context, user models.User) error {
|
||||||
|
hashedPassword, err := argon2id.CreateHash(user.Password, argon2id.DefaultParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.Password = hashedPassword
|
||||||
|
|
||||||
|
return h.db.CreateUser(ctx, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) GetNewVideos(ctx context.Context, username string) ([]*models.Video, error) {
|
||||||
|
return h.db.GetNewVideos(ctx, username)
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/config"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
|
||||||
|
db_mock "gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/mocks/db"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testConfig config.Config
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
testConfig = config.TestConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNewVideos(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
l := log.NewNopLogger()
|
||||||
|
|
||||||
|
handler := New(l, &db_mock.DBMock{
|
||||||
|
GetNewVideosFunc: func(ctx context.Context, username string) ([]*models.Video, error) {
|
||||||
|
return []*models.Video{
|
||||||
|
{
|
||||||
|
VideoID: "test",
|
||||||
|
ChannelName: "test",
|
||||||
|
Title: "test",
|
||||||
|
PublishedTime: time.Now(),
|
||||||
|
WatchTime: nil,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
resp, err := handler.GetNewVideos(context.TODO(), "username")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
if assert.NotNil(t, resp) {
|
||||||
|
assert.Equal(t, resp[0].VideoID, "test")
|
||||||
|
assert.Equal(t, resp[0].Title, "test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthMiddleware will authenticate against a static API key
|
||||||
|
func AuthMiddleware(db db.DB) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
username, password, ok := c.Request.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid basic auth header"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
authenticated, err := db.AuthenticateUser(c.Request.Context(), username, password)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !authenticated {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("username", username)
|
||||||
|
|
||||||
|
// handle request
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
db_mock "gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/mocks/db"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestServer() *http.Server {
|
||||||
|
db := &db_mock.DBMock{
|
||||||
|
AuthenticateUserFunc: func(ctx context.Context, username, password string) (bool, error) {
|
||||||
|
return username == "username" && password == "password", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
// Middlewares are executed top to bottom in a stack-like manner
|
||||||
|
router.Use(
|
||||||
|
gin.Recovery(), // Recovery needs to go before other middlewares to catch panics
|
||||||
|
AuthMiddleware(db),
|
||||||
|
)
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
c.String(http.StatusOK, "OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
return &http.Server{Handler: router}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuccessfulAuthentication(t *testing.T) {
|
||||||
|
server := setupTestServer()
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/", nil)
|
||||||
|
req.Header.Add("Authorization", "Basic dXNlcm5hbWU6cGFzc3dvcmQ=") // username:password
|
||||||
|
server.Handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Equal(t, "OK", w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMissingAuthorizationHeader(t *testing.T) {
|
||||||
|
server := setupTestServer()
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/", nil)
|
||||||
|
server.Handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
|
assert.Equal(t, `{"error":"invalid authorization header"}`, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrongCredentials(t *testing.T) {
|
||||||
|
server := setupTestServer()
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/", nil)
|
||||||
|
req.Header.Add("Authorization", "Basic d3Jvbmc=")
|
||||||
|
server.Handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
|
assert.Equal(t, `{"error":"invalid API Key"}`, w.Body.String())
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package ytrssil_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/config"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/handler"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/auth"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/httpserver/ytrssil"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testConfig config.Config
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// always use UTC
|
||||||
|
time.Local = time.UTC
|
||||||
|
testConfig = config.TestConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestServer(t *testing.T) *http.Server {
|
||||||
|
l := log.NewNopLogger()
|
||||||
|
|
||||||
|
handler := handler.New(l, nil)
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router, err := ytrssil.SetupGinRouter(
|
||||||
|
l,
|
||||||
|
handler,
|
||||||
|
auth.AuthMiddleware(nil),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
return &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%v", testConfig.Gin.Port),
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHealthz(t *testing.T) {
|
||||||
|
server := setupTestServer(t)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/healthz", nil)
|
||||||
|
server.Handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.Equal(t, "healthy", w.Body.String())
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package ytrssil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/handler"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/lib/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
log log.Logger
|
||||||
|
handler handler.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(log log.Logger, handler handler.Handler) (*server, error) {
|
||||||
|
return &server{log: log, handler: handler}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Healthz(c *gin.Context) {
|
||||||
|
c.String(http.StatusOK, "healthy")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupGinRouter sets up routes for all APIs on a Gin server (aka router)
|
||||||
|
func SetupGinRouter(l log.Logger, handler handler.Handler, authMiddleware func(c *gin.Context)) (*gin.Engine, error) {
|
||||||
|
engine := gin.New()
|
||||||
|
// Middlewares are executed top to bottom in a stack-like manner
|
||||||
|
engine.Use(
|
||||||
|
gin.LoggerWithFormatter(log.GinFormatterWithUTCAndBodySize),
|
||||||
|
gin.Recovery(), // Recovery needs to go before other middlewares to catch panics
|
||||||
|
)
|
||||||
|
|
||||||
|
srv, err := NewServer(l, handler)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
engine.GET("/healthz", srv.Healthz)
|
||||||
|
engine.POST("/register", srv.CreateUser)
|
||||||
|
|
||||||
|
// all APIs go in this routing group and require authentication
|
||||||
|
api := engine.Group("/api")
|
||||||
|
api.Use(authMiddleware)
|
||||||
|
{
|
||||||
|
api.GET("videos/new", srv.GetNewVideos)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine, nil
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package ytrssil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/models"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *server) CreateUser(c *gin.Context) {
|
||||||
|
var user models.User
|
||||||
|
err := c.BindJSON(&user)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.handler.CreateUser(c.Request.Context(), user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"msg": "user created"})
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package ytrssil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/models"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *server) GetNewVideos(c *gin.Context) {
|
||||||
|
username := c.MustGet("username").(string)
|
||||||
|
videos, err := s.handler.GetNewVideos(c.Request.Context(), username)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.GetNewVideosResponse{
|
||||||
|
Videos: videos,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
stdlog "log"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-kit/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Log(keyvals ...interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger() Logger {
|
||||||
|
var logger log.Logger
|
||||||
|
logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
|
||||||
|
stdlog.SetOutput(log.NewStdlibAdapter(logger))
|
||||||
|
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
|
||||||
|
// We're wrapping gotkit, so DefaultCaller would show: log.go:23
|
||||||
|
logger = log.With(logger, "caller", log.Caller(3))
|
||||||
|
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNopLogger is go-kit/log.NewNopLogger
|
||||||
|
func NewNopLogger() log.Logger {
|
||||||
|
return log.NewNopLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSyncLogger is useful for debugging, use sparingly
|
||||||
|
func NewSyncLogger() log.Logger {
|
||||||
|
return log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Str is used to log unknown types using fmt
|
||||||
|
func Str(i any) string { return fmt.Sprintf("%+v", i) }
|
||||||
|
|
||||||
|
// GinFormatterWithUTCAndBodySize is the default gin loggger with:
|
||||||
|
// - UTC times
|
||||||
|
// - logs the response size in bytes
|
||||||
|
func GinFormatterWithUTCAndBodySize(param gin.LogFormatterParams) string {
|
||||||
|
var statusColor, methodColor, resetColor string
|
||||||
|
if param.IsOutputColor() {
|
||||||
|
statusColor = param.StatusCodeColor()
|
||||||
|
methodColor = param.MethodColor()
|
||||||
|
resetColor = param.ResetColor()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[DUNE] %v |%s %3d %s| %13v | %6v bytes | %15s |%s %-7s %s %#v\n%s",
|
||||||
|
param.TimeStamp.UTC().Format("2006/01/02 15:04:05.000"),
|
||||||
|
statusColor, param.StatusCode, resetColor,
|
||||||
|
param.Latency,
|
||||||
|
param.BodySize,
|
||||||
|
param.ClientIP,
|
||||||
|
methodColor, param.Method, resetColor,
|
||||||
|
param.Path,
|
||||||
|
param.ErrorMessage,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
DROP TABLE IF EXISTS user_videos;
|
||||||
|
DROP TABLE IF EXISTS users;
|
||||||
|
DROP TABLE IF EXISTS videos;
|
||||||
|
DROP TABLE IF EXISTS channels;
|
|
@ -0,0 +1,24 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS channels (
|
||||||
|
id text NOT NULL PRIMARY KEY,
|
||||||
|
name text NOT NULL,
|
||||||
|
feed_url text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS videos (
|
||||||
|
id text NOT NULL PRIMARY KEY,
|
||||||
|
title text NOT NULL,
|
||||||
|
published_timestamp timestamp with time zone NOT NULL,
|
||||||
|
channel_id text NOT NULL REFERENCES channels(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
username text NOT NULL PRIMARY KEY,
|
||||||
|
password text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_videos (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username text NOT NULL REFERENCES users(username),
|
||||||
|
video_id text NOT NULL REFERENCES videos(id),
|
||||||
|
watch_timestamp timestamp with time zone
|
||||||
|
);
|
|
@ -0,0 +1,189 @@
|
||||||
|
// Code generated by moq; DO NOT EDIT.
|
||||||
|
// github.com/matryer/moq
|
||||||
|
|
||||||
|
package db_mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/db"
|
||||||
|
"gitea.theedgeofrage.com/TheEdgeOfRage/ytrssil-api/models"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure, that DBMock does implement db.DB.
|
||||||
|
// If this is not the case, regenerate this file with moq.
|
||||||
|
var _ db.DB = &DBMock{}
|
||||||
|
|
||||||
|
// DBMock is a mock implementation of db.DB.
|
||||||
|
//
|
||||||
|
// func TestSomethingThatUsesDB(t *testing.T) {
|
||||||
|
//
|
||||||
|
// // make and configure a mocked db.DB
|
||||||
|
// mockedDB := &DBMock{
|
||||||
|
// AuthenticateUserFunc: func(ctx context.Context, username string, password string) (bool, error) {
|
||||||
|
// panic("mock out the AuthenticateUser method")
|
||||||
|
// },
|
||||||
|
// CreateUserFunc: func(ctx context.Context, user models.User) error {
|
||||||
|
// panic("mock out the CreateUser method")
|
||||||
|
// },
|
||||||
|
// GetNewVideosFunc: func(ctx context.Context, username string) ([]*models.Video, error) {
|
||||||
|
// panic("mock out the GetNewVideos method")
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // use mockedDB in code that requires db.DB
|
||||||
|
// // and then make assertions.
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
type DBMock struct {
|
||||||
|
// AuthenticateUserFunc mocks the AuthenticateUser method.
|
||||||
|
AuthenticateUserFunc func(ctx context.Context, username string, password string) (bool, error)
|
||||||
|
|
||||||
|
// CreateUserFunc mocks the CreateUser method.
|
||||||
|
CreateUserFunc func(ctx context.Context, user models.User) error
|
||||||
|
|
||||||
|
// GetNewVideosFunc mocks the GetNewVideos method.
|
||||||
|
GetNewVideosFunc func(ctx context.Context, username string) ([]*models.Video, error)
|
||||||
|
|
||||||
|
// calls tracks calls to the methods.
|
||||||
|
calls struct {
|
||||||
|
// AuthenticateUser holds details about calls to the AuthenticateUser method.
|
||||||
|
AuthenticateUser []struct {
|
||||||
|
// Ctx is the ctx argument value.
|
||||||
|
Ctx context.Context
|
||||||
|
// Username is the username argument value.
|
||||||
|
Username string
|
||||||
|
// Password is the password argument value.
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
// CreateUser holds details about calls to the CreateUser method.
|
||||||
|
CreateUser []struct {
|
||||||
|
// Ctx is the ctx argument value.
|
||||||
|
Ctx context.Context
|
||||||
|
// User is the user argument value.
|
||||||
|
User models.User
|
||||||
|
}
|
||||||
|
// GetNewVideos holds details about calls to the GetNewVideos method.
|
||||||
|
GetNewVideos []struct {
|
||||||
|
// Ctx is the ctx argument value.
|
||||||
|
Ctx context.Context
|
||||||
|
// Username is the username argument value.
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lockAuthenticateUser sync.RWMutex
|
||||||
|
lockCreateUser sync.RWMutex
|
||||||
|
lockGetNewVideos sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticateUser calls AuthenticateUserFunc.
|
||||||
|
func (mock *DBMock) AuthenticateUser(ctx context.Context, username string, password string) (bool, error) {
|
||||||
|
if mock.AuthenticateUserFunc == nil {
|
||||||
|
panic("DBMock.AuthenticateUserFunc: method is nil but DB.AuthenticateUser was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
mock.lockAuthenticateUser.Lock()
|
||||||
|
mock.calls.AuthenticateUser = append(mock.calls.AuthenticateUser, callInfo)
|
||||||
|
mock.lockAuthenticateUser.Unlock()
|
||||||
|
return mock.AuthenticateUserFunc(ctx, username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticateUserCalls gets all the calls that were made to AuthenticateUser.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedDB.AuthenticateUserCalls())
|
||||||
|
func (mock *DBMock) AuthenticateUserCalls() []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
mock.lockAuthenticateUser.RLock()
|
||||||
|
calls = mock.calls.AuthenticateUser
|
||||||
|
mock.lockAuthenticateUser.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser calls CreateUserFunc.
|
||||||
|
func (mock *DBMock) CreateUser(ctx context.Context, user models.User) error {
|
||||||
|
if mock.CreateUserFunc == nil {
|
||||||
|
panic("DBMock.CreateUserFunc: method is nil but DB.CreateUser was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Ctx context.Context
|
||||||
|
User models.User
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
User: user,
|
||||||
|
}
|
||||||
|
mock.lockCreateUser.Lock()
|
||||||
|
mock.calls.CreateUser = append(mock.calls.CreateUser, callInfo)
|
||||||
|
mock.lockCreateUser.Unlock()
|
||||||
|
return mock.CreateUserFunc(ctx, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUserCalls gets all the calls that were made to CreateUser.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedDB.CreateUserCalls())
|
||||||
|
func (mock *DBMock) CreateUserCalls() []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
User models.User
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
User models.User
|
||||||
|
}
|
||||||
|
mock.lockCreateUser.RLock()
|
||||||
|
calls = mock.calls.CreateUser
|
||||||
|
mock.lockCreateUser.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNewVideos calls GetNewVideosFunc.
|
||||||
|
func (mock *DBMock) GetNewVideos(ctx context.Context, username string) ([]*models.Video, error) {
|
||||||
|
if mock.GetNewVideosFunc == nil {
|
||||||
|
panic("DBMock.GetNewVideosFunc: method is nil but DB.GetNewVideos was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Username string
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
mock.lockGetNewVideos.Lock()
|
||||||
|
mock.calls.GetNewVideos = append(mock.calls.GetNewVideos, callInfo)
|
||||||
|
mock.lockGetNewVideos.Unlock()
|
||||||
|
return mock.GetNewVideosFunc(ctx, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNewVideosCalls gets all the calls that were made to GetNewVideos.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedDB.GetNewVideosCalls())
|
||||||
|
func (mock *DBMock) GetNewVideosCalls() []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Username string
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
mock.lockGetNewVideos.RLock()
|
||||||
|
calls = mock.calls.GetNewVideos
|
||||||
|
mock.lockGetNewVideos.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
// YouTube ID of the channel
|
||||||
|
ChannelID string `json:"channel_id" dynamodbav:"channel_id"`
|
||||||
|
// Name of the channel
|
||||||
|
Name string `json:"name" dynamodbav:"name"`
|
||||||
|
// Feed is the URL for the RSS feed
|
||||||
|
FeedURL string `json:"feed_url" dynamodbav:"feed_url"`
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type GetNewVideosResponse struct {
|
||||||
|
Videos []*Video `json:"videos"`
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Username string `json:"username" db:"username"`
|
||||||
|
Password string `json:"password" db:"password"`
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Video struct {
|
||||||
|
// YouTube ID of the video
|
||||||
|
VideoID string `json:"video_id" db:"video_id"`
|
||||||
|
// Name of the channel the video belongs to
|
||||||
|
ChannelName string `json:"channel_name" db:"channel_name"`
|
||||||
|
// Title of the video
|
||||||
|
Title string `json:"title" db:"title"`
|
||||||
|
// Video publish timestamp
|
||||||
|
PublishedTime time.Time `json:"published_timestamp" db:"published_timestamp"`
|
||||||
|
// Video watch timestamp
|
||||||
|
WatchTime *time.Time `json:"watch_timestamp" db:"watch_timestamp"`
|
||||||
|
}
|
Loading…
Reference in New Issue