Skip to content

Commit

Permalink
Merge branch 'release/v0.5.0'
Browse files Browse the repository at this point in the history
jirenius committed Jul 10, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 372a82d + cfcaf3e commit d8efe1c
Showing 20 changed files with 667 additions and 77 deletions.
74 changes: 74 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Build

on:
push:
branches:
- develop
- master
pull_request:

jobs:

go-legacy-test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ '1.20', '1.21' ]
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Go get
run: go get -t ./...
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22.4'
- name: Go get
run: go get -t ./...
- name: Build
run: go build -v ./...
- name: Test
run: go test -v -covermode=atomic -coverprofile=cover.out -coverpkg=. ./...
- name: Install goveralls
run: go install github.com/mattn/goveralls@latest
- name: Send coverage
env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: goveralls -coverprofile=cover.out -service=github

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22.4'
- name: Install checks
run: |
go install honnef.co/go/tools/cmd/staticcheck@latest
go install github.com/client9/misspell/cmd/misspell@latest
- name: Go get
run: go get -t ./...
- name: Go vet
run: go vet $(go list ./... | grep -v /vendor/)
- name: Go mod
run: go mod tidy; git diff --exit-code go.mod go.sum
- name: Go fmt
run: go fmt $(go list ./... | grep -v /vendor/); git diff --exit-code
- name: Staticcheck
run: staticcheck -checks all,-ST1000 ./...
- name: Misspell
run: misspell -error -locale US .
31 changes: 31 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Release

on:
push:
tags:
- '*'

# Make sure the GITHUB_TOKEN has permission to upload to our releases
permissions:
contents: write

jobs:

create_release:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Create release draft
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref_name }}
body: |
A new release
draft: true
prerelease: false
14 changes: 0 additions & 14 deletions .travis.yml

This file was deleted.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<p align="center"><a href="https://resgate.io" target="_blank" rel="noopener noreferrer"><img width="100" src="https://resgate.io/img/resgate-logo.png" alt="Resgate logo"></a></p>
<h2 align="center"><b>RES Service for Go</b><br/>Synchronize Your Clients</h2>
<p align="center">
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
<a href="http://goreportcard.com/report/jirenius/go-res"><img src="http://goreportcard.com/badge/github.com/jirenius/go-res" alt="Report Card"></a>
<a href="https://travis-ci.com/jirenius/go-res"><img src="https://travis-ci.com/jirenius/go-res.svg?branch=master" alt="Build Status"></a>
<a href="https://github.com/jirenius/go-res/actions/workflows/build.yml?query=branch%3Amaster"><img src="https://img.shields.io/github/actions/workflow/status/jirenius/go-res/build.yml?branch=master" alt="Build Status"></a>
<a href="https://coveralls.io/github/jirenius/go-res?branch=master"><img src="https://coveralls.io/repos/github/jirenius/go-res/badge.svg?branch=master" alt="Coverage"></a>
<a href="https://pkg.go.dev/github.com/jirenius/go-res"><img src="https://img.shields.io/static/v1?label=reference&message=go.dev&color=5673ae" alt="Reference"></a>
</p>
18 changes: 14 additions & 4 deletions codec.go
Original file line number Diff line number Diff line change
@@ -11,23 +11,33 @@ type resRequest struct {
RemoteAddr string `json:"remoteAddr"`
URI string `json:"uri"`
Query string `json:"query"`
IsHTTP bool `json:"isHttp"`
}

type metaObject struct {
Status int `json:"status,omitempty"`
Header map[string][]string `json:"header,omitempty"`
}

type successResponse struct {
Result interface{} `json:"result"`
Meta *metaObject `json:"meta,omitempty"`
}

type resourceResponse struct {
Resource Ref `json:"resource"`
Resource Ref `json:"resource"`
Meta *metaObject `json:"meta,omitempty"`
}

type errorResponse struct {
Error *Error `json:"error"`
Error *Error `json:"error"`
Meta *metaObject `json:"meta,omitempty"`
}

type accessResponse struct {
Get bool `json:"get,omitempty"`
Call string `json:"call,omitempty"`
Get bool `json:"get,omitempty"`
Call string `json:"call,omitempty"`
Meta *metaObject `json:"meta,omitempty"`
}

