mirror of
https://github.com/elyby/chrly.git
synced 2025-05-31 14:11:51 +05:30
@@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- `ely.skinsystem.{hostname}.app.mojang_textures.usernames.textures_miss`
|
- `ely.skinsystem.{hostname}.app.mojang_textures.usernames.textures_miss`
|
||||||
- All incoming requests are now logging to the console in
|
- All incoming requests are now logging to the console in
|
||||||
[Apache Common Log Format](http://httpd.apache.org/docs/2.2/logs.html#common).
|
[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).
|
||||||
|
- Graceful server shutdown.
|
||||||
|
- Panics in http are now logged in Sentry.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- `ely.skinsystem.{hostname}.app.mojang_textures.usernames.iteration_size` and
|
- `ely.skinsystem.{hostname}.app.mojang_textures.usernames.iteration_size` and
|
||||||
|
13
Gopkg.lock
generated
13
Gopkg.lock
generated
@@ -63,6 +63,18 @@
|
|||||||
pruneopts = ""
|
pruneopts = ""
|
||||||
revision = "919484f041ea21e7e27be291cee1d6af7bc98864"
|
revision = "919484f041ea21e7e27be291cee1d6af7bc98864"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:8c9f13aac9e92f3754ea591b39ada87b9f89f1e75c4b90ccbd0b1084069c436f"
|
||||||
|
name = "github.com/goava/di"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"internal/reflection",
|
||||||
|
"internal/stacktrace",
|
||||||
|
]
|
||||||
|
pruneopts = ""
|
||||||
|
revision = "1eb6eb721bf050edff0efbf15c31636def701b4b"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:65c7ed49d9f36dd4752e43013323fa9229db60b29aa4f5a75aaecda3130c74e2"
|
digest = "1:65c7ed49d9f36dd4752e43013323fa9229db60b29aa4f5a75aaecda3130c74e2"
|
||||||
name = "github.com/gorilla/mux"
|
name = "github.com/gorilla/mux"
|
||||||
@@ -321,6 +333,7 @@
|
|||||||
"github.com/asaskevich/EventBus",
|
"github.com/asaskevich/EventBus",
|
||||||
"github.com/etherlabsio/healthcheck",
|
"github.com/etherlabsio/healthcheck",
|
||||||
"github.com/getsentry/raven-go",
|
"github.com/getsentry/raven-go",
|
||||||
|
"github.com/goava/di",
|
||||||
"github.com/gorilla/mux",
|
"github.com/gorilla/mux",
|
||||||
"github.com/h2non/gock",
|
"github.com/h2non/gock",
|
||||||
"github.com/mediocregopher/radix.v2/pool",
|
"github.com/mediocregopher/radix.v2/pool",
|
||||||
|
@@ -45,6 +45,10 @@ ignored = ["github.com/elyby/chrly"]
|
|||||||
name = "github.com/etherlabsio/healthcheck"
|
name = "github.com/etherlabsio/healthcheck"
|
||||||
version = "2.0.3"
|
version = "2.0.3"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/goava/di"
|
||||||
|
branch = "master"
|
||||||
|
|
||||||
# Testing dependencies
|
# Testing dependencies
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
|
51
README.md
51
README.md
@@ -122,7 +122,7 @@ docker-compose up -d app
|
|||||||
<td><code>true</code></td>
|
<td><code>true</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>MOJANG_TEXTURES_UUIDS_PROVIDER_DRIVER</td>
|
<td id="remote-mojang-uuids-provider">MOJANG_TEXTURES_UUIDS_PROVIDER_DRIVER</td>
|
||||||
<td>
|
<td>
|
||||||
Specifies the preferred provider of the Mojang's UUIDs. Takes <code>remote</code> value.
|
Specifies the preferred provider of the Mojang's UUIDs. Takes <code>remote</code> value.
|
||||||
In any other case, the local queue will be used.
|
In any other case, the local queue will be used.
|
||||||
@@ -308,6 +308,55 @@ response will be:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Worker mode
|
||||||
|
|
||||||
|
The worker mode can be used in cooperation with the [remote server mode](#remote-mojang-uuids-provider)
|
||||||
|
to exchange Mojang usernames to UUIDs. This mode by itself doesn't solve the problem of
|
||||||
|
[extremely strict limits](https://github.com/elyby/chrly/issues/10) on the number of requests to the Mojang's API.
|
||||||
|
But with a proxying load balancer (e.g. HAProxy, Nginx, etc.) it's easy to build a cluster of workers,
|
||||||
|
which will multiply the bandwidth of the exchanging usernames to its UUIDs.
|
||||||
|
|
||||||
|
The instructions for setting up a proxy load balancer are outside the context of this documentation,
|
||||||
|
but you get the idea ;)
|
||||||
|
|
||||||
|
#### `GET /api/worker/mojang-uuid/{username}`
|
||||||
|
|
||||||
|
Performs [batch usernames exchange to UUIDs](https://github.com/elyby/chrly/issues/1) and returns the result in the
|
||||||
|
[same format as it returns from the Mojang's API](https://wiki.vg/Mojang_API#Username_-.3E_UUID_at_time):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "3e3ee6c35afa48abb61e8cd8c42fc0d9",
|
||||||
|
"name": "ErickSkrauch"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note**: the results aren't cached.
|
||||||
|
|
||||||
|
### Health check
|
||||||
|
|
||||||
|
#### `GET /healthcheck`
|
||||||
|
|
||||||
|
This endpoint can be used to programmatically check the status of the server.
|
||||||
|
If all internal checks are successful, the server will return `200` status code with the following body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "OK"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If any of the checks fails, the server will return `503` status code with the following body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "Service Unavailable",
|
||||||
|
"errors": {
|
||||||
|
"mojang-batch-uuids-provider-queue-length": "the maximum number of tasks in the queue has been exceeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
First of all you should install the [latest stable version of Go](https://golang.org/doc/install) and set `GOPATH`
|
First of all you should install the [latest stable version of Go](https://golang.org/doc/install) and set `GOPATH`
|
||||||
|
@@ -1,103 +0,0 @@
|
|||||||
package bootstrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/getsentry/raven-go"
|
|
||||||
"github.com/mono83/slf"
|
|
||||||
"github.com/mono83/slf/rays"
|
|
||||||
"github.com/mono83/slf/recievers/sentry"
|
|
||||||
"github.com/mono83/slf/recievers/statsd"
|
|
||||||
"github.com/mono83/slf/recievers/writer"
|
|
||||||
"github.com/mono83/slf/wd"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"github.com/elyby/chrly/dispatcher"
|
|
||||||
"github.com/elyby/chrly/http"
|
|
||||||
"github.com/elyby/chrly/mojangtextures"
|
|
||||||
"github.com/elyby/chrly/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CreateLogger(sentryAddr string) (slf.Logger, error) {
|
|
||||||
wd.AddReceiver(writer.New(writer.Options{
|
|
||||||
Marker: false,
|
|
||||||
TimeFormat: "15:04:05.000",
|
|
||||||
}))
|
|
||||||
|
|
||||||
if sentryAddr != "" {
|
|
||||||
ravenClient, err := raven.New(sentryAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ravenClient.SetEnvironment("production")
|
|
||||||
ravenClient.SetDefaultLoggerName("sentry-watchdog-receiver")
|
|
||||||
programVersion := version.Version()
|
|
||||||
if programVersion != "" {
|
|
||||||
raven.SetRelease(programVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
sentryReceiver, err := sentry.NewReceiverWithCustomRaven(ravenClient, &sentry.Config{
|
|
||||||
MinLevel: "warn",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
wd.AddReceiver(sentryReceiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
return wd.New("", "").WithParams(rays.Host), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateStatsReceiver(statsdAddr string) (slf.StatsReporter, error) {
|
|
||||||
hostname, _ := os.Hostname()
|
|
||||||
statsdReceiver, err := statsd.NewReceiver(statsd.Config{
|
|
||||||
Address: statsdAddr,
|
|
||||||
Prefix: "ely.skinsystem." + hostname + ".app.",
|
|
||||||
FlushEvery: 1,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
wd.AddReceiver(statsdReceiver)
|
|
||||||
|
|
||||||
return wd.New("", "").WithParams(rays.Host), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
viper.SetDefault("queue.loop_delay", 2*time.Second+500*time.Millisecond)
|
|
||||||
viper.SetDefault("queue.batch_size", 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateMojangUUIDsProvider(emitter http.Emitter) (mojangtextures.UUIDsProvider, error) {
|
|
||||||
var uuidsProvider mojangtextures.UUIDsProvider
|
|
||||||
preferredUuidsProvider := viper.GetString("mojang_textures.uuids_provider.driver")
|
|
||||||
if preferredUuidsProvider == "remote" {
|
|
||||||
remoteUrl, err := url.Parse(viper.GetString("mojang_textures.uuids_provider.url"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Unable to parse remote url: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
uuidsProvider = &mojangtextures.RemoteApiUuidsProvider{
|
|
||||||
Emitter: emitter,
|
|
||||||
Url: *remoteUrl,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uuidsProvider = &mojangtextures.BatchUuidsProvider{
|
|
||||||
Emitter: emitter,
|
|
||||||
IterationDelay: viper.GetDuration("queue.loop_delay"),
|
|
||||||
IterationSize: viper.GetInt("queue.batch_size"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return uuidsProvider, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateEventDispatcher() dispatcher.EventDispatcher {
|
|
||||||
return dispatcher.New()
|
|
||||||
}
|
|
39
cmd/root.go
39
cmd/root.go
@@ -2,14 +2,16 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
|
|
||||||
|
. "github.com/goava/di"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"github.com/elyby/chrly/di"
|
||||||
|
"github.com/elyby/chrly/http"
|
||||||
"github.com/elyby/chrly/version"
|
"github.com/elyby/chrly/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +30,32 @@ func Execute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldGetContainer() *Container {
|
||||||
|
container, err := di.New()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServer(modules []string) {
|
||||||
|
container := shouldGetContainer()
|
||||||
|
|
||||||
|
var config *viper.Viper
|
||||||
|
err := container.Resolve(&config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Set("modules", modules)
|
||||||
|
|
||||||
|
err = container.Invoke(http.StartServer)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
}
|
}
|
||||||
@@ -37,10 +65,3 @@ func initConfig() {
|
|||||||
replacer := strings.NewReplacer(".", "_")
|
replacer := strings.NewReplacer(".", "_")
|
||||||
viper.SetEnvKeyReplacer(replacer)
|
viper.SetEnvKeyReplacer(replacer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForExitSignal() os.Signal {
|
|
||||||
ch := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
|
|
||||||
return <-ch
|
|
||||||
}
|
|
||||||
|
127
cmd/serve.go
127
cmd/serve.go
@@ -1,142 +1,17 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/mono83/slf/wd"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"github.com/elyby/chrly/bootstrap"
|
|
||||||
"github.com/elyby/chrly/db"
|
|
||||||
"github.com/elyby/chrly/eventsubscribers"
|
|
||||||
"github.com/elyby/chrly/http"
|
|
||||||
"github.com/elyby/chrly/mojangtextures"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var serveCmd = &cobra.Command{
|
var serveCmd = &cobra.Command{
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
Short: "Starts HTTP handler for the skins system",
|
Short: "Starts HTTP handler for the skins system",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
dispatcher := bootstrap.CreateEventDispatcher()
|
startServer([]string{"skinsystem", "api"})
|
||||||
|
|
||||||
// TODO: this is a mess, need to organize this code somehow to make services initialization more compact
|
|
||||||
logger, err := bootstrap.CreateLogger(viper.GetString("sentry.dsn"))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Cannot initialize logger: %v", err)
|
|
||||||
}
|
|
||||||
logger.Info("Logger successfully initialized")
|
|
||||||
|
|
||||||
(&eventsubscribers.Logger{Logger: logger}).ConfigureWithDispatcher(dispatcher)
|
|
||||||
|
|
||||||
statsdAddr := viper.GetString("statsd.addr")
|
|
||||||
if statsdAddr != "" {
|
|
||||||
statsdReporter, err := bootstrap.CreateStatsReceiver(statsdAddr)
|
|
||||||
if err != nil {
|
|
||||||
logger.Emergency("Invalid statsd configuration :err", wd.ErrParam(err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
(&eventsubscribers.StatsReporter{StatsReporter: statsdReporter}).ConfigureWithDispatcher(dispatcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
storageFactory := db.StorageFactory{Config: viper.GetViper()}
|
|
||||||
|
|
||||||
logger.Info("Initializing skins repository")
|
|
||||||
redisFactory := storageFactory.CreateFactory("redis")
|
|
||||||
skinsRepo, err := redisFactory.CreateSkinsRepository()
|
|
||||||
if err != nil {
|
|
||||||
logger.Emergency("Error on creating skins repo: :err", wd.ErrParam(err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
logger.Info("Skins repository successfully initialized")
|
|
||||||
|
|
||||||
logger.Info("Initializing capes repository")
|
|
||||||
filesystemFactory := storageFactory.CreateFactory("filesystem")
|
|
||||||
capesRepo, err := filesystemFactory.CreateCapesRepository()
|
|
||||||
if err != nil {
|
|
||||||
logger.Emergency("Error on creating capes repo: :err", wd.ErrParam(err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
logger.Info("Capes repository successfully initialized")
|
|
||||||
|
|
||||||
var mojangTexturesProvider http.MojangTexturesProvider
|
|
||||||
if viper.GetBool("mojang_textures.enabled") {
|
|
||||||
logger.Info("Preparing Mojang's textures queue")
|
|
||||||
mojangUuidsRepository, err := redisFactory.CreateMojangUuidsRepository()
|
|
||||||
if err != nil {
|
|
||||||
logger.Emergency("Error on creating mojang uuids repo: :err", wd.ErrParam(err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
uuidsProvider, err := bootstrap.CreateMojangUUIDsProvider(dispatcher)
|
|
||||||
if err != nil {
|
|
||||||
logger.Emergency("Unable to create mojang uuids provider: :err", wd.ErrParam(err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
texturesStorage := mojangtextures.NewInMemoryTexturesStorage()
|
|
||||||
texturesStorage.Start()
|
|
||||||
mojangTexturesProvider = &mojangtextures.Provider{
|
|
||||||
Emitter: dispatcher,
|
|
||||||
UUIDsProvider: uuidsProvider,
|
|
||||||
TexturesProvider: &mojangtextures.MojangApiTexturesProvider{
|
|
||||||
Emitter: dispatcher,
|
|
||||||
},
|
|
||||||
Storage: &mojangtextures.SeparatedStorage{
|
|
||||||
UuidsStorage: mojangUuidsRepository,
|
|
||||||
TexturesStorage: texturesStorage,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
logger.Info("Mojang's textures queue is successfully initialized")
|
|
||||||
} else {
|
|
||||||
logger.Info("Mojang's textures queue is disabled")
|
|
||||||
mojangTexturesProvider = &mojangtextures.NilProvider{}
|
|
||||||
}
|
|
||||||
|
|
||||||
address := fmt.Sprintf("%s:%d", viper.GetString("server.host"), viper.GetInt("server.port"))
|
|
||||||
handler := (&http.Skinsystem{
|
|
||||||
Emitter: dispatcher,
|
|
||||||
SkinsRepo: skinsRepo,
|
|
||||||
CapesRepo: capesRepo,
|
|
||||||
MojangTexturesProvider: mojangTexturesProvider,
|
|
||||||
Authenticator: &http.JwtAuth{
|
|
||||||
Key: []byte(viper.GetString("chrly.secret")),
|
|
||||||
Emitter: dispatcher,
|
|
||||||
},
|
|
||||||
TexturesExtraParamName: viper.GetString("textures.extra_param_name"),
|
|
||||||
TexturesExtraParamValue: viper.GetString("textures.extra_param_value"),
|
|
||||||
}).CreateHandler()
|
|
||||||
|
|
||||||
finishChan := make(chan bool)
|
|
||||||
go func() {
|
|
||||||
logger.Info("Starting the app, HTTP on: :addr", wd.StringParam("addr", address))
|
|
||||||
if err := http.Serve(address, handler); err != nil {
|
|
||||||
logger.Emergency("Error in main(): :err", wd.ErrParam(err))
|
|
||||||
finishChan <- true
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
s := waitForExitSignal()
|
|
||||||
logger.Info("Got signal: :signal, exiting", wd.StringParam("signal", s.String()))
|
|
||||||
finishChan <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-finishChan
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.AddCommand(serveCmd)
|
RootCmd.AddCommand(serveCmd)
|
||||||
viper.SetDefault("server.host", "")
|
|
||||||
viper.SetDefault("server.port", 80)
|
|
||||||
viper.SetDefault("storage.redis.host", "localhost")
|
|
||||||
viper.SetDefault("storage.redis.port", 6379)
|
|
||||||
viper.SetDefault("storage.redis.poll", 10)
|
|
||||||
viper.SetDefault("storage.filesystem.basePath", "data")
|
|
||||||
viper.SetDefault("storage.filesystem.capesDirName", "capes")
|
|
||||||
viper.SetDefault("mojang_textures.enabled", true)
|
|
||||||
}
|
}
|
||||||
|
11
cmd/token.go
11
cmd/token.go
@@ -7,15 +7,20 @@ import (
|
|||||||
"github.com/elyby/chrly/http"
|
"github.com/elyby/chrly/http"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var tokenCmd = &cobra.Command{
|
var tokenCmd = &cobra.Command{
|
||||||
Use: "token",
|
Use: "token",
|
||||||
Short: "Creates a new token, which allows to interact with Chrly API",
|
Short: "Creates a new token, which allows to interact with Chrly API",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
jwtAuth := &http.JwtAuth{Key: []byte(viper.GetString("chrly.secret"))}
|
container := shouldGetContainer()
|
||||||
token, err := jwtAuth.NewToken(http.SkinScope)
|
var auth *http.JwtAuth
|
||||||
|
err := container.Resolve(&auth)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := auth.NewToken(http.SkinScope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Unable to create new token. The error is %v\n", err)
|
log.Fatalf("Unable to create new token. The error is %v\n", err)
|
||||||
}
|
}
|
||||||
|
@@ -1,96 +1,17 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/etherlabsio/healthcheck"
|
|
||||||
"github.com/mono83/slf/wd"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"github.com/elyby/chrly/bootstrap"
|
|
||||||
"github.com/elyby/chrly/eventsubscribers"
|
|
||||||
"github.com/elyby/chrly/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var workerCmd = &cobra.Command{
|
var workerCmd = &cobra.Command{
|
||||||
Use: "worker",
|
Use: "worker",
|
||||||
Short: "Starts HTTP handler for the Mojang usernames to UUIDs worker",
|
Short: "Starts HTTP handler for the Mojang usernames to UUIDs worker",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
dispatcher := bootstrap.CreateEventDispatcher()
|
startServer([]string{"worker"})
|
||||||
|
|
||||||
// TODO: need to find a way to unify this initialization with the serve command
|
|
||||||
logger, err := bootstrap.CreateLogger(viper.GetString("sentry.dsn"))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Cannot initialize logger: %v", err)
|
|
||||||
}
|
|
||||||
logger.Info("Logger successfully initialized")
|
|
||||||
|
|
||||||
(&eventsubscribers.Logger{Logger: logger}).ConfigureWithDispatcher(dispatcher)
|
|
||||||
|
|
||||||
statsdAddr := viper.GetString("statsd.addr")
|
|
||||||
if statsdAddr != "" {
|
|
||||||
statsdReporter, err := bootstrap.CreateStatsReceiver(statsdAddr)
|
|
||||||
if err != nil {
|
|
||||||
logger.Emergency("Invalid statsd configuration :err", wd.ErrParam(err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
(&eventsubscribers.StatsReporter{StatsReporter: statsdReporter}).ConfigureWithDispatcher(dispatcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
uuidsProvider, err := bootstrap.CreateMojangUUIDsProvider(dispatcher)
|
|
||||||
if err != nil {
|
|
||||||
logger.Emergency("Unable to parse remote url :err", wd.ErrParam(err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
address := fmt.Sprintf("%s:%d", viper.GetString("server.host"), viper.GetInt("server.port"))
|
|
||||||
handler := (&http.UUIDsWorker{
|
|
||||||
Emitter: dispatcher,
|
|
||||||
UUIDsProvider: uuidsProvider,
|
|
||||||
}).CreateHandler()
|
|
||||||
handler.Handle("/healthcheck", healthcheck.Handler(
|
|
||||||
healthcheck.WithChecker(
|
|
||||||
"mojang-batch-uuids-provider-response",
|
|
||||||
eventsubscribers.MojangBatchUuidsProviderResponseChecker(
|
|
||||||
dispatcher,
|
|
||||||
viper.GetDuration("healthcheck.mojang_batch_uuids_provider_cool_down_duration"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
healthcheck.WithChecker(
|
|
||||||
"mojang-batch-uuids-provider-queue-length",
|
|
||||||
eventsubscribers.MojangBatchUuidsProviderQueueLengthChecker(
|
|
||||||
dispatcher,
|
|
||||||
viper.GetInt("healthcheck.mojang_batch_uuids_provider_queue_length_limit"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)).Methods("GET")
|
|
||||||
|
|
||||||
finishChan := make(chan bool)
|
|
||||||
go func() {
|
|
||||||
logger.Info("Starting the worker, HTTP on: :addr", wd.StringParam("addr", address))
|
|
||||||
if err := http.Serve(address, handler); err != nil {
|
|
||||||
logger.Error("Error in main(): :err", wd.ErrParam(err))
|
|
||||||
finishChan <- true
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
s := waitForExitSignal()
|
|
||||||
logger.Info("Got signal: :code, exiting.", wd.StringParam("code", s.String()))
|
|
||||||
finishChan <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-finishChan
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.AddCommand(workerCmd)
|
RootCmd.AddCommand(workerCmd)
|
||||||
viper.SetDefault("healthcheck.mojang_batch_uuids_provider_cool_down_duration", time.Minute)
|
|
||||||
viper.SetDefault("healthcheck.mojang_batch_uuids_provider_queue_length_limit", 50)
|
|
||||||
}
|
}
|
||||||
|
@@ -2,35 +2,11 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/elyby/chrly/http"
|
"github.com/elyby/chrly/http"
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"github.com/elyby/chrly/mojangtextures"
|
"github.com/elyby/chrly/mojangtextures"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StorageFactory struct {
|
|
||||||
Config *viper.Viper
|
|
||||||
}
|
|
||||||
|
|
||||||
type RepositoriesCreator interface {
|
type RepositoriesCreator interface {
|
||||||
CreateSkinsRepository() (http.SkinsRepository, error)
|
CreateSkinsRepository() (http.SkinsRepository, error)
|
||||||
CreateCapesRepository() (http.CapesRepository, error)
|
CreateCapesRepository() (http.CapesRepository, error)
|
||||||
CreateMojangUuidsRepository() (mojangtextures.UuidsStorage, error)
|
CreateMojangUuidsRepository() (mojangtextures.UuidsStorage, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *StorageFactory) CreateFactory(backend string) RepositoriesCreator {
|
|
||||||
switch backend {
|
|
||||||
case "redis":
|
|
||||||
return &RedisFactory{
|
|
||||||
Host: factory.Config.GetString("storage.redis.host"),
|
|
||||||
Port: factory.Config.GetInt("storage.redis.port"),
|
|
||||||
PoolSize: factory.Config.GetInt("storage.redis.poolSize"),
|
|
||||||
}
|
|
||||||
case "filesystem":
|
|
||||||
return &FilesystemFactory{
|
|
||||||
BasePath: factory.Config.GetString("storage.filesystem.basePath"),
|
|
||||||
CapesDirName: factory.Config.GetString("storage.filesystem.capesDirName"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@@ -49,13 +49,17 @@ type filesStorage struct {
|
|||||||
|
|
||||||
func (repository *filesStorage) FindByUsername(username string) (*model.Cape, error) {
|
func (repository *filesStorage) FindByUsername(username string) (*model.Cape, error) {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return nil, &http.CapeNotFoundError{Who: username}
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
capePath := path.Join(repository.path, strings.ToLower(username)+".png")
|
capePath := path.Join(repository.path, strings.ToLower(username)+".png")
|
||||||
file, err := os.Open(capePath)
|
file, err := os.Open(capePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &http.CapeNotFoundError{Who: username}
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &model.Cape{
|
return &model.Cape{
|
||||||
|
31
db/redis.go
31
db/redis.go
@@ -5,7 +5,6 @@ import (
|
|||||||
"compress/zlib"
|
"compress/zlib"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/elyby/chrly/http"
|
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -15,6 +14,7 @@ import (
|
|||||||
"github.com/mediocregopher/radix.v2/redis"
|
"github.com/mediocregopher/radix.v2/redis"
|
||||||
"github.com/mediocregopher/radix.v2/util"
|
"github.com/mediocregopher/radix.v2/util"
|
||||||
|
|
||||||
|
"github.com/elyby/chrly/http"
|
||||||
"github.com/elyby/chrly/model"
|
"github.com/elyby/chrly/model"
|
||||||
"github.com/elyby/chrly/mojangtextures"
|
"github.com/elyby/chrly/mojangtextures"
|
||||||
)
|
)
|
||||||
@@ -147,14 +147,10 @@ func (db *redisDb) StoreUuid(username string, uuid string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findByUsername(username string, conn util.Cmder) (*model.Skin, error) {
|
func findByUsername(username string, conn util.Cmder) (*model.Skin, error) {
|
||||||
if username == "" {
|
|
||||||
return nil, &http.SkinNotFoundError{Who: username}
|
|
||||||
}
|
|
||||||
|
|
||||||
redisKey := buildUsernameKey(username)
|
redisKey := buildUsernameKey(username)
|
||||||
response := conn.Cmd("GET", redisKey)
|
response := conn.Cmd("GET", redisKey)
|
||||||
if !response.IsType(redis.Str) {
|
if response.IsType(redis.Nil) {
|
||||||
return nil, &http.SkinNotFoundError{Who: username}
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
encodedResult, err := response.Bytes()
|
encodedResult, err := response.Bytes()
|
||||||
@@ -180,11 +176,14 @@ func findByUsername(username string, conn util.Cmder) (*model.Skin, error) {
|
|||||||
|
|
||||||
func findByUserId(id int, conn util.Cmder) (*model.Skin, error) {
|
func findByUserId(id int, conn util.Cmder) (*model.Skin, error) {
|
||||||
response := conn.Cmd("HGET", accountIdToUsernameKey, id)
|
response := conn.Cmd("HGET", accountIdToUsernameKey, id)
|
||||||
if !response.IsType(redis.Str) {
|
if response.IsType(redis.Nil) {
|
||||||
return nil, &http.SkinNotFoundError{Who: "unknown"}
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
username, _ := response.Str()
|
username, err := response.Str()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return findByUsername(username, conn)
|
return findByUsername(username, conn)
|
||||||
}
|
}
|
||||||
@@ -192,9 +191,7 @@ func findByUserId(id int, conn util.Cmder) (*model.Skin, error) {
|
|||||||
func removeByUserId(id int, conn util.Cmder) error {
|
func removeByUserId(id int, conn util.Cmder) error {
|
||||||
record, err := findByUserId(id, conn)
|
record, err := findByUserId(id, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*http.SkinNotFoundError); !ok {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Cmd("MULTI")
|
conn.Cmd("MULTI")
|
||||||
@@ -212,13 +209,13 @@ func removeByUserId(id int, conn util.Cmder) error {
|
|||||||
func removeByUsername(username string, conn util.Cmder) error {
|
func removeByUsername(username string, conn util.Cmder) error {
|
||||||
record, err := findByUsername(username, conn)
|
record, err := findByUsername(username, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*http.SkinNotFoundError); ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if record == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
conn.Cmd("MULTI")
|
conn.Cmd("MULTI")
|
||||||
|
|
||||||
conn.Cmd("DEL", buildUsernameKey(record.Username))
|
conn.Cmd("DEL", buildUsernameKey(record.Username))
|
||||||
|
14
di/config.go
Normal file
14
di/config.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package di
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goava/di"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var config = di.Options(
|
||||||
|
di.Provide(newConfig),
|
||||||
|
)
|
||||||
|
|
||||||
|
func newConfig() *viper.Viper {
|
||||||
|
return viper.GetViper()
|
||||||
|
}
|
66
di/db.go
Normal file
66
di/db.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package di
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goava/di"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
. "github.com/elyby/chrly/db"
|
||||||
|
"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.
|
||||||
|
|
||||||
|
func newSkinsRepository(factory *RedisFactory) (http.SkinsRepository, error) {
|
||||||
|
return factory.CreateSkinsRepository()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCapesRepository(factory *FilesystemFactory) (http.CapesRepository, error) {
|
||||||
|
return factory.CreateCapesRepository()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMojangUUIDsRepository(factory *RedisFactory) (mojangtextures.UuidsStorage, error) {
|
||||||
|
return factory.CreateMojangUuidsRepository()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMojangSignedTexturesStorage() mojangtextures.TexturesStorage {
|
||||||
|
texturesStorage := mojangtextures.NewInMemoryTexturesStorage()
|
||||||
|
texturesStorage.Start()
|
||||||
|
|
||||||
|
return texturesStorage
|
||||||
|
}
|
20
di/di.go
Normal file
20
di/di.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package di
|
||||||
|
|
||||||
|
import "github.com/goava/di"
|
||||||
|
|
||||||
|
func New() (*di.Container, error) {
|
||||||
|
container, err := di.New(
|
||||||
|
config,
|
||||||
|
dispatcher,
|
||||||
|
logger,
|
||||||
|
db,
|
||||||
|
mojangTextures,
|
||||||
|
handlers,
|
||||||
|
server,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return container, nil
|
||||||
|
}
|
36
di/dispatcher.go
Normal file
36
di/dispatcher.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package di
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goava/di"
|
||||||
|
"github.com/mono83/slf"
|
||||||
|
|
||||||
|
d "github.com/elyby/chrly/dispatcher"
|
||||||
|
"github.com/elyby/chrly/eventsubscribers"
|
||||||
|
"github.com/elyby/chrly/http"
|
||||||
|
"github.com/elyby/chrly/mojangtextures"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dispatcher = di.Options(
|
||||||
|
di.Provide(newDispatcher,
|
||||||
|
di.As(new(d.Emitter)),
|
||||||
|
di.As(new(d.Subscriber)),
|
||||||
|
di.As(new(http.Emitter)),
|
||||||
|
di.As(new(mojangtextures.Emitter)),
|
||||||
|
di.As(new(eventsubscribers.Subscriber)),
|
||||||
|
),
|
||||||
|
di.Invoke(enableEventsHandlers),
|
||||||
|
)
|
||||||
|
|
||||||
|
func newDispatcher() d.Dispatcher {
|
||||||
|
return d.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableEventsHandlers(
|
||||||
|
dispatcher d.Subscriber,
|
||||||
|
logger slf.Logger,
|
||||||
|
statsReporter slf.StatsReporter,
|
||||||
|
) {
|
||||||
|
// TODO: use idea from https://github.com/goava/di/issues/10#issuecomment-615869852
|
||||||
|
(&eventsubscribers.Logger{Logger: logger}).ConfigureWithDispatcher(dispatcher)
|
||||||
|
(&eventsubscribers.StatsReporter{StatsReporter: statsReporter}).ConfigureWithDispatcher(dispatcher)
|
||||||
|
}
|
151
di/handlers.go
Normal file
151
di/handlers.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package di
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/etherlabsio/healthcheck"
|
||||||
|
"github.com/goava/di"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
. "github.com/elyby/chrly/http"
|
||||||
|
"github.com/elyby/chrly/mojangtextures"
|
||||||
|
)
|
||||||
|
|
||||||
|
var handlers = di.Options(
|
||||||
|
di.Provide(newHandlerFactory, di.As(new(http.Handler))),
|
||||||
|
di.Provide(newSkinsystemHandler, di.WithName("skinsystem")),
|
||||||
|
di.Provide(newApiHandler, di.WithName("api")),
|
||||||
|
di.Provide(newUUIDsWorkerHandler, di.WithName("worker")),
|
||||||
|
)
|
||||||
|
|
||||||
|
func newHandlerFactory(
|
||||||
|
container *di.Container,
|
||||||
|
config *viper.Viper,
|
||||||
|
emitter Emitter,
|
||||||
|
) (*mux.Router, error) {
|
||||||
|
enabledModules := config.GetStringSlice("modules")
|
||||||
|
|
||||||
|
// gorilla.mux has no native way to combine multiple routers.
|
||||||
|
// The hack used later in the code works for prefixes in addresses, but leads to misbehavior
|
||||||
|
// if you set an empty prefix. Since the main application should be mounted at the root prefix,
|
||||||
|
// we use it as the base router
|
||||||
|
var router *mux.Router
|
||||||
|
if hasValue(enabledModules, "skinsystem") {
|
||||||
|
if err := container.Resolve(&router, di.Name("skinsystem")); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
router = mux.NewRouter()
|
||||||
|
}
|
||||||
|
|
||||||
|
router.StrictSlash(true)
|
||||||
|
requestEventsMiddleware := CreateRequestEventsMiddleware(emitter, "skinsystem")
|
||||||
|
router.Use(requestEventsMiddleware)
|
||||||
|
// NotFoundHandler doesn't call for registered middlewares, so we must wrap it manually.
|
||||||
|
// See https://github.com/gorilla/mux/issues/416#issuecomment-600079279
|
||||||
|
router.NotFoundHandler = requestEventsMiddleware(http.HandlerFunc(NotFoundHandler))
|
||||||
|
|
||||||
|
// Enable the worker module before api to allow gorilla.mux to correctly find the target router
|
||||||
|
// as it uses the first matching and /api overrides the more accurate /api/worker
|
||||||
|
if hasValue(enabledModules, "worker") {
|
||||||
|
var workerRouter *mux.Router
|
||||||
|
if err := container.Resolve(&workerRouter, di.Name("worker")); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(router, "/api/worker", workerRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasValue(enabledModules, "api") {
|
||||||
|
var apiRouter *mux.Router
|
||||||
|
if err := container.Resolve(&apiRouter, di.Name("api")); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var authenticator Authenticator
|
||||||
|
if err := container.Resolve(&authenticator); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiRouter.Use(CreateAuthenticationMiddleware(authenticator))
|
||||||
|
|
||||||
|
mount(router, "/api", apiRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve health checkers last, because all the services required by the application
|
||||||
|
// must first be initialized and each of them can publish its own checkers
|
||||||
|
var healthCheckers []*namedHealthChecker
|
||||||
|
if container.Has(&healthCheckers) {
|
||||||
|
if err := container.Resolve(&healthCheckers); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
checkersOptions := make([]healthcheck.Option, len(healthCheckers))
|
||||||
|
for i, checker := range healthCheckers {
|
||||||
|
checkersOptions[i] = healthcheck.WithChecker(checker.Name, checker.Checker)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.Handle("/healthcheck", healthcheck.Handler()).Methods("GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
return router, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSkinsystemHandler(
|
||||||
|
config *viper.Viper,
|
||||||
|
emitter Emitter,
|
||||||
|
skinsRepository SkinsRepository,
|
||||||
|
capesRepository CapesRepository,
|
||||||
|
mojangTexturesProvider MojangTexturesProvider,
|
||||||
|
) *mux.Router {
|
||||||
|
config.SetDefault("textures.extra_param_name", "chrly")
|
||||||
|
config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
|
||||||
|
|
||||||
|
return (&Skinsystem{
|
||||||
|
Emitter: emitter,
|
||||||
|
SkinsRepo: skinsRepository,
|
||||||
|
CapesRepo: capesRepository,
|
||||||
|
MojangTexturesProvider: mojangTexturesProvider,
|
||||||
|
TexturesExtraParamName: config.GetString("textures.extra_param_name"),
|
||||||
|
TexturesExtraParamValue: config.GetString("textures.extra_param_value"),
|
||||||
|
}).Handler()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newApiHandler(emitter Emitter, skinsRepository SkinsRepository) *mux.Router {
|
||||||
|
return (&Api{
|
||||||
|
Emitter: emitter,
|
||||||
|
SkinsRepo: skinsRepository,
|
||||||
|
}).Handler()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUUIDsWorkerHandler(mojangUUIDsProvider *mojangtextures.BatchUuidsProvider) *mux.Router {
|
||||||
|
return (&UUIDsWorker{
|
||||||
|
MojangUuidsProvider: mojangUUIDsProvider,
|
||||||
|
}).Handler()
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasValue(slice []string, needle string) bool {
|
||||||
|
for _, value := range slice {
|
||||||
|
if value == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func mount(router *mux.Router, path string, handler http.Handler) {
|
||||||
|
router.PathPrefix(path).Handler(
|
||||||
|
http.StripPrefix(
|
||||||
|
strings.TrimSuffix(path, "/"),
|
||||||
|
handler,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type namedHealthChecker struct {
|
||||||
|
Name string
|
||||||
|
Checker healthcheck.Checker
|
||||||
|
}
|
97
di/logger.go
Normal file
97
di/logger.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package di
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/getsentry/raven-go"
|
||||||
|
"github.com/goava/di"
|
||||||
|
"github.com/mono83/slf"
|
||||||
|
"github.com/mono83/slf/rays"
|
||||||
|
"github.com/mono83/slf/recievers/sentry"
|
||||||
|
"github.com/mono83/slf/recievers/statsd"
|
||||||
|
"github.com/mono83/slf/recievers/writer"
|
||||||
|
"github.com/mono83/slf/wd"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"github.com/elyby/chrly/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = di.Options(
|
||||||
|
di.Provide(newLogger),
|
||||||
|
di.Provide(newSentry),
|
||||||
|
di.Provide(newStatsReporter),
|
||||||
|
)
|
||||||
|
|
||||||
|
type loggerParams struct {
|
||||||
|
di.Inject
|
||||||
|
|
||||||
|
SentryRaven *raven.Client `di:"" optional:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogger(params loggerParams) slf.Logger {
|
||||||
|
dispatcher := &slf.Dispatcher{}
|
||||||
|
dispatcher.AddReceiver(writer.New(writer.Options{
|
||||||
|
Marker: false,
|
||||||
|
TimeFormat: "15:04:05.000",
|
||||||
|
}))
|
||||||
|
|
||||||
|
if params.SentryRaven != nil {
|
||||||
|
sentryReceiver, _ := sentry.NewReceiverWithCustomRaven(
|
||||||
|
params.SentryRaven,
|
||||||
|
&sentry.Config{
|
||||||
|
MinLevel: "warn",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
dispatcher.AddReceiver(sentryReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := wd.Custom("", "", dispatcher)
|
||||||
|
logger.WithParams(rays.Host)
|
||||||
|
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSentry(config *viper.Viper) (*raven.Client, error) {
|
||||||
|
sentryAddr := config.GetString("sentry.dsn")
|
||||||
|
if sentryAddr == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ravenClient, err := raven.New(sentryAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ravenClient.SetEnvironment("production")
|
||||||
|
ravenClient.SetDefaultLoggerName("sentry-watchdog-receiver")
|
||||||
|
ravenClient.SetRelease(version.Version())
|
||||||
|
|
||||||
|
raven.DefaultClient = ravenClient
|
||||||
|
|
||||||
|
return ravenClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStatsReporter(config *viper.Viper) (slf.StatsReporter, error) {
|
||||||
|
dispatcher := &slf.Dispatcher{}
|
||||||
|
|
||||||
|
statsdAddr := config.GetString("statsd.addr")
|
||||||
|
if statsdAddr == "" {
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
statsdReceiver, err := statsd.NewReceiver(statsd.Config{
|
||||||
|
Address: statsdAddr,
|
||||||
|
Prefix: "ely.skinsystem." + hostname + ".app.",
|
||||||
|
FlushEvery: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcher.AddReceiver(statsdReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wd.Custom("", "", dispatcher), nil
|
||||||
|
}
|
148
di/mojang_textures.go
Normal file
148
di/mojang_textures.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package di
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/goava/di"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
es "github.com/elyby/chrly/eventsubscribers"
|
||||||
|
"github.com/elyby/chrly/http"
|
||||||
|
"github.com/elyby/chrly/mojangtextures"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mojangTextures = di.Options(
|
||||||
|
di.Provide(newMojangTexturesProviderFactory),
|
||||||
|
di.Provide(newMojangTexturesProvider),
|
||||||
|
di.Provide(newMojangTexturesUuidsProviderFactory),
|
||||||
|
di.Provide(newMojangTexturesBatchUUIDsProvider),
|
||||||
|
di.Provide(newMojangTexturesRemoteUUIDsProvider),
|
||||||
|
di.Provide(newMojangSignedTexturesProvider),
|
||||||
|
di.Provide(newMojangTexturesStorageFactory),
|
||||||
|
)
|
||||||
|
|
||||||
|
func newMojangTexturesProviderFactory(
|
||||||
|
container *di.Container,
|
||||||
|
config *viper.Viper,
|
||||||
|
) (http.MojangTexturesProvider, error) {
|
||||||
|
config.SetDefault("mojang_textures.enabled", true)
|
||||||
|
if !config.GetBool("mojang_textures.enabled") {
|
||||||
|
return &mojangtextures.NilProvider{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider *mojangtextures.Provider
|
||||||
|
err := container.Resolve(&provider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMojangTexturesProvider(
|
||||||
|
emitter mojangtextures.Emitter,
|
||||||
|
uuidsProvider mojangtextures.UUIDsProvider,
|
||||||
|
texturesProvider mojangtextures.TexturesProvider,
|
||||||
|
storage mojangtextures.Storage,
|
||||||
|
) *mojangtextures.Provider {
|
||||||
|
return &mojangtextures.Provider{
|
||||||
|
Emitter: emitter,
|
||||||
|
UUIDsProvider: uuidsProvider,
|
||||||
|
TexturesProvider: texturesProvider,
|
||||||
|
Storage: storage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMojangTexturesUuidsProviderFactory(
|
||||||
|
config *viper.Viper,
|
||||||
|
container *di.Container,
|
||||||
|
) (mojangtextures.UUIDsProvider, error) {
|
||||||
|
preferredUuidsProvider := config.GetString("mojang_textures.uuids_provider.driver")
|
||||||
|
if preferredUuidsProvider == "remote" {
|
||||||
|
var provider *mojangtextures.RemoteApiUuidsProvider
|
||||||
|
err := container.Resolve(&provider)
|
||||||
|
|
||||||
|
return provider, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider *mojangtextures.BatchUuidsProvider
|
||||||
|
err := container.Resolve(&provider)
|
||||||
|
|
||||||
|
return provider, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMojangTexturesBatchUUIDsProvider(
|
||||||
|
container *di.Container,
|
||||||
|
config *viper.Viper,
|
||||||
|
emitter mojangtextures.Emitter,
|
||||||
|
) (*mojangtextures.BatchUuidsProvider, error) {
|
||||||
|
if err := container.Provide(func(emitter es.Subscriber, config *viper.Viper) *namedHealthChecker {
|
||||||
|
config.SetDefault("healthcheck.mojang_batch_uuids_provider_cool_down_duration", time.Minute)
|
||||||
|
|
||||||
|
return &namedHealthChecker{
|
||||||
|
Name: "mojang-batch-uuids-provider-response",
|
||||||
|
Checker: es.MojangBatchUuidsProviderResponseChecker(
|
||||||
|
emitter,
|
||||||
|
config.GetDuration("healthcheck.mojang_batch_uuids_provider_cool_down_duration"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := container.Provide(func(emitter es.Subscriber, config *viper.Viper) *namedHealthChecker {
|
||||||
|
config.SetDefault("healthcheck.mojang_batch_uuids_provider_queue_length_limit", 50)
|
||||||
|
|
||||||
|
return &namedHealthChecker{
|
||||||
|
Name: "mojang-batch-uuids-provider-queue-length",
|
||||||
|
Checker: es.MojangBatchUuidsProviderQueueLengthChecker(
|
||||||
|
emitter,
|
||||||
|
config.GetInt("healthcheck.mojang_batch_uuids_provider_queue_length_limit"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.SetDefault("queue.loop_delay", 2*time.Second+500*time.Millisecond)
|
||||||
|
config.SetDefault("queue.batch_size", 10)
|
||||||
|
|
||||||
|
return &mojangtextures.BatchUuidsProvider{
|
||||||
|
Emitter: emitter,
|
||||||
|
IterationDelay: config.GetDuration("queue.loop_delay"),
|
||||||
|
IterationSize: config.GetInt("queue.batch_size"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMojangTexturesRemoteUUIDsProvider(
|
||||||
|
config *viper.Viper,
|
||||||
|
emitter mojangtextures.Emitter,
|
||||||
|
) (*mojangtextures.RemoteApiUuidsProvider, error) {
|
||||||
|
remoteUrl, err := url.Parse(config.GetString("mojang_textures.uuids_provider.url"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse remote url: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mojangtextures.RemoteApiUuidsProvider{
|
||||||
|
Emitter: emitter,
|
||||||
|
Url: *remoteUrl,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMojangSignedTexturesProvider(emitter mojangtextures.Emitter) mojangtextures.TexturesProvider {
|
||||||
|
return &mojangtextures.MojangApiTexturesProvider{
|
||||||
|
Emitter: emitter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMojangTexturesStorageFactory(
|
||||||
|
uuidsStorage mojangtextures.UuidsStorage,
|
||||||
|
texturesStorage mojangtextures.TexturesStorage,
|
||||||
|
) mojangtextures.Storage {
|
||||||
|
return &mojangtextures.SeparatedStorage{
|
||||||
|
UuidsStorage: uuidsStorage,
|
||||||
|
TexturesStorage: texturesStorage,
|
||||||
|
}
|
||||||
|
}
|
65
di/server.go
Normal file
65
di/server.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package di
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/getsentry/raven-go"
|
||||||
|
"github.com/goava/di"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
. "github.com/elyby/chrly/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var server = di.Options(
|
||||||
|
di.Provide(newAuthenticator, di.As(new(Authenticator))),
|
||||||
|
di.Provide(newServer),
|
||||||
|
)
|
||||||
|
|
||||||
|
func newAuthenticator(config *viper.Viper, emitter Emitter) (*JwtAuth, error) {
|
||||||
|
key := config.GetString("chrly.secret")
|
||||||
|
if key == "" {
|
||||||
|
return nil, errors.New("chrly.secret must be set in order to use authenticator")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &JwtAuth{
|
||||||
|
Key: []byte(key),
|
||||||
|
Emitter: emitter,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverParams struct {
|
||||||
|
di.Inject
|
||||||
|
|
||||||
|
Config *viper.Viper `di:""`
|
||||||
|
Handler http.Handler `di:""`
|
||||||
|
Sentry *raven.Client `di:"" optional:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServer(params serverParams) *http.Server {
|
||||||
|
params.Config.SetDefault("server.host", "")
|
||||||
|
params.Config.SetDefault("server.port", 80)
|
||||||
|
|
||||||
|
handler := params.Handler
|
||||||
|
if params.Sentry != nil {
|
||||||
|
// raven.Recoverer uses DefaultClient and nothing can be done about it
|
||||||
|
// To avoid code duplication, if the Sentry service is successfully initiated,
|
||||||
|
// it will also replace DefaultClient, so raven.Recoverer will work with the instance
|
||||||
|
// created in the application constructor
|
||||||
|
handler = raven.Recoverer(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
address := fmt.Sprintf("%s:%d", params.Config.GetString("server.host"), params.Config.GetInt("server.port"))
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 5 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
MaxHeaderBytes: 1 << 16,
|
||||||
|
Handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
@@ -2,25 +2,33 @@ package dispatcher
|
|||||||
|
|
||||||
import "github.com/asaskevich/EventBus"
|
import "github.com/asaskevich/EventBus"
|
||||||
|
|
||||||
type EventDispatcher interface {
|
type Subscriber interface {
|
||||||
Subscribe(topic string, fn interface{})
|
Subscribe(topic string, fn interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Emitter interface {
|
||||||
Emit(topic string, args ...interface{})
|
Emit(topic string, args ...interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalEventDispatcher struct {
|
type Dispatcher interface {
|
||||||
|
Subscriber
|
||||||
|
Emitter
|
||||||
|
}
|
||||||
|
|
||||||
|
type localEventDispatcher struct {
|
||||||
bus EventBus.Bus
|
bus EventBus.Bus
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *LocalEventDispatcher) Subscribe(topic string, fn interface{}) {
|
func (d *localEventDispatcher) Subscribe(topic string, fn interface{}) {
|
||||||
_ = d.bus.Subscribe(topic, fn)
|
_ = d.bus.Subscribe(topic, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *LocalEventDispatcher) Emit(topic string, args ...interface{}) {
|
func (d *localEventDispatcher) Emit(topic string, args ...interface{}) {
|
||||||
d.bus.Publish(topic, args...)
|
d.bus.Publish(topic, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() EventDispatcher {
|
func New() Dispatcher {
|
||||||
return &LocalEventDispatcher{
|
return &localEventDispatcher{
|
||||||
bus: EventBus.New(),
|
bus: EventBus.New(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,36 +9,36 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/elyby/chrly/api/mojang"
|
"github.com/elyby/chrly/api/mojang"
|
||||||
"github.com/elyby/chrly/bootstrap"
|
"github.com/elyby/chrly/dispatcher"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMojangBatchUuidsProviderChecker(t *testing.T) {
|
func TestMojangBatchUuidsProviderChecker(t *testing.T) {
|
||||||
t.Run("empty state", func(t *testing.T) {
|
t.Run("empty state", func(t *testing.T) {
|
||||||
dispatcher := bootstrap.CreateEventDispatcher()
|
d := dispatcher.New()
|
||||||
checker := MojangBatchUuidsProviderResponseChecker(dispatcher, time.Millisecond)
|
checker := MojangBatchUuidsProviderResponseChecker(d, time.Millisecond)
|
||||||
assert.Nil(t, checker(context.Background()))
|
assert.Nil(t, checker(context.Background()))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("when no error occurred", func(t *testing.T) {
|
t.Run("when no error occurred", func(t *testing.T) {
|
||||||
dispatcher := bootstrap.CreateEventDispatcher()
|
d := dispatcher.New()
|
||||||
checker := MojangBatchUuidsProviderResponseChecker(dispatcher, time.Millisecond)
|
checker := MojangBatchUuidsProviderResponseChecker(d, time.Millisecond)
|
||||||
dispatcher.Emit("mojang_textures:batch_uuids_provider:result", []string{"username"}, []*mojang.ProfileInfo{}, nil)
|
d.Emit("mojang_textures:batch_uuids_provider:result", []string{"username"}, []*mojang.ProfileInfo{}, nil)
|
||||||
assert.Nil(t, checker(context.Background()))
|
assert.Nil(t, checker(context.Background()))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("when error occurred", func(t *testing.T) {
|
t.Run("when error occurred", func(t *testing.T) {
|
||||||
dispatcher := bootstrap.CreateEventDispatcher()
|
d := dispatcher.New()
|
||||||
checker := MojangBatchUuidsProviderResponseChecker(dispatcher, time.Millisecond)
|
checker := MojangBatchUuidsProviderResponseChecker(d, time.Millisecond)
|
||||||
err := errors.New("some error occurred")
|
err := errors.New("some error occurred")
|
||||||
dispatcher.Emit("mojang_textures:batch_uuids_provider:result", []string{"username"}, nil, err)
|
d.Emit("mojang_textures:batch_uuids_provider:result", []string{"username"}, nil, err)
|
||||||
assert.Equal(t, err, checker(context.Background()))
|
assert.Equal(t, err, checker(context.Background()))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should reset value after passed duration", func(t *testing.T) {
|
t.Run("should reset value after passed duration", func(t *testing.T) {
|
||||||
dispatcher := bootstrap.CreateEventDispatcher()
|
d := dispatcher.New()
|
||||||
checker := MojangBatchUuidsProviderResponseChecker(dispatcher, 20*time.Millisecond)
|
checker := MojangBatchUuidsProviderResponseChecker(d, 20*time.Millisecond)
|
||||||
err := errors.New("some error occurred")
|
err := errors.New("some error occurred")
|
||||||
dispatcher.Emit("mojang_textures:batch_uuids_provider:result", []string{"username"}, nil, err)
|
d.Emit("mojang_textures:batch_uuids_provider:result", []string{"username"}, nil, err)
|
||||||
assert.Equal(t, err, checker(context.Background()))
|
assert.Equal(t, err, checker(context.Background()))
|
||||||
time.Sleep(40 * time.Millisecond)
|
time.Sleep(40 * time.Millisecond)
|
||||||
assert.Nil(t, checker(context.Background()))
|
assert.Nil(t, checker(context.Background()))
|
||||||
@@ -47,22 +47,22 @@ func TestMojangBatchUuidsProviderChecker(t *testing.T) {
|
|||||||
|
|
||||||
func TestMojangBatchUuidsProviderQueueLengthChecker(t *testing.T) {
|
func TestMojangBatchUuidsProviderQueueLengthChecker(t *testing.T) {
|
||||||
t.Run("empty state", func(t *testing.T) {
|
t.Run("empty state", func(t *testing.T) {
|
||||||
dispatcher := bootstrap.CreateEventDispatcher()
|
d := dispatcher.New()
|
||||||
checker := MojangBatchUuidsProviderQueueLengthChecker(dispatcher, 10)
|
checker := MojangBatchUuidsProviderQueueLengthChecker(d, 10)
|
||||||
assert.Nil(t, checker(context.Background()))
|
assert.Nil(t, checker(context.Background()))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("less than allowed limit", func(t *testing.T) {
|
t.Run("less than allowed limit", func(t *testing.T) {
|
||||||
dispatcher := bootstrap.CreateEventDispatcher()
|
d := dispatcher.New()
|
||||||
checker := MojangBatchUuidsProviderQueueLengthChecker(dispatcher, 10)
|
checker := MojangBatchUuidsProviderQueueLengthChecker(d, 10)
|
||||||
dispatcher.Emit("mojang_textures:batch_uuids_provider:round", []string{"username"}, 9)
|
d.Emit("mojang_textures:batch_uuids_provider:round", []string{"username"}, 9)
|
||||||
assert.Nil(t, checker(context.Background()))
|
assert.Nil(t, checker(context.Background()))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("greater than allowed limit", func(t *testing.T) {
|
t.Run("greater than allowed limit", func(t *testing.T) {
|
||||||
dispatcher := bootstrap.CreateEventDispatcher()
|
d := dispatcher.New()
|
||||||
checker := MojangBatchUuidsProviderQueueLengthChecker(dispatcher, 10)
|
checker := MojangBatchUuidsProviderQueueLengthChecker(d, 10)
|
||||||
dispatcher.Emit("mojang_textures:batch_uuids_provider:round", []string{"username"}, 10)
|
d.Emit("mojang_textures:batch_uuids_provider:round", []string{"username"}, 10)
|
||||||
checkResult := checker(context.Background())
|
checkResult := checker(context.Background())
|
||||||
if assert.Error(t, checkResult) {
|
if assert.Error(t, checkResult) {
|
||||||
assert.Equal(t, "the maximum number of tasks in the queue has been exceeded", checkResult.Error())
|
assert.Equal(t, "the maximum number of tasks in the queue has been exceeded", checkResult.Error())
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package eventsubscribers
|
package eventsubscribers
|
||||||
|
|
||||||
|
import "github.com/elyby/chrly/dispatcher"
|
||||||
|
|
||||||
type Subscriber interface {
|
type Subscriber interface {
|
||||||
Subscribe(topic string, fn interface{})
|
dispatcher.Subscriber
|
||||||
}
|
}
|
||||||
|
225
http/api.go
Normal file
225
http/api.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/thedevsaddam/govalidator"
|
||||||
|
|
||||||
|
"github.com/elyby/chrly/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
//noinspection GoSnakeCaseUsage
|
||||||
|
const UUID_ANY = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||||
|
|
||||||
|
var regexUuidAny = regexp.MustCompile(UUID_ANY)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
govalidator.AddCustomRule("skinUploadingNotAvailable", func(field string, rule string, message string, value interface{}) error {
|
||||||
|
if message == "" {
|
||||||
|
message = "Skin uploading is temporary unavailable"
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(message)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add ability to validate any possible uuid form
|
||||||
|
govalidator.AddCustomRule("uuid_any", func(field string, rule string, message string, value interface{}) error {
|
||||||
|
str := value.(string)
|
||||||
|
if !regexUuidAny.MatchString(str) {
|
||||||
|
if message == "" {
|
||||||
|
message = fmt.Sprintf("The %s field must contain valid UUID", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Api struct {
|
||||||
|
Emitter
|
||||||
|
SkinsRepo SkinsRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Api) Handler() *mux.Router {
|
||||||
|
router := mux.NewRouter().StrictSlash(true)
|
||||||
|
router.HandleFunc("/skins", ctx.postSkinHandler).Methods(http.MethodPost)
|
||||||
|
router.HandleFunc("/skins/id:{id:[0-9]+}", ctx.deleteSkinByUserIdHandler).Methods(http.MethodDelete)
|
||||||
|
router.HandleFunc("/skins/{username}", ctx.deleteSkinByUsernameHandler).Methods(http.MethodDelete)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
validationErrors := validatePostSkinRequest(req)
|
||||||
|
if validationErrors != nil {
|
||||||
|
apiBadRequest(resp, validationErrors)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identityId, _ := strconv.Atoi(req.Form.Get("identityId"))
|
||||||
|
username := req.Form.Get("username")
|
||||||
|
|
||||||
|
record, err := ctx.findIdentityOrCleanup(identityId, username)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Emit("skinsystem:error", fmt.Errorf("error on requesting a skin from the repository: %w", err))
|
||||||
|
apiServerError(resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if record == nil {
|
||||||
|
record = &model.Skin{
|
||||||
|
UserId: identityId,
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skinId, _ := strconv.Atoi(req.Form.Get("skinId"))
|
||||||
|
is18, _ := strconv.ParseBool(req.Form.Get("is1_8"))
|
||||||
|
isSlim, _ := strconv.ParseBool(req.Form.Get("isSlim"))
|
||||||
|
|
||||||
|
record.Uuid = req.Form.Get("uuid")
|
||||||
|
record.SkinId = skinId
|
||||||
|
record.Is1_8 = is18
|
||||||
|
record.IsSlim = isSlim
|
||||||
|
record.Url = req.Form.Get("url")
|
||||||
|
record.MojangTextures = req.Form.Get("mojangTextures")
|
||||||
|
record.MojangSignature = req.Form.Get("mojangSignature")
|
||||||
|
|
||||||
|
err = ctx.SkinsRepo.Save(record)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Emit("skinsystem:error", fmt.Errorf("unable to save record to the repository: %w", err))
|
||||||
|
apiServerError(resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Api) deleteSkinByUserIdHandler(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
id, _ := strconv.Atoi(mux.Vars(req)["id"])
|
||||||
|
skin, err := ctx.SkinsRepo.FindByUserId(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)
|
||||||
|
ctx.deleteSkin(skin, err, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter) {
|
||||||
|
if err != nil {
|
||||||
|
ctx.Emit("skinsystem:error", fmt.Errorf("unable to find skin info from the repository: %w", err))
|
||||||
|
apiServerError(resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if skin == nil {
|
||||||
|
apiNotFound(resp, "Cannot find record for the requested identifier")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.SkinsRepo.RemoveByUserId(skin.UserId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Emit("skinsystem:error", fmt.Errorf("cannot delete skin by error: %w", err))
|
||||||
|
apiServerError(resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Api) findIdentityOrCleanup(identityId int, username string) (*model.Skin, error) {
|
||||||
|
record, err := ctx.SkinsRepo.FindByUserId(identityId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if record != nil {
|
||||||
|
// 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)
|
||||||
|
record.Username = username
|
||||||
|
}
|
||||||
|
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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)
|
||||||
|
record.UserId = identityId
|
||||||
|
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePostSkinRequest(request *http.Request) map[string][]string {
|
||||||
|
const maxMultipartMemory int64 = 32 << 20
|
||||||
|
const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both"
|
||||||
|
|
||||||
|
_ = request.ParseMultipartForm(maxMultipartMemory)
|
||||||
|
|
||||||
|
validationRules := govalidator.MapData{
|
||||||
|
"identityId": {"required", "numeric", "min:1"},
|
||||||
|
"username": {"required"},
|
||||||
|
"uuid": {"required", "uuid_any"},
|
||||||
|
"skinId": {"required", "numeric", "min:1"},
|
||||||
|
"url": {"url"},
|
||||||
|
"file:skin": {"ext:png", "size:24576", "mime:image/png"},
|
||||||
|
"is1_8": {"bool"},
|
||||||
|
"isSlim": {"bool"},
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldAppendSkinRequiredError := false
|
||||||
|
url := request.Form.Get("url")
|
||||||
|
_, _, skinErr := request.FormFile("skin")
|
||||||
|
if (url != "" && skinErr == nil) || (url == "" && skinErr != nil) {
|
||||||
|
shouldAppendSkinRequiredError = true
|
||||||
|
} else if skinErr == nil {
|
||||||
|
validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable")
|
||||||
|
} else if url != "" {
|
||||||
|
validationRules["is1_8"] = append(validationRules["is1_8"], "required")
|
||||||
|
validationRules["isSlim"] = append(validationRules["isSlim"], "required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mojangTextures := request.Form.Get("mojangTextures")
|
||||||
|
if mojangTextures != "" {
|
||||||
|
validationRules["mojangSignature"] = []string{"required"}
|
||||||
|
}
|
||||||
|
|
||||||
|
validator := govalidator.New(govalidator.Options{
|
||||||
|
Request: request,
|
||||||
|
Rules: validationRules,
|
||||||
|
RequiredDefault: false,
|
||||||
|
FormSize: maxMultipartMemory,
|
||||||
|
})
|
||||||
|
validationResults := validator.Validate()
|
||||||
|
if shouldAppendSkinRequiredError {
|
||||||
|
validationResults["url"] = append(validationResults["url"], oneOfSkinOrUrlMessage)
|
||||||
|
validationResults["skin"] = append(validationResults["skin"], oneOfSkinOrUrlMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(validationResults) != 0 {
|
||||||
|
return validationResults
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
442
http/api_test.go
Normal file
442
http/api_test.go
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/elyby/chrly/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
/***************
|
||||||
|
* Setup mocks *
|
||||||
|
***************/
|
||||||
|
|
||||||
|
type apiTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
App *Api
|
||||||
|
|
||||||
|
SkinsRepository *skinsRepositoryMock
|
||||||
|
Emitter *emitterMock
|
||||||
|
}
|
||||||
|
|
||||||
|
/********************
|
||||||
|
* Setup test suite *
|
||||||
|
********************/
|
||||||
|
|
||||||
|
func (suite *apiTestSuite) SetupTest() {
|
||||||
|
suite.SkinsRepository = &skinsRepositoryMock{}
|
||||||
|
suite.Emitter = &emitterMock{}
|
||||||
|
|
||||||
|
suite.App = &Api{
|
||||||
|
SkinsRepo: suite.SkinsRepository,
|
||||||
|
Emitter: suite.Emitter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *apiTestSuite) TearDownTest() {
|
||||||
|
suite.SkinsRepository.AssertExpectations(suite.T())
|
||||||
|
suite.Emitter.AssertExpectations(suite.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *apiTestSuite) RunSubTest(name string, subTest func()) {
|
||||||
|
suite.SetupTest()
|
||||||
|
suite.Run(name, subTest)
|
||||||
|
suite.TearDownTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*************
|
||||||
|
* Run tests *
|
||||||
|
*************/
|
||||||
|
|
||||||
|
func TestApi(t *testing.T) {
|
||||||
|
suite.Run(t, new(apiTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*************************
|
||||||
|
* Post skin tests cases *
|
||||||
|
*************************/
|
||||||
|
|
||||||
|
type postSkinTestCase struct {
|
||||||
|
Name string
|
||||||
|
Form io.Reader
|
||||||
|
BeforeTest func(suite *apiTestSuite)
|
||||||
|
AfterTest func(suite *apiTestSuite, response *http.Response)
|
||||||
|
}
|
||||||
|
|
||||||
|
var postSkinTestsCases = []*postSkinTestCase{
|
||||||
|
{
|
||||||
|
Name: "Upload new identity with textures data",
|
||||||
|
Form: bytes.NewBufferString(url.Values{
|
||||||
|
"identityId": {"1"},
|
||||||
|
"username": {"mock_username"},
|
||||||
|
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||||
|
"skinId": {"5"},
|
||||||
|
"is1_8": {"0"},
|
||||||
|
"isSlim": {"0"},
|
||||||
|
"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.Equal(1, model.UserId)
|
||||||
|
suite.Equal("mock_username", model.Username)
|
||||||
|
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||||
|
suite.Equal(5, model.SkinId)
|
||||||
|
suite.False(model.Is1_8)
|
||||||
|
suite.False(model.IsSlim)
|
||||||
|
suite.Equal("http://example.com/skin.png", model.Url)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})).Times(1).Return(nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(201, response.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.Empty(body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Update exists identity by changing only textures data",
|
||||||
|
Form: bytes.NewBufferString(url.Values{
|
||||||
|
"identityId": {"1"},
|
||||||
|
"username": {"mock_username"},
|
||||||
|
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||||
|
"skinId": {"5"},
|
||||||
|
"is1_8": {"1"},
|
||||||
|
"isSlim": {"1"},
|
||||||
|
"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.Equal(1, model.UserId)
|
||||||
|
suite.Equal("mock_username", model.Username)
|
||||||
|
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||||
|
suite.Equal(5, model.SkinId)
|
||||||
|
suite.True(model.Is1_8)
|
||||||
|
suite.True(model.IsSlim)
|
||||||
|
suite.Equal("http://textures-server.com/skin.png", model.Url)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})).Times(1).Return(nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(201, response.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.Empty(body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Update exists identity by changing its identityId",
|
||||||
|
Form: bytes.NewBufferString(url.Values{
|
||||||
|
"identityId": {"2"},
|
||||||
|
"username": {"mock_username"},
|
||||||
|
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||||
|
"skinId": {"5"},
|
||||||
|
"is1_8": {"0"},
|
||||||
|
"isSlim": {"0"},
|
||||||
|
"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.Equal(2, model.UserId)
|
||||||
|
suite.Equal("mock_username", model.Username)
|
||||||
|
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})).Times(1).Return(nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(201, response.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.Empty(body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Update exists identity by changing its username",
|
||||||
|
Form: bytes.NewBufferString(url.Values{
|
||||||
|
"identityId": {"1"},
|
||||||
|
"username": {"changed_username"},
|
||||||
|
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||||
|
"skinId": {"5"},
|
||||||
|
"is1_8": {"0"},
|
||||||
|
"isSlim": {"0"},
|
||||||
|
"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.Equal(1, model.UserId)
|
||||||
|
suite.Equal("changed_username", model.Username)
|
||||||
|
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})).Times(1).Return(nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(201, response.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.Empty(body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Handle an error when loading the data from the repository",
|
||||||
|
Form: bytes.NewBufferString(url.Values{
|
||||||
|
"identityId": {"1"},
|
||||||
|
"username": {"mock_username"},
|
||||||
|
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||||
|
"skinId": {"5"},
|
||||||
|
"is1_8": {"1"},
|
||||||
|
"isSlim": {"1"},
|
||||||
|
"url": {"http://textures-server.com/skin.png"},
|
||||||
|
}.Encode()),
|
||||||
|
BeforeTest: func(suite *apiTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||||
|
err := errors.New("mock error")
|
||||||
|
suite.SkinsRepository.On("Save", 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)
|
||||||
|
})).Once()
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(500, response.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.Empty(body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Handle an error when saving the data into the repository",
|
||||||
|
Form: bytes.NewBufferString(url.Values{
|
||||||
|
"identityId": {"1"},
|
||||||
|
"username": {"changed_username"},
|
||||||
|
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||||
|
"skinId": {"5"},
|
||||||
|
"is1_8": {"0"},
|
||||||
|
"isSlim": {"0"},
|
||||||
|
"url": {"http://example.com/skin.png"},
|
||||||
|
}.Encode()),
|
||||||
|
BeforeTest: func(suite *apiTestSuite) {
|
||||||
|
err := errors.New("mock error")
|
||||||
|
suite.SkinsRepository.On("FindByUserId", 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)
|
||||||
|
})).Once()
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(500, response.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.Empty(body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *apiTestSuite) TestPostSkin() {
|
||||||
|
for _, testCase := range postSkinTestsCases {
|
||||||
|
suite.RunSubTest(testCase.Name, func() {
|
||||||
|
testCase.BeforeTest(suite)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://chrly/skins", testCase.Form)
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
testCase.AfterTest(suite, w.Result())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.RunSubTest("Get errors about required fields", func() {
|
||||||
|
req := httptest.NewRequest("POST", "http://chrly/skins", bytes.NewBufferString(url.Values{
|
||||||
|
"mojangTextures": {"someBase64EncodedString"},
|
||||||
|
}.Encode()))
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
suite.Equal(400, resp.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
suite.JSONEq(`{
|
||||||
|
"errors": {
|
||||||
|
"identityId": [
|
||||||
|
"The identityId field is required",
|
||||||
|
"The identityId field must be numeric",
|
||||||
|
"The identityId field must be minimum 1 char"
|
||||||
|
],
|
||||||
|
"skinId": [
|
||||||
|
"The skinId field is required",
|
||||||
|
"The skinId field must be numeric",
|
||||||
|
"The skinId field must be minimum 1 char"
|
||||||
|
],
|
||||||
|
"username": [
|
||||||
|
"The username field is required"
|
||||||
|
],
|
||||||
|
"uuid": [
|
||||||
|
"The uuid field is required",
|
||||||
|
"The uuid field must contain valid UUID"
|
||||||
|
],
|
||||||
|
"url": [
|
||||||
|
"One of url or skin should be provided, but not both"
|
||||||
|
],
|
||||||
|
"skin": [
|
||||||
|
"One of url or skin should be provided, but not both"
|
||||||
|
],
|
||||||
|
"mojangSignature": [
|
||||||
|
"The mojangSignature field is required"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`, string(body))
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.RunSubTest("Upload textures with skin as file", func() {
|
||||||
|
inputBody := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(inputBody)
|
||||||
|
|
||||||
|
part, _ := writer.CreateFormFile("skin", "char.png")
|
||||||
|
_, _ = part.Write(loadSkinFile())
|
||||||
|
|
||||||
|
_ = writer.WriteField("identityId", "1")
|
||||||
|
_ = writer.WriteField("username", "mock_user")
|
||||||
|
_ = writer.WriteField("uuid", "0f657aa8-bfbe-415d-b700-5750090d3af3")
|
||||||
|
_ = writer.WriteField("skinId", "5")
|
||||||
|
|
||||||
|
err := writer.Close()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://chrly/skins", inputBody)
|
||||||
|
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
suite.Equal(400, resp.StatusCode)
|
||||||
|
responseBody, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
suite.JSONEq(`{
|
||||||
|
"errors": {
|
||||||
|
"skin": [
|
||||||
|
"Skin uploading is temporary unavailable"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`, string(responseBody))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**************************************
|
||||||
|
* Delete skin by user id tests cases *
|
||||||
|
**************************************/
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("DELETE", "http://chrly/skins/id:1", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
suite.Equal(204, resp.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
suite.Empty(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.RunSubTest("Try to remove not exists identity id", func() {
|
||||||
|
suite.SkinsRepository.On("FindByUserId", 1).Return(nil, nil)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("DELETE", "http://chrly/skins/id:1", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
suite.Equal(404, resp.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
suite.JSONEq(`[
|
||||||
|
"Cannot find record for the requested identifier"
|
||||||
|
]`, string(body))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************
|
||||||
|
* Delete skin by username tests cases *
|
||||||
|
***************************************/
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("DELETE", "http://chrly/skins/mock_username", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
suite.Equal(204, resp.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
suite.Empty(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.RunSubTest("Try to remove not exists identity username", func() {
|
||||||
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("DELETE", "http://chrly/skins/mock_username", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
defer resp.Body.Close()
|
||||||
|
suite.Equal(404, resp.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
suite.JSONEq(`[
|
||||||
|
"Cannot find record for the requested identifier"
|
||||||
|
]`, string(body))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*************
|
||||||
|
* Utilities *
|
||||||
|
*************/
|
||||||
|
|
||||||
|
// base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png
|
||||||
|
var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==")
|
||||||
|
|
||||||
|
func loadSkinFile() []byte {
|
||||||
|
result := make([]byte, 92)
|
||||||
|
_, err := base64.StdEncoding.Decode(result, OnePxPng)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
51
http/http.go
51
http/http.go
@@ -1,34 +1,51 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/mono83/slf"
|
||||||
|
"github.com/mono83/slf/wd"
|
||||||
|
|
||||||
|
"github.com/elyby/chrly/dispatcher"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Emitter interface {
|
type Emitter interface {
|
||||||
Emit(name string, args ...interface{})
|
dispatcher.Emitter
|
||||||
}
|
}
|
||||||
|
|
||||||
func Serve(address string, handler http.Handler) error {
|
func StartServer(server *http.Server, logger slf.Logger) {
|
||||||
listener, err := net.Listen("tcp", address)
|
done := make(chan bool, 1)
|
||||||
if err != nil {
|
go func() {
|
||||||
return err
|
logger.Info("Starting the server, HTTP on: :addr", wd.StringParam("addr", server.Addr))
|
||||||
}
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Emergency("Error in main(): :err", wd.ErrParam(err))
|
||||||
|
}
|
||||||
|
|
||||||
server := &http.Server{
|
close(done)
|
||||||
ReadTimeout: 5 * time.Second,
|
}()
|
||||||
WriteTimeout: 5 * time.Second,
|
|
||||||
IdleTimeout: 60 * time.Second,
|
|
||||||
MaxHeaderBytes: 1 << 16,
|
|
||||||
Handler: handler,
|
|
||||||
}
|
|
||||||
|
|
||||||
return server.Serve(listener)
|
go func() {
|
||||||
|
s := waitForExitSignal()
|
||||||
|
logger.Info("Got signal: :signal, starting graceful shutdown", wd.StringParam("signal", s.String()))
|
||||||
|
server.Shutdown(context.Background())
|
||||||
|
logger.Info("Graceful shutdown succeed, exiting", wd.StringParam("signal", s.String()))
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForExitSignal() os.Signal {
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(ch, os.Interrupt, os.Kill)
|
||||||
|
|
||||||
|
return <-ch
|
||||||
}
|
}
|
||||||
|
|
||||||
type loggingResponseWriter struct {
|
type loggingResponseWriter struct {
|
||||||
@@ -78,7 +95,7 @@ func CreateAuthenticationMiddleware(checker Authenticator) mux.MiddlewareFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NotFound(response http.ResponseWriter, _ *http.Request) {
|
func NotFoundHandler(response http.ResponseWriter, _ *http.Request) {
|
||||||
data, _ := json.Marshal(map[string]string{
|
data, _ := json.Marshal(map[string]string{
|
||||||
"status": "404",
|
"status": "404",
|
||||||
"message": "Not Found",
|
"message": "Not Found",
|
||||||
|
@@ -93,13 +93,13 @@ func TestCreateAuthenticationMiddleware(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNotFound(t *testing.T) {
|
func TestNotFoundHandler(t *testing.T) {
|
||||||
assert := testify.New(t)
|
assert := testify.New(t)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://example.com", nil)
|
req := httptest.NewRequest("GET", "http://example.com", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
NotFound(w, req)
|
NotFoundHandler(w, req)
|
||||||
|
|
||||||
resp := w.Result()
|
resp := w.Result()
|
||||||
assert.Equal(404, resp.StatusCode)
|
assert.Equal(404, resp.StatusCode)
|
||||||
|
@@ -3,49 +3,16 @@ package http
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/thedevsaddam/govalidator"
|
|
||||||
|
|
||||||
"github.com/elyby/chrly/api/mojang"
|
"github.com/elyby/chrly/api/mojang"
|
||||||
"github.com/elyby/chrly/model"
|
"github.com/elyby/chrly/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
//noinspection GoSnakeCaseUsage
|
|
||||||
const UUID_ANY = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
|
||||||
|
|
||||||
var regexUuidAny = regexp.MustCompile(UUID_ANY)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
govalidator.AddCustomRule("skinUploadingNotAvailable", func(field string, rule string, message string, value interface{}) error {
|
|
||||||
if message == "" {
|
|
||||||
message = "Skin uploading is temporary unavailable"
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New(message)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add ability to validate any possible uuid form
|
|
||||||
govalidator.AddCustomRule("uuid_any", func(field string, rule string, message string, value interface{}) error {
|
|
||||||
str := value.(string)
|
|
||||||
if !regexUuidAny.MatchString(str) {
|
|
||||||
if message == "" {
|
|
||||||
message = fmt.Sprintf("The %s field must contain valid UUID", field)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type SkinsRepository interface {
|
type SkinsRepository interface {
|
||||||
FindByUsername(username string) (*model.Skin, error)
|
FindByUsername(username string) (*model.Skin, error)
|
||||||
FindByUserId(id int) (*model.Skin, error)
|
FindByUserId(id int) (*model.Skin, error)
|
||||||
@@ -58,67 +25,37 @@ type CapesRepository interface {
|
|||||||
FindByUsername(username string) (*model.Cape, error)
|
FindByUsername(username string) (*model.Cape, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SkinNotFoundError struct {
|
|
||||||
Who string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e SkinNotFoundError) Error() string {
|
|
||||||
return "skin data not found"
|
|
||||||
}
|
|
||||||
|
|
||||||
type CapeNotFoundError struct {
|
|
||||||
Who string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e CapeNotFoundError) Error() string {
|
|
||||||
return "cape file not found"
|
|
||||||
}
|
|
||||||
|
|
||||||
type MojangTexturesProvider interface {
|
type MojangTexturesProvider interface {
|
||||||
GetForUsername(username string) (*mojang.SignedTexturesResponse, error)
|
GetForUsername(username string) (*mojang.SignedTexturesResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Skinsystem struct {
|
type Skinsystem struct {
|
||||||
Emitter
|
Emitter
|
||||||
TexturesExtraParamName string
|
|
||||||
TexturesExtraParamValue string
|
|
||||||
SkinsRepo SkinsRepository
|
SkinsRepo SkinsRepository
|
||||||
CapesRepo CapesRepository
|
CapesRepo CapesRepository
|
||||||
MojangTexturesProvider MojangTexturesProvider
|
MojangTexturesProvider MojangTexturesProvider
|
||||||
Authenticator Authenticator
|
TexturesExtraParamName string
|
||||||
|
TexturesExtraParamValue string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) CreateHandler() *mux.Router {
|
func (ctx *Skinsystem) Handler() *mux.Router {
|
||||||
requestEventsMiddleware := CreateRequestEventsMiddleware(ctx.Emitter, "skinsystem")
|
|
||||||
|
|
||||||
router := mux.NewRouter().StrictSlash(true)
|
router := mux.NewRouter().StrictSlash(true)
|
||||||
router.Use(requestEventsMiddleware)
|
|
||||||
|
|
||||||
router.HandleFunc("/skins/{username}", ctx.Skin).Methods(http.MethodGet)
|
router.HandleFunc("/skins/{username}", ctx.skinHandler).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/cloaks/{username}", ctx.Cape).Methods(http.MethodGet).Name("cloaks")
|
router.HandleFunc("/cloaks/{username}", ctx.capeHandler).Methods(http.MethodGet).Name("cloaks")
|
||||||
router.HandleFunc("/textures/{username}", ctx.Textures).Methods(http.MethodGet)
|
router.HandleFunc("/textures/{username}", ctx.texturesHandler).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/textures/signed/{username}", ctx.SignedTextures).Methods(http.MethodGet)
|
router.HandleFunc("/textures/signed/{username}", ctx.signedTexturesHandler).Methods(http.MethodGet)
|
||||||
// Legacy
|
// Legacy
|
||||||
router.HandleFunc("/skins", ctx.SkinGET).Methods(http.MethodGet)
|
router.HandleFunc("/skins", ctx.skinGetHandler).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/cloaks", ctx.CapeGET).Methods(http.MethodGet)
|
router.HandleFunc("/cloaks", ctx.capeGetHandler).Methods(http.MethodGet)
|
||||||
// API
|
|
||||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
|
||||||
apiRouter.Use(CreateAuthenticationMiddleware(ctx.Authenticator))
|
|
||||||
apiRouter.HandleFunc("/skins", ctx.PostSkin).Methods(http.MethodPost)
|
|
||||||
apiRouter.HandleFunc("/skins/id:{id:[0-9]+}", ctx.DeleteSkinByUserId).Methods(http.MethodDelete)
|
|
||||||
apiRouter.HandleFunc("/skins/{username}", ctx.DeleteSkinByUsername).Methods(http.MethodDelete)
|
|
||||||
// 404
|
|
||||||
// NotFoundHandler doesn't call for registered middlewares, so we must wrap it manually.
|
|
||||||
// See https://github.com/gorilla/mux/issues/416#issuecomment-600079279
|
|
||||||
router.NotFoundHandler = requestEventsMiddleware(http.HandlerFunc(NotFound))
|
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) Skin(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) skinHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
username := parseUsername(mux.Vars(request)["username"])
|
username := parseUsername(mux.Vars(request)["username"])
|
||||||
rec, err := ctx.SkinsRepo.FindByUsername(username)
|
rec, err := ctx.SkinsRepo.FindByUsername(username)
|
||||||
if err == nil && rec.SkinId != 0 {
|
if err == nil && rec != nil && rec.SkinId != 0 {
|
||||||
http.Redirect(response, request, rec.Url, 301)
|
http.Redirect(response, request, rec.Url, 301)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -139,7 +76,7 @@ func (ctx *Skinsystem) Skin(response http.ResponseWriter, request *http.Request)
|
|||||||
http.Redirect(response, request, skin.Url, 301)
|
http.Redirect(response, request, skin.Url, 301)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) SkinGET(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
username := request.URL.Query().Get("name")
|
username := request.URL.Query().Get("name")
|
||||||
if username == "" {
|
if username == "" {
|
||||||
response.WriteHeader(http.StatusBadRequest)
|
response.WriteHeader(http.StatusBadRequest)
|
||||||
@@ -149,13 +86,13 @@ func (ctx *Skinsystem) SkinGET(response http.ResponseWriter, request *http.Reque
|
|||||||
mux.Vars(request)["username"] = username
|
mux.Vars(request)["username"] = username
|
||||||
mux.Vars(request)["converted"] = "1"
|
mux.Vars(request)["converted"] = "1"
|
||||||
|
|
||||||
ctx.Skin(response, request)
|
ctx.skinHandler(response, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) Cape(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
username := parseUsername(mux.Vars(request)["username"])
|
username := parseUsername(mux.Vars(request)["username"])
|
||||||
rec, err := ctx.CapesRepo.FindByUsername(username)
|
rec, err := ctx.CapesRepo.FindByUsername(username)
|
||||||
if err == nil {
|
if err == nil && rec != nil {
|
||||||
request.Header.Set("Content-Type", "image/png")
|
request.Header.Set("Content-Type", "image/png")
|
||||||
_, _ = io.Copy(response, rec.File)
|
_, _ = io.Copy(response, rec.File)
|
||||||
return
|
return
|
||||||
@@ -177,7 +114,7 @@ func (ctx *Skinsystem) Cape(response http.ResponseWriter, request *http.Request)
|
|||||||
http.Redirect(response, request, cape.Url, 301)
|
http.Redirect(response, request, cape.Url, 301)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) CapeGET(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) capeGetHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
username := request.URL.Query().Get("name")
|
username := request.URL.Query().Get("name")
|
||||||
if username == "" {
|
if username == "" {
|
||||||
response.WriteHeader(http.StatusBadRequest)
|
response.WriteHeader(http.StatusBadRequest)
|
||||||
@@ -187,19 +124,18 @@ func (ctx *Skinsystem) CapeGET(response http.ResponseWriter, request *http.Reque
|
|||||||
mux.Vars(request)["username"] = username
|
mux.Vars(request)["username"] = username
|
||||||
mux.Vars(request)["converted"] = "1"
|
mux.Vars(request)["converted"] = "1"
|
||||||
|
|
||||||
ctx.Cape(response, request)
|
ctx.capeHandler(response, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) Textures(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) texturesHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
username := parseUsername(mux.Vars(request)["username"])
|
username := parseUsername(mux.Vars(request)["username"])
|
||||||
|
|
||||||
var textures *mojang.TexturesResponse
|
var textures *mojang.TexturesResponse
|
||||||
skin, skinErr := ctx.SkinsRepo.FindByUsername(username)
|
skin, skinErr := ctx.SkinsRepo.FindByUsername(username)
|
||||||
_, capeErr := ctx.CapesRepo.FindByUsername(username)
|
cape, capeErr := ctx.CapesRepo.FindByUsername(username)
|
||||||
if (skinErr == nil && skin.SkinId != 0) || capeErr == nil {
|
if (skinErr == nil && skin != nil && skin.SkinId != 0) || (capeErr == nil && cape != nil) {
|
||||||
textures = &mojang.TexturesResponse{}
|
textures = &mojang.TexturesResponse{}
|
||||||
|
if skinErr == nil && skin != nil && skin.SkinId != 0 {
|
||||||
if skinErr == nil && skin.SkinId != 0 {
|
|
||||||
skinTextures := &mojang.SkinTexturesResponse{
|
skinTextures := &mojang.SkinTexturesResponse{
|
||||||
Url: skin.Url,
|
Url: skin.Url,
|
||||||
}
|
}
|
||||||
@@ -213,7 +149,7 @@ func (ctx *Skinsystem) Textures(response http.ResponseWriter, request *http.Requ
|
|||||||
textures.Skin = skinTextures
|
textures.Skin = skinTextures
|
||||||
}
|
}
|
||||||
|
|
||||||
if capeErr == nil {
|
if capeErr == nil && cape != nil {
|
||||||
textures.Cape = &mojang.CapeTexturesResponse{
|
textures.Cape = &mojang.CapeTexturesResponse{
|
||||||
Url: request.URL.Scheme + "://" + request.Host + "/cloaks/" + username,
|
Url: request.URL.Scheme + "://" + request.Host + "/cloaks/" + username,
|
||||||
}
|
}
|
||||||
@@ -233,6 +169,7 @@ func (ctx *Skinsystem) Textures(response http.ResponseWriter, request *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
textures = texturesProp.Textures
|
textures = texturesProp.Textures
|
||||||
|
// TODO: return 204 in case when there is no skin and cape on mojang textures
|
||||||
}
|
}
|
||||||
|
|
||||||
responseData, _ := json.Marshal(textures)
|
responseData, _ := json.Marshal(textures)
|
||||||
@@ -240,13 +177,13 @@ func (ctx *Skinsystem) Textures(response http.ResponseWriter, request *http.Requ
|
|||||||
_, _ = response.Write(responseData)
|
_, _ = response.Write(responseData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) SignedTextures(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) signedTexturesHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
username := parseUsername(mux.Vars(request)["username"])
|
username := parseUsername(mux.Vars(request)["username"])
|
||||||
|
|
||||||
var responseData *mojang.SignedTexturesResponse
|
var responseData *mojang.SignedTexturesResponse
|
||||||
|
|
||||||
rec, err := ctx.SkinsRepo.FindByUsername(username)
|
rec, err := ctx.SkinsRepo.FindByUsername(username)
|
||||||
if err == nil && rec.SkinId != 0 && rec.MojangTextures != "" {
|
if err == nil && rec != nil && rec.SkinId != 0 && rec.MojangTextures != "" {
|
||||||
responseData = &mojang.SignedTexturesResponse{
|
responseData = &mojang.SignedTexturesResponse{
|
||||||
Id: strings.Replace(rec.Uuid, "-", "", -1),
|
Id: strings.Replace(rec.Uuid, "-", "", -1),
|
||||||
Name: rec.Username,
|
Name: rec.Username,
|
||||||
@@ -271,8 +208,8 @@ func (ctx *Skinsystem) SignedTextures(response http.ResponseWriter, request *htt
|
|||||||
}
|
}
|
||||||
|
|
||||||
responseData.Props = append(responseData.Props, &mojang.Property{
|
responseData.Props = append(responseData.Props, &mojang.Property{
|
||||||
Name: getStringOrDefault(ctx.TexturesExtraParamName, "chrly"),
|
Name: ctx.TexturesExtraParamName,
|
||||||
Value: getStringOrDefault(ctx.TexturesExtraParamValue, "how do you tame a horse in Minecraft?"),
|
Value: ctx.TexturesExtraParamValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
responseJson, _ := json.Marshal(responseData)
|
responseJson, _ := json.Marshal(responseData)
|
||||||
@@ -280,166 +217,6 @@ func (ctx *Skinsystem) SignedTextures(response http.ResponseWriter, request *htt
|
|||||||
_, _ = response.Write(responseJson)
|
_, _ = response.Write(responseJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) PostSkin(resp http.ResponseWriter, req *http.Request) {
|
|
||||||
validationErrors := validatePostSkinRequest(req)
|
|
||||||
if validationErrors != nil {
|
|
||||||
apiBadRequest(resp, validationErrors)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
identityId, _ := strconv.Atoi(req.Form.Get("identityId"))
|
|
||||||
username := req.Form.Get("username")
|
|
||||||
|
|
||||||
record, err := findIdentity(ctx.SkinsRepo, identityId, username)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Emit("skinsystem:error", fmt.Errorf("error on requesting a skin from the repository: %w", err))
|
|
||||||
apiServerError(resp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
skinId, _ := strconv.Atoi(req.Form.Get("skinId"))
|
|
||||||
is18, _ := strconv.ParseBool(req.Form.Get("is1_8"))
|
|
||||||
isSlim, _ := strconv.ParseBool(req.Form.Get("isSlim"))
|
|
||||||
|
|
||||||
record.Uuid = req.Form.Get("uuid")
|
|
||||||
record.SkinId = skinId
|
|
||||||
record.Is1_8 = is18
|
|
||||||
record.IsSlim = isSlim
|
|
||||||
record.Url = req.Form.Get("url")
|
|
||||||
record.MojangTextures = req.Form.Get("mojangTextures")
|
|
||||||
record.MojangSignature = req.Form.Get("mojangSignature")
|
|
||||||
|
|
||||||
err = ctx.SkinsRepo.Save(record)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Emit("skinsystem:error", fmt.Errorf("unable to save record to the repository: %w", err))
|
|
||||||
apiServerError(resp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.WriteHeader(http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *Skinsystem) DeleteSkinByUserId(resp http.ResponseWriter, req *http.Request) {
|
|
||||||
id, _ := strconv.Atoi(mux.Vars(req)["id"])
|
|
||||||
skin, err := ctx.SkinsRepo.FindByUserId(id)
|
|
||||||
ctx.deleteSkin(skin, err, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *Skinsystem) DeleteSkinByUsername(resp http.ResponseWriter, req *http.Request) {
|
|
||||||
username := mux.Vars(req)["username"]
|
|
||||||
skin, err := ctx.SkinsRepo.FindByUsername(username)
|
|
||||||
ctx.deleteSkin(skin, err, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *Skinsystem) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter) {
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(*SkinNotFoundError); ok {
|
|
||||||
apiNotFound(resp, "Cannot find record for the requested identifier")
|
|
||||||
} else {
|
|
||||||
ctx.Emit("skinsystem:error", fmt.Errorf("unable to find skin info from the repository: %w", err))
|
|
||||||
apiServerError(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ctx.SkinsRepo.RemoveByUserId(skin.UserId)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Emit("skinsystem:error", fmt.Errorf("cannot delete skin by error: %w", err))
|
|
||||||
apiServerError(resp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validatePostSkinRequest(request *http.Request) map[string][]string {
|
|
||||||
const maxMultipartMemory int64 = 32 << 20
|
|
||||||
const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both"
|
|
||||||
|
|
||||||
_ = request.ParseMultipartForm(maxMultipartMemory)
|
|
||||||
|
|
||||||
validationRules := govalidator.MapData{
|
|
||||||
"identityId": {"required", "numeric", "min:1"},
|
|
||||||
"username": {"required"},
|
|
||||||
"uuid": {"required", "uuid_any"},
|
|
||||||
"skinId": {"required", "numeric", "min:1"},
|
|
||||||
"url": {"url"},
|
|
||||||
"file:skin": {"ext:png", "size:24576", "mime:image/png"},
|
|
||||||
"is1_8": {"bool"},
|
|
||||||
"isSlim": {"bool"},
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldAppendSkinRequiredError := false
|
|
||||||
url := request.Form.Get("url")
|
|
||||||
_, _, skinErr := request.FormFile("skin")
|
|
||||||
if (url != "" && skinErr == nil) || (url == "" && skinErr != nil) {
|
|
||||||
shouldAppendSkinRequiredError = true
|
|
||||||
} else if skinErr == nil {
|
|
||||||
validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable")
|
|
||||||
} else if url != "" {
|
|
||||||
validationRules["is1_8"] = append(validationRules["is1_8"], "required")
|
|
||||||
validationRules["isSlim"] = append(validationRules["isSlim"], "required")
|
|
||||||
}
|
|
||||||
|
|
||||||
mojangTextures := request.Form.Get("mojangTextures")
|
|
||||||
if mojangTextures != "" {
|
|
||||||
validationRules["mojangSignature"] = []string{"required"}
|
|
||||||
}
|
|
||||||
|
|
||||||
validator := govalidator.New(govalidator.Options{
|
|
||||||
Request: request,
|
|
||||||
Rules: validationRules,
|
|
||||||
RequiredDefault: false,
|
|
||||||
FormSize: maxMultipartMemory,
|
|
||||||
})
|
|
||||||
validationResults := validator.Validate()
|
|
||||||
if shouldAppendSkinRequiredError {
|
|
||||||
validationResults["url"] = append(validationResults["url"], oneOfSkinOrUrlMessage)
|
|
||||||
validationResults["skin"] = append(validationResults["skin"], oneOfSkinOrUrlMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(validationResults) != 0 {
|
|
||||||
return validationResults
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findIdentity(repo SkinsRepository, identityId int, username string) (*model.Skin, error) {
|
|
||||||
var record *model.Skin
|
|
||||||
record, err := repo.FindByUserId(identityId)
|
|
||||||
if err != nil {
|
|
||||||
if _, isSkinNotFound := err.(*SkinNotFoundError); !isSkinNotFound {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
record, err = repo.FindByUsername(username)
|
|
||||||
if err == nil {
|
|
||||||
_ = repo.RemoveByUsername(username)
|
|
||||||
record.UserId = identityId
|
|
||||||
} else {
|
|
||||||
record = &model.Skin{
|
|
||||||
UserId: identityId,
|
|
||||||
Username: username,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if record.Username != username {
|
|
||||||
_ = repo.RemoveByUserId(identityId)
|
|
||||||
record.Username = username
|
|
||||||
}
|
|
||||||
|
|
||||||
return record, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseUsername(username string) string {
|
func parseUsername(username string) string {
|
||||||
return strings.TrimSuffix(username, ".png")
|
return strings.TrimSuffix(username, ".png")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStringOrDefault(value string, def string) string {
|
|
||||||
if value != "" {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
@@ -2,16 +2,11 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -102,7 +97,6 @@ type skinsystemTestSuite struct {
|
|||||||
SkinsRepository *skinsRepositoryMock
|
SkinsRepository *skinsRepositoryMock
|
||||||
CapesRepository *capesRepositoryMock
|
CapesRepository *capesRepositoryMock
|
||||||
MojangTexturesProvider *mojangTexturesProviderMock
|
MojangTexturesProvider *mojangTexturesProviderMock
|
||||||
Auth *authCheckerMock
|
|
||||||
Emitter *emitterMock
|
Emitter *emitterMock
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,15 +108,15 @@ func (suite *skinsystemTestSuite) SetupTest() {
|
|||||||
suite.SkinsRepository = &skinsRepositoryMock{}
|
suite.SkinsRepository = &skinsRepositoryMock{}
|
||||||
suite.CapesRepository = &capesRepositoryMock{}
|
suite.CapesRepository = &capesRepositoryMock{}
|
||||||
suite.MojangTexturesProvider = &mojangTexturesProviderMock{}
|
suite.MojangTexturesProvider = &mojangTexturesProviderMock{}
|
||||||
suite.Auth = &authCheckerMock{}
|
|
||||||
suite.Emitter = &emitterMock{}
|
suite.Emitter = &emitterMock{}
|
||||||
|
|
||||||
suite.App = &Skinsystem{
|
suite.App = &Skinsystem{
|
||||||
SkinsRepo: suite.SkinsRepository,
|
SkinsRepo: suite.SkinsRepository,
|
||||||
CapesRepo: suite.CapesRepository,
|
CapesRepo: suite.CapesRepository,
|
||||||
MojangTexturesProvider: suite.MojangTexturesProvider,
|
MojangTexturesProvider: suite.MojangTexturesProvider,
|
||||||
Authenticator: suite.Auth,
|
Emitter: suite.Emitter,
|
||||||
Emitter: suite.Emitter,
|
TexturesExtraParamName: "texturesParamName",
|
||||||
|
TexturesExtraParamValue: "texturesParamValue",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +124,6 @@ func (suite *skinsystemTestSuite) TearDownTest() {
|
|||||||
suite.SkinsRepository.AssertExpectations(suite.T())
|
suite.SkinsRepository.AssertExpectations(suite.T())
|
||||||
suite.CapesRepository.AssertExpectations(suite.T())
|
suite.CapesRepository.AssertExpectations(suite.T())
|
||||||
suite.MojangTexturesProvider.AssertExpectations(suite.T())
|
suite.MojangTexturesProvider.AssertExpectations(suite.T())
|
||||||
suite.Auth.AssertExpectations(suite.T())
|
|
||||||
suite.Emitter.AssertExpectations(suite.T())
|
suite.Emitter.AssertExpectations(suite.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +165,7 @@ var skinsTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
|
Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@@ -183,7 +176,7 @@ var skinsTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures",
|
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@@ -193,7 +186,7 @@ var skinsTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username doesn't exists on the local storage and doesn't exists on Mojang",
|
Name: "Username doesn't exists on the local storage and doesn't exists on Mojang",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@@ -205,28 +198,24 @@ var skinsTestsCases = []*skinsystemTestCase{
|
|||||||
func (suite *skinsystemTestSuite) TestSkin() {
|
func (suite *skinsystemTestSuite) TestSkin() {
|
||||||
for _, testCase := range skinsTestsCases {
|
for _, testCase := range skinsTestsCases {
|
||||||
suite.RunSubTest(testCase.Name, func() {
|
suite.RunSubTest(testCase.Name, func() {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
testCase.BeforeTest(suite)
|
testCase.BeforeTest(suite)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username", nil)
|
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
testCase.AfterTest(suite, w.Result())
|
testCase.AfterTest(suite, w.Result())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.RunSubTest("Pass username with png extension", func() {
|
suite.RunSubTest("Pass username with png extension", func() {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username.png", nil)
|
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username.png", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
resp := w.Result()
|
resp := w.Result()
|
||||||
suite.Equal(301, resp.StatusCode)
|
suite.Equal(301, resp.StatusCode)
|
||||||
@@ -237,27 +226,23 @@ func (suite *skinsystemTestSuite) TestSkin() {
|
|||||||
func (suite *skinsystemTestSuite) TestSkinGET() {
|
func (suite *skinsystemTestSuite) TestSkinGET() {
|
||||||
for _, testCase := range skinsTestsCases {
|
for _, testCase := range skinsTestsCases {
|
||||||
suite.RunSubTest(testCase.Name, func() {
|
suite.RunSubTest(testCase.Name, func() {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
testCase.BeforeTest(suite)
|
testCase.BeforeTest(suite)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/skins?name=mock_username", nil)
|
req := httptest.NewRequest("GET", "http://chrly/skins?name=mock_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
testCase.AfterTest(suite, w.Result())
|
testCase.AfterTest(suite, w.Result())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.RunSubTest("Do not pass name param", func() {
|
suite.RunSubTest("Do not pass name param", func() {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/skins", nil)
|
req := httptest.NewRequest("GET", "http://chrly/skins", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
resp := w.Result()
|
resp := w.Result()
|
||||||
suite.Equal(400, resp.StatusCode)
|
suite.Equal(400, resp.StatusCode)
|
||||||
@@ -284,7 +269,7 @@ var capesTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
|
Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, &CapeNotFoundError{Who: "mock_username"})
|
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, true), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, true), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@@ -295,7 +280,7 @@ var capesTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures",
|
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, &CapeNotFoundError{Who: "mock_username"})
|
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@@ -305,7 +290,7 @@ var capesTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username doesn't exists on the local storage and doesn't exists on Mojang",
|
Name: "Username doesn't exists on the local storage and doesn't exists on Mojang",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, &CapeNotFoundError{Who: "mock_username"})
|
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@@ -317,28 +302,24 @@ var capesTestsCases = []*skinsystemTestCase{
|
|||||||
func (suite *skinsystemTestSuite) TestCape() {
|
func (suite *skinsystemTestSuite) TestCape() {
|
||||||
for _, testCase := range capesTestsCases {
|
for _, testCase := range capesTestsCases {
|
||||||
suite.RunSubTest(testCase.Name, func() {
|
suite.RunSubTest(testCase.Name, func() {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
testCase.BeforeTest(suite)
|
testCase.BeforeTest(suite)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username", nil)
|
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
testCase.AfterTest(suite, w.Result())
|
testCase.AfterTest(suite, w.Result())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.RunSubTest("Pass username with png extension", func() {
|
suite.RunSubTest("Pass username with png extension", func() {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil)
|
suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username.png", nil)
|
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username.png", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
resp := w.Result()
|
resp := w.Result()
|
||||||
suite.Equal(200, resp.StatusCode)
|
suite.Equal(200, resp.StatusCode)
|
||||||
@@ -351,27 +332,23 @@ func (suite *skinsystemTestSuite) TestCape() {
|
|||||||
func (suite *skinsystemTestSuite) TestCapeGET() {
|
func (suite *skinsystemTestSuite) TestCapeGET() {
|
||||||
for _, testCase := range capesTestsCases {
|
for _, testCase := range capesTestsCases {
|
||||||
suite.RunSubTest(testCase.Name, func() {
|
suite.RunSubTest(testCase.Name, func() {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
testCase.BeforeTest(suite)
|
testCase.BeforeTest(suite)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/cloaks?name=mock_username", nil)
|
req := httptest.NewRequest("GET", "http://chrly/cloaks?name=mock_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
testCase.AfterTest(suite, w.Result())
|
testCase.AfterTest(suite, w.Result())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.RunSubTest("Do not pass name param", func() {
|
suite.RunSubTest("Do not pass name param", func() {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/cloaks", nil)
|
req := httptest.NewRequest("GET", "http://chrly/cloaks", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
resp := w.Result()
|
resp := w.Result()
|
||||||
suite.Equal(400, resp.StatusCode)
|
suite.Equal(400, resp.StatusCode)
|
||||||
@@ -387,7 +364,7 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
Name: "Username exists and has skin, no cape",
|
Name: "Username exists and has skin, no cape",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||||
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, &CapeNotFoundError{Who: "mock_username"})
|
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
suite.Equal(200, response.StatusCode)
|
suite.Equal(200, response.StatusCode)
|
||||||
@@ -404,7 +381,7 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
Name: "Username exists and has slim skin, no cape",
|
Name: "Username exists and has slim skin, no cape",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil)
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil)
|
||||||
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, &CapeNotFoundError{Who: "mock_username"})
|
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
suite.Equal(200, response.StatusCode)
|
suite.Equal(200, response.StatusCode)
|
||||||
@@ -423,7 +400,7 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username exists and has cape, no skin",
|
Name: "Username exists and has cape, no skin",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil)
|
suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@@ -460,8 +437,8 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username not exists, but Mojang profile available",
|
Name: "Username not exists, but Mojang profile available",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{})
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, &CapeNotFoundError{})
|
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponse(true, true), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponse(true, true), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@@ -481,8 +458,8 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username not exists and Mojang profile unavailable",
|
Name: "Username not exists and Mojang profile unavailable",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{})
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, &CapeNotFoundError{})
|
suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@@ -494,14 +471,12 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
func (suite *skinsystemTestSuite) TestTextures() {
|
func (suite *skinsystemTestSuite) TestTextures() {
|
||||||
for _, testCase := range texturesTestsCases {
|
for _, testCase := range texturesTestsCases {
|
||||||
suite.RunSubTest(testCase.Name, func() {
|
suite.RunSubTest(testCase.Name, func() {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
testCase.BeforeTest(suite)
|
testCase.BeforeTest(suite)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/textures/mock_username", nil)
|
req := httptest.NewRequest("GET", "http://chrly/textures/mock_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
testCase.AfterTest(suite, w.Result())
|
testCase.AfterTest(suite, w.Result())
|
||||||
})
|
})
|
||||||
@@ -540,8 +515,8 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
|||||||
"value": "mocked textures base64"
|
"value": "mocked textures base64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "chrly",
|
"name": "texturesParamName",
|
||||||
"value": "how do you tame a horse in Minecraft?"
|
"value": "texturesParamValue"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`, string(body))
|
}`, string(body))
|
||||||
@@ -551,7 +526,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
|||||||
Name: "Username not exists",
|
Name: "Username not exists",
|
||||||
AllowProxy: false,
|
AllowProxy: false,
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
suite.Equal(204, response.StatusCode)
|
suite.Equal(204, response.StatusCode)
|
||||||
@@ -578,7 +553,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
|||||||
Name: "Username not exists, but Mojang profile is available and proxying is enabled",
|
Name: "Username not exists, but Mojang profile is available and proxying is enabled",
|
||||||
AllowProxy: true,
|
AllowProxy: true,
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@@ -594,8 +569,8 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
|||||||
"value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIsInByb2ZpbGVJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn19fQ=="
|
"value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIsInByb2ZpbGVJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn19fQ=="
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "chrly",
|
"name": "texturesParamName",
|
||||||
"value": "how do you tame a horse in Minecraft?"
|
"value": "texturesParamValue"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`, string(body))
|
}`, string(body))
|
||||||
@@ -605,7 +580,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
|||||||
Name: "Username not exists, Mojang profile is unavailable too and proxying is enabled",
|
Name: "Username not exists, Mojang profile is unavailable too and proxying is enabled",
|
||||||
AllowProxy: true,
|
AllowProxy: true,
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@@ -619,8 +594,6 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
|||||||
func (suite *skinsystemTestSuite) TestSignedTextures() {
|
func (suite *skinsystemTestSuite) TestSignedTextures() {
|
||||||
for _, testCase := range signedTexturesTestsCases {
|
for _, testCase := range signedTexturesTestsCases {
|
||||||
suite.RunSubTest(testCase.Name, func() {
|
suite.RunSubTest(testCase.Name, func() {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
testCase.BeforeTest(suite)
|
testCase.BeforeTest(suite)
|
||||||
|
|
||||||
var target string
|
var target string
|
||||||
@@ -633,417 +606,13 @@ func (suite *skinsystemTestSuite) TestSignedTextures() {
|
|||||||
req := httptest.NewRequest("GET", target, nil)
|
req := httptest.NewRequest("GET", target, nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
testCase.AfterTest(suite, w.Result())
|
testCase.AfterTest(suite, w.Result())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*************************
|
|
||||||
* Post skin tests cases *
|
|
||||||
*************************/
|
|
||||||
|
|
||||||
type postSkinTestCase struct {
|
|
||||||
Name string
|
|
||||||
Form io.Reader
|
|
||||||
ExpectSuccess bool
|
|
||||||
BeforeTest func(suite *skinsystemTestSuite)
|
|
||||||
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
|
|
||||||
}
|
|
||||||
|
|
||||||
var postSkinTestsCases = []*postSkinTestCase{
|
|
||||||
{
|
|
||||||
Name: "Upload new identity with textures data",
|
|
||||||
Form: bytes.NewBufferString(url.Values{
|
|
||||||
"identityId": {"1"},
|
|
||||||
"username": {"mock_username"},
|
|
||||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
|
||||||
"skinId": {"5"},
|
|
||||||
"is1_8": {"0"},
|
|
||||||
"isSlim": {"0"},
|
|
||||||
"url": {"http://example.com/skin.png"},
|
|
||||||
}.Encode()),
|
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
|
||||||
suite.SkinsRepository.On("FindByUserId", 1).Return(nil, &SkinNotFoundError{Who: "unknown"})
|
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
|
||||||
suite.SkinsRepository.On("Save", 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)
|
|
||||||
suite.Equal(5, model.SkinId)
|
|
||||||
suite.False(model.Is1_8)
|
|
||||||
suite.False(model.IsSlim)
|
|
||||||
suite.Equal("http://example.com/skin.png", model.Url)
|
|
||||||
|
|
||||||
return true
|
|
||||||
})).Times(1).Return(nil)
|
|
||||||
},
|
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
|
||||||
suite.Equal(201, response.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
suite.Empty(body)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Update exists identity by changing only textures data",
|
|
||||||
Form: bytes.NewBufferString(url.Values{
|
|
||||||
"identityId": {"1"},
|
|
||||||
"username": {"mock_username"},
|
|
||||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
|
||||||
"skinId": {"5"},
|
|
||||||
"is1_8": {"1"},
|
|
||||||
"isSlim": {"1"},
|
|
||||||
"url": {"http://textures-server.com/skin.png"},
|
|
||||||
}.Encode()),
|
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
|
||||||
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
|
||||||
suite.SkinsRepository.On("Save", 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)
|
|
||||||
suite.Equal(5, model.SkinId)
|
|
||||||
suite.True(model.Is1_8)
|
|
||||||
suite.True(model.IsSlim)
|
|
||||||
suite.Equal("http://textures-server.com/skin.png", model.Url)
|
|
||||||
|
|
||||||
return true
|
|
||||||
})).Times(1).Return(nil)
|
|
||||||
},
|
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
|
||||||
suite.Equal(201, response.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
suite.Empty(body)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Update exists identity by changing its identityId",
|
|
||||||
Form: bytes.NewBufferString(url.Values{
|
|
||||||
"identityId": {"2"},
|
|
||||||
"username": {"mock_username"},
|
|
||||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
|
||||||
"skinId": {"5"},
|
|
||||||
"is1_8": {"0"},
|
|
||||||
"isSlim": {"0"},
|
|
||||||
"url": {"http://example.com/skin.png"},
|
|
||||||
}.Encode()),
|
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
|
||||||
suite.SkinsRepository.On("FindByUserId", 2).Return(nil, &SkinNotFoundError{Who: "unknown"})
|
|
||||||
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.Equal(2, model.UserId)
|
|
||||||
suite.Equal("mock_username", model.Username)
|
|
||||||
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
|
||||||
|
|
||||||
return true
|
|
||||||
})).Times(1).Return(nil)
|
|
||||||
},
|
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
|
||||||
suite.Equal(201, response.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
suite.Empty(body)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Update exists identity by changing its username",
|
|
||||||
Form: bytes.NewBufferString(url.Values{
|
|
||||||
"identityId": {"1"},
|
|
||||||
"username": {"changed_username"},
|
|
||||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
|
||||||
"skinId": {"5"},
|
|
||||||
"is1_8": {"0"},
|
|
||||||
"isSlim": {"0"},
|
|
||||||
"url": {"http://example.com/skin.png"},
|
|
||||||
}.Encode()),
|
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
|
||||||
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.Equal(1, model.UserId)
|
|
||||||
suite.Equal("changed_username", model.Username)
|
|
||||||
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
|
||||||
|
|
||||||
return true
|
|
||||||
})).Times(1).Return(nil)
|
|
||||||
},
|
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
|
||||||
suite.Equal(201, response.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
suite.Empty(body)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Handle an error when loading the data from the repository",
|
|
||||||
Form: bytes.NewBufferString(url.Values{
|
|
||||||
"identityId": {"1"},
|
|
||||||
"username": {"mock_username"},
|
|
||||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
|
||||||
"skinId": {"5"},
|
|
||||||
"is1_8": {"1"},
|
|
||||||
"isSlim": {"1"},
|
|
||||||
"url": {"http://textures-server.com/skin.png"},
|
|
||||||
}.Encode()),
|
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
|
||||||
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
|
||||||
err := errors.New("mock error")
|
|
||||||
suite.SkinsRepository.On("Save", 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)
|
|
||||||
})).Once()
|
|
||||||
},
|
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
|
||||||
suite.Equal(500, response.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
suite.Empty(body)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Handle an error when saving the data into the repository",
|
|
||||||
Form: bytes.NewBufferString(url.Values{
|
|
||||||
"identityId": {"1"},
|
|
||||||
"username": {"changed_username"},
|
|
||||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
|
||||||
"skinId": {"5"},
|
|
||||||
"is1_8": {"0"},
|
|
||||||
"isSlim": {"0"},
|
|
||||||
"url": {"http://example.com/skin.png"},
|
|
||||||
}.Encode()),
|
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
|
||||||
err := errors.New("mock error")
|
|
||||||
suite.SkinsRepository.On("FindByUserId", 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)
|
|
||||||
})).Once()
|
|
||||||
},
|
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
|
||||||
suite.Equal(500, response.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
suite.Empty(body)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *skinsystemTestSuite) TestPostSkin() {
|
|
||||||
for _, testCase := range postSkinTestsCases {
|
|
||||||
suite.RunSubTest(testCase.Name, func() {
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
|
||||||
testCase.BeforeTest(suite)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "http://chrly/api/skins", testCase.Form)
|
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
|
||||||
|
|
||||||
testCase.AfterTest(suite, w.Result())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
suite.RunSubTest("Get errors about required fields", func() {
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(url.Values{
|
|
||||||
"mojangTextures": {"someBase64EncodedString"},
|
|
||||||
}.Encode()))
|
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
defer resp.Body.Close()
|
|
||||||
suite.Equal(400, resp.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
suite.JSONEq(`{
|
|
||||||
"errors": {
|
|
||||||
"identityId": [
|
|
||||||
"The identityId field is required",
|
|
||||||
"The identityId field must be numeric",
|
|
||||||
"The identityId field must be minimum 1 char"
|
|
||||||
],
|
|
||||||
"skinId": [
|
|
||||||
"The skinId field is required",
|
|
||||||
"The skinId field must be numeric",
|
|
||||||
"The skinId field must be minimum 1 char"
|
|
||||||
],
|
|
||||||
"username": [
|
|
||||||
"The username field is required"
|
|
||||||
],
|
|
||||||
"uuid": [
|
|
||||||
"The uuid field is required",
|
|
||||||
"The uuid field must contain valid UUID"
|
|
||||||
],
|
|
||||||
"url": [
|
|
||||||
"One of url or skin should be provided, but not both"
|
|
||||||
],
|
|
||||||
"skin": [
|
|
||||||
"One of url or skin should be provided, but not both"
|
|
||||||
],
|
|
||||||
"mojangSignature": [
|
|
||||||
"The mojangSignature field is required"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}`, string(body))
|
|
||||||
})
|
|
||||||
|
|
||||||
suite.RunSubTest("Send request without authorization", func() {
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
req := httptest.NewRequest("POST", "http://chrly/api/skins", nil)
|
|
||||||
req.Header.Add("Authorization", "Bearer invalid.jwt.token")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
suite.Auth.On("Authenticate", mock.Anything).Return(errors.New("Cannot parse passed JWT token"))
|
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
defer resp.Body.Close()
|
|
||||||
suite.Equal(403, resp.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
suite.JSONEq(`{
|
|
||||||
"error": "Cannot parse passed JWT token"
|
|
||||||
}`, string(body))
|
|
||||||
})
|
|
||||||
|
|
||||||
suite.RunSubTest("Upload textures with skin as file", func() {
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
|
||||||
|
|
||||||
inputBody := &bytes.Buffer{}
|
|
||||||
writer := multipart.NewWriter(inputBody)
|
|
||||||
|
|
||||||
part, _ := writer.CreateFormFile("skin", "char.png")
|
|
||||||
_, _ = part.Write(loadSkinFile())
|
|
||||||
|
|
||||||
_ = writer.WriteField("identityId", "1")
|
|
||||||
_ = writer.WriteField("username", "mock_user")
|
|
||||||
_ = writer.WriteField("uuid", "0f657aa8-bfbe-415d-b700-5750090d3af3")
|
|
||||||
_ = writer.WriteField("skinId", "5")
|
|
||||||
|
|
||||||
err := writer.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "http://chrly/api/skins", inputBody)
|
|
||||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
defer resp.Body.Close()
|
|
||||||
suite.Equal(400, resp.StatusCode)
|
|
||||||
responseBody, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
suite.JSONEq(`{
|
|
||||||
"errors": {
|
|
||||||
"skin": [
|
|
||||||
"Skin uploading is temporary unavailable"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}`, string(responseBody))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**************************************
|
|
||||||
* Delete skin by user id tests cases *
|
|
||||||
**************************************/
|
|
||||||
|
|
||||||
func (suite *skinsystemTestSuite) TestDeleteByUserId() {
|
|
||||||
suite.RunSubTest("Delete skin by its identity id", func() {
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
|
||||||
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
|
||||||
suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:1", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
defer resp.Body.Close()
|
|
||||||
suite.Equal(204, resp.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
suite.Empty(body)
|
|
||||||
})
|
|
||||||
|
|
||||||
suite.RunSubTest("Try to remove not exists identity id", func() {
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
|
||||||
suite.SkinsRepository.On("FindByUserId", 1).Return(nil, &SkinNotFoundError{Who: "unknown"})
|
|
||||||
|
|
||||||
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:1", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
defer resp.Body.Close()
|
|
||||||
suite.Equal(404, resp.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
suite.JSONEq(`[
|
|
||||||
"Cannot find record for the requested identifier"
|
|
||||||
]`, string(body))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************
|
|
||||||
* Delete skin by username tests cases *
|
|
||||||
***************************************/
|
|
||||||
|
|
||||||
func (suite *skinsystemTestSuite) TestDeleteByUsername() {
|
|
||||||
suite.RunSubTest("Delete skin by its identity username", func() {
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
|
||||||
suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_username", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
defer resp.Body.Close()
|
|
||||||
suite.Equal(204, resp.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
suite.Empty(body)
|
|
||||||
})
|
|
||||||
|
|
||||||
suite.RunSubTest("Try to remove not exists identity username", func() {
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
|
||||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
|
||||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
|
||||||
|
|
||||||
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_username", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
defer resp.Body.Close()
|
|
||||||
suite.Equal(404, resp.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
suite.JSONEq(`[
|
|
||||||
"Cannot find record for the requested identifier"
|
|
||||||
]`, string(body))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************
|
/****************
|
||||||
* Custom tests *
|
* Custom tests *
|
||||||
****************/
|
****************/
|
||||||
@@ -1118,16 +687,3 @@ func createMojangResponse(includeSkin bool, includeCape bool) *mojang.SignedText
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
// base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png
|
|
||||||
var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==")
|
|
||||||
|
|
||||||
func loadSkinFile() []byte {
|
|
||||||
result := make([]byte, 92)
|
|
||||||
_, err := base64.StdEncoding.Decode(result, OnePxPng)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
@@ -7,37 +7,26 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/elyby/chrly/api/mojang"
|
"github.com/elyby/chrly/api/mojang"
|
||||||
"github.com/elyby/chrly/mojangtextures"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UuidsProvider interface {
|
type MojangUuidsProvider interface {
|
||||||
GetUuid(username string) (*mojang.ProfileInfo, error)
|
GetUuid(username string) (*mojang.ProfileInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UUIDsWorker struct {
|
type UUIDsWorker struct {
|
||||||
Emitter
|
MojangUuidsProvider
|
||||||
UUIDsProvider mojangtextures.UUIDsProvider
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *UUIDsWorker) CreateHandler() *mux.Router {
|
func (ctx *UUIDsWorker) Handler() *mux.Router {
|
||||||
requestEventsMiddleware := CreateRequestEventsMiddleware(ctx.Emitter, "skinsystem") // This prefix should be unified
|
|
||||||
|
|
||||||
router := mux.NewRouter().StrictSlash(true)
|
router := mux.NewRouter().StrictSlash(true)
|
||||||
router.Use(requestEventsMiddleware)
|
router.Handle("/mojang-uuid/{username}", http.HandlerFunc(ctx.getUUIDHandler)).Methods("GET")
|
||||||
|
|
||||||
router.Handle("/api/worker/mojang-uuid/{username}", http.HandlerFunc(ctx.GetUUID)).Methods("GET")
|
|
||||||
|
|
||||||
// 404
|
|
||||||
// NotFoundHandler doesn't call for registered middlewares, so we must wrap it manually.
|
|
||||||
// See https://github.com/gorilla/mux/issues/416#issuecomment-600079279
|
|
||||||
router.NotFoundHandler = requestEventsMiddleware(http.HandlerFunc(NotFound))
|
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *UUIDsWorker) GetUUID(response http.ResponseWriter, request *http.Request) {
|
func (ctx *UUIDsWorker) getUUIDHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
username := parseUsername(mux.Vars(request)["username"])
|
username := mux.Vars(request)["username"]
|
||||||
profile, err := ctx.UUIDsProvider.GetUuid(username)
|
profile, err := ctx.GetUuid(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*mojang.TooManyRequestsError); ok {
|
if _, ok := err.(*mojang.TooManyRequestsError); ok {
|
||||||
response.WriteHeader(http.StatusTooManyRequests)
|
response.WriteHeader(http.StatusTooManyRequests)
|
||||||
|
@@ -37,7 +37,6 @@ type uuidsWorkerTestSuite struct {
|
|||||||
App *UUIDsWorker
|
App *UUIDsWorker
|
||||||
|
|
||||||
UuidsProvider *uuidsProviderMock
|
UuidsProvider *uuidsProviderMock
|
||||||
Emitter *emitterMock
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/********************
|
/********************
|
||||||
@@ -46,17 +45,14 @@ type uuidsWorkerTestSuite struct {
|
|||||||
|
|
||||||
func (suite *uuidsWorkerTestSuite) SetupTest() {
|
func (suite *uuidsWorkerTestSuite) SetupTest() {
|
||||||
suite.UuidsProvider = &uuidsProviderMock{}
|
suite.UuidsProvider = &uuidsProviderMock{}
|
||||||
suite.Emitter = &emitterMock{}
|
|
||||||
|
|
||||||
suite.App = &UUIDsWorker{
|
suite.App = &UUIDsWorker{
|
||||||
UUIDsProvider: suite.UuidsProvider,
|
MojangUuidsProvider: suite.UuidsProvider,
|
||||||
Emitter: suite.Emitter,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *uuidsWorkerTestSuite) TearDownTest() {
|
func (suite *uuidsWorkerTestSuite) TearDownTest() {
|
||||||
suite.UuidsProvider.AssertExpectations(suite.T())
|
suite.UuidsProvider.AssertExpectations(suite.T())
|
||||||
suite.Emitter.AssertExpectations(suite.T())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *uuidsWorkerTestSuite) RunSubTest(name string, subTest func()) {
|
func (suite *uuidsWorkerTestSuite) RunSubTest(name string, subTest func()) {
|
||||||
@@ -87,8 +83,6 @@ var getUuidTestsCases = []*uuidsWorkerTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Success provider response",
|
Name: "Success provider response",
|
||||||
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, 200)
|
|
||||||
suite.UuidsProvider.On("GetUuid", "mock_username").Return(&mojang.ProfileInfo{
|
suite.UuidsProvider.On("GetUuid", "mock_username").Return(&mojang.ProfileInfo{
|
||||||
Id: "0fcc38620f1845f3a54e1b523c1bd1c7",
|
Id: "0fcc38620f1845f3a54e1b523c1bd1c7",
|
||||||
Name: "mock_username",
|
Name: "mock_username",
|
||||||
@@ -107,8 +101,6 @@ var getUuidTestsCases = []*uuidsWorkerTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Receive empty response from UUIDs provider",
|
Name: "Receive empty response from UUIDs provider",
|
||||||
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, 204)
|
|
||||||
suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, nil)
|
suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *uuidsWorkerTestSuite, response *http.Response) {
|
AfterTest: func(suite *uuidsWorkerTestSuite, response *http.Response) {
|
||||||
@@ -120,8 +112,6 @@ var getUuidTestsCases = []*uuidsWorkerTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Receive error from UUIDs provider",
|
Name: "Receive error from UUIDs provider",
|
||||||
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, 500)
|
|
||||||
err := errors.New("this is an error")
|
err := errors.New("this is an error")
|
||||||
suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, err)
|
suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, err)
|
||||||
},
|
},
|
||||||
@@ -137,8 +127,6 @@ var getUuidTestsCases = []*uuidsWorkerTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Receive Too Many Requests from UUIDs provider",
|
Name: "Receive Too Many Requests from UUIDs provider",
|
||||||
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
||||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, 429)
|
|
||||||
err := &mojang.TooManyRequestsError{}
|
err := &mojang.TooManyRequestsError{}
|
||||||
suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, err)
|
suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, err)
|
||||||
},
|
},
|
||||||
@@ -155,10 +143,10 @@ func (suite *uuidsWorkerTestSuite) TestGetUUID() {
|
|||||||
suite.RunSubTest(testCase.Name, func() {
|
suite.RunSubTest(testCase.Name, func() {
|
||||||
testCase.BeforeTest(suite)
|
testCase.BeforeTest(suite)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/api/worker/mojang-uuid/mock_username", nil)
|
req := httptest.NewRequest("GET", "http://chrly/mojang-uuid/mock_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
|
||||||
testCase.AfterTest(suite, w.Result())
|
testCase.AfterTest(suite, w.Result())
|
||||||
})
|
})
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/elyby/chrly/api/mojang"
|
"github.com/elyby/chrly/api/mojang"
|
||||||
|
"github.com/elyby/chrly/dispatcher"
|
||||||
)
|
)
|
||||||
|
|
||||||
type broadcastResult struct {
|
type broadcastResult struct {
|
||||||
@@ -72,7 +73,7 @@ type TexturesProvider interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Emitter interface {
|
type Emitter interface {
|
||||||
Emit(name string, args ...interface{})
|
dispatcher.Emitter
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
|
Reference in New Issue
Block a user