snapshot of https://github.com/traggo/server.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
build / build (push) Failing after 3s

This commit is contained in:
upstream-mirror
2026-09-16 05:24:45 +05:00
commit cda7c1a474
272 changed files with 31973 additions and 0 deletions

29
auth/cleanup.go Normal file
View File

@@ -0,0 +1,29 @@
package auth
import (
"time"
"github.com/jinzhu/gorm"
"github.com/jmattheis/go-timemath"
"github.com/rs/zerolog/log"
"github.com/traggo/server/model"
)
var timeNow = time.Now
// CleanUp clean up expired devices
func CleanUp(db *gorm.DB, interval time.Duration, close chan bool) {
for {
select {
case <-time.After(interval):
affected := db.Where("type = ? AND active_at < ?", model.TypeLongExpiry, timemath.Second.Subtract(timeNow(), model.TypeLongExpiry.Seconds())).
Or("type = ? AND active_at < ?", model.TypeShortExpiry, timemath.Second.Subtract(timeNow(), model.TypeShortExpiry.Seconds())).
Delete(new(model.Device)).RowsAffected
if affected > 0 {
log.Debug().Int64("amount", affected).Msg("removed devices")
}
case <-close:
return
}
}
}

138
auth/cleanup_test.go Normal file
View File

@@ -0,0 +1,138 @@
package auth
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/traggo/server/model"
"github.com/traggo/server/test"
)
func TestCleanUp_stops(t *testing.T) {
db := test.InMemoryDB(t)
defer db.Close()
methodDone := make(chan struct{})
stopCleanUp := make(chan bool)
go func() {
CleanUp(db.DB, time.Microsecond*10, stopCleanUp)
methodDone <- struct{}{}
}()
stopCleanUp <- true
select {
case <-methodDone:
// expected
case <-time.After(time.Second):
t.Fail()
}
}
func TestCleanUp_removeExpiredDevices(t *testing.T) {
now := test.Time("2018-06-30T18:30:00Z")
timeDispose := fakeTime(now)
defer timeDispose()
db := test.InMemoryDB(t)
defer db.Close()
db.User(2)
db.Create(&model.Device{
ID: 1,
Token: "abc",
UserID: 2,
Name: "android 1",
Type: model.TypeShortExpiry,
ActiveAt: test.Time("2018-06-30T17:30:00Z"),
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
})
db.Create(&model.Device{
ID: 2,
Token: "abc2",
UserID: 2,
Name: "android 2",
Type: model.TypeShortExpiry,
ActiveAt: test.Time("2018-06-30T17:29:59Z"),
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
})
db.Create(&model.Device{
ID: 3,
Token: "abc3",
UserID: 2,
Name: "android 3",
Type: model.TypeLongExpiry,
ActiveAt: test.Time("2018-05-31T18:30:00Z"),
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
})
db.Create(&model.Device{
ID: 4,
Token: "abc4",
UserID: 2,
Name: "android 4",
Type: model.TypeLongExpiry,
ActiveAt: test.Time("2018-05-31T18:29:59Z"),
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
})
db.Create(&model.Device{
ID: 5,
Token: "abc5",
UserID: 2,
Name: "android 5",
Type: model.TypeNoExpiry,
ActiveAt: test.Time("2002-06-30T18:30:00Z"),
CreatedAt: test.Time("2000-06-30T18:30:00Z"),
})
stopCleanUp := make(chan bool)
go CleanUp(db.DB, time.Microsecond*10, stopCleanUp)
<-time.After(time.Millisecond * 20)
expected := []model.Device{
{
ID: 1,
Token: "abc",
UserID: 2,
Name: "android 1",
Type: model.TypeShortExpiry,
ActiveAt: test.Time("2018-06-30T17:30:00Z"),
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
},
{
ID: 3,
Token: "abc3",
UserID: 2,
Name: "android 3",
Type: model.TypeLongExpiry,
ActiveAt: test.Time("2018-05-31T18:30:00Z"),
CreatedAt: test.Time("2009-06-30T18:30:00Z"),
},
{
ID: 5,
Token: "abc5",
UserID: 2,
Name: "android 5",
Type: model.TypeNoExpiry,
ActiveAt: test.Time("2002-06-30T18:30:00Z"),
CreatedAt: test.Time("2000-06-30T18:30:00Z"),
},
}
var devices []model.Device
db.Find(&devices)
assert.Equal(t, expected, devices)
stopCleanUp <- true
}
func fakeTime(t time.Time) func() {
old := timeNow
timeNow = func() time.Time {
return t
}
return func() {
timeNow = old
}
}

26
auth/hasrole.go Normal file
View File