type modelResponse struct {
18 changes: 17 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/jirenius/go-res

go 1.12
go 1.18

require (
github.com/dgraph-io/badger v1.6.2
@@ -10,5 +10,21 @@ require (
github.com/nats-io/nats-server/v2 v2.1.8
github.com/nats-io/nats.go v1.10.0
github.com/rs/xid v1.2.1
)

require (
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/dgraph-io/ristretto v0.0.2 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/golang/protobuf v1.4.0 // indirect
github.com/nats-io/jwt v0.3.2 // indirect
github.com/nats-io/nkeys v0.1.4 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/stretchr/testify v1.6.1 // indirect
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e // indirect
google.golang.org/protobuf v1.22.0 // indirect
)
3 changes: 0 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
@@ -41,10 +41,8 @@ github.com/jirenius/taskqueue v1.1.0 h1:V7Z/XeR515i96YmohoD1rXawAcGBONrd8+LGfckZ
github.com/jirenius/taskqueue v1.1.0/go.mod h1:/x5dz4AGqzGI6FmIC+0rmbx6JBUGgJiQzb71JFy/JRg=
github.com/jirenius/timerqueue v1.0.0 h1:TgcUQlrxKBBHYmStXPzLdMPJFfmqkWZZ1s7BA5G1d9E=
github.com/jirenius/timerqueue v1.0.0/go.mod h1:pUEjy16BUruJMjLIsjWvWQh9Bu9CSXCIfGADZf37WIk=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -109,7 +107,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.22.0 h1:cJv5/xdbk1NnMPR1VP9+HU6gupuG9MLBoH1r6RHZ2MY=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
2 changes: 1 addition & 1 deletion mux.go
Original file line number Diff line number Diff line change
@@ -266,7 +266,7 @@ func (m *Mux) ValidateListeners() (err error) {
errs = append(errs, "no handler registered for pattern: "+mergePattern(m.FullPath(), pathSliceToString(n, path, mountIdx)))
}
})
if err != nil {
if errs != nil {
return errors.New(strings.Join(errs, "\n"))
}
return nil
155 changes: 127 additions & 28 deletions request.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"runtime/debug"
"strconv"
"time"
@@ -26,6 +27,8 @@ type Request struct {
method string
msg *nats.Msg
replied bool // Flag telling if a reply has been made
rheader http.Header
status int

// Fields from the request data
cid string
@@ -35,6 +38,7 @@ type Request struct {
host string
remoteAddr string
uri string
isHTTP bool
}

// AccessRequest has methods for responding to access requests.
@@ -43,6 +47,9 @@ type AccessRequest interface {
CID() string
RawToken() json.RawMessage
ParseToken(interface{})
IsHTTP() bool
SetResponseStatus(code int)
ResponseHeader() http.Header
Access(get bool, call string)
AccessDenied()
AccessGranted()
@@ -99,6 +106,9 @@ type CallRequest interface {
RawToken() json.RawMessage
ParseParams(interface{})
ParseToken(interface{})
IsHTTP() bool
SetResponseStatus(code int)
ResponseHeader() http.Header
OK(result interface{})
Resource(rid string)
NotFound()
@@ -139,6 +149,9 @@ type AuthRequest interface {
Host() string
RemoteAddr() string
URI() string
IsHTTP() bool
SetResponseStatus(code int)
ResponseHeader() http.Header
OK(result interface{})
Resource(rid string)
NotFound()
@@ -239,15 +252,58 @@ func (r *Request) URI() string {
return r.uri
}

// IsHTTP returns true if the request originates from a client HTTP or WebSocket
// connection that has yet to be responded to by the gateway.
//
// Only valid for auth, access, and call requests.
func (r *Request) IsHTTP() bool {
return r.isHTTP
}

// SetResponseStatus sets the HTTP response status code for the client
// connection. If IsHTTP is not true, the call will panic. A zero (0) value
// means no/default status code.
//
// See: https://resgate.io/docs/specification/res-service-protocol/#status-codes
//
// Only valid for auth, access, and call requests.
func (r *Request) SetResponseStatus(code int) {
if !r.isHTTP {
panic("call to SetResponseStatus when IsHTTP is false")
}
if r.replied {
panic("call to SetResponseStatus after reply")
}
r.status = code
}

// ResponseHeader returns the header map to use in the response for the client
// connection. If IsHTTP is not true, the call will panic.
//
// Only valid for auth, access, and call requests.
func (r *Request) ResponseHeader() http.Header {
if !r.isHTTP {
panic("call to ResponseHeader when IsHTTP is false")
}
if r.replied {
panic("call to ResponseHeader after reply")
}
if r.rheader == nil {
r.rheader = make(http.Header)
}
return r.rheader
}

// OK sends a successful result response to a request.
// The result may be nil.
//
// Only valid for call and auth requests.
func (r *Request) OK(result interface{}) {
if result == nil {
m := r.meta()
if result == nil && m == nil {
r.reply(responseSuccess)
} else {
r.success(result)
r.success(result, m)
}
}

@@ -260,51 +316,75 @@ func (r *Request) Resource(rid string) {
if !ref.IsValid() {
panic("res: invalid resource ID: " + rid)
}
data, err := json.Marshal(resourceResponse{Resource: ref})
data, err := json.Marshal(resourceResponse{Resource: ref, Meta: r.meta()})
if err != nil {
r.error(ToError(err))
r.error(ToError(err), nil)
return
}
r.reply(data)
}

// Error sends a custom error response for the request.
func (r *Request) Error(err error) {
r.error(ToError(err))
r.error(ToError(err), r.meta())
}

// NotFound sends a system.notFound response for the request.
func (r *Request) NotFound() {
r.reply(responseNotFound)
m := r.meta()
if m == nil {
r.reply(responseNotFound)
} else {
r.error(ErrNotFound, m)
}
}

// MethodNotFound sends a system.methodNotFound response for the request.
//
// Only valid for call and auth requests.
func (r *Request) MethodNotFound() {
r.reply(responseMethodNotFound)
m := r.meta()
if m == nil {
r.reply(responseMethodNotFound)
} else {
r.error(ErrMethodNotFound, m)
}
}

// InvalidParams sends a system.invalidParams response.
// An empty message will default to "Invalid parameters".
//
// Only valid for call and auth requests.
func (r *Request) InvalidParams(message string) {
m := r.meta()
var err *Error
if message == "" {
r.reply(responseInvalidParams)
if m == nil {
r.reply(responseInvalidParams)
return
}
err = ErrInvalidParams
} else {
r.error(&Error{Code: CodeInvalidParams, Message: message})
err = &Error{Code: CodeInvalidParams, Message: message}
}
r.error(err, m)
}

// InvalidQuery sends a system.invalidQuery response.
// An empty message will default to "Invalid query".
func (r *Request) InvalidQuery(message string) {
m := r.meta()
var err *Error
if message == "" {
r.reply(responseInvalidQuery)
if m == nil {
r.reply(responseInvalidQuery)
return
}
err = ErrInvalidQuery
} else {
r.error(&Error{Code: CodeInvalidQuery, Message: message})
err = &Error{Code: CodeInvalidQuery, Message: message}
}
r.error(err, m)
}

// Access sends a successful response.
@@ -317,17 +397,22 @@ func (r *Request) InvalidQuery(message string) {
// Only valid for access requests.
func (r *Request) Access(get bool, call string) {
if !get && call == "" {
r.reply(responseAccessDenied)
} else {
r.success(accessResponse{Get: get, Call: call})
r.AccessDenied()
return
}
r.success(accessResponse{Get: get, Call: call}, r.meta())
}

// AccessDenied sends a system.accessDenied response.
//
// Only valid for access requests.
func (r *Request) AccessDenied() {
r.reply(responseAccessDenied)
m := r.meta()
if m == nil {
r.reply(responseAccessDenied)
} else {
r.error(ErrAccessDenied, m)
}
}

// AccessGranted a successful response granting full access to the resource.
@@ -337,7 +422,12 @@ func (r *Request) AccessDenied() {
//
// Only valid for access requests.
func (r *Request) AccessGranted() {
r.reply(responseAccessGranted)
m := r.meta()
if m == nil {
r.reply(responseAccessGranted)
} else {
r.success(accessResponse{Get: true, Call: "*"}, m)
}
}

// Model sends a successful model response for the get request.
@@ -359,7 +449,7 @@ func (r *Request) QueryModel(model interface{}, query string) {
// model sends a successful model response for the get request.
func (r *Request) model(model interface{}, query string) {
// [TODO] Marshal model to a json.RawMessage to see if it is a JSON object
r.success(modelResponse{Model: model, Query: query})
r.success(modelResponse{Model: model, Query: query}, nil)
}

// Collection sends a successful collection response for the get request.
@@ -381,7 +471,7 @@ func (r *Request) QueryCollection(collection interface{}, query string) {
// collection sends a successful collection response for the get request.
func (r *Request) collection(collection interface{}, query string) {
// [TODO] Marshal collection to a json.RawMessage to see if it is a JSON array
r.success(collectionResponse{Collection: collection, Query: query})
r.success(collectionResponse{Collection: collection, Query: query}, nil)
}

// New sends a successful response for the new call request.
@@ -394,7 +484,7 @@ func (r *Request) New(rid Ref) {
if !rid.IsValid() {
panic("res: invalid reference RID: " + rid)
}
r.success(rid)
r.success(rid, nil)
}

// ParseParams unmarshals the JSON encoded parameters and stores the result in p.
@@ -454,20 +544,29 @@ func (r *Request) ForValue() bool {
return false
}

// meta returns a metaObject if any of the meta response values are set,
// otherwise it returns nil.
func (r *Request) meta() *metaObject {
if len(r.rheader) == 0 && r.status == 0 {
return nil
}
return &metaObject{Header: r.rheader, Status: r.status}
}

// success sends a successful response as a reply.
func (r *Request) success(result interface{}) {
data, err := json.Marshal(successResponse{Result: result})
func (r *Request) success(result interface{}, m *metaObject) {
data, err := json.Marshal(successResponse{Result: result, Meta: m})
if err != nil {
r.error(ToError(err))
r.error(ToError(err), nil)
return
}

r.reply(data)
}

// error sends an error response as a reply.
func (r *Request) error(e *Error) {
data, err := json.Marshal(errorResponse{Error: e})
func (r *Request) error(e *Error, m *metaObject) {
data, err := json.Marshal(errorResponse{Error: e, Meta: m})
if err != nil {
data = responseInternalError
}
@@ -502,7 +601,7 @@ func (r *Request) executeHandler() {
switch e := v.(type) {
case *Error:
if !r.replied {
r.error(e)
r.error(e, r.meta())
// Return without logging as panicing with a *Error is considered
// a valid way of sending an error response.
return
@@ -511,17 +610,17 @@ func (r *Request) executeHandler() {
case error:
str = e.Error()
if !r.replied {
r.error(ToError(e))
r.error(ToError(e), r.meta())
}
case string:
str = e
if !r.replied {
r.error(ToError(errors.New(e)))
r.error(ToError(errors.New(e)), r.meta())
}
default:
str = fmt.Sprintf("%v", e)
if !r.replied {
r.error(ToError(errors.New(str)))
r.error(ToError(errors.New(str)), r.meta())
}
}

1 change: 1 addition & 0 deletions restest/codec.go
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ type Request struct {
RemoteAddr string `json:"remoteAddr,omitempty"`
URI string `json:"uri,omitempty"`
Query string `json:"query,omitempty"`
IsHTTP bool `json:"isHttp,omitempty"`
}

// DefaultCallRequest returns a default call request.
4 changes: 4 additions & 0 deletions restest/mockconn.go
Original file line number Diff line number Diff line change
@@ -322,6 +322,10 @@ func (c *MockConn) AssertNoSubscription(subj string) {
func (c *MockConn) GetMsg() *Msg {
select {
case r := <-c.rch:
// Channel is closed
if r == nil {
return nil
}
return &Msg{
Msg: r,
c: c,
4 changes: 4 additions & 0 deletions restest/session.go
Original file line number Diff line number Diff line change
@@ -81,6 +81,10 @@ func NewSession(t *testing.T, service *res.Service, opts ...func(*SessionConfig)

if !s.cfg.NoReset {
msg := s.GetMsg()
if msg == nil {
// The channel is closed
t.Fatal("expected a system.reset, but got no message")
}
if s.cfg.ValidateReset {
msg.AssertSystemReset(cfg.ResetResources, cfg.ResetAccess)
} else {
4 changes: 1 addition & 3 deletions scripts/check.sh
Original file line number Diff line number Diff line change
@@ -9,8 +9,6 @@ fi
echo "Checking with go vet..."
go vet ./...
echo "Checking with staticcheck..."
staticcheck ./...
echo "Checking with golint..."
golint -set_exit_status ./...
staticcheck -checks all,-ST1000 ./...
echo "Checking with misspell..."
misspell -error -locale US .
11 changes: 2 additions & 9 deletions scripts/cover.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
#!/bin/bash -e
# Run from directory above via ./scripts/cover.sh

go test -covermode=atomic -coverprofile=./cover.out -coverpkg=. ./...

# If we have an arg, assume travis run and push to coveralls. Otherwise launch browser results
if [[ -n $1 ]]; then
$HOME/gopath/bin/goveralls -coverprofile=cover.out -service travis-ci
rm -rf ./cover.out
else
go tool cover -html=cover.out
fi
go test -v -covermode=atomic -coverprofile=./cover.out -coverpkg=. ./...
go tool cover -html=cover.out
8 changes: 2 additions & 6 deletions scripts/install-checks.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
#!/bin/bash -e

pushd /tmp > /dev/null
go get -u github.com/mattn/goveralls
go get -u honnef.co/go/tools/cmd/staticcheck
go get -u golang.org/x/lint/golint
go get -u github.com/client9/misspell/cmd/misspell
popd > /dev/null
go install honnef.co/go/tools/cmd/staticcheck@latest
go install github.com/client9/misspell/cmd/misspell@latest
2 changes: 1 addition & 1 deletion scripts/lint.sh
Original file line number Diff line number Diff line change
@@ -2,6 +2,6 @@
# Run from directory above via ./scripts/lint.sh

$(exit $(go fmt ./... | wc -l))
go mod tidy
go vet ./...
golint ./...
misspell -error -locale US ./...
5 changes: 3 additions & 2 deletions service.go
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ import (
)

// Supported RES protocol version.
const protocolVersion = "1.2.2"
const protocolVersion = "1.2.3"

// The default size of the in channel receiving messages from NATS Server.
const defaultInChannelSize = 1024
@@ -1116,7 +1116,7 @@ func (s *Service) processRequest(m *nats.Msg, rtype, rname, method string, mh *M
if err != nil {
r = &Request{resource: resource{s: s}, msg: m}
s.errorf("Error unmarshaling incoming request: %s", err)
r.error(ToError(err))
r.error(ToError(err), nil)
return
}
}
@@ -1141,6 +1141,7 @@ func (s *Service) processRequest(m *nats.Msg, rtype, rname, method string, mh *M
host: rc.Host,
remoteAddr: rc.RemoteAddr,
uri: rc.URI,
isHTTP: rc.IsHTTP,
}

r.executeHandler()
52 changes: 51 additions & 1 deletion test/00service_test.go
Original file line number Diff line number Diff line change
@@ -36,7 +36,7 @@ func TestService(t *testing.T) {
// Test that the service returns the correct protocol version
func TestServiceProtocolVersion(t *testing.T) {
s := res.NewService("test")
restest.AssertEqualJSON(t, "ProtocolVersion()", s.ProtocolVersion(), "1.2.2")
restest.AssertEqualJSON(t, "ProtocolVersion()", s.ProtocolVersion(), "1.2.3")
}

// Test that the service can be served without error
@@ -330,3 +330,53 @@ func TestServiceTokenReset(t *testing.T) {
})
}
}

// Test ServiceSetWorkerCount panics when called after starting service
func TestServiceSetWorkerCount_AfterStart_Panics(t *testing.T) {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Access(res.AccessGranted))
}, func(s *restest.Session) {
restest.AssertPanic(t, func() {
s.Service().SetWorkerCount(5)
})
})
}

// Test ServiceSetWorkerCount does not panic when zero
func TestServiceSetWorkerCount_ZeroWorkerCount_DoesNotPanic(t *testing.T) {
runTest(t, func(s *res.Service) {
s.SetWorkerCount(0) // Default worker count should be used
}, nil, restest.WithoutReset)
}

// Test ServiceSetWorkerCount does not panic when greater than zero
func TestServiceSetWorkerCount_GreaterThanZero_DoesNotPanic(t *testing.T) {
runTest(t, func(s *res.Service) {
s.SetWorkerCount(5)
}, nil, restest.WithoutReset)
}

// Test ServiceSetInChannelSize panics when called after starting service
func TestServiceSetInChannelSize_AfterStart_Panics(t *testing.T) {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Access(res.AccessGranted))
}, func(s *restest.Session) {
restest.AssertPanic(t, func() {
s.Service().SetInChannelSize(10)
})
})
}

// Test ServiceSetInChannelSize does not panic when zero
func TestServiceSetInChannelSize_ZeroWorkerCount_DoesNotPanic(t *testing.T) {
runTest(t, func(s *res.Service) {
s.SetInChannelSize(0) // Default in channel size should be used
}, nil, restest.WithoutReset)
}

// Test ServiceSetInChannelSize does not panic when greater than zero
func TestServiceSetInChannelSize_GreaterThanZero_DoesNotPanic(t *testing.T) {
runTest(t, func(s *res.Service) {
s.SetInChannelSize(10)
}, nil, restest.WithoutReset)
}
327 changes: 327 additions & 0 deletions test/09meta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
package test

import (
"encoding/json"
"fmt"
"testing"

"github.com/jirenius/go-res"
"github.com/jirenius/go-res/restest"
)

var metaTestTbl = []struct {
Name string
Status int
Header map[string][]string
ExpectedMeta interface{}
}{
{
Name: "status code",
Status: 402,
ExpectedMeta: json.RawMessage(`{"status":402}`),
},
{
Name: "single header",
Header: map[string][]string{"Location": {"https://example.com"}},
ExpectedMeta: json.RawMessage(`{"header":{"Location":["https://example.com"]}}`),
},
{
Name: "multiple headers",
Header: map[string][]string{"Set-Cookie": {"foo=bar", "zoo=baz"}},
ExpectedMeta: json.RawMessage(`{"header":{"Set-Cookie":["foo=bar","zoo=baz"]}}`),
},
{
Name: "status and header",
Status: 303,
Header: map[string][]string{"Location": {"https://example.com"}},
ExpectedMeta: json.RawMessage(`{"status":303,"header":{"Location":["https://example.com"]}}`),
},
}

// Test auth request with meta data and a successful response.
func TestMeta_AuthRequestWithSuccessResponse_MetaInResponse(t *testing.T) {
for _, k := range []struct {
Name string
Result interface{}
}{
{Name: "nil result", Result: nil},
{Name: "custom result", Result: mock.Result},
} {
for _, l := range metaTestTbl {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Auth("method", func(r res.AuthRequest) {
r.SetResponseStatus(l.Status)
for k, v := range l.Header {
r.ResponseHeader()[k] = v
}
r.OK(k.Result)
}))
}, func(s *restest.Session) {
req := mock.DefaultRequest()
req.IsHTTP = true
s.Auth("test.model", "method", req).
Response().
AssertPayload(map[string]interface{}{
"result": k.Result,
"meta": l.ExpectedMeta,
})
}, restest.WithTest(fmt.Sprintf("%s with %s", k.Name, l.Name)))
}
}
}

// Test auth request with meta data and an error response.
func TestMeta_AuthRequestWithErrorResponse_MetaInResponse(t *testing.T) {
for _, k := range []struct {
Name string
Respond func(r res.AuthRequest)
ExpectedError error
}{
{Name: "CustomError()", Respond: func(r res.AuthRequest) { r.Error(mock.CustomError) }, ExpectedError: mock.CustomError},
{Name: "NotFound()", Respond: func(r res.AuthRequest) { r.NotFound() }, ExpectedError: res.ErrNotFound},
{Name: "MethodNotFound()", Respond: func(r res.AuthRequest) { r.MethodNotFound() }, ExpectedError: res.ErrMethodNotFound},
{Name: `InvalidParams("")`, Respond: func(r res.AuthRequest) { r.InvalidParams("") }, ExpectedError: res.ErrInvalidParams},
{Name: `InvalidParams("foo")`, Respond: func(r res.AuthRequest) { r.InvalidParams("foo") }, ExpectedError: &res.Error{Code: res.CodeInvalidParams, Message: "foo"}},
{Name: `InvalidQuery("")`, Respond: func(r res.AuthRequest) { r.InvalidQuery("") }, ExpectedError: res.ErrInvalidQuery},
{Name: `InvalidQuery("foo")`, Respond: func(r res.AuthRequest) { r.InvalidQuery("foo") }, ExpectedError: &res.Error{Code: res.CodeInvalidQuery, Message: "foo"}},
} {
for _, l := range metaTestTbl {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Auth("method", func(r res.AuthRequest) {
r.SetResponseStatus(l.Status)
for k, v := range l.Header {
r.ResponseHeader()[k] = v
}
k.Respond(r)
}))
}, func(s *restest.Session) {
req := mock.DefaultRequest()
req.IsHTTP = true
s.Auth("test.model", "method", req).
Response().
AssertPayload(map[string]interface{}{
"error": k.ExpectedError,
"meta": l.ExpectedMeta,
})
}, restest.WithTest(fmt.Sprintf("%s with %s", k.Name, l.Name)))
}
}
}

// Test auth request with meta data and a resource response.
func TestMeta_AuthRequestWithResourceResponse_MetaInResponse(t *testing.T) {
rid := "test.foo"
for _, l := range metaTestTbl {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Auth("method", func(r res.AuthRequest) {
r.SetResponseStatus(l.Status)
for k, v := range l.Header {
r.ResponseHeader()[k] = v
}
r.Resource(rid)
}))
}, func(s *restest.Session) {
req := mock.DefaultRequest()
req.IsHTTP = true
s.Auth("test.model", "method", req).
Response().
AssertPayload(map[string]interface{}{
"resource": res.Ref(rid),
"meta": l.ExpectedMeta,
})
}, restest.WithTest(l.Name))
}
}

// Test call request with meta data and a successful response.
func TestMeta_CallRequestWithSuccessResponse_MetaInResponse(t *testing.T) {
for _, k := range []struct {
Name string
Result interface{}
}{
{Name: "nil result", Result: nil},
{Name: "custom result", Result: mock.Result},
} {
for _, l := range metaTestTbl {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Call("method", func(r res.CallRequest) {
r.SetResponseStatus(l.Status)
for k, v := range l.Header {
r.ResponseHeader()[k] = v
}
r.OK(k.Result)
}))
}, func(s *restest.Session) {
req := mock.DefaultRequest()
req.IsHTTP = true
s.Call("test.model", "method", req).
Response().
AssertPayload(map[string]interface{}{
"result": k.Result,
"meta": l.ExpectedMeta,
})
}, restest.WithTest(fmt.Sprintf("%s with %s", k.Name, l.Name)))
}
}
}

// Test call request with meta data and an error response.
func TestMeta_CallRequestWithErrorResponse_MetaInResponse(t *testing.T) {
for _, k := range []struct {
Name string
Respond func(r res.CallRequest)
ExpectedError error
}{
{Name: "CustomError()", Respond: func(r res.CallRequest) { r.Error(mock.CustomError) }, ExpectedError: mock.CustomError},
{Name: "NotFound()", Respond: func(r res.CallRequest) { r.NotFound() }, ExpectedError: res.ErrNotFound},
{Name: "MethodNotFound()", Respond: func(r res.CallRequest) { r.MethodNotFound() }, ExpectedError: res.ErrMethodNotFound},
{Name: `InvalidParams("")`, Respond: func(r res.CallRequest) { r.InvalidParams("") }, ExpectedError: res.ErrInvalidParams},
{Name: `InvalidParams("foo")`, Respond: func(r res.CallRequest) { r.InvalidParams("foo") }, ExpectedError: &res.Error{Code: res.CodeInvalidParams, Message: "foo"}},
{Name: `InvalidQuery("")`, Respond: func(r res.CallRequest) { r.InvalidQuery("") }, ExpectedError: res.ErrInvalidQuery},
{Name: `InvalidQuery("foo")`, Respond: func(r res.CallRequest) { r.InvalidQuery("foo") }, ExpectedError: &res.Error{Code: res.CodeInvalidQuery, Message: "foo"}},
} {
for _, l := range metaTestTbl {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Call("method", func(r res.CallRequest) {
r.SetResponseStatus(l.Status)
for k, v := range l.Header {
r.ResponseHeader()[k] = v
}
r.Error(mock.CustomError)
}))
}, func(s *restest.Session) {
req := mock.DefaultRequest()
req.IsHTTP = true
s.Call("test.model", "method", req).
Response().
AssertPayload(map[string]interface{}{
"error": mock.CustomError,
"meta": l.ExpectedMeta,
})
}, restest.WithTest(fmt.Sprintf("%s with %s", k.Name, l.Name)))
}
}
}

