Merge pull request #49 from umputun/feature/application
Feature/application
This commit is contained in:
+1
-14
@@ -1,19 +1,6 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.10.x"
|
||||
|
||||
before_install:
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
- go get github.com/mattn/goveralls
|
||||
|
||||
install:
|
||||
- docker --version
|
||||
- docker-compose --version
|
||||
|
||||
script:
|
||||
- docker build .
|
||||
|
||||
after_success:
|
||||
- go test $(go list -e ./... | grep -v vendor) -covermode=count -coverprofile=coverage.out
|
||||
- goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN
|
||||
- docker build --build-arg COVERALLS_TOKEN=$COVERALLS_TOKEN --build-arg TRAVIS_JOB_ID=$TRAVIS_JOB_ID --build-arg TRAVIS_PULL_REQUEST=$TRAVIS_PULL_REQUEST --build-arg TRAVIS_BRANCH=$TRAVIS_BRANCH .
|
||||
|
||||
+11
-1
@@ -1,9 +1,15 @@
|
||||
FROM umputun/baseimage:buildgo-latest as build-backend
|
||||
|
||||
ARG COVERALLS_TOKEN
|
||||
ARG TRAVIS_JOB_ID
|
||||
ARG TRAVIS_PULL_REQUEST
|
||||
ARG TRAVIS_BRANCH
|
||||
|
||||
WORKDIR /go/src/github.com/umputun/remark
|
||||
|
||||
ADD app /go/src/github.com/umputun/remark/app
|
||||
ADD vendor /go/src/github.com/umputun/remark/vendor
|
||||
ADD .git /go/src/github.com/umputun/remark/.git
|
||||
|
||||
RUN cd app && go test $(go list -e ./... | grep -v vendor)
|
||||
|
||||
@@ -13,7 +19,11 @@ RUN gometalinter --disable-all --deadline=300s --vendor --enable=vet --enable=ve
|
||||
|
||||
RUN mkdir -p target && /script/coverage.sh
|
||||
|
||||
ADD .git /go/src/github.com/umputun/remark/.git
|
||||
RUN if [ "x$COVERALLS_TOKEN" = "x" ] ; then \
|
||||
echo coverall not enabled ; \
|
||||
else go get github.com/mattn/goveralls && \
|
||||
goveralls -coverprofile=.cover/cover.out -service=travis-ci -repotoken $COVERALLS_TOKEN; fi
|
||||
|
||||
RUN go build -o remark -ldflags "-X main.revision=$(git rev-parse --abbrev-ref HEAD)-$(git describe --abbrev=7 --always --tags)-$(date +%Y%m%d-%H:%M:%S) -s -w" ./app
|
||||
|
||||
|
||||
|
||||
+105
-51
@@ -1,11 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/bbolt"
|
||||
@@ -22,7 +25,8 @@ import (
|
||||
"github.com/umputun/remark/app/rest/proxy"
|
||||
)
|
||||
|
||||
var opts struct {
|
||||
// Opts with command line flags and env
|
||||
type Opts struct {
|
||||
BoltPath string `long:"bolt" env:"BOLTDB_PATH" default:"./var" description:"parent dir for bolt files"`
|
||||
Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","`
|
||||
RemarkURL string `long:"url" env:"REMARK_URL" default:"https://remark42.com" description:"url to remark"`
|
||||
@@ -58,47 +62,62 @@ var opts struct {
|
||||
|
||||
var revision = "unknown"
|
||||
|
||||
// Application holds all active objects
|
||||
type Application struct {
|
||||
Opts
|
||||
srv *api.Rest
|
||||
importer *api.Import
|
||||
exporter migrator.Exporter
|
||||
terminated chan struct{}
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Printf("remark %s\n", revision)
|
||||
|
||||
var opts Opts
|
||||
p := flags.NewParser(&opts, flags.Default)
|
||||
if _, e := p.ParseArgs(os.Args[1:]); e != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog(opts.Dbg)
|
||||
log.Print("[INFO] started remark")
|
||||
|
||||
if err := makeDirs(opts.BoltPath, opts.BackupLocation, opts.AvatarStore); err != nil {
|
||||
log.Fatalf("[ERROR] can't create directories, %+v", err)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() { // catch signal and invoke graceful termination
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
<-stop
|
||||
log.Print("[WARN] interrupt signal")
|
||||
cancel()
|
||||
}()
|
||||
|
||||
app, err := New(opts)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] failed to setup application, %+v", err)
|
||||
}
|
||||
err = app.Run(ctx)
|
||||
log.Printf("[INFO] remark terminated %s", err)
|
||||
}
|
||||
|
||||
dataStore := makeBoltStore(opts.Sites)
|
||||
// New prepares application and return it with all active parts
|
||||
// doesn't start anything
|
||||
func New(opts Opts) (*Application, error) {
|
||||
setupLog(opts.Dbg)
|
||||
|
||||
if opts.DevPasswd != "" {
|
||||
log.Printf("[WARN] running in dev mode")
|
||||
if err := makeDirs(opts.BoltPath, opts.BackupLocation, opts.AvatarStore); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataService := service.DataStore{
|
||||
Interface: dataStore,
|
||||
Interface: makeBoltStore(opts.Sites, opts.BoltPath),
|
||||
EditDuration: 5 * time.Minute,
|
||||
Secret: opts.SecretKey,
|
||||
MaxCommentSize: opts.MaxCommentSize,
|
||||
}
|
||||
|
||||
exporter := migrator.Remark{DataStore: &dataService}
|
||||
cache := rest.NewLoadingCache(rest.MaxValueSize(opts.MaxCachedValue), rest.MaxKeys(opts.MaxCachedItems),
|
||||
rest.PostFlushFn(postFlushFn))
|
||||
cache := rest.NewLoadingCache(rest.MaxValSize(opts.MaxCachedValue), rest.MaxKeys(opts.MaxCachedItems),
|
||||
rest.PostFlushFn(postFlushFn(opts.Sites, opts.Port)))
|
||||
|
||||
activateBackup(&exporter)
|
||||
|
||||
importSrv := api.Import{
|
||||
Version: revision,
|
||||
Cache: cache,
|
||||
NativeImporter: &migrator.Remark{DataStore: &dataService},
|
||||
DisqusImporter: &migrator.Disqus{DataStore: &dataService},
|
||||
SecretKey: opts.SecretKey,
|
||||
}
|
||||
go importSrv.Run(opts.Port + 1)
|
||||
jwtService := auth.NewJWT(opts.SecretKey, strings.HasPrefix(opts.RemarkURL, "https://"), 7*24*time.Hour)
|
||||
|
||||
avatarProxy := &proxy.Avatar{
|
||||
StorePath: opts.AvatarStore,
|
||||
@@ -106,46 +125,79 @@ func main() {
|
||||
RemarkURL: strings.TrimSuffix(opts.RemarkURL, "/"),
|
||||
}
|
||||
|
||||
jwtService := auth.NewJWT(opts.SecretKey, strings.HasPrefix(opts.RemarkURL, "https://"), 7*24*time.Hour)
|
||||
exporter := &migrator.Remark{DataStore: &dataService}
|
||||
|
||||
srv := api.Rest{
|
||||
importer := &api.Import{
|
||||
Version: revision,
|
||||
Cache: cache,
|
||||
NativeImporter: &migrator.Remark{DataStore: &dataService},
|
||||
DisqusImporter: &migrator.Disqus{DataStore: &dataService},
|
||||
SecretKey: opts.SecretKey,
|
||||
}
|
||||
|
||||
srv := &api.Rest{
|
||||
Version: revision,
|
||||
DataService: dataService,
|
||||
Exporter: &exporter,
|
||||
Exporter: exporter,
|
||||
WebRoot: opts.WebRoot,
|
||||
ImageProxy: proxy.Image{Enabled: opts.ImageProxy, RoutePath: "/api/v1/img", RemarkURL: opts.RemarkURL},
|
||||
Authenticator: auth.Authenticator{
|
||||
JWTService: jwtService,
|
||||
Admins: opts.Admins,
|
||||
Providers: makeAuthProviders(jwtService, avatarProxy),
|
||||
Providers: makeAuthProviders(jwtService, avatarProxy, opts),
|
||||
AvatarProxy: avatarProxy,
|
||||
DevPasswd: opts.DevPasswd,
|
||||
},
|
||||
Cache: cache,
|
||||
}
|
||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = opts.LowScore, opts.CriticalScore
|
||||
srv.Run(opts.Port)
|
||||
tch := make(chan struct{})
|
||||
return &Application{srv: srv, importer: importer, exporter: exporter, Opts: opts, terminated: tch}, nil
|
||||
}
|
||||
|
||||
// Run all application objects
|
||||
func (a *Application) Run(ctx context.Context) error {
|
||||
if a.DevPasswd != "" {
|
||||
log.Printf("[WARN] running in dev mode")
|
||||
}
|
||||
|
||||
go func() {
|
||||
// shutdown on context cancellation
|
||||
<-ctx.Done()
|
||||
a.srv.Shutdown()
|
||||
a.importer.Shutdown()
|
||||
}()
|
||||
a.activateBackup(ctx) // runs in goroutine for each site
|
||||
go a.importer.Run(a.Port + 1)
|
||||
a.srv.Run(a.Port)
|
||||
close(a.terminated)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait for application completion (termination)
|
||||
func (a *Application) Wait() {
|
||||
<-a.terminated
|
||||
}
|
||||
|
||||
// activateBackup runs background backups for each site
|
||||
func activateBackup(exporter migrator.Exporter) {
|
||||
for _, siteID := range opts.Sites {
|
||||
func (a *Application) activateBackup(ctx context.Context) {
|
||||
for _, siteID := range a.Sites {
|
||||
backup := migrator.AutoBackup{
|
||||
Exporter: exporter,
|
||||
BackupLocation: opts.BackupLocation,
|
||||
Exporter: a.exporter,
|
||||
BackupLocation: a.BackupLocation,
|
||||
SiteID: siteID,
|
||||
KeepMax: opts.MaxBackupFiles,
|
||||
KeepMax: a.MaxBackupFiles,
|
||||
Duration: 24 * time.Hour,
|
||||
}
|
||||
go backup.Do()
|
||||
go backup.Do(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// makeBoltStore creates store for all sites
|
||||
func makeBoltStore(siteNames []string) engine.Interface {
|
||||
func makeBoltStore(siteNames []string, path string) engine.Interface {
|
||||
sites := []engine.BoltSite{}
|
||||
for _, site := range siteNames {
|
||||
sites = append(sites, engine.BoltSite{SiteID: site, FileName: fmt.Sprintf("%s/%s.db", opts.BoltPath, site)})
|
||||
sites = append(sites, engine.BoltSite{SiteID: site, FileName: fmt.Sprintf("%s/%s.db", path, site)})
|
||||
}
|
||||
result, err := engine.NewBoltDB(bolt.Options{Timeout: 30 * time.Second}, sites...)
|
||||
if err != nil {
|
||||
@@ -183,7 +235,7 @@ func makeDirs(dirs ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeAuthProviders(jwtService *auth.JWT, avatarProxy *proxy.Avatar) (providers []auth.Provider) {
|
||||
func makeAuthProviders(jwtService *auth.JWT, avatarProxy *proxy.Avatar, opts Opts) (providers []auth.Provider) {
|
||||
|
||||
makeParams := func(cid, secret string) auth.Params {
|
||||
return auth.Params{
|
||||
@@ -214,23 +266,25 @@ func makeAuthProviders(jwtService *auth.JWT, avatarProxy *proxy.Avatar) (provide
|
||||
}
|
||||
|
||||
// post-flush callback invoked by cache after each flush in async way
|
||||
func postFlushFn() {
|
||||
func postFlushFn(sites []string, port int) func() {
|
||||
|
||||
// list of heavy urls for pre-heating on cache change
|
||||
urls := []string{
|
||||
"http://localhost:%d/api/v1/list?site=%s",
|
||||
"http://localhost:%d/api/v1/last/50?site=%s",
|
||||
}
|
||||
return func() {
|
||||
// list of heavy urls for pre-heating on cache change
|
||||
urls := []string{
|
||||
"http://localhost:%d/api/v1/list?site=%s",
|
||||
"http://localhost:%d/api/v1/last/50?site=%s",
|
||||
}
|
||||
|
||||
for _, site := range opts.Sites {
|
||||
for _, u := range urls {
|
||||
resp, err := http.Get(fmt.Sprintf(u, opts.Port, site))
|
||||
if err != nil {
|
||||
log.Printf("[WARN] failed to refresh cached list for %s, %s", site, err)
|
||||
return
|
||||
}
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
log.Printf("[WARN] failed to close response body, %s", err)
|
||||
for _, site := range sites {
|
||||
for _, u := range urls {
|
||||
resp, err := http.Get(fmt.Sprintf(u, port, site))
|
||||
if err != nil {
|
||||
log.Printf("[WARN] failed to refresh cached list for %s, %s", site, err)
|
||||
return
|
||||
}
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
log.Printf("[WARN] failed to close response body, %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
flags "github.com/jessevdk/go-flags"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApplication(t *testing.T) {
|
||||
app, ctx := prepApp(t, 18080, 500*time.Millisecond)
|
||||
go app.Run(ctx)
|
||||
time.Sleep(100 * time.Millisecond) // let server start
|
||||
|
||||
// send ping
|
||||
resp, err := http.Get("http://localhost:18080/api/v1/ping")
|
||||
require.Nil(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "pong", string(body))
|
||||
|
||||
// add comment
|
||||
resp, err = http.Post("http://dev:password@localhost:18080/api/v1/comment", "json",
|
||||
strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "remark"}}`))
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
body, _ = ioutil.ReadAll(resp.Body)
|
||||
t.Log(string(body))
|
||||
app.Wait()
|
||||
}
|
||||
|
||||
func TestApplicationShutdown(t *testing.T) {
|
||||
app, ctx := prepApp(t, 18090, 500*time.Millisecond)
|
||||
st := time.Now()
|
||||
app.Run(ctx)
|
||||
assert.True(t, time.Since(st).Seconds() < 1, "should take about 500msec")
|
||||
app.Wait()
|
||||
}
|
||||
|
||||
func TestApplicationMainSignal(t *testing.T) {
|
||||
os.Args = []string{"test", "--secret=123456", "--bolt=/tmp/xyz", "--backup=/tmp", "--avatars=/tmp", "--port=18100"}
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
|
||||
}()
|
||||
st := time.Now()
|
||||
main()
|
||||
assert.True(t, time.Since(st).Seconds() < 1, "should take about 500msec")
|
||||
}
|
||||
|
||||
func prepApp(t *testing.T, port int, duration time.Duration) (*Application, context.Context) {
|
||||
// prepare options
|
||||
opts := Opts{}
|
||||
p := flags.NewParser(&opts, flags.Default)
|
||||
p.ParseArgs([]string{"--secret=123456", "--dev-passwd=password"})
|
||||
opts.AvatarStore, opts.BackupLocation = "/tmp", "/tmp"
|
||||
opts.BoltPath = fmt.Sprintf("/tmp/%d", port)
|
||||
opts.GithubCSEC, opts.GithubCID = "csec", "cid"
|
||||
opts.GoogleCSEC, opts.GoogleCID = "csec", "cid"
|
||||
opts.FacebookCSEC, opts.FacebookCID = "csec", "cid"
|
||||
opts.Port = port
|
||||
|
||||
os.Remove(opts.BoltPath + "/remark.db")
|
||||
|
||||
// create app
|
||||
app, err := New(opts)
|
||||
require.Nil(t, err)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(duration)
|
||||
log.Print("[TEST] terminate app")
|
||||
cancel()
|
||||
}()
|
||||
return app, ctx
|
||||
}
|
||||
+15
-7
@@ -2,6 +2,7 @@ package migrator
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -23,17 +24,24 @@ type AutoBackup struct {
|
||||
}
|
||||
|
||||
// Do runs daily export to local files, keeps up to keepMax backups for given siteID
|
||||
func (ab AutoBackup) Do() {
|
||||
func (ab AutoBackup) Do(ctx context.Context) {
|
||||
log.Printf("[INFO] activate auto-backup for %s", ab.BackupLocation)
|
||||
tick := time.NewTicker(ab.Duration)
|
||||
log.Printf("[DEBUG] first backup at %s", time.Now().Add(ab.Duration))
|
||||
for range tick.C {
|
||||
if _, err := ab.makeBackup(); err != nil {
|
||||
log.Printf("[WARN] auto-backup for %s failed, %s", ab.SiteID, err)
|
||||
continue
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
if _, err := ab.makeBackup(); err != nil {
|
||||
log.Printf("[WARN] auto-backup for %s failed, %s", ab.SiteID, err)
|
||||
continue
|
||||
}
|
||||
ab.removeOldBackupFiles()
|
||||
log.Printf("[DEBUG] next backup at %s", time.Now().Add(ab.Duration))
|
||||
case <-ctx.Done():
|
||||
log.Printf("[WARN] terminated autobackup for %s", ab.SiteID)
|
||||
return
|
||||
}
|
||||
ab.removeOldBackupFiles()
|
||||
log.Printf("[DEBUG] next backup at %s", time.Now().Add(ab.Duration))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -11,7 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMigrator_RemoveOldBackupFiles(t *testing.T) {
|
||||
func TestBackup_RemoveOldBackupFiles(t *testing.T) {
|
||||
loc := "/tmp/remark-backups.test"
|
||||
defer os.RemoveAll(loc)
|
||||
|
||||
@@ -36,7 +37,7 @@ func TestMigrator_RemoveOldBackupFiles(t *testing.T) {
|
||||
assert.Equal(t, "backup-site2-20171210.gz", ff[3].Name())
|
||||
}
|
||||
|
||||
func TestMigrator_MakeBackup(t *testing.T) {
|
||||
func TestBackup_MakeBackup(t *testing.T) {
|
||||
loc := "/tmp/remark-backups.test"
|
||||
defer os.RemoveAll(loc)
|
||||
os.MkdirAll(loc, 0700)
|
||||
@@ -52,6 +53,26 @@ func TestMigrator_MakeBackup(t *testing.T) {
|
||||
assert.Equal(t, int64(52), fi.Size())
|
||||
}
|
||||
|
||||
func TestBackup_Do(t *testing.T) {
|
||||
loc := "/tmp/remark-backups.test"
|
||||
defer os.RemoveAll(loc)
|
||||
os.MkdirAll(loc, 0700)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
bk := AutoBackup{BackupLocation: loc, SiteID: "site1", KeepMax: 3, Exporter: &mockExporter{}, Duration: 600 * time.Millisecond}
|
||||
bk.Do(ctx)
|
||||
|
||||
expFile := fmt.Sprintf("/tmp/remark-backups.test/backup-site1-%s.gz", time.Now().Format("20060102"))
|
||||
fi, err := os.Lstat(expFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(52), fi.Size())
|
||||
}
|
||||
|
||||
type mockExporter struct{}
|
||||
|
||||
func (mock *mockExporter) Export(w io.Writer, siteID string) (int, error) {
|
||||
|
||||
+27
-3
@@ -1,10 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/didip/tollbooth"
|
||||
@@ -24,6 +26,9 @@ type Import struct {
|
||||
NativeImporter migrator.Importer
|
||||
DisqusImporter migrator.Importer
|
||||
SecretKey string
|
||||
|
||||
httpServer *http.Server
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// Run the listener and request's router, activate rest server
|
||||
@@ -31,12 +36,31 @@ type Import struct {
|
||||
func (s *Import) Run(port int) {
|
||||
log.Printf("[INFO] activate import server on port %d", port)
|
||||
router := s.routes()
|
||||
httpServer := &http.Server{Addr: fmt.Sprintf("127.0.0.1:%d", port), Handler: router}
|
||||
err := httpServer.ListenAndServe()
|
||||
|
||||
s.lock.Lock()
|
||||
s.httpServer = &http.Server{Addr: fmt.Sprintf("127.0.0.1:%d", port), Handler: router}
|
||||
s.lock.Unlock()
|
||||
|
||||
err := s.httpServer.ListenAndServe()
|
||||
log.Printf("[WARN] http server terminated, %s", err)
|
||||
}
|
||||
|
||||
func (s Import) routes() chi.Router {
|
||||
// Shutdown import http server
|
||||
func (s *Import) Shutdown() {
|
||||
log.Print("[WARN] shutdown import server")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
s.lock.Lock()
|
||||
if err := s.httpServer.Shutdown(ctx); err != nil {
|
||||
log.Printf("[DEBUG] importer shutdown error, %s", err)
|
||||
}
|
||||
s.lock.Unlock()
|
||||
|
||||
log.Print("[DEBUG] shutdown import server completed")
|
||||
}
|
||||
|
||||
func (s *Import) routes() chi.Router {
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.RealIP, Recoverer)
|
||||
router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second))
|
||||
|
||||
@@ -30,6 +30,7 @@ func TestImport(t *testing.T) {
|
||||
req, err := http.NewRequest("POST", ts.URL+"/api/v1/admin/import?site=radio-t&provider=native&secret=123456", r)
|
||||
assert.Nil(t, err)
|
||||
resp, err := client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
+21
-1
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/didip/tollbooth"
|
||||
@@ -43,7 +45,9 @@ type Rest struct {
|
||||
Critical int
|
||||
}
|
||||
|
||||
httpServer *http.Server
|
||||
httpServer *http.Server
|
||||
lock sync.Mutex
|
||||
|
||||
adminService admin
|
||||
}
|
||||
|
||||
@@ -63,6 +67,7 @@ func (s *Rest) Run(port int) {
|
||||
|
||||
router := s.routes()
|
||||
|
||||
s.lock.Lock()
|
||||
s.httpServer = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
Handler: router,
|
||||
@@ -70,10 +75,25 @@ func (s *Rest) Run(port int) {
|
||||
WriteTimeout: 5 * time.Second,
|
||||
IdleTimeout: 30 * time.Second,
|
||||
}
|
||||
s.lock.Unlock()
|
||||
|
||||
err := s.httpServer.ListenAndServe()
|
||||
log.Printf("[WARN] http server terminated, %s", err)
|
||||
}
|
||||
|
||||
// Shutdown rest http server
|
||||
func (s *Rest) Shutdown() {
|
||||
log.Print("[WARN] shutdown rest server")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
s.lock.Lock()
|
||||
if err := s.httpServer.Shutdown(ctx); err != nil {
|
||||
log.Printf("[DEBUG] rest shutdown error, %s", err)
|
||||
}
|
||||
log.Print("[DEBUG] shutdown rest server completed")
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
func (s *Rest) routes() chi.Router {
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.RealIP, Recoverer)
|
||||
|
||||
@@ -87,17 +87,20 @@ func TestAuthRequired(t *testing.T) {
|
||||
|
||||
client := &http.Client{Timeout: 1 * time.Second}
|
||||
req, err := http.NewRequest("GET", server.URL+"/auth", nil)
|
||||
require.NoError(t, err)
|
||||
req = withBasicAuth(req, "dev", "123456")
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 201, resp.StatusCode, "valid auth user")
|
||||
|
||||
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
|
||||
require.NoError(t, err)
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 401, resp.StatusCode, "no auth user")
|
||||
|
||||
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
|
||||
require.NoError(t, err)
|
||||
req = withBasicAuth(req, "dev", "xyz")
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
@@ -115,17 +118,20 @@ func TestAuthNotRequired(t *testing.T) {
|
||||
|
||||
client := &http.Client{Timeout: 1 * time.Second}
|
||||
req, err := http.NewRequest("GET", server.URL+"/auth", nil)
|
||||
require.NoError(t, err)
|
||||
req = withBasicAuth(req, "dev", "123456")
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 201, resp.StatusCode, "valid auth user")
|
||||
|
||||
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
|
||||
require.NoError(t, err)
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 201, resp.StatusCode, "no auth user")
|
||||
|
||||
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
|
||||
require.NoError(t, err)
|
||||
req = withBasicAuth(req, "dev", "ZZZZ123456")
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
@@ -143,6 +149,7 @@ func TestAdminRequired(t *testing.T) {
|
||||
|
||||
client := &http.Client{Timeout: 1 * time.Second}
|
||||
req, err := http.NewRequest("GET", server.URL+"/auth", nil)
|
||||
require.NoError(t, err)
|
||||
req = withBasicAuth(req, "dev", "123456")
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
@@ -150,6 +157,7 @@ func TestAdminRequired(t *testing.T) {
|
||||
|
||||
devUser.Admin = false
|
||||
req, err = http.NewRequest("GET", server.URL+"/auth", nil)
|
||||
require.NoError(t, err)
|
||||
req = withBasicAuth(req, "dev", "123456")
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -27,6 +27,7 @@ func TestLogin(t *testing.T) {
|
||||
}()
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
require.Nil(t, err)
|
||||
client := &http.Client{Jar: jar, Timeout: 5 * time.Second}
|
||||
resp, err := client.Get("http://localhost:8981/login")
|
||||
assert.Nil(t, err)
|
||||
|
||||
+2
-2
@@ -82,9 +82,9 @@ func (lc *loadingCache) allowed(data []byte) bool {
|
||||
// CacheOption func type
|
||||
type CacheOption func(lc *loadingCache) error
|
||||
|
||||
// MaxValueSize functional option defines the largest value's size allowed to be cached
|
||||
// MaxValSize functional option defines the largest value's size allowed to be cached
|
||||
// By default it is 0, which means unlimited.
|
||||
func MaxValueSize(max int) CacheOption {
|
||||
func MaxValSize(max int) CacheOption {
|
||||
return func(lc *loadingCache) error {
|
||||
lc.maxValueSize = max
|
||||
return nil
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestLoadingCache_Get(t *testing.T) {
|
||||
func TestLoadingCache_MaxKeys(t *testing.T) {
|
||||
var postFnCall, coldCalls int32
|
||||
lc := NewLoadingCache(CleanupInterval(200*time.Millisecond), PostFlushFn(func() { atomic.AddInt32(&postFnCall, 1) }),
|
||||
MaxKeys(5), MaxValueSize(10))
|
||||
MaxKeys(5), MaxValSize(10))
|
||||
|
||||
// put 5 keys to cache
|
||||
for i := 0; i < 5; i++ {
|
||||
@@ -97,7 +97,7 @@ func TestLoadingCache_MaxKeys(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadingCache_MaxSize(t *testing.T) {
|
||||
lc := NewLoadingCache(CleanupInterval(200*time.Millisecond), MaxKeys(5), MaxValueSize(10))
|
||||
lc := NewLoadingCache(CleanupInterval(200*time.Millisecond), MaxKeys(5), MaxValSize(10))
|
||||
|
||||
// put good size value to cache and make sure it cached
|
||||
res, err := lc.Get("key-Z", time.Minute, func() ([]byte, error) {
|
||||
@@ -157,6 +157,7 @@ func TestLoadingCache_Parallel(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < 1000; i++ {
|
||||
wg.Add(1)
|
||||
i := i
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
res, err := lc.Get("key", time.Minute, func() ([]byte, error) {
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestImage_Routes(t *testing.T) {
|
||||
img := Image{Enabled: true, RemarkURL: "https://demo.remark42.com", RoutePath: "/api/v1/proxy"}
|
||||
router := img.Routes()
|
||||
|
||||
httpSrv := imgHttpServer(t)
|
||||
httpSrv := imgHTTPServer(t)
|
||||
defer httpSrv.Close()
|
||||
ts := httptest.NewServer(router)
|
||||
defer ts.Close()
|
||||
@@ -88,7 +88,7 @@ func TestPicture_Convert(t *testing.T) {
|
||||
assert.Equal(t, `<img src="/img?src=aHR0cDovL3JhZGlvLXQuY29tL2ltZzMucG5n"/> xyz <img src="/img?src=aHR0cDovL2ltYWdlcy5wZXhlbHMuY29tLzY3NjM2L2ltZzQuanBlZw==">`, r)
|
||||
}
|
||||
|
||||
func imgHttpServer(t *testing.T) *httptest.Server {
|
||||
func imgHTTPServer(t *testing.T) *httptest.Server {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/image/img1.png" {
|
||||
t.Log("http img request", r.URL)
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
var testDb = "/tmp/test-remark.db"
|
||||
|
||||
func TestBoltDB_CreateAndFind(t *testing.T) {
|
||||
var b = prep(t)
|
||||
defer os.Remove(testDb)
|
||||
var b = prep(t)
|
||||
|
||||
res, err := b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time")
|
||||
assert.Nil(t, err)
|
||||
@@ -30,8 +30,8 @@ func TestBoltDB_CreateAndFind(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBoltDB_Delete(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
b := prep(t)
|
||||
defer os.Remove(testDb)
|
||||
|
||||
loc := store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}
|
||||
res, err := b.Find(loc, "time")
|
||||
@@ -218,7 +218,7 @@ func TestBoltDB_GetForUser(t *testing.T) {
|
||||
func prep(t *testing.T) *BoltDB {
|
||||
os.Remove(testDb)
|
||||
|
||||
boltStore, err := NewBoltDB(bolt.Options{}, BoltSite{FileName: "/tmp/test-remark.db", SiteID: "radio-t"})
|
||||
boltStore, err := NewBoltDB(bolt.Options{}, BoltSite{FileName: testDb, SiteID: "radio-t"})
|
||||
assert.Nil(t, err)
|
||||
b := boltStore
|
||||
|
||||
|
||||
Reference in New Issue
Block a user