@@ -0,0 +1,26 @@
package auth
import (
"context"
"errors"
"github.com/99designs/gqlgen/graphql"
"github.com/traggo/server/generated/gqlmodel"
)
// HasRole checks if the current user has sufficient permissions.
func HasRole() func(ctx context.Context, obj interface{}, next graphql.Resolver, role gqlmodel.Role) (res interface{}, err error) {
return func(ctx context.Context, obj interface{}, next graphql.Resolver, role gqlmodel.Role) (interface{}, error) {
user := GetUser(ctx)
if user == nil {
return nil, errors.New("you need to login")
}
if role == gqlmodel.RoleAdmin && !user.Admin {
return nil, errors.New("permission denied")
}
return next(ctx)
}
}

55
auth/hasrole_test.go Normal file
View File

@@ -0,0 +1,55 @@
package auth_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/traggo/server/auth"
"github.com/traggo/server/generated/gqlmodel"
"github.com/traggo/server/test/fake"
)
var (
successResp = struct{}{}
)
func TestHasRole_requiredUser_givenAdmin_succeeds(t *testing.T) {
res, err := auth.HasRole()(fake.UserWithPerm(1, true), nil, noop, gqlmodel.RoleUser)
assert.Nil(t, err)
assert.Equal(t, successResp, res)
}
func TestHasRole_requiredAdmin_givenAdmin_succeeds(t *testing.T) {
res, err := auth.HasRole()(fake.UserWithPerm(1, true), nil, noop, gqlmodel.RoleAdmin)
assert.Nil(t, err)
assert.Equal(t, successResp, res)
}
func TestHasRole_requiredUser_givenUser_succeeds(t *testing.T) {
res, err := auth.HasRole()(fake.UserWithPerm(1, false), nil, noop, gqlmodel.RoleUser)
assert.Nil(t, err)
assert.Equal(t, successResp, res)
}
func TestHasRole_requiredAdmin_givenUser_fails(t *testing.T) {
res, err := auth.HasRole()(fake.UserWithPerm(1, false), nil, noop, gqlmodel.RoleAdmin)
assert.Nil(t, res)
assert.EqualError(t, err, "permission denied")
}
func TestHasRole_requiredAdmin_noUserGiven_fails(t *testing.T) {
res, err := auth.HasRole()(context.Background(), nil, noop, gqlmodel.RoleAdmin)
assert.Nil(t, res)
assert.EqualError(t, err, "you need to login")
}
func TestHasRole_requiredUser_noUserGiven_fails(t *testing.T) {
res, err := auth.HasRole()(context.Background(), nil, noop, gqlmodel.RoleUser)
assert.Nil(t, res)
assert.EqualError(t, err, "you need to login")
}
func noop(context.Context) (res interface{}, err error) {
return successResp, nil
}

182
auth/middleware.go Normal file
View File