// Test call request with meta data and a resource response.
func TestMeta_CallRequestWithResourceResponse_MetaInResponse(t *testing.T) {
rid := "test.foo"
for _, l := range metaTestTbl {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Call("method", func(r res.CallRequest) {
r.SetResponseStatus(l.Status)
for k, v := range l.Header {
r.ResponseHeader()[k] = v
}
r.Resource(rid)
}))
}, func(s *restest.Session) {
req := mock.DefaultRequest()
req.IsHTTP = true
s.Call("test.model", "method", req).
Response().
AssertPayload(map[string]interface{}{
"resource": res.Ref(rid),
"meta": l.ExpectedMeta,
})
}, restest.WithTest(l.Name))
}
}

// Test access request with meta data and a successful response.
func TestMeta_AccessRequestWithSuccessResponse_MetaInResponse(t *testing.T) {
for _, k := range []struct {
Name string
Respond func(r res.AccessRequest)
ExpectedResult interface{}
}{
{Name: "AccessGranted()", Respond: func(r res.AccessRequest) { r.AccessGranted() }, ExpectedResult: json.RawMessage(`{"get":true,"call":"*"}`)},
{Name: `Access(true, "foo")`, Respond: func(r res.AccessRequest) { r.Access(true, "foo") }, ExpectedResult: json.RawMessage(`{"get":true,"call":"foo"}`)},
} {
for _, l := range metaTestTbl {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Access(func(r res.AccessRequest) {
r.SetResponseStatus(l.Status)
for k, v := range l.Header {
r.ResponseHeader()[k] = v
}
k.Respond(r)
}))
}, func(s *restest.Session) {
req := mock.DefaultRequest()
req.IsHTTP = true
s.Access("test.model", req).
Response().
AssertPayload(map[string]interface{}{
"result": k.ExpectedResult,
"meta": l.ExpectedMeta,
})
}, restest.WithTest(fmt.Sprintf("%s with %s", k.Name, l.Name)))
}
}
}

