mirror of
				https://github.com/elyby/chrly.git
				synced 2025-05-31 14:11:51 +05:30 
			
		
		
		
	Merge branch 'db_rework'
This commit is contained in:
		| @@ -22,8 +22,10 @@ jobs: | ||||
|     # Tests stage | ||||
|     - name: Unit tests | ||||
|       stage: Tests | ||||
|       services: | ||||
|         - redis | ||||
|       script: | ||||
|         - go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... | ||||
|         - go test -v -race --tags redis -coverprofile=coverage.txt -covermode=atomic ./... | ||||
|         - bash <(curl -s https://codecov.io/bash) | ||||
|  | ||||
|     # FOSSA is don't feel so good, let it rest | ||||
|   | ||||
| @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | ||||
|     - `ely.skinsystem.{hostname}.app.mojang_textures.usernames.textures_miss` | ||||
| - All incoming requests are now logging to the console in | ||||
|   [Apache Common Log Format](http://httpd.apache.org/docs/2.2/logs.html#common). | ||||
| - Added `/healthcheck` endpoint (at the moment checks are only available for the batch Mojang UUIDs provider). | ||||
| - Added `/healthcheck` endpoint. | ||||
| - Graceful server shutdown. | ||||
| - Panics in http are now logged in Sentry. | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								Gopkg.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Gopkg.lock
									
									
									
										generated
									
									
									
								
							| @@ -350,6 +350,7 @@ | ||||
|     "github.com/spf13/viper", | ||||
|     "github.com/stretchr/testify/assert", | ||||
|     "github.com/stretchr/testify/mock", | ||||
|     "github.com/stretchr/testify/require", | ||||
|     "github.com/stretchr/testify/suite", | ||||
|     "github.com/tevino/abool", | ||||
|     "github.com/thedevsaddam/govalidator", | ||||
|   | ||||
| @@ -1,9 +0,0 @@ | ||||
| package db | ||||
|  | ||||
| type ParamRequired struct { | ||||
| 	Param string | ||||
| } | ||||
|  | ||||
| func (e ParamRequired) Error() string { | ||||
| 	return "Required parameter not provided" | ||||
| } | ||||
| @@ -1,12 +0,0 @@ | ||||
| package db | ||||
|  | ||||
| import ( | ||||
| 	"github.com/elyby/chrly/http" | ||||
| 	"github.com/elyby/chrly/mojangtextures" | ||||
| ) | ||||
|  | ||||
| type RepositoriesCreator interface { | ||||
| 	CreateSkinsRepository() (http.SkinsRepository, error) | ||||
| 	CreateCapesRepository() (http.CapesRepository, error) | ||||
| 	CreateMojangUuidsRepository() (mojangtextures.UuidsStorage, error) | ||||
| } | ||||
| @@ -1,68 +0,0 @@ | ||||
| package db | ||||
|  | ||||
| import ( | ||||
| 	"github.com/elyby/chrly/http" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/elyby/chrly/model" | ||||
| 	"github.com/elyby/chrly/mojangtextures" | ||||
| ) | ||||
|  | ||||
| type FilesystemFactory struct { | ||||
| 	BasePath     string | ||||
| 	CapesDirName string | ||||
| } | ||||
|  | ||||
| func (f FilesystemFactory) CreateSkinsRepository() (http.SkinsRepository, error) { | ||||
| 	panic("skins repository not supported for this storage type") | ||||
| } | ||||
|  | ||||
| func (f FilesystemFactory) CreateCapesRepository() (http.CapesRepository, error) { | ||||
| 	if err := f.validateFactoryConfig(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &filesStorage{path: path.Join(f.BasePath, f.CapesDirName)}, nil | ||||
| } | ||||
|  | ||||
| func (f FilesystemFactory) CreateMojangUuidsRepository() (mojangtextures.UuidsStorage, error) { | ||||
| 	panic("implement me") | ||||
| } | ||||
|  | ||||
| func (f FilesystemFactory) validateFactoryConfig() error { | ||||
| 	if f.BasePath == "" { | ||||
| 		return &ParamRequired{"basePath"} | ||||
| 	} | ||||
|  | ||||
| 	if f.CapesDirName == "" { | ||||
| 		f.CapesDirName = "capes" | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type filesStorage struct { | ||||
| 	path string | ||||
| } | ||||
|  | ||||
| func (repository *filesStorage) FindByUsername(username string) (*model.Cape, error) { | ||||
| 	if username == "" { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	capePath := path.Join(repository.path, strings.ToLower(username)+".png") | ||||
| 	file, err := os.Open(capePath) | ||||
| 	if err != nil { | ||||
| 		if os.IsNotExist(err) { | ||||
| 			return nil, nil | ||||
| 		} | ||||
|  | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &model.Cape{ | ||||
| 		File: file, | ||||
| 	}, nil | ||||
| } | ||||
							
								
								
									
										33
									
								
								db/fs/fs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								db/fs/fs.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| package fs | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/elyby/chrly/model" | ||||
| ) | ||||
|  | ||||
| func New(basePath string) (*Filesystem, error) { | ||||
| 	return &Filesystem{path: basePath}, nil | ||||
| } | ||||
|  | ||||
| type Filesystem struct { | ||||
| 	path string | ||||
| } | ||||
|  | ||||
| func (f *Filesystem) FindCapeByUsername(username string) (*model.Cape, error) { | ||||
| 	capePath := path.Join(f.path, strings.ToLower(username)+".png") | ||||
| 	file, err := os.Open(capePath) | ||||
| 	if err != nil { | ||||
| 		if os.IsNotExist(err) { | ||||
| 			return nil, nil | ||||
| 		} | ||||
|  | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &model.Cape{ | ||||
| 		File: file, | ||||
| 	}, nil | ||||
| } | ||||
							
								
								
									
										56
									
								
								db/fs/fs_integration_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								db/fs/fs_integration_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| package fs | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func TestNew(t *testing.T) { | ||||
| 	fs, err := New("base/path") | ||||
| 	require.Nil(t, err) | ||||
| 	require.Equal(t, "base/path", fs.path) | ||||
| } | ||||
|  | ||||
| func TestFilesystem(t *testing.T) { | ||||
| 	t.Run("FindCapeByUsername", func(t *testing.T) { | ||||
| 		dir, err := ioutil.TempDir("", "capes") | ||||
| 		if err != nil { | ||||
| 			panic(fmt.Errorf("cannot crete temp directory for tests: %w", err)) | ||||
| 		} | ||||
| 		defer os.RemoveAll(dir) | ||||
|  | ||||
| 		t.Run("exists cape", func(t *testing.T) { | ||||
| 			file, err := os.Create(path.Join(dir, "username.png")) | ||||
| 			if err != nil { | ||||
| 				panic(fmt.Errorf("cannot create temp skin for tests: %w", err)) | ||||
| 			} | ||||
| 			defer os.Remove(file.Name()) | ||||
|  | ||||
| 			fs, _ := New(dir) | ||||
| 			cape, err := fs.FindCapeByUsername("username") | ||||
| 			require.Nil(t, err) | ||||
| 			require.NotNil(t, cape) | ||||
| 			capeFile, _ := cape.File.(*os.File) | ||||
| 			require.Equal(t, file.Name(), capeFile.Name()) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("not exists cape", func(t *testing.T) { | ||||
| 			fs, _ := New(dir) | ||||
| 			cape, err := fs.FindCapeByUsername("username") | ||||
| 			require.Nil(t, err) | ||||
| 			require.Nil(t, cape) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("empty username", func(t *testing.T) { | ||||
| 			fs, _ := New(dir) | ||||
| 			cape, err := fs.FindCapeByUsername("") | ||||
| 			require.Nil(t, err) | ||||
| 			require.Nil(t, cape) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| @@ -1,10 +1,9 @@ | ||||
| package db | ||||
| package redis | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"compress/zlib" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| @@ -14,69 +13,31 @@ import ( | ||||
| 	"github.com/mediocregopher/radix.v2/redis" | ||||
| 	"github.com/mediocregopher/radix.v2/util" | ||||
| 
 | ||||
| 	"github.com/elyby/chrly/http" | ||||
| 	"github.com/elyby/chrly/model" | ||||
| 	"github.com/elyby/chrly/mojangtextures" | ||||
| ) | ||||
| 
 | ||||
| type RedisFactory struct { | ||||
| 	Host     string | ||||
| 	Port     int | ||||
| 	PoolSize int | ||||
| 	pool     *pool.Pool | ||||
| } | ||||
| var now = time.Now | ||||
| 
 | ||||
| func (f *RedisFactory) CreateSkinsRepository() (http.SkinsRepository, error) { | ||||
| 	return f.createInstance() | ||||
| } | ||||
| 
 | ||||
| func (f *RedisFactory) CreateCapesRepository() (http.CapesRepository, error) { | ||||
| 	panic("capes repository not supported for this storage type") | ||||
| } | ||||
| 
 | ||||
| func (f *RedisFactory) CreateMojangUuidsRepository() (mojangtextures.UuidsStorage, error) { | ||||
| 	return f.createInstance() | ||||
| } | ||||
| 
 | ||||
| func (f *RedisFactory) createInstance() (*redisDb, error) { | ||||
| 	p, err := f.getPool() | ||||
| func New(addr string, poolSize int) (*Redis, error) { | ||||
| 	conn, err := pool.New("tcp", addr, poolSize) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &redisDb{p}, nil | ||||
| 	return &Redis{ | ||||
| 		pool: conn, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (f *RedisFactory) getPool() (*pool.Pool, error) { | ||||
| 	if f.pool == nil { | ||||
| 		if f.Host == "" { | ||||
| 			return nil, &ParamRequired{"host"} | ||||
| 		} | ||||
| const accountIdToUsernameKey = "hash:username-to-account-id" // TODO: this should be actually "hash:user-id-to-username" | ||||
| const mojangUsernameToUuidKey = "hash:mojang-username-to-uuid" | ||||
| 
 | ||||
| 		if f.Port == 0 { | ||||
| 			return nil, &ParamRequired{"port"} | ||||
| 		} | ||||
| 
 | ||||
| 		addr := fmt.Sprintf("%s:%d", f.Host, f.Port) | ||||
| 		conn, err := pool.New("tcp", addr, f.PoolSize) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		f.pool = conn | ||||
| 	} | ||||
| 
 | ||||
| 	return f.pool, nil | ||||
| } | ||||
| 
 | ||||
| type redisDb struct { | ||||
| type Redis struct { | ||||
| 	pool *pool.Pool | ||||
| } | ||||
| 
 | ||||
| const accountIdToUsernameKey = "hash:username-to-account-id" | ||||
| const mojangUsernameToUuidKey = "hash:mojang-username-to-uuid" | ||||
| 
 | ||||
| func (db *redisDb) FindByUsername(username string) (*model.Skin, error) { | ||||
| func (db *Redis) FindSkinByUsername(username string) (*model.Skin, error) { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| @@ -86,66 +47,6 @@ func (db *redisDb) FindByUsername(username string) (*model.Skin, error) { | ||||
| 	return findByUsername(username, conn) | ||||
| } | ||||
| 
 | ||||
| func (db *redisDb) FindByUserId(id int) (*model.Skin, error) { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	return findByUserId(id, conn) | ||||
| } | ||||
| 
 | ||||
| func (db *redisDb) Save(skin *model.Skin) error { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	return save(skin, conn) | ||||
| } | ||||
| 
 | ||||
| func (db *redisDb) RemoveByUserId(id int) error { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	return removeByUserId(id, conn) | ||||
| } | ||||
| 
 | ||||
| func (db *redisDb) RemoveByUsername(username string) error { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	return removeByUsername(username, conn) | ||||
| } | ||||
| 
 | ||||
| func (db *redisDb) GetUuid(username string) (string, error) { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	return findMojangUuidByUsername(username, conn) | ||||
| } | ||||
| 
 | ||||
| func (db *redisDb) StoreUuid(username string, uuid string) error { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	return storeMojangUuid(username, uuid, conn) | ||||
| } | ||||
| 
 | ||||
| func findByUsername(username string, conn util.Cmder) (*model.Skin, error) { | ||||
| 	redisKey := buildUsernameKey(username) | ||||
| 	response := conn.Cmd("GET", redisKey) | ||||
| @@ -153,11 +54,7 @@ func findByUsername(username string, conn util.Cmder) (*model.Skin, error) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	encodedResult, err := response.Bytes() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	encodedResult, _ := response.Bytes() | ||||
| 	result, err := zlibDecode(encodedResult) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| @@ -174,6 +71,16 @@ func findByUsername(username string, conn util.Cmder) (*model.Skin, error) { | ||||
| 	return skin, nil | ||||
| } | ||||
| 
 | ||||
| func (db *Redis) FindSkinByUserId(id int) (*model.Skin, error) { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	return findByUserId(id, conn) | ||||
| } | ||||
| 
 | ||||
| func findByUserId(id int, conn util.Cmder) (*model.Skin, error) { | ||||
| 	response := conn.Cmd("HGET", accountIdToUsernameKey, id) | ||||
| 	if response.IsType(redis.Nil) { | ||||
| @@ -188,42 +95,14 @@ func findByUserId(id int, conn util.Cmder) (*model.Skin, error) { | ||||
| 	return findByUsername(username, conn) | ||||
| } | ||||
| 
 | ||||
| func removeByUserId(id int, conn util.Cmder) error { | ||||
| 	record, err := findByUserId(id, conn) | ||||
| func (db *Redis) SaveSkin(skin *model.Skin) error { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	conn.Cmd("MULTI") | ||||
| 
 | ||||
| 	conn.Cmd("HDEL", accountIdToUsernameKey, id) | ||||
| 	if record != nil { | ||||
| 		conn.Cmd("DEL", buildUsernameKey(record.Username)) | ||||
| 	} | ||||
| 
 | ||||
| 	conn.Cmd("EXEC") | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func removeByUsername(username string, conn util.Cmder) error { | ||||
| 	record, err := findByUsername(username, conn) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if record == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	conn.Cmd("MULTI") | ||||
| 
 | ||||
| 	conn.Cmd("DEL", buildUsernameKey(record.Username)) | ||||
| 	conn.Cmd("HDEL", accountIdToUsernameKey, record.UserId) | ||||
| 
 | ||||
| 	conn.Cmd("EXEC") | ||||
| 
 | ||||
| 	return nil | ||||
| 	return save(skin, conn) | ||||
| } | ||||
| 
 | ||||
| func save(skin *model.Skin, conn util.Cmder) error { | ||||
| @@ -249,6 +128,74 @@ func save(skin *model.Skin, conn util.Cmder) error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (db *Redis) RemoveSkinByUserId(id int) error { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	return removeByUserId(id, conn) | ||||
| } | ||||
| 
 | ||||
| func removeByUserId(id int, conn util.Cmder) error { | ||||
| 	record, err := findByUserId(id, conn) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	conn.Cmd("MULTI") | ||||
| 
 | ||||
| 	conn.Cmd("HDEL", accountIdToUsernameKey, id) | ||||
| 	if record != nil { | ||||
| 		conn.Cmd("DEL", buildUsernameKey(record.Username)) | ||||
| 	} | ||||
| 
 | ||||
| 	conn.Cmd("EXEC") | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (db *Redis) RemoveSkinByUsername(username string) error { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	return removeByUsername(username, conn) | ||||
| } | ||||
| 
 | ||||
| func removeByUsername(username string, conn util.Cmder) error { | ||||
| 	record, err := findByUsername(username, conn) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if record == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	conn.Cmd("MULTI") | ||||
| 
 | ||||
| 	conn.Cmd("DEL", buildUsernameKey(record.Username)) | ||||
| 	conn.Cmd("HDEL", accountIdToUsernameKey, record.UserId) | ||||
| 
 | ||||
| 	conn.Cmd("EXEC") | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (db *Redis) GetUuid(username string) (string, error) { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	return findMojangUuidByUsername(username, conn) | ||||
| } | ||||
| 
 | ||||
| func findMojangUuidByUsername(username string, conn util.Cmder) (string, error) { | ||||
| 	response := conn.Cmd("HGET", mojangUsernameToUuidKey, strings.ToLower(username)) | ||||
| 	if response.IsType(redis.Nil) { | ||||
| @@ -259,15 +206,25 @@ func findMojangUuidByUsername(username string, conn util.Cmder) (string, error) | ||||
| 	parts := strings.Split(data, ":") | ||||
| 	timestamp, _ := strconv.ParseInt(parts[1], 10, 64) | ||||
| 	storedAt := time.Unix(timestamp, 0) | ||||
| 	if storedAt.Add(time.Hour * 24 * 30).Before(time.Now()) { | ||||
| 	if storedAt.Add(time.Hour * 24 * 30).Before(now()) { | ||||
| 		return "", &mojangtextures.ValueNotFound{} | ||||
| 	} | ||||
| 
 | ||||
| 	return parts[0], nil | ||||
| } | ||||
| 
 | ||||
| func (db *Redis) StoreUuid(username string, uuid string) error { | ||||
| 	conn, err := db.pool.Get() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer db.pool.Put(conn) | ||||
| 
 | ||||
| 	return storeMojangUuid(username, uuid, conn) | ||||
| } | ||||
| 
 | ||||
| func storeMojangUuid(username string, uuid string, conn util.Cmder) error { | ||||
| 	value := uuid + ":" + strconv.FormatInt(time.Now().Unix(), 10) | ||||
| 	value := uuid + ":" + strconv.FormatInt(now().Unix(), 10) | ||||
| 	res := conn.Cmd("HSET", mojangUsernameToUuidKey, strings.ToLower(username), value) | ||||
| 	if res.IsType(redis.Err) { | ||||
| 		return res.Err | ||||
| @@ -276,6 +233,15 @@ func storeMojangUuid(username string, uuid string, conn util.Cmder) error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (db *Redis) Ping() error { | ||||
| 	r := db.pool.Cmd("PING") | ||||
| 	if r.Err != nil { | ||||
| 		return r.Err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func buildUsernameKey(username string) string { | ||||
| 	return "username:" + strings.ToLower(username) | ||||
| } | ||||
| @@ -291,14 +257,14 @@ func zlibEncode(str []byte) []byte { | ||||
| 
 | ||||
| func zlibDecode(bts []byte) ([]byte, error) { | ||||
| 	buff := bytes.NewReader(bts) | ||||
| 	reader, readError := zlib.NewReader(buff) | ||||
| 	if readError != nil { | ||||
| 		return nil, readError | ||||
| 	reader, err := zlib.NewReader(buff) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	resultBuffer := new(bytes.Buffer) | ||||
| 	_, _ = io.Copy(resultBuffer, reader) | ||||
| 	reader.Close() | ||||
| 	_ = reader.Close() | ||||
| 
 | ||||
| 	return resultBuffer.Bytes(), nil | ||||
| } | ||||
							
								
								
									
										361
									
								
								db/redis/redis_integration_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										361
									
								
								db/redis/redis_integration_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,361 @@ | ||||
| // +build redis | ||||
|  | ||||
| package redis | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/mediocregopher/radix.v2/redis" | ||||
| 	assert "github.com/stretchr/testify/require" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
|  | ||||
| 	"github.com/elyby/chrly/model" | ||||
| 	"github.com/elyby/chrly/mojangtextures" | ||||
| ) | ||||
|  | ||||
| const redisAddr = "localhost:6379" | ||||
|  | ||||
| func TestNew(t *testing.T) { | ||||
| 	t.Run("should connect", func(t *testing.T) { | ||||
| 		conn, err := New(redisAddr, 12) | ||||
| 		assert.Nil(t, err) | ||||
| 		assert.NotNil(t, conn) | ||||
| 		internalPool := reflect.ValueOf(conn.pool).Elem().FieldByName("pool") | ||||
| 		assert.Equal(t, 12, internalPool.Cap()) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("should return error", func(t *testing.T) { | ||||
| 		conn, err := New("localhost:12345", 12) // Use localhost to avoid DNS resolution | ||||
| 		assert.Error(t, err) | ||||
| 		assert.Nil(t, conn) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| type redisTestSuite struct { | ||||
| 	suite.Suite | ||||
|  | ||||
| 	Redis *Redis | ||||
|  | ||||
| 	cmd func(cmd string, args ...interface{}) *redis.Resp | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) SetupSuite() { | ||||
| 	conn, err := New(redisAddr, 10) | ||||
| 	if err != nil { | ||||
| 		panic(fmt.Errorf("cannot establish connection to redis: %w", err)) | ||||
| 	} | ||||
|  | ||||
| 	suite.Redis = conn | ||||
| 	suite.cmd = conn.pool.Cmd | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) SetupTest() { | ||||
| 	// Cleanup database before the each test | ||||
| 	suite.cmd("FLUSHALL") | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) TearDownTest() { | ||||
| 	// Restore time.Now func | ||||
| 	now = time.Now | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) RunSubTest(name string, subTest func()) { | ||||
| 	suite.SetupTest() | ||||
| 	suite.Run(name, subTest) | ||||
| } | ||||
|  | ||||
| func TestRedis(t *testing.T) { | ||||
| 	suite.Run(t, new(redisTestSuite)) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * JSON with zlib encoding | ||||
|  * { | ||||
|  *     userId: 1, | ||||
|  *     uuid: "fd5da1e4d66d4d17aadee2446093896d", | ||||
|  *     username: "Mock", | ||||
|  *     skinId: 1, | ||||
|  *     url: "http://localhost/skin.png", | ||||
|  *     is1_8: true, | ||||
|  *     isSlim: false, | ||||
|  *     mojangTextures: "mock-mojang-textures", | ||||
|  *     mojangSignature: "mock-mojang-signature" | ||||
|  * } | ||||
|  */ | ||||
| var skinRecord = []byte{ | ||||
| 	0x78, 0x9c, 0x5c, 0xce, 0x4b, 0x4a, 0x4, 0x41, 0xc, 0xc6, 0xf1, 0xbb, 0x7c, 0xeb, 0x1a, 0xdb, 0xd6, 0xb2, | ||||
| 	0x9c, 0xc9, 0xd, 0x5c, 0x88, 0x8b, 0xd1, 0xb5, 0x84, 0x4e, 0xa6, 0xa7, 0xec, 0x7a, 0xc, 0xf5, 0x0, 0x41, | ||||
| 	0xbc, 0xbb, 0xb4, 0xd2, 0xa, 0x2e, 0xf3, 0xe3, 0x9f, 0x90, 0xf, 0xf4, 0xaa, 0xe5, 0x41, 0x40, 0xa3, 0x41, | ||||
| 	0xef, 0x5e, 0x40, 0x38, 0xc9, 0x9d, 0xf0, 0xa8, 0x56, 0x9c, 0x13, 0x2b, 0xe3, 0x3d, 0xb3, 0xa8, 0xde, 0x58, | ||||
| 	0xeb, 0xae, 0xf, 0xb7, 0xfb, 0x83, 0x13, 0x98, 0xef, 0xa5, 0xc4, 0x51, 0x41, 0x78, 0xcc, 0xd3, 0x2, 0x83, | ||||
| 	0xba, 0xf8, 0xb4, 0x9d, 0x29, 0x1, 0x84, 0x73, 0x6b, 0x17, 0x1a, 0x86, 0x90, 0x27, 0xe, 0xe7, 0x5c, 0xdb, | ||||
| 	0xb0, 0x16, 0x57, 0x97, 0x34, 0xc3, 0xc0, 0xd7, 0xf1, 0x75, 0xf, 0x6a, 0xa5, 0xeb, 0x3a, 0x1c, 0x83, 0x8f, | ||||
| 	0xa0, 0x13, 0x87, 0xaa, 0x6, 0x31, 0xbf, 0x71, 0x9a, 0x9f, 0xf5, 0xbd, 0xf5, 0xa2, 0x15, 0x84, 0x98, 0xa7, | ||||
| 	0x65, 0xf7, 0xa3, 0xbb, 0xb6, 0xf1, 0xd6, 0x1d, 0xfd, 0x9c, 0x78, 0xa5, 0x7f, 0x61, 0xfd, 0x75, 0x83, 0xa7, | ||||
| 	0x20, 0x2f, 0x7f, 0xff, 0xe2, 0xf3, 0x2b, 0x0, 0x0, 0xff, 0xff, 0x6f, 0xdd, 0x51, 0x71, | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) TestFindSkinByUsername() { | ||||
| 	suite.RunSubTest("exists record", func() { | ||||
| 		suite.cmd("SET", "username:mock", skinRecord) | ||||
|  | ||||
| 		skin, err := suite.Redis.FindSkinByUsername("Mock") | ||||
| 		suite.Require().Nil(err) | ||||
| 		suite.Require().NotNil(skin) | ||||
| 		suite.Require().Equal(1, skin.UserId) | ||||
| 		suite.Require().Equal("fd5da1e4d66d4d17aadee2446093896d", skin.Uuid) | ||||
| 		suite.Require().Equal("Mock", skin.Username) | ||||
| 		suite.Require().Equal(1, skin.SkinId) | ||||
| 		suite.Require().Equal("http://localhost/skin.png", skin.Url) | ||||
| 		suite.Require().True(skin.Is1_8) | ||||
| 		suite.Require().False(skin.IsSlim) | ||||
| 		suite.Require().Equal("mock-mojang-textures", skin.MojangTextures) | ||||
| 		suite.Require().Equal("mock-mojang-signature", skin.MojangSignature) | ||||
| 		suite.Require().Equal(skin.Username, skin.OldUsername) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("not exists record", func() { | ||||
| 		skin, err := suite.Redis.FindSkinByUsername("Mock") | ||||
| 		suite.Require().Nil(err) | ||||
| 		suite.Require().Nil(skin) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("invalid zlib encoding", func() { | ||||
| 		suite.cmd("SET", "username:mock", "this is really not zlib") | ||||
| 		skin, err := suite.Redis.FindSkinByUsername("Mock") | ||||
| 		suite.Require().Nil(skin) | ||||
| 		suite.Require().EqualError(err, "zlib: invalid header") | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("invalid json encoding", func() { | ||||
| 		suite.cmd("SET", "username:mock", []byte{ | ||||
| 			0x78, 0x9c, 0xca, 0x48, 0xcd, 0xc9, 0xc9, 0x57, 0x28, 0xcf, 0x2f, 0xca, 0x49, 0x1, 0x4, 0x0, 0x0, 0xff, | ||||
| 			0xff, 0x1a, 0xb, 0x4, 0x5d, | ||||
| 		}) | ||||
| 		skin, err := suite.Redis.FindSkinByUsername("Mock") | ||||
| 		suite.Require().Nil(skin) | ||||
| 		suite.Require().EqualError(err, "invalid character 'h' looking for beginning of value") | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) TestFindSkinByUserId() { | ||||
| 	suite.RunSubTest("exists record", func() { | ||||
| 		suite.cmd("SET", "username:mock", skinRecord) | ||||
| 		suite.cmd("HSET", "hash:username-to-account-id", 1, "Mock") | ||||
|  | ||||
| 		skin, err := suite.Redis.FindSkinByUserId(1) | ||||
| 		suite.Require().Nil(err) | ||||
| 		suite.Require().NotNil(skin) | ||||
| 		suite.Require().Equal(1, skin.UserId) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("not exists record", func() { | ||||
| 		skin, err := suite.Redis.FindSkinByUserId(1) | ||||
| 		suite.Require().Nil(err) | ||||
| 		suite.Require().Nil(skin) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("exists hash record, but no skin record", func() { | ||||
| 		suite.cmd("HSET", "hash:username-to-account-id", 1, "Mock") | ||||
| 		skin, err := suite.Redis.FindSkinByUserId(1) | ||||
| 		suite.Require().Nil(err) | ||||
| 		suite.Require().Nil(skin) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) TestSaveSkin() { | ||||
| 	suite.RunSubTest("save new entity", func() { | ||||
| 		err := suite.Redis.SaveSkin(&model.Skin{ | ||||
| 			UserId:          1, | ||||
| 			Uuid:            "fd5da1e4d66d4d17aadee2446093896d", | ||||
| 			Username:        "Mock", | ||||
| 			SkinId:          1, | ||||
| 			Url:             "http://localhost/skin.png", | ||||
| 			Is1_8:           true, | ||||
| 			IsSlim:          false, | ||||
| 			MojangTextures:  "mock-mojang-textures", | ||||
| 			MojangSignature: "mock-mojang-signature", | ||||
| 		}) | ||||
| 		suite.Require().Nil(err) | ||||
|  | ||||
| 		usernameResp := suite.cmd("GET", "username:mock") | ||||
| 		suite.Require().False(usernameResp.IsType(redis.Nil)) | ||||
| 		bytes, _ := usernameResp.Bytes() | ||||
| 		suite.Require().Equal(skinRecord, bytes) | ||||
|  | ||||
| 		idResp := suite.cmd("HGET", "hash:username-to-account-id", 1) | ||||
| 		suite.Require().False(usernameResp.IsType(redis.Nil)) | ||||
| 		str, _ := idResp.Str() | ||||
| 		suite.Require().Equal("Mock", str) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("save exists record with changed username", func() { | ||||
| 		suite.cmd("SET", "username:mock", skinRecord) | ||||
| 		suite.cmd("HSET", "hash:username-to-account-id", 1, "Mock") | ||||
|  | ||||
| 		err := suite.Redis.SaveSkin(&model.Skin{ | ||||
| 			UserId:          1, | ||||
| 			Uuid:            "fd5da1e4d66d4d17aadee2446093896d", | ||||
| 			Username:        "NewMock", | ||||
| 			SkinId:          1, | ||||
| 			Url:             "http://localhost/skin.png", | ||||
| 			Is1_8:           true, | ||||
| 			IsSlim:          false, | ||||
| 			MojangTextures:  "mock-mojang-textures", | ||||
| 			MojangSignature: "mock-mojang-signature", | ||||
| 			OldUsername:     "Mock", | ||||
| 		}) | ||||
| 		suite.Require().Nil(err) | ||||
|  | ||||
| 		usernameResp := suite.cmd("GET", "username:newmock") | ||||
| 		suite.Require().False(usernameResp.IsType(redis.Nil)) | ||||
| 		bytes, _ := usernameResp.Bytes() | ||||
| 		suite.Require().Equal([]byte{ | ||||
| 			0x78, 0x9c, 0x5c, 0x8e, 0xcb, 0x4e, 0xc3, 0x40, 0xc, 0x45, 0xff, 0xe5, 0xae, 0xa7, 0x84, 0x40, 0x18, 0x5a, | ||||
| 			0xff, 0x1, 0xb, 0x60, 0x51, 0x58, 0x23, 0x2b, 0x76, 0xd3, 0x21, 0xf3, 0xa8, 0xe6, 0x21, 0x90, 0x10, 0xff, | ||||
| 			0x8e, 0x52, 0x14, 0x90, 0xba, 0xf4, 0xd1, 0xf1, 0xd5, 0xf9, 0x42, 0x2b, 0x9a, 0x1f, 0x4, 0xd4, 0x1b, 0xb4, | ||||
| 			0xe6, 0x4, 0x84, 0x83, 0xdc, 0x9, 0xf7, 0x3a, 0x88, 0xb5, 0x32, 0x48, 0x7f, 0xcf, 0x2c, 0xaa, 0x37, 0xc3, | ||||
| 			0x60, 0xaf, 0x77, 0xb7, 0xdb, 0x9d, 0x15, 0x98, 0xf3, 0x53, 0xe4, 0xa0, 0x20, 0x3c, 0xe9, 0xc7, 0x63, 0x1a, | ||||
| 			0x67, 0x18, 0x94, 0xd9, 0xc5, 0x75, 0x29, 0x7b, 0x10, 0x8e, 0xb5, 0x9e, 0xa8, 0xeb, 0x7c, 0x1a, 0xd9, 0x1f, | ||||
| 			0x53, 0xa9, 0xdd, 0x62, 0x5c, 0x9d, 0xe2, 0x4, 0x3, 0x57, 0xfa, 0xb7, 0x2d, 0xa8, 0xe6, 0xa6, 0xcb, 0xb1, | ||||
| 			0xf7, 0x2e, 0x80, 0xe, 0xec, 0x8b, 0x1a, 0x84, 0xf4, 0xce, 0x71, 0x7a, 0xd1, 0xcf, 0xda, 0xb2, 0x16, 0x10, | ||||
| 			0x42, 0x1a, 0xe7, 0xcd, 0x2f, 0xdd, 0xd4, 0x15, 0xaf, 0xde, 0xde, 0x4d, 0x91, 0x17, 0x74, 0x21, 0x96, 0x3f, | ||||
| 			0x6e, 0xf0, 0xec, 0xe5, 0xf5, 0x3f, 0xf9, 0xdc, 0xfb, 0xfd, 0x13, 0x0, 0x0, 0xff, 0xff, 0xca, 0xc3, 0x54, | ||||
| 			0x25, | ||||
| 		}, bytes) | ||||
|  | ||||
| 		oldUsernameResp := suite.cmd("GET", "username:mock") | ||||
| 		suite.Require().True(oldUsernameResp.IsType(redis.Nil)) | ||||
|  | ||||
| 		idResp := suite.cmd("HGET", "hash:username-to-account-id", 1) | ||||
| 		suite.Require().False(usernameResp.IsType(redis.Nil)) | ||||
| 		str, _ := idResp.Str() | ||||
| 		suite.Require().Equal("NewMock", str) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) TestRemoveSkinByUserId() { | ||||
| 	suite.RunSubTest("exists record", func() { | ||||
| 		suite.cmd("SET", "username:mock", skinRecord) | ||||
| 		suite.cmd("HSET", "hash:username-to-account-id", 1, "Mock") | ||||
|  | ||||
| 		err := suite.Redis.RemoveSkinByUserId(1) | ||||
| 		suite.Require().Nil(err) | ||||
|  | ||||
| 		usernameResp := suite.cmd("GET", "username:mock") | ||||
| 		suite.Require().True(usernameResp.IsType(redis.Nil)) | ||||
|  | ||||
| 		idResp := suite.cmd("HGET", "hash:username-to-account-id", 1) | ||||
| 		suite.Require().True(idResp.IsType(redis.Nil)) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("exists only id", func() { | ||||
| 		suite.cmd("HSET", "hash:username-to-account-id", 1, "Mock") | ||||
|  | ||||
| 		err := suite.Redis.RemoveSkinByUserId(1) | ||||
| 		suite.Require().Nil(err) | ||||
|  | ||||
| 		idResp := suite.cmd("HGET", "hash:username-to-account-id", 1) | ||||
| 		suite.Require().True(idResp.IsType(redis.Nil)) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("error when querying skin record", func() { | ||||
| 		suite.cmd("SET", "username:mock", "invalid zlib") | ||||
| 		suite.cmd("HSET", "hash:username-to-account-id", 1, "Mock") | ||||
|  | ||||
| 		err := suite.Redis.RemoveSkinByUserId(1) | ||||
| 		suite.Require().EqualError(err, "zlib: invalid header") | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) TestRemoveSkinByUsername() { | ||||
| 	suite.RunSubTest("exists record", func() { | ||||
| 		suite.cmd("SET", "username:mock", skinRecord) | ||||
| 		suite.cmd("HSET", "hash:username-to-account-id", 1, "Mock") | ||||
|  | ||||
| 		err := suite.Redis.RemoveSkinByUsername("Mock") | ||||
| 		suite.Require().Nil(err) | ||||
|  | ||||
| 		usernameResp := suite.cmd("GET", "username:mock") | ||||
| 		suite.Require().True(usernameResp.IsType(redis.Nil)) | ||||
|  | ||||
| 		idResp := suite.cmd("HGET", "hash:username-to-account-id", 1) | ||||
| 		suite.Require().True(idResp.IsType(redis.Nil)) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("exists only username", func() { | ||||
| 		suite.cmd("SET", "username:mock", skinRecord) | ||||
|  | ||||
| 		err := suite.Redis.RemoveSkinByUsername("Mock") | ||||
| 		suite.Require().Nil(err) | ||||
|  | ||||
| 		usernameResp := suite.cmd("GET", "username:mock") | ||||
| 		suite.Require().True(usernameResp.IsType(redis.Nil)) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("no records", func() { | ||||
| 		err := suite.Redis.RemoveSkinByUsername("Mock") | ||||
| 		suite.Require().Nil(err) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("error when querying skin record", func() { | ||||
| 		suite.cmd("SET", "username:mock", "invalid zlib") | ||||
|  | ||||
| 		err := suite.Redis.RemoveSkinByUsername("Mock") | ||||
| 		suite.Require().EqualError(err, "zlib: invalid header") | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) TestGetUuid() { | ||||
| 	suite.RunSubTest("exists record", func() { | ||||
| 		suite.cmd("HSET", | ||||
| 			"hash:mojang-username-to-uuid", | ||||
| 			"mock", | ||||
| 			fmt.Sprintf("%s:%d", "d3ca513eb3e14946b58047f2bd3530fd", time.Now().Unix()), | ||||
| 		) | ||||
|  | ||||
| 		uuid, err := suite.Redis.GetUuid("Mock") | ||||
| 		suite.Require().Nil(err) | ||||
| 		suite.Require().Equal("d3ca513eb3e14946b58047f2bd3530fd", uuid) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("not exists record", func() { | ||||
| 		uuid, err := suite.Redis.GetUuid("Mock") | ||||
| 		suite.Require().Empty(uuid) | ||||
| 		suite.Require().IsType(new(mojangtextures.ValueNotFound), err) | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("exists, but expired record", func() { | ||||
| 		suite.cmd("HSET", | ||||
| 			"hash:mojang-username-to-uuid", | ||||
| 			"mock", | ||||
| 			fmt.Sprintf("%s:%d", "d3ca513eb3e14946b58047f2bd3530fd", time.Now().Add(-1*time.Hour*24*31).Unix()), | ||||
| 		) | ||||
|  | ||||
| 		uuid, err := suite.Redis.GetUuid("Mock") | ||||
| 		suite.Require().Empty(uuid) | ||||
| 		suite.Require().IsType(new(mojangtextures.ValueNotFound), err) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) TestStoreUuid() { | ||||
| 	now = func() time.Time { | ||||
| 		return time.Date(2020, 04, 21, 02, 10, 16, 0, time.UTC) | ||||
| 	} | ||||
|  | ||||
| 	err := suite.Redis.StoreUuid("Mock", "d3ca513eb3e14946b58047f2bd3530fd") | ||||
| 	suite.Require().Nil(err) | ||||
|  | ||||
| 	resp := suite.cmd("HGET", "hash:mojang-username-to-uuid", "mock") | ||||
| 	suite.Require().False(resp.IsType(redis.Nil)) | ||||
| 	str, _ := resp.Str() | ||||
| 	suite.Require().Equal(str, "d3ca513eb3e14946b58047f2bd3530fd:1587435016") | ||||
| } | ||||
|  | ||||
| func (suite *redisTestSuite) TestPing() { | ||||
| 	err := suite.Redis.Ping() | ||||
| 	suite.Require().Nil(err) | ||||
| } | ||||
							
								
								
									
										85
									
								
								di/db.go
									
									
									
									
									
								
							
							
						
						
									
										85
									
								
								di/db.go
									
									
									
									
									
								
							| @@ -1,61 +1,68 @@ | ||||
| package di | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"path" | ||||
|  | ||||
| 	"github.com/goava/di" | ||||
| 	"github.com/spf13/viper" | ||||
|  | ||||
| 	. "github.com/elyby/chrly/db" | ||||
| 	"github.com/elyby/chrly/db/fs" | ||||
| 	"github.com/elyby/chrly/db/redis" | ||||
| 	es "github.com/elyby/chrly/eventsubscribers" | ||||
| 	"github.com/elyby/chrly/http" | ||||
| 	"github.com/elyby/chrly/mojangtextures" | ||||
| ) | ||||
|  | ||||
| var db = di.Options( | ||||
| 	di.Provide(newRedisFactory), | ||||
| 	di.Provide(newFSFactory), | ||||
| 	di.Provide(newSkinsRepository), | ||||
| 	di.Provide(newCapesRepository), | ||||
| 	di.Provide(newMojangUUIDsRepository), | ||||
| 	di.Provide(newMojangSignedTexturesStorage), | ||||
| ) | ||||
|  | ||||
| func newRedisFactory(config *viper.Viper) *RedisFactory { | ||||
| 	config.SetDefault("storage.redis.host", "localhost") | ||||
| 	config.SetDefault("storage.redis.port", 6379) | ||||
| 	config.SetDefault("storage.redis.poll", 10) | ||||
|  | ||||
| 	return &RedisFactory{ | ||||
| 		Host:     config.GetString("storage.redis.host"), | ||||
| 		Port:     config.GetInt("storage.redis.port"), | ||||
| 		PoolSize: config.GetInt("storage.redis.poolSize"), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func newFSFactory(config *viper.Viper) *FilesystemFactory { | ||||
| 	config.SetDefault("storage.filesystem.basePath", "data") | ||||
| 	config.SetDefault("storage.filesystem.capesDirName", "capes") | ||||
|  | ||||
| 	return &FilesystemFactory{ | ||||
| 		BasePath:     config.GetString("storage.filesystem.basePath"), | ||||
| 		CapesDirName: config.GetString("storage.filesystem.capesDirName"), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // v4 had the idea that it would be possible to separate backends for storing skins and capes. | ||||
| // But in v5 the storage will be unified, so this is just temporary constructors before large reworking. | ||||
| // | ||||
| // Since there are no options for selecting target backends, | ||||
| // all constants in this case point to static specific implementations. | ||||
| var db = di.Options( | ||||
| 	di.Provide(newRedis, | ||||
| 		di.As(new(http.SkinsRepository)), | ||||
| 		di.As(new(mojangtextures.UuidsStorage)), | ||||
| 	), | ||||
| 	di.Provide(newFSFactory, | ||||
| 		di.As(new(http.CapesRepository)), | ||||
| 	), | ||||
| 	di.Provide(newMojangSignedTexturesStorage), | ||||
| ) | ||||
|  | ||||
| func newSkinsRepository(factory *RedisFactory) (http.SkinsRepository, error) { | ||||
| 	return factory.CreateSkinsRepository() | ||||
| func newRedis(container *di.Container, config *viper.Viper) (*redis.Redis, error) { | ||||
| 	config.SetDefault("storage.redis.host", "localhost") | ||||
| 	config.SetDefault("storage.redis.port", 6379) | ||||
| 	config.SetDefault("storage.redis.poll", 10) | ||||
|  | ||||
| 	conn, err := redis.New( | ||||
| 		fmt.Sprintf("%s:%d", config.GetString("storage.redis.host"), config.GetInt("storage.redis.port")), | ||||
| 		config.GetInt("storage.redis.poolSize"), | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := container.Provide(func() *namedHealthChecker { | ||||
| 		return &namedHealthChecker{ | ||||
| 			Name:    "redis", | ||||
| 			Checker: es.DatabaseChecker(conn), | ||||
| 		} | ||||
| 	}); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return conn, nil | ||||
| } | ||||
|  | ||||
| func newCapesRepository(factory *FilesystemFactory) (http.CapesRepository, error) { | ||||
| 	return factory.CreateCapesRepository() | ||||
| } | ||||
| func newFSFactory(config *viper.Viper) (*fs.Filesystem, error) { | ||||
| 	config.SetDefault("storage.filesystem.basePath", "data") | ||||
| 	config.SetDefault("storage.filesystem.capesDirName", "capes") | ||||
|  | ||||
| func newMojangUUIDsRepository(factory *RedisFactory) (mojangtextures.UuidsStorage, error) { | ||||
| 	return factory.CreateMojangUuidsRepository() | ||||
| 	return fs.New(path.Join( | ||||
| 		config.GetString("storage.filesystem.basePath"), | ||||
| 		config.GetString("storage.filesystem.capesDirName"), | ||||
| 	)) | ||||
| } | ||||
|  | ||||
| func newMojangSignedTexturesStorage() mojangtextures.TexturesStorage { | ||||
|   | ||||
| @@ -87,7 +87,7 @@ func newHandlerFactory( | ||||
| 			checkersOptions[i] = healthcheck.WithChecker(checker.Name, checker.Checker) | ||||
| 		} | ||||
|  | ||||
| 		router.Handle("/healthcheck", healthcheck.Handler()).Methods("GET") | ||||
| 		router.Handle("/healthcheck", healthcheck.Handler(checkersOptions...)).Methods("GET") | ||||
| 	} | ||||
|  | ||||
| 	return router, nil | ||||
|   | ||||
| @@ -11,6 +11,26 @@ import ( | ||||
| 	"github.com/elyby/chrly/api/mojang" | ||||
| ) | ||||
|  | ||||
| type Pingable interface { | ||||
| 	Ping() error | ||||
| } | ||||
|  | ||||
| func DatabaseChecker(connection Pingable) healthcheck.CheckerFunc { | ||||
| 	return func(ctx context.Context) error { | ||||
| 		done := make(chan error) | ||||
| 		go func() { | ||||
| 			done <- connection.Ping() | ||||
| 		}() | ||||
|  | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			return errors.New("check timeout") | ||||
| 		case err := <-done: | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func MojangBatchUuidsProviderResponseChecker(dispatcher Subscriber, resetDuration time.Duration) healthcheck.CheckerFunc { | ||||
| 	var mutex sync.Mutex | ||||
| 	var lastCallErr error | ||||
|   | ||||
| @@ -7,18 +7,56 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/mock" | ||||
|  | ||||
| 	"github.com/elyby/chrly/api/mojang" | ||||
| 	"github.com/elyby/chrly/dispatcher" | ||||
| ) | ||||
|  | ||||
| type pingableMock struct { | ||||
| 	mock.Mock | ||||
| } | ||||
|  | ||||
| func (p *pingableMock) Ping() error { | ||||
| 	args := p.Called() | ||||
| 	return args.Error(0) | ||||
| } | ||||
|  | ||||
| func TestDatabaseChecker(t *testing.T) { | ||||
| 	t.Run("no error", func(t *testing.T) { | ||||
| 		p := &pingableMock{} | ||||
| 		p.On("Ping").Return(nil) | ||||
| 		checker := DatabaseChecker(p) | ||||
| 		assert.Nil(t, checker(context.Background())) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("with error", func(t *testing.T) { | ||||
| 		err := errors.New("mock error") | ||||
| 		p := &pingableMock{} | ||||
| 		p.On("Ping").Return(err) | ||||
| 		checker := DatabaseChecker(p) | ||||
| 		assert.Equal(t, err, checker(context.Background())) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("context timeout", func(t *testing.T) { | ||||
| 		p := &pingableMock{} | ||||
| 		waitChan := make(chan time.Time, 1) | ||||
| 		p.On("Ping").WaitUntil(waitChan).Return(nil) | ||||
|  | ||||
| 		ctx, _ := context.WithTimeout(context.Background(), 0) | ||||
| 		checker := DatabaseChecker(p) | ||||
| 		assert.Errorf(t, checker(ctx), "check timeout") | ||||
| 		close(waitChan) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestMojangBatchUuidsProviderChecker(t *testing.T) { | ||||
| 	t.Run("empty state", func(t *testing.T) { | ||||
| 		d := dispatcher.New() | ||||
| 		checker := MojangBatchUuidsProviderResponseChecker(d, time.Millisecond) | ||||
| 		assert.Nil(t, checker(context.Background())) | ||||
| 	}) | ||||
|  | ||||
| 	// | ||||
| 	t.Run("when no error occurred", func(t *testing.T) { | ||||
| 		d := dispatcher.New() | ||||
| 		checker := MojangBatchUuidsProviderResponseChecker(d, time.Millisecond) | ||||
|   | ||||
							
								
								
									
										16
									
								
								http/api.go
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								http/api.go
									
									
									
									
									
								
							| @@ -92,7 +92,7 @@ func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) { | ||||
| 	record.MojangTextures = req.Form.Get("mojangTextures") | ||||
| 	record.MojangSignature = req.Form.Get("mojangSignature") | ||||
|  | ||||
| 	err = ctx.SkinsRepo.Save(record) | ||||
| 	err = ctx.SkinsRepo.SaveSkin(record) | ||||
| 	if err != nil { | ||||
| 		ctx.Emit("skinsystem:error", fmt.Errorf("unable to save record to the repository: %w", err)) | ||||
| 		apiServerError(resp) | ||||
| @@ -104,13 +104,13 @@ func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) { | ||||
|  | ||||
| func (ctx *Api) deleteSkinByUserIdHandler(resp http.ResponseWriter, req *http.Request) { | ||||
| 	id, _ := strconv.Atoi(mux.Vars(req)["id"]) | ||||
| 	skin, err := ctx.SkinsRepo.FindByUserId(id) | ||||
| 	skin, err := ctx.SkinsRepo.FindSkinByUserId(id) | ||||
| 	ctx.deleteSkin(skin, err, resp) | ||||
| } | ||||
|  | ||||
| func (ctx *Api) deleteSkinByUsernameHandler(resp http.ResponseWriter, req *http.Request) { | ||||
| 	username := mux.Vars(req)["username"] | ||||
| 	skin, err := ctx.SkinsRepo.FindByUsername(username) | ||||
| 	skin, err := ctx.SkinsRepo.FindSkinByUsername(username) | ||||
| 	ctx.deleteSkin(skin, err, resp) | ||||
| } | ||||
|  | ||||
| @@ -126,7 +126,7 @@ func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = ctx.SkinsRepo.RemoveByUserId(skin.UserId) | ||||
| 	err = ctx.SkinsRepo.RemoveSkinByUserId(skin.UserId) | ||||
| 	if err != nil { | ||||
| 		ctx.Emit("skinsystem:error", fmt.Errorf("cannot delete skin by error: %w", err)) | ||||
| 		apiServerError(resp) | ||||
| @@ -137,7 +137,7 @@ func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter | ||||
| } | ||||
|  | ||||
| func (ctx *Api) findIdentityOrCleanup(identityId int, username string) (*model.Skin, error) { | ||||
| 	record, err := ctx.SkinsRepo.FindByUserId(identityId) | ||||
| 	record, err := ctx.SkinsRepo.FindSkinByUserId(identityId) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @@ -146,7 +146,7 @@ func (ctx *Api) findIdentityOrCleanup(identityId int, username string) (*model.S | ||||
| 		// The username may have changed in the external database, | ||||
| 		// so we need to remove the old association | ||||
| 		if record.Username != username { | ||||
| 			_ = ctx.SkinsRepo.RemoveByUserId(identityId) | ||||
| 			_ = ctx.SkinsRepo.RemoveSkinByUserId(identityId) | ||||
| 			record.Username = username | ||||
| 		} | ||||
|  | ||||
| @@ -155,14 +155,14 @@ func (ctx *Api) findIdentityOrCleanup(identityId int, username string) (*model.S | ||||
|  | ||||
| 	// If the requested id was not found, then username was reassigned to another user | ||||
| 	// who has not uploaded his data to Chrly yet | ||||
| 	record, err = ctx.SkinsRepo.FindByUsername(username) | ||||
| 	record, err = ctx.SkinsRepo.FindSkinByUsername(username) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// If the target username does exist, clear it as it will be reassigned to the new user | ||||
| 	if record != nil { | ||||
| 		_ = ctx.SkinsRepo.RemoveByUsername(username) | ||||
| 		_ = ctx.SkinsRepo.RemoveSkinByUsername(username) | ||||
| 		record.UserId = identityId | ||||
|  | ||||
| 		return record, nil | ||||
|   | ||||
| @@ -88,9 +88,9 @@ var postSkinTestsCases = []*postSkinTestCase{ | ||||
| 			"url":        {"http://example.com/skin.png"}, | ||||
| 		}.Encode()), | ||||
| 		BeforeTest: func(suite *apiTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUserId", 1).Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool { | ||||
| 			suite.SkinsRepository.On("FindSkinByUserId", 1).Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("SaveSkin", mock.MatchedBy(func(model *model.Skin) bool { | ||||
| 				suite.Equal(1, model.UserId) | ||||
| 				suite.Equal("mock_username", model.Username) | ||||
| 				suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid) | ||||
| @@ -120,8 +120,8 @@ var postSkinTestsCases = []*postSkinTestCase{ | ||||
| 			"url":        {"http://textures-server.com/skin.png"}, | ||||
| 		}.Encode()), | ||||
| 		BeforeTest: func(suite *apiTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool { | ||||
| 			suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.SkinsRepository.On("SaveSkin", mock.MatchedBy(func(model *model.Skin) bool { | ||||
| 				suite.Equal(1, model.UserId) | ||||
| 				suite.Equal("mock_username", model.Username) | ||||
| 				suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid) | ||||
| @@ -151,10 +151,10 @@ var postSkinTestsCases = []*postSkinTestCase{ | ||||
| 			"url":        {"http://example.com/skin.png"}, | ||||
| 		}.Encode()), | ||||
| 		BeforeTest: func(suite *apiTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUserId", 2).Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.SkinsRepository.On("RemoveByUsername", "mock_username").Times(1).Return(nil) | ||||
| 			suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool { | ||||
| 			suite.SkinsRepository.On("FindSkinByUserId", 2).Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.SkinsRepository.On("RemoveSkinByUsername", "mock_username").Times(1).Return(nil) | ||||
| 			suite.SkinsRepository.On("SaveSkin", mock.MatchedBy(func(model *model.Skin) bool { | ||||
| 				suite.Equal(2, model.UserId) | ||||
| 				suite.Equal("mock_username", model.Username) | ||||
| 				suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid) | ||||
| @@ -180,9 +180,9 @@ var postSkinTestsCases = []*postSkinTestCase{ | ||||
| 			"url":        {"http://example.com/skin.png"}, | ||||
| 		}.Encode()), | ||||
| 		BeforeTest: func(suite *apiTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.SkinsRepository.On("RemoveByUserId", 1).Times(1).Return(nil) | ||||
| 			suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool { | ||||
| 			suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.SkinsRepository.On("RemoveSkinByUserId", 1).Times(1).Return(nil) | ||||
| 			suite.SkinsRepository.On("SaveSkin", mock.MatchedBy(func(model *model.Skin) bool { | ||||
| 				suite.Equal(1, model.UserId) | ||||
| 				suite.Equal("changed_username", model.Username) | ||||
| 				suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid) | ||||
| @@ -208,9 +208,9 @@ var postSkinTestsCases = []*postSkinTestCase{ | ||||
| 			"url":        {"http://textures-server.com/skin.png"}, | ||||
| 		}.Encode()), | ||||
| 		BeforeTest: func(suite *apiTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil) | ||||
| 			err := errors.New("mock error") | ||||
| 			suite.SkinsRepository.On("Save", mock.Anything).Return(err) | ||||
| 			suite.SkinsRepository.On("SaveSkin", mock.Anything).Return(err) | ||||
| 			suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool { | ||||
| 				return cErr.Error() == "unable to save record to the repository: mock error" && | ||||
| 					errors.Is(cErr, err) | ||||
| @@ -235,7 +235,7 @@ var postSkinTestsCases = []*postSkinTestCase{ | ||||
| 		}.Encode()), | ||||
| 		BeforeTest: func(suite *apiTestSuite) { | ||||
| 			err := errors.New("mock error") | ||||
| 			suite.SkinsRepository.On("FindByUserId", 1).Return(nil, err) | ||||
| 			suite.SkinsRepository.On("FindSkinByUserId", 1).Return(nil, err) | ||||
| 			suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool { | ||||
| 				return cErr.Error() == "error on requesting a skin from the repository: mock error" && | ||||
| 					errors.Is(cErr, err) | ||||
| @@ -352,8 +352,8 @@ func (suite *apiTestSuite) TestPostSkin() { | ||||
|  | ||||
| func (suite *apiTestSuite) TestDeleteByUserId() { | ||||
| 	suite.RunSubTest("Delete skin by its identity id", func() { | ||||
| 		suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil) | ||||
| 		suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil) | ||||
| 		suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil) | ||||
| 		suite.SkinsRepository.On("RemoveSkinByUserId", 1).Once().Return(nil) | ||||
|  | ||||
| 		req := httptest.NewRequest("DELETE", "http://chrly/skins/id:1", nil) | ||||
| 		w := httptest.NewRecorder() | ||||
| @@ -368,7 +368,7 @@ func (suite *apiTestSuite) TestDeleteByUserId() { | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("Try to remove not exists identity id", func() { | ||||
| 		suite.SkinsRepository.On("FindByUserId", 1).Return(nil, nil) | ||||
| 		suite.SkinsRepository.On("FindSkinByUserId", 1).Return(nil, nil) | ||||
|  | ||||
| 		req := httptest.NewRequest("DELETE", "http://chrly/skins/id:1", nil) | ||||
| 		w := httptest.NewRecorder() | ||||
| @@ -391,8 +391,8 @@ func (suite *apiTestSuite) TestDeleteByUserId() { | ||||
|  | ||||
| func (suite *apiTestSuite) TestDeleteByUsername() { | ||||
| 	suite.RunSubTest("Delete skin by its identity username", func() { | ||||
| 		suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
| 		suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil) | ||||
| 		suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
| 		suite.SkinsRepository.On("RemoveSkinByUserId", 1).Once().Return(nil) | ||||
|  | ||||
| 		req := httptest.NewRequest("DELETE", "http://chrly/skins/mock_username", nil) | ||||
| 		w := httptest.NewRecorder() | ||||
| @@ -407,7 +407,7 @@ func (suite *apiTestSuite) TestDeleteByUsername() { | ||||
| 	}) | ||||
|  | ||||
| 	suite.RunSubTest("Try to remove not exists identity username", func() { | ||||
| 		suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 		suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
|  | ||||
| 		req := httptest.NewRequest("DELETE", "http://chrly/skins/mock_username", nil) | ||||
| 		w := httptest.NewRecorder() | ||||
|   | ||||
| @@ -14,15 +14,15 @@ import ( | ||||
| ) | ||||
|  | ||||
| type SkinsRepository interface { | ||||
| 	FindByUsername(username string) (*model.Skin, error) | ||||
| 	FindByUserId(id int) (*model.Skin, error) | ||||
| 	Save(skin *model.Skin) error | ||||
| 	RemoveByUserId(id int) error | ||||
| 	RemoveByUsername(username string) error | ||||
| 	FindSkinByUsername(username string) (*model.Skin, error) | ||||
| 	FindSkinByUserId(id int) (*model.Skin, error) | ||||
| 	SaveSkin(skin *model.Skin) error | ||||
| 	RemoveSkinByUserId(id int) error | ||||
| 	RemoveSkinByUsername(username string) error | ||||
| } | ||||
|  | ||||
| type CapesRepository interface { | ||||
| 	FindByUsername(username string) (*model.Cape, error) | ||||
| 	FindCapeByUsername(username string) (*model.Cape, error) | ||||
| } | ||||
|  | ||||
| type MojangTexturesProvider interface { | ||||
| @@ -54,7 +54,7 @@ func (ctx *Skinsystem) Handler() *mux.Router { | ||||
|  | ||||
| func (ctx *Skinsystem) skinHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 	username := parseUsername(mux.Vars(request)["username"]) | ||||
| 	rec, err := ctx.SkinsRepo.FindByUsername(username) | ||||
| 	rec, err := ctx.SkinsRepo.FindSkinByUsername(username) | ||||
| 	if err == nil && rec != nil && rec.SkinId != 0 { | ||||
| 		http.Redirect(response, request, rec.Url, 301) | ||||
| 		return | ||||
| @@ -91,7 +91,7 @@ func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *htt | ||||
|  | ||||
| func (ctx *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 	username := parseUsername(mux.Vars(request)["username"]) | ||||
| 	rec, err := ctx.CapesRepo.FindByUsername(username) | ||||
| 	rec, err := ctx.CapesRepo.FindCapeByUsername(username) | ||||
| 	if err == nil && rec != nil { | ||||
| 		request.Header.Set("Content-Type", "image/png") | ||||
| 		_, _ = io.Copy(response, rec.File) | ||||
| @@ -131,8 +131,8 @@ func (ctx *Skinsystem) texturesHandler(response http.ResponseWriter, request *ht | ||||
| 	username := parseUsername(mux.Vars(request)["username"]) | ||||
|  | ||||
| 	var textures *mojang.TexturesResponse | ||||
| 	skin, skinErr := ctx.SkinsRepo.FindByUsername(username) | ||||
| 	cape, capeErr := ctx.CapesRepo.FindByUsername(username) | ||||
| 	skin, skinErr := ctx.SkinsRepo.FindSkinByUsername(username) | ||||
| 	cape, capeErr := ctx.CapesRepo.FindCapeByUsername(username) | ||||
| 	if (skinErr == nil && skin != nil && skin.SkinId != 0) || (capeErr == nil && cape != nil) { | ||||
| 		textures = &mojang.TexturesResponse{} | ||||
| 		if skinErr == nil && skin != nil && skin.SkinId != 0 { | ||||
| @@ -185,7 +185,7 @@ func (ctx *Skinsystem) signedTexturesHandler(response http.ResponseWriter, reque | ||||
|  | ||||
| 	var responseData *mojang.SignedTexturesResponse | ||||
|  | ||||
| 	rec, err := ctx.SkinsRepo.FindByUsername(username) | ||||
| 	rec, err := ctx.SkinsRepo.FindSkinByUsername(username) | ||||
| 	if err == nil && rec != nil && rec.SkinId != 0 && rec.MojangTextures != "" { | ||||
| 		responseData = &mojang.SignedTexturesResponse{ | ||||
| 			Id:   strings.Replace(rec.Uuid, "-", "", -1), | ||||
|   | ||||
| @@ -26,7 +26,7 @@ type skinsRepositoryMock struct { | ||||
| 	mock.Mock | ||||
| } | ||||
|  | ||||
| func (m *skinsRepositoryMock) FindByUsername(username string) (*model.Skin, error) { | ||||
| func (m *skinsRepositoryMock) FindSkinByUsername(username string) (*model.Skin, error) { | ||||
| 	args := m.Called(username) | ||||
| 	var result *model.Skin | ||||
| 	if casted, ok := args.Get(0).(*model.Skin); ok { | ||||
| @@ -36,7 +36,7 @@ func (m *skinsRepositoryMock) FindByUsername(username string) (*model.Skin, erro | ||||
| 	return result, args.Error(1) | ||||
| } | ||||
|  | ||||
| func (m *skinsRepositoryMock) FindByUserId(id int) (*model.Skin, error) { | ||||
| func (m *skinsRepositoryMock) FindSkinByUserId(id int) (*model.Skin, error) { | ||||
| 	args := m.Called(id) | ||||
| 	var result *model.Skin | ||||
| 	if casted, ok := args.Get(0).(*model.Skin); ok { | ||||
| @@ -46,17 +46,17 @@ func (m *skinsRepositoryMock) FindByUserId(id int) (*model.Skin, error) { | ||||
| 	return result, args.Error(1) | ||||
| } | ||||
|  | ||||
| func (m *skinsRepositoryMock) Save(skin *model.Skin) error { | ||||
| func (m *skinsRepositoryMock) SaveSkin(skin *model.Skin) error { | ||||
| 	args := m.Called(skin) | ||||
| 	return args.Error(0) | ||||
| } | ||||
|  | ||||
| func (m *skinsRepositoryMock) RemoveByUserId(id int) error { | ||||
| func (m *skinsRepositoryMock) RemoveSkinByUserId(id int) error { | ||||
| 	args := m.Called(id) | ||||
| 	return args.Error(0) | ||||
| } | ||||
|  | ||||
| func (m *skinsRepositoryMock) RemoveByUsername(username string) error { | ||||
| func (m *skinsRepositoryMock) RemoveSkinByUsername(username string) error { | ||||
| 	args := m.Called(username) | ||||
| 	return args.Error(0) | ||||
| } | ||||
| @@ -65,7 +65,7 @@ type capesRepositoryMock struct { | ||||
| 	mock.Mock | ||||
| } | ||||
|  | ||||
| func (m *capesRepositoryMock) FindByUsername(username string) (*model.Cape, error) { | ||||
| func (m *capesRepositoryMock) FindCapeByUsername(username string) (*model.Cape, error) { | ||||
| 	args := m.Called(username) | ||||
| 	var result *model.Cape | ||||
| 	if casted, ok := args.Get(0).(*model.Cape); ok { | ||||
| @@ -155,7 +155,7 @@ var skinsTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username exists in the local storage", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| 			suite.Equal(301, response.StatusCode) | ||||
| @@ -165,7 +165,7 @@ var skinsTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| @@ -176,7 +176,7 @@ var skinsTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| @@ -186,7 +186,7 @@ var skinsTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username doesn't exists on the local storage and doesn't exists on Mojang", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| @@ -210,7 +210,7 @@ func (suite *skinsystemTestSuite) TestSkin() { | ||||
| 	} | ||||
|  | ||||
| 	suite.RunSubTest("Pass username with png extension", func() { | ||||
| 		suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
| 		suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
|  | ||||
| 		req := httptest.NewRequest("GET", "http://chrly/skins/mock_username.png", nil) | ||||
| 		w := httptest.NewRecorder() | ||||
| @@ -257,7 +257,7 @@ var capesTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username exists in the local storage", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil) | ||||
| 			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| 			suite.Equal(200, response.StatusCode) | ||||
| @@ -269,7 +269,7 @@ var capesTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, true), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| @@ -280,7 +280,7 @@ var capesTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| @@ -290,7 +290,7 @@ var capesTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username doesn't exists on the local storage and doesn't exists on Mojang", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| @@ -314,7 +314,7 @@ func (suite *skinsystemTestSuite) TestCape() { | ||||
| 	} | ||||
|  | ||||
| 	suite.RunSubTest("Pass username with png extension", func() { | ||||
| 		suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil) | ||||
| 		suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil) | ||||
|  | ||||
| 		req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username.png", nil) | ||||
| 		w := httptest.NewRecorder() | ||||
| @@ -363,8 +363,8 @@ var texturesTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username exists and has skin, no cape", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| 			suite.Equal(200, response.StatusCode) | ||||
| @@ -380,8 +380,8 @@ var texturesTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username exists and has slim skin, no cape", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil) | ||||
| 			suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil) | ||||
| 			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| 			suite.Equal(200, response.StatusCode) | ||||
| @@ -400,8 +400,8 @@ var texturesTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username exists and has cape, no skin", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| 			suite.Equal(200, response.StatusCode) | ||||
| @@ -417,8 +417,8 @@ var texturesTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username exists and has both skin and cape", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) | ||||
| 			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| 			suite.Equal(200, response.StatusCode) | ||||
| @@ -437,8 +437,8 @@ var texturesTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username not exists, but Mojang profile available", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponse(true, true), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| @@ -458,8 +458,8 @@ var texturesTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username not exists, but Mojang profile available, but there is no textures", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponse(false, false), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| @@ -469,8 +469,8 @@ var texturesTestsCases = []*skinsystemTestCase{ | ||||
| 	{ | ||||
| 		Name: "Username not exists and Mojang profile unavailable", | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| @@ -512,7 +512,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{ | ||||
| 		Name:       "Username exists", | ||||
| 		AllowProxy: false, | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| 			suite.Equal(200, response.StatusCode) | ||||
| @@ -539,7 +539,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{ | ||||
| 		Name:       "Username not exists", | ||||
| 		AllowProxy: false, | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| 			suite.Equal(204, response.StatusCode) | ||||
| @@ -554,7 +554,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{ | ||||
| 			skinModel := createSkinModel("mock_username", true) | ||||
| 			skinModel.MojangTextures = "" | ||||
| 			skinModel.MojangSignature = "" | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(skinModel, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skinModel, nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| 			suite.Equal(204, response.StatusCode) | ||||
| @@ -566,7 +566,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{ | ||||
| 		Name:       "Username not exists, but Mojang profile is available and proxying is enabled", | ||||
| 		AllowProxy: true, | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
| @@ -593,7 +593,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{ | ||||
| 		Name:       "Username not exists, Mojang profile is unavailable too and proxying is enabled", | ||||
| 		AllowProxy: true, | ||||
| 		BeforeTest: func(suite *skinsystemTestSuite) { | ||||
| 			suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) | ||||
| 			suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil) | ||||
| 		}, | ||||
| 		AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user