@@ -0,0 +1,182 @@
package auth
import (
"context"
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
"github.com/rs/zerolog/log"
"github.com/traggo/server/model"
)
type key string
var (
traggoSession = "traggo"
createSessionKey key = "createSession"
destroySessionKey key = "destroySession"
userKey key = "user"
deviceKey key = "device"
)
// Middleware is the auth middleware which sets user and device context parameters.
func Middleware(db *gorm.DB) mux.MiddlewareFunc {
return func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
handler.ServeHTTP(writer, sessionCallbacks(reqisterUser(request, writer, db), writer))
})
}
}
func sessionCallbacks(request *http.Request, writer http.ResponseWriter) *http.Request {
createSession := func(token string, maxAge int) {
http.SetCookie(writer, &http.Cookie{
Name: traggoSession,
Value: token,
MaxAge: maxAge,
})
}
destroySession := func() {
http.SetCookie(writer, &http.Cookie{
Name: traggoSession,
Value: "",
MaxAge: -1,
})
}
return request.WithContext(WithDestroySession(WithCreateSession(request.Context(), createSession), destroySession))
}
func reqisterUser(request *http.Request, writer http.ResponseWriter, db *gorm.DB) *http.Request {
token, err := getToken(request)
if err != nil {
return request
}
device := &model.Device{}
if db.Where("token = ?", token).Find(device).RecordNotFound() {
log.Debug().Str("token", token).Msg("No device with token found")
return request
}
user := &model.User{}
if db.Find(user, device.UserID).RecordNotFound() {
log.Panic().Int("userID", device.UserID).Int("deviceID", device.ID).Msg("User not found")
}
log.Debug().Int("userid", device.UserID).Str("username", user.Name).Msg("User found")
impersonate := request.Header.Get("X-Traggo-Impersonate")
if impersonate != "" {
if !user.Admin {
log.Info().Str("impersonate", impersonate).Msg("Trying to impersonate without being admin")
return request
}
userID, err := strconv.Atoi(impersonate)
if err != nil {
log.Info().Str("impersonate", impersonate).Msg("Unable to parse impersonation header")
return request
}
impersonateUser := &model.User{}
if db.Find(impersonateUser, userID).RecordNotFound() {
log.Info().Int("userid", userID).Msg("Impersonation user not found")
return request
}
user = impersonateUser
log.Info().Str("username", user.Name).Msg("Impersonation user")
}
if device.ActiveAt.Before(time.Now().Add(5 * -time.Minute)) {
log.Debug().Int("deviceId", device.ID).Str("deviceName", device.Name).Msg("update device activeAt")
device.ActiveAt = timeNow()
db.Save(device)
if cookie, err := request.Cookie(traggoSession); err == nil && cookie != nil {
cookie.MaxAge = device.Type.Seconds()
http.SetCookie(writer, cookie)
}
}
return request.WithContext(WithUser(WithDevice(request.Context(), device), user))
}
func getToken(request *http.Request) (string, error) {
if value := request.Header.Get("Authorization"); value != "" && strings.HasPrefix(value, "traggo ") {
log.Debug().Msg("Using authorization header for authentication")
key := strings.TrimPrefix(value, "traggo ")
return key, nil
}
if cookie, err := request.Cookie("traggo"); err == nil && cookie != nil && cookie.Value != "" {
log.Debug().Msg("Using cookie for authentication")
return cookie.Value, nil
}
if token := request.FormValue("token"); token != "" {
log.Debug().Msg("Using query parameter for authentication")
return token, nil
}
return "", errors.New("no token found")
}
// WithDevice adds the authenticated device to the context.
func WithDevice(ctx context.Context, device *model.Device) context.Context {
return context.WithValue(
ctx,
deviceKey,
device)
}
// WithUser adds the authenticated user to the context.
func WithUser(ctx context.Context, user *model.User) context.Context {
return context.WithValue(
ctx,
userKey,
user)
}
// WithCreateSession adds the create session method
func WithCreateSession(ctx context.Context, f func(token string, age int)) context.Context {
return context.WithValue(ctx, createSessionKey, f)
}
// WithDestroySession adds the destroy session method
func WithDestroySession(ctx context.Context, f func()) context.Context {
return context.WithValue(ctx, destroySessionKey, f)
}
// GetUser returns the authenticated user or panics.
func GetUser(ctx context.Context) *model.User {
if user, ok := ctx.Value(userKey).(*model.User); ok {
return user
}
return nil
}
// GetDevice returns the authenticated device or nil.
func GetDevice(ctx context.Context) *model.Device {
if device, ok := ctx.Value(deviceKey).(*model.Device); ok {
return device
}
return nil
}
// GetCreateSession gets the create session callback
func GetCreateSession(ctx context.Context) func(token string, age int) {
if f, ok := ctx.Value(createSessionKey).(func(string, int)); ok {
return f
}
panic("create session must exist")
}
// GetDestroySession gets the destroy session callback
func GetDestroySession(ctx context.Context) func() {
if f, ok := ctx.Value(destroySessionKey).(func()); ok {
return f
}
panic("create session must exist")
}

328
auth/middleware_test.go Normal file
View File