// Test access request with meta data and an error response.
func TestMeta_AccessRequestWithErrorResponse_MetaInResponse(t *testing.T) {
for _, k := range []struct {
Name string
Respond func(r res.AccessRequest)
ExpectedError error
}{
{Name: "AccessDenied()", Respond: func(r res.AccessRequest) { r.AccessDenied() }, ExpectedError: res.ErrAccessDenied},
{Name: `Access(false, "")`, Respond: func(r res.AccessRequest) { r.Access(false, "") }, ExpectedError: res.ErrAccessDenied},
{Name: "CustomError()", Respond: func(r res.AccessRequest) { r.Error(mock.CustomError) }, ExpectedError: mock.CustomError},
{Name: "NotFound()", Respond: func(r res.AccessRequest) { r.NotFound() }, ExpectedError: res.ErrNotFound},
{Name: `InvalidQuery("")`, Respond: func(r res.AccessRequest) { r.InvalidQuery("") }, ExpectedError: res.ErrInvalidQuery},
{Name: `InvalidQuery("foo")`, Respond: func(r res.AccessRequest) { r.InvalidQuery("foo") }, ExpectedError: &res.Error{Code: res.CodeInvalidQuery, Message: "foo"}},
} {
for _, l := range metaTestTbl {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Access(func(r res.AccessRequest) {
r.SetResponseStatus(l.Status)
for k, v := range l.Header {
r.ResponseHeader()[k] = v
}
k.Respond(r)
}))
}, func(s *restest.Session) {
req := mock.DefaultRequest()
req.IsHTTP = true
s.Access("test.model", req).
Response().
AssertPayload(map[string]interface{}{
"error": k.ExpectedError,
"meta": l.ExpectedMeta,
})
}, restest.WithTest(fmt.Sprintf("%s with %s", k.Name, l.Name)))
}
}
}

// Test using SetResponseStatus on call request, when IsHTTP is false, causes panic.
func TestMeta_SetResponseStatusWhenIsHTTPIsFalse_Panics(t *testing.T) {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Call("method", func(r res.CallRequest) {
r.SetResponseStatus(402)
r.OK(nil)
}))
}, func(s *restest.Session) {
s.Call("test.model", "method", nil).
Response().
AssertErrorCode(res.CodeInternalError)
})
}

// Test using ResponseHeader on call request, when IsHTTP is false, causes panic.
func TestMeta_ResponseHeaderWhenIsHTTPIsFalse_Panics(t *testing.T) {
runTest(t, func(s *res.Service) {
s.Handle("model", res.Call("method", func(r res.CallRequest) {
_ = r.ResponseHeader()
r.OK(nil)
}))
}, func(s *restest.Session) {
s.Call("test.model", "method", nil).
Response().
AssertErrorCode(res.CodeInternalError)
})
}
7 changes: 5 additions & 2 deletions types.go
Original file line number Diff line number Diff line change
@@ -43,10 +43,13 @@ type SoftRef string
// For strings, numbers, booleans, and null values, it marshals into a primitive value, eg.:
//
// json.Marshal(res.DataValue{nil}) // Result: null
type DataValue struct {
Data interface{} `json:"data"`
type DataValue[T any] struct {
Data T `json:"data"`
}

// NewDataValue creates a new DataValue with the given data.
func NewDataValue[T any](data T) DataValue[T] { return DataValue[T]{Data: data} }

const (
refPrefix = `{"rid":`
softRefSuffix = `,"soft":true}`

0 comments on commit d8efe1c

Please sign in to comment.