Compare commits
4 Commits
85f50eb
...
auth-modul
Author | SHA1 | Date | |
---|---|---|---|
f01b1ef3b2 | |||
c721d46218 | |||
a13dfffd5c | |||
19cd4d56e7 |
@ -125,23 +125,34 @@ func copyDir(writer *zip.Writer, baseDir string, zipBasePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(writer *zip.Writer, srcPath string, zipPath string) error {
|
func copyFile(writer *zip.Writer, srcPath string, zipPath string) error {
|
||||||
r, err := os.Open(srcPath)
|
srcFile, err := os.Open(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcStat, err := os.Stat(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := r.Close(); err != nil {
|
if err := srcFile.Close(); err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
f, err := writer.Create(zipPath)
|
fileHeader := &zip.FileHeader{
|
||||||
|
Name: zipPath,
|
||||||
|
Modified: srcStat.ModTime().UTC(),
|
||||||
|
Method: zip.Deflate,
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := writer.CreateHeader(fileHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = io.Copy(f, r); err != nil {
|
if _, err = io.Copy(file, srcFile); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
go.mod
1
go.mod
@ -6,6 +6,7 @@ require modernc.org/sqlite v1.20.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect
|
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073 // indirect
|
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073 // indirect
|
||||||
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8 // indirect
|
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8 // indirect
|
||||||
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8 // indirect
|
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -110,6 +110,8 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyL
|
|||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e h1:eeyMpoxANuWNQ9O2auv4wXxJsrXzLUhdHaOmNWEGkRY=
|
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e h1:eeyMpoxANuWNQ9O2auv4wXxJsrXzLUhdHaOmNWEGkRY=
|
||||||
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
@ -26,7 +26,7 @@ func defaultHandlerOptions() *HandlerOptions {
|
|||||||
Bus: memory.NewBus(),
|
Bus: memory.NewBus(),
|
||||||
SockJS: sockjsOptions,
|
SockJS: sockjsOptions,
|
||||||
ServerModuleFactories: make([]app.ServerModuleFactory, 0),
|
ServerModuleFactories: make([]app.ServerModuleFactory, 0),
|
||||||
UploadMaxFileSize: 1024 * 10, // 10Mb
|
UploadMaxFileSize: 10 << (10 * 2), // 10Mb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
package module
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
func assertType[T any](v goja.Value, rt *goja.Runtime) T {
|
|
||||||
if c, ok := v.Export().(T); ok {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(rt.NewTypeError(fmt.Sprintf("expected value to be a '%T', got '%T'", new(T), v.Export())))
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertContext(v goja.Value, r *goja.Runtime) context.Context {
|
|
||||||
return assertType[context.Context](v, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertObject(v goja.Value, r *goja.Runtime) map[string]any {
|
|
||||||
return assertType[map[string]any](v, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertString(v goja.Value, r *goja.Runtime) string {
|
|
||||||
return assertType[string](v, r)
|
|
||||||
}
|
|
84
pkg/module/auth/module.go
Normal file
84
pkg/module/auth/module.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AnonymousSubject = "anonymous"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module struct {
|
||||||
|
server *app.Server
|
||||||
|
keyFunc jwt.Keyfunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Name() string {
|
||||||
|
return "auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Export(export *goja.Object) {
|
||||||
|
if err := export.Set("getSubject", m.getSubject); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'getSubject' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("ANONYMOUS", AnonymousSubject); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'ANONYMOUS_USER' property"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) getSubject(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
|
|
||||||
|
req, ok := ctx.Value(module.ContextKeyOriginRequest).(*http.Request)
|
||||||
|
if !ok {
|
||||||
|
panic(errors.New("could not find http request in context"))
|
||||||
|
}
|
||||||
|
|
||||||
|
rawToken := strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ")
|
||||||
|
if rawToken == "" {
|
||||||
|
rawToken = req.URL.Query().Get("token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rawToken == "" {
|
||||||
|
return rt.ToValue(AnonymousSubject)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.Parse(rawToken, m.keyFunc)
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid {
|
||||||
|
panic(errors.Errorf("invalid jwt token: '%v'", token.Raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
mapClaims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
panic(errors.Errorf("unexpected claims type '%T'", token.Claims))
|
||||||
|
}
|
||||||
|
|
||||||
|
subject, exists := mapClaims["sub"]
|
||||||
|
if !exists {
|
||||||
|
return rt.ToValue(AnonymousSubject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rt.ToValue(subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModuleFactory(keyFunc jwt.Keyfunc) app.ServerModuleFactory {
|
||||||
|
return func(server *app.Server) app.ServerModule {
|
||||||
|
return &Module{
|
||||||
|
server: server,
|
||||||
|
keyFunc: keyFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
pkg/module/auth/module_test.go
Normal file
121
pkg/module/auth/module_test.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cdr.dev/slog"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuthModule(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
|
||||||
|
keyFunc, secret := getKeyFunc()
|
||||||
|
|
||||||
|
server := app.NewServer(
|
||||||
|
module.ConsoleModuleFactory(),
|
||||||
|
ModuleFactory(keyFunc),
|
||||||
|
)
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile("testdata/auth.js")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Load("testdata/auth.js", string(data)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Start(); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer server.Stop()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "/foo", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"sub": "jdoe",
|
||||||
|
"nbf": time.Now().UTC().Unix(),
|
||||||
|
})
|
||||||
|
|
||||||
|
rawToken, err := token.SignedString(secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Authorization", "Bearer "+rawToken)
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), module.ContextKeyOriginRequest, req)
|
||||||
|
|
||||||
|
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthAnonymousModule(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
|
||||||
|
keyFunc, _ := getKeyFunc()
|
||||||
|
|
||||||
|
server := app.NewServer(
|
||||||
|
module.ConsoleModuleFactory(),
|
||||||
|
ModuleFactory(keyFunc),
|
||||||
|
)
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile("testdata/auth_anonymous.js")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Load("testdata/auth_anonymous.js", string(data)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Start(); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer server.Stop()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "/foo", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), module.ContextKeyOriginRequest, req)
|
||||||
|
|
||||||
|
if _, err := server.ExecFuncByName("testAuth", ctx); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeyFunc() (jwt.Keyfunc, []byte) {
|
||||||
|
secret := []byte("not_so_secret")
|
||||||
|
|
||||||
|
keyFunc := func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyFunc, secret
|
||||||
|
}
|
9
pkg/module/auth/testdata/auth.js
vendored
Normal file
9
pkg/module/auth/testdata/auth.js
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
function testAuth(ctx) {
|
||||||
|
var subject = auth.getSubject(ctx);
|
||||||
|
|
||||||
|
if (subject !== "jdoe") {
|
||||||
|
throw new Error("subject: expected 'jdoe', got '"+subject+"'");
|
||||||
|
}
|
||||||
|
}
|
9
pkg/module/auth/testdata/auth_anonymous.js
vendored
Normal file
9
pkg/module/auth/testdata/auth_anonymous.js
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
function testAuth(ctx) {
|
||||||
|
var subject = auth.getSubject(ctx);
|
||||||
|
|
||||||
|
if (subject !== auth.ANONYMOUS) {
|
||||||
|
throw new Error("subject: expected '"+auth.ANONYMOUS+"', got '"+subject+"'");
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -26,8 +27,8 @@ func (m *ContextModule) new(call goja.FunctionCall, rt *goja.Runtime) goja.Value
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *ContextModule) with(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *ContextModule) with(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
rawValues := assertObject(call.Argument(1), rt)
|
rawValues := util.AssertObject(call.Argument(1), rt)
|
||||||
|
|
||||||
values := make(map[ContextKey]any)
|
values := make(map[ContextKey]any)
|
||||||
for k, v := range rawValues {
|
for k, v := range rawValues {
|
||||||
@ -40,8 +41,8 @@ func (m *ContextModule) with(call goja.FunctionCall, rt *goja.Runtime) goja.Valu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *ContextModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *ContextModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
rawKey := assertString(call.Argument(1), rt)
|
rawKey := util.AssertString(call.Argument(1), rt)
|
||||||
|
|
||||||
value := ctx.Value(ContextKey(rawKey))
|
value := ctx.Value(ContextKey(rawKey))
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -58,7 +59,7 @@ func (m *NetModule) send(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|||||||
ContextKeySessionID: sessionID,
|
ContextKeySessionID: sessionID,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
ctx = assertContext(firstArg, rt)
|
ctx = util.AssertContext(firstArg, rt)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := call.Argument(1).Export()
|
data := call.Argument(1).Export()
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
@ -51,7 +52,7 @@ func (m *RPCModule) Export(export *goja.Object) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *RPCModule) register(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *RPCModule) register(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
fnName := assertString(call.Argument(0), rt)
|
fnName := util.AssertString(call.Argument(0), rt)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
callable goja.Callable
|
callable goja.Callable
|
||||||
@ -78,7 +79,7 @@ func (m *RPCModule) register(call goja.FunctionCall, rt *goja.Runtime) goja.Valu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *RPCModule) unregister(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *RPCModule) unregister(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
fnName := assertString(call.Argument(0), rt)
|
fnName := util.AssertString(call.Argument(0), rt)
|
||||||
|
|
||||||
m.callbacks.Delete(fnName)
|
m.callbacks.Delete(fnName)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
@ -47,7 +48,7 @@ func (m *StoreModule) Export(export *goja.Object) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *StoreModule) upsert(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *StoreModule) upsert(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
collection := m.assertCollection(call.Argument(1), rt)
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
document := m.assertDocument(call.Argument(2), rt)
|
document := m.assertDocument(call.Argument(2), rt)
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ func (m *StoreModule) upsert(call goja.FunctionCall, rt *goja.Runtime) goja.Valu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *StoreModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *StoreModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
collection := m.assertCollection(call.Argument(1), rt)
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
documentID := m.assertDocumentID(call.Argument(2), rt)
|
documentID := m.assertDocumentID(call.Argument(2), rt)
|
||||||
|
|
||||||
@ -84,7 +85,7 @@ type queryOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *StoreModule) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *StoreModule) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
collection := m.assertCollection(call.Argument(1), rt)
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
filter := m.assertFilter(call.Argument(2), rt)
|
filter := m.assertFilter(call.Argument(2), rt)
|
||||||
queryOptions := m.assertQueryOptions(call.Argument(3), rt)
|
queryOptions := m.assertQueryOptions(call.Argument(3), rt)
|
||||||
@ -125,7 +126,7 @@ func (m *StoreModule) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *StoreModule) delete(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *StoreModule) delete(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := assertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
collection := m.assertCollection(call.Argument(1), rt)
|
collection := m.assertCollection(call.Argument(1), rt)
|
||||||
documentID := m.assertDocumentID(call.Argument(2), rt)
|
documentID := m.assertDocumentID(call.Argument(2), rt)
|
||||||
|
|
||||||
|
28
pkg/module/util/assert.go
Normal file
28
pkg/module/util/assert.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AssertType[T any](v goja.Value, rt *goja.Runtime) T {
|
||||||
|
if c, ok := v.Export().(T); ok {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(rt.NewTypeError(fmt.Sprintf("expected value to be a '%T', got '%T'", new(T), v.Export())))
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertContext(v goja.Value, r *goja.Runtime) context.Context {
|
||||||
|
return AssertType[context.Context](v, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertObject(v goja.Value, r *goja.Runtime) map[string]any {
|
||||||
|
return AssertType[map[string]any](v, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertString(v goja.Value, r *goja.Runtime) string {
|
||||||
|
return AssertType[string](v, r)
|
||||||
|
}
|
@ -31,12 +31,17 @@ func (d Document) ID() (DocumentID, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
id, ok := rawID.(string)
|
strID, ok := rawID.(string)
|
||||||
if ok {
|
if ok {
|
||||||
return "", false
|
return DocumentID(strID), true
|
||||||
}
|
}
|
||||||
|
|
||||||
return DocumentID(id), true
|
docID, ok := rawID.(DocumentID)
|
||||||
|
if ok {
|
||||||
|
return docID, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Document) CreatedAt() (time.Time, bool) {
|
func (d Document) CreatedAt() (time.Time, bool) {
|
||||||
@ -54,7 +59,7 @@ func (d Document) timeAttr(attr string) (time.Time, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t, ok := rawTime.(time.Time)
|
t, ok := rawTime.(time.Time)
|
||||||
if ok {
|
if !ok {
|
||||||
return time.Time{}, false
|
return time.Time{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,10 +188,6 @@ func (s *DocumentStore) Upsert(ctx context.Context, collection string, document
|
|||||||
id = storage.NewDocumentID()
|
id = storage.NewDocumentID()
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(document, storage.DocumentAttrID)
|
|
||||||
delete(document, storage.DocumentAttrCreatedAt)
|
|
||||||
delete(document, storage.DocumentAttrUpdatedAt)
|
|
||||||
|
|
||||||
args := []any{id, collection, JSONMap(document), now, now}
|
args := []any{id, collection, JSONMap(document), now, now}
|
||||||
|
|
||||||
row := tx.QueryRowContext(ctx, query, args...)
|
row := tx.QueryRowContext(ctx, query, args...)
|
||||||
|
@ -7,8 +7,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDocumentStore(t *testing.T, store storage.DocumentStore) {
|
func TestDocumentStore(t *testing.T, store storage.DocumentStore) {
|
||||||
t.Run("Query", func(t *testing.T) {
|
t.Run("Ops", func(t *testing.T) {
|
||||||
// t.Parallel()
|
// t.Parallel()
|
||||||
testDocumentStoreQuery(t, store)
|
testDocumentStoreOps(t, store)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
212
pkg/storage/testsuite/document_store_ops.go
Normal file
212
pkg/storage/testsuite/document_store_ops.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
package testsuite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type documentStoreOpsTestCase struct {
|
||||||
|
Name string
|
||||||
|
Run func(ctx context.Context, store storage.DocumentStore) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var documentStoreOpsTestCases = []documentStoreOpsTestCase{
|
||||||
|
{
|
||||||
|
Name: "Basic query",
|
||||||
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
collection := "simple_select"
|
||||||
|
|
||||||
|
docs := []storage.Document{
|
||||||
|
{
|
||||||
|
"attr1": "Foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attr1": "Bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range docs {
|
||||||
|
if _, err := store.Upsert(ctx, collection, d); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := filter.New(
|
||||||
|
filter.NewEqOperator(map[string]interface{}{
|
||||||
|
"attr1": "Foo",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
results, err := store.Query(ctx, collection, filter, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 1, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := "Foo", results[0]["attr1"]; e != g {
|
||||||
|
return errors.Errorf("results[0][\"Attr1\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Query with 'IN' operator",
|
||||||
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
docs := []storage.Document{
|
||||||
|
{
|
||||||
|
"counter": 1,
|
||||||
|
"tags": []string{"foo", "bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"counter": 1,
|
||||||
|
"tags": []string{"nope"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
collection := "in_operator"
|
||||||
|
|
||||||
|
for _, doc := range docs {
|
||||||
|
if _, err := store.Upsert(ctx, collection, doc); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := filter.New(
|
||||||
|
filter.NewAndOperator(
|
||||||
|
filter.NewEqOperator(map[string]any{
|
||||||
|
"counter": 1,
|
||||||
|
}),
|
||||||
|
filter.NewInOperator(map[string]any{
|
||||||
|
"tags": "foo",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
results, err := store.Query(ctx, collection, filter, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 1, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Double upsert",
|
||||||
|
Run: func(ctx context.Context, store storage.DocumentStore) error {
|
||||||
|
collection := "double_upsert"
|
||||||
|
|
||||||
|
oriDoc := storage.Document{
|
||||||
|
"attr1": "Foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert document for the first time
|
||||||
|
upsertedDoc, err := store.Upsert(ctx, collection, oriDoc)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, exists := upsertedDoc.ID()
|
||||||
|
if !exists {
|
||||||
|
return errors.New("id, exists := upsertedDoc.ID(): 'exists' should be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == storage.DocumentID("") {
|
||||||
|
return errors.New("id, exists := upsertedDoc.ID(): 'id' should not be an empty string")
|
||||||
|
}
|
||||||
|
|
||||||
|
createdAt, exists := upsertedDoc.CreatedAt()
|
||||||
|
if !exists {
|
||||||
|
return errors.New("createdAt, exists := upsertedDoc.CreatedAt(): 'exists' should be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if createdAt.IsZero() {
|
||||||
|
return errors.New("createdAt, exists := upsertedDoc.CreatedAt(): 'createdAt' should not be zero time")
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAt, exists := upsertedDoc.UpdatedAt()
|
||||||
|
if !exists {
|
||||||
|
return errors.New("updatedAt, exists := upsertedDoc.UpdatedAt(): 'exists' should be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedAt.IsZero() {
|
||||||
|
return errors.New("updatedAt, exists := upsertedDoc.UpdatedAt(): 'updatedAt' should not be zero time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := oriDoc["attr1"], upsertedDoc["attr1"]; e != g {
|
||||||
|
return errors.Errorf("upsertedDoc[\"attr1\"]: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that document does not have unexpected properties
|
||||||
|
if e, g := 4, len(upsertedDoc); e != g {
|
||||||
|
return errors.Errorf("len(upsertedDoc): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert document for the second time
|
||||||
|
upsertedDoc2, err := store.Upsert(ctx, collection, upsertedDoc)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spew.Dump(upsertedDoc, upsertedDoc2)
|
||||||
|
|
||||||
|
prevID, _ := upsertedDoc.ID()
|
||||||
|
newID, _ := upsertedDoc2.ID()
|
||||||
|
|
||||||
|
if e, g := prevID, newID; e != g {
|
||||||
|
return errors.Errorf("newID: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdAt1, _ := upsertedDoc.CreatedAt()
|
||||||
|
createdAt2, _ := upsertedDoc2.CreatedAt()
|
||||||
|
|
||||||
|
if e, g := createdAt1, createdAt2; e != g {
|
||||||
|
return errors.Errorf("upsertedDoc2.CreatedAt(): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAt1, _ := upsertedDoc.UpdatedAt()
|
||||||
|
updatedAt2, _ := upsertedDoc2.UpdatedAt()
|
||||||
|
|
||||||
|
if e, g := updatedAt1, updatedAt2; e == g {
|
||||||
|
return errors.New("upsertedDoc2.UpdatedAt() should have been different than upsertedDoc.UpdatedAt()")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that there is no additional created document in the collection
|
||||||
|
|
||||||
|
results, err := store.Query(ctx, collection, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := 1, len(results); e != g {
|
||||||
|
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDocumentStoreOps(t *testing.T, store storage.DocumentStore) {
|
||||||
|
for _, tc := range documentStoreOpsTestCases {
|
||||||
|
func(tc documentStoreOpsTestCase) {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
if err := tc.Run(context.Background(), store); err != nil {
|
||||||
|
t.Errorf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}(tc)
|
||||||
|
}
|
||||||
|
}
|
@ -1,128 +0,0 @@
|
|||||||
package testsuite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type documentStoreQueryTestCase struct {
|
|
||||||
Name string
|
|
||||||
Before func(ctx context.Context, store storage.DocumentStore) error
|
|
||||||
Collection string
|
|
||||||
Filter *filter.Filter
|
|
||||||
QueryOptionsFuncs []storage.QueryOptionFunc
|
|
||||||
After func(t *testing.T, results []storage.Document, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
var documentStoreQueryTestCases = []documentStoreQueryTestCase{
|
|
||||||
{
|
|
||||||
Name: "Simple select",
|
|
||||||
Before: func(ctx context.Context, store storage.DocumentStore) error {
|
|
||||||
doc1 := storage.Document{
|
|
||||||
"attr1": "Foo",
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := store.Upsert(ctx, "simple_select", doc1); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
doc2 := storage.Document{
|
|
||||||
"attr1": "Bar",
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := store.Upsert(ctx, "simple_select", doc2); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
Collection: "simple_select",
|
|
||||||
Filter: filter.New(
|
|
||||||
filter.NewEqOperator(map[string]interface{}{
|
|
||||||
"attr1": "Foo",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
After: func(t *testing.T, results []storage.Document, err error) {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if e, g := 1, len(results); e != g {
|
|
||||||
t.Errorf("len(results): expected '%v', got '%v'", e, g)
|
|
||||||
}
|
|
||||||
|
|
||||||
if e, g := "Foo", results[0]["attr1"]; e != g {
|
|
||||||
t.Errorf("results[0][\"Attr1\"]: expected '%v', got '%v'", e, g)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "IN Operator",
|
|
||||||
Before: func(ctx context.Context, store storage.DocumentStore) error {
|
|
||||||
docs := []storage.Document{
|
|
||||||
{
|
|
||||||
"counter": 1,
|
|
||||||
"tags": []string{"foo", "bar"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"counter": 1,
|
|
||||||
"tags": []string{"nope"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, doc := range docs {
|
|
||||||
if _, err := store.Upsert(ctx, "in_operator", doc); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
Collection: "in_operator",
|
|
||||||
Filter: filter.New(
|
|
||||||
filter.NewAndOperator(
|
|
||||||
filter.NewEqOperator(map[string]any{
|
|
||||||
"counter": 1,
|
|
||||||
}),
|
|
||||||
filter.NewInOperator(map[string]any{
|
|
||||||
"tags": "foo",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
After: func(t *testing.T, results []storage.Document, err error) {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if e, g := 1, len(results); e != g {
|
|
||||||
t.Errorf("len(results): expected '%v', got '%v'", e, g)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDocumentStoreQuery(t *testing.T, store storage.DocumentStore) {
|
|
||||||
for _, tc := range documentStoreQueryTestCases {
|
|
||||||
func(tc documentStoreQueryTestCase) {
|
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
|
||||||
// t.Parallel()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
if tc.Before != nil {
|
|
||||||
if err := tc.Before(ctx, store); err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
documents, err := store.Query(ctx, tc.Collection, tc.Filter, tc.QueryOptionsFuncs...)
|
|
||||||
|
|
||||||
tc.After(t, documents, err)
|
|
||||||
})
|
|
||||||
}(tc)
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user