@@ -0,0 +1,328 @@
package auth
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/traggo/server/test"
)
func TestMiddleware_noAuthentication_noAuthentication(t *testing.T) {
db := test.InMemoryDB(t)
defer db.Close()
spy := &requestSpy{}
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/test", nil))
ctx := spy.req.Context()
assert.Nil(t, GetUser(ctx))
assert.Nil(t, GetDevice(ctx))
}
func TestMiddleware_query_notExistingToken_noAuthentication(t *testing.T) {
db := test.InMemoryDB(t)
defer db.Close()
spy := &requestSpy{}
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/test?token=abc", nil))
ctx := spy.req.Context()
assert.Nil(t, GetUser(ctx))
assert.Nil(t, GetDevice(ctx))
}
func TestMiddleware_query_validToken_authenticates(t *testing.T) {
now := test.Time("2018-06-30T18:30:00Z")
timeDispose := fakeTime(now)
defer timeDispose()
db := test.InMemoryDB(t)
defer db.Close()
builder := db.User(1)
user := builder.User
device := builder.NewDevice(2, "abc", "test")
spy := &requestSpy{}
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/test?token=abc", nil))
ctx := spy.req.Context()
assert.Equal(t, &user, GetUser(ctx))
device.ActiveAt = now
assert.Equal(t, &device, GetDevice(ctx))
}
func TestMiddleware_header_notExistingToken_noAuthentication(t *testing.T) {
db := test.InMemoryDB(t)
defer db.Close()
spy := &requestSpy{}
request := httptest.NewRequest("GET", "/test?token=abc", nil)
request.Header["Authorization"] = []string{"traggo abc"}
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request)
ctx := spy.req.Context()
assert.Nil(t, GetUser(ctx))
assert.Nil(t, GetDevice(ctx))
}
func TestMiddleware_header_validToken_authenticates(t *testing.T) {
now := test.Time("2018-06-30T18:30:00Z")
timeDispose := fakeTime(now)
defer timeDispose()
db := test.InMemoryDB(t)
defer db.Close()
builder := db.User(1)
user := builder.User
device := builder.NewDevice(2, "abc", "test")
spy := &requestSpy{}
request := httptest.NewRequest("GET", "/test", nil)
request.Header.Set("Authorization", "traggo abc")
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request)
ctx := spy.req.Context()
assert.Equal(t, &user, GetUser(ctx))
device.ActiveAt = now
assert.Equal(t, &device, GetDevice(ctx))
}
func TestMiddleware_header_validToken_invalidAuthenticationType_noAuthentication(t *testing.T) {
db := test.InMemoryDB(t)
defer db.Close()
db.User(1).NewDevice(2, "abc", "test")
spy := &requestSpy{}
request := httptest.NewRequest("GET", "/test", nil)
request.Header.Set("Authorization", "basic abc")
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request)
ctx := spy.req.Context()
assert.Nil(t, GetUser(ctx))
assert.Nil(t, GetDevice(ctx))
}
func TestMiddleware_cookie_notExistingToken_noAuthentication(t *testing.T) {
db := test.InMemoryDB(t)
defer db.Close()
spy := &requestSpy{}
request := httptest.NewRequest("GET", "/test?token=abc", nil)
request.AddCookie(&http.Cookie{
Name: "traggo",
Value: "abc",
})
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request)
ctx := spy.req.Context()
assert.Nil(t, GetUser(ctx))
assert.Nil(t, GetDevice(ctx))
}
func TestMiddleware_cookie_validToken_authenticates(t *testing.T) {
now := test.Time("2018-06-30T18:30:00Z")
timeDispose := fakeTime(now)
defer timeDispose()
db := test.InMemoryDB(t)
defer db.Close()
builder := db.User(1)
user := builder.User
device := builder.NewDevice(2, "abc", "test")
spy := &requestSpy{}
request := httptest.NewRequest("GET", "/test?token=abc", nil)
request.AddCookie(&http.Cookie{
Name: "traggo",
Value: "abc",
})
Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request)
ctx := spy.req.Context()
assert.Equal(t, &user, GetUser(ctx))
device.ActiveAt = now
assert.Equal(t, &device, GetDevice(ctx))
}
func TestMiddleware_createSession_setsCookie(t *testing.T) {
db := test.InMemoryDB(t)
defer db.Close()
spy := &requestSpy{}
recorder := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/test", nil)
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
ctx := spy.req.Context()
GetCreateSession(ctx)("new token", 60)
cookieHeader := recorder.Header().Get("Set-Cookie")
assert.Equal(t, `traggo="new token"; Max-Age=60`, cookieHeader)
}
func TestMiddleware_noCallbackExecuted_noCookieSet(t *testing.T) {
db := test.InMemoryDB(t)
defer db.Close()
spy := &requestSpy{}
recorder := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/test", nil)
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
cookieHeader := recorder.Header().Get("Set-Cookie")
assert.Equal(t, "", cookieHeader)
}
func TestMiddleware_destroySession_destroysCookie(t *testing.T) {
db := test.InMemoryDB(t)
defer db.Close()
spy := &requestSpy{}
recorder := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/test", nil)
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
ctx := spy.req.Context()
GetDestroySession(ctx)()
cookieHeader := recorder.Header().Get("Set-Cookie")
assert.Equal(t, "traggo=; Max-Age=0", cookieHeader)
}
func TestMiddleware_impersonate_no_admin(t *testing.T) {
now := test.Time("2018-06-30T18:30:00Z")
timeDispose := fakeTime(now)
defer timeDispose()
db := test.InMemoryDB(t)
defer db.Close()
builder := db.User(1)
builder.NewDevice(2, "abc", "test")
spy := &requestSpy{}
recorder := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/test?token=abc", nil)
request.Header.Set("X-Traggo-Impersonate", "empty")
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
response := recorder.Result()
assert.Equal(t, 200, response.StatusCode)
ctx := spy.req.Context()
assert.Nil(t, GetUser(ctx))
assert.Nil(t, GetDevice(ctx))
}
func TestMiddleware_impersonate_invalid_personate_header(t *testing.T) {
now := test.Time("2018-06-30T18:30:00Z")
timeDispose := fakeTime(now)
defer timeDispose()
db := test.InMemoryDB(t)
defer db.Close()
builder := db.User(1)
user := builder.User
user.Admin = true
db.Save(user)
builder.NewDevice(2, "abc", "test")
spy := &requestSpy{}
recorder := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/test?token=abc", nil)
request.Header.Set("X-Traggo-Impersonate", "invalid")
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
response := recorder.Result()
assert.Equal(t, 200, response.StatusCode)
ctx := spy.req.Context()
assert.Nil(t, GetUser(ctx))
assert.Nil(t, GetDevice(ctx))
}
func TestMiddleware_impersonate_non_existing_user(t *testing.T) {
now := test.Time("2018-06-30T18:30:00Z")
timeDispose := fakeTime(now)
defer timeDispose()
db := test.InMemoryDB(t)
defer db.Close()
builder := db.User(1)
user := builder.User
user.Admin = true
db.Save(user)
builder.NewDevice(2, "abc", "test")
spy := &requestSpy{}
recorder := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/test?token=abc", nil)
request.Header.Set("X-Traggo-Impersonate", "42")
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
response := recorder.Result()
assert.Equal(t, 200, response.StatusCode)
ctx := spy.req.Context()
assert.Nil(t, GetUser(ctx))
assert.Nil(t, GetDevice(ctx))
}
func TestMiddleware_impersonate_happy(t *testing.T) {
now := test.Time("2018-06-30T18:30:00Z")
timeDispose := fakeTime(now)
defer timeDispose()
db := test.InMemoryDB(t)
defer db.Close()
admin_builder := db.User(1)
admin_user := admin_builder.User
admin_user.Admin = true
db.Save(admin_user)
admin_device := admin_builder.NewDevice(2, "abc", "test")
builder := db.User(2)
user := builder.User
spy := &requestSpy{}
recorder := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/test?token=abc", nil)
request.Header.Set("X-Traggo-Impersonate", "2")
Middleware(db.DB)(spy).ServeHTTP(recorder, request)
response := recorder.Result()
assert.Equal(t, 200, response.StatusCode)
ctx := spy.req.Context()
assert.Equal(t, &user, GetUser(ctx))
admin_device.ActiveAt = now
assert.Equal(t, &admin_device, GetDevice(ctx))
}
func TestGetCreateSession_panicsWhenMiddlewareWasNotExecuted(t *testing.T) {
assert.Panics(t, func() {
GetCreateSession(context.Background())
})
}
func TestGetDestroySession_panicsWhenMiddlewareWasNotExecuted(t *testing.T) {
assert.Panics(t, func() {
GetDestroySession(context.Background())
})
}
type requestSpy struct {
req *http.Request
}
func (s *requestSpy) ServeHTTP(_ http.ResponseWriter, req *http.Request) {
s.req = req
}

43
auth/rand/random.go Normal file
View File

@@ -0,0 +1,43 @@
package rand
import (
"crypto/rand"
"github.com/rs/zerolog/log"
)
var (
chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
randRead = rand.Read
)
func init() {
// exits with 1 when crypto/rand is not available
Token(1)
}
func randBytes(count int) ([]byte, error) {
bytes := make([]byte, count)
_, err := randRead(bytes)
return bytes, err
}
// Token returns a random token.
func Token(count int) string {
bytes, err := randBytes(count)
if err != nil {
log.Panic().Msg("crypto/rand is not available")
}
return bytesToString(bytes, count)
}
func bytesToString(bytes []byte, count int) string {
token := make([]rune, count)
for index, item := range bytes {
token[index] = chars[int(item)%len(chars)]
}
return string(token)
}

30
auth/rand/random_test.go Normal file
View File

@@ -0,0 +1,30 @@
package rand
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestToken(t *testing.T) {
for i := 1; i < 10; i++ {
assert.Len(t, Token(i), i)
}
assert.Len(t, Token(120), 120)
assert.Len(t, Token(222), 222)
assert.Len(t, Token(555665), 555665)
}
func TestToken_panics(t *testing.T) {
old := randRead
defer func() {
randRead = old
}()
randRead = func(b []byte) (n int, err error) {
return 0, errors.New("oops")
}
assert.Panics(t, func() {
Token(1)
})
}