add token type with permission checks and composable matchers
This commit is contained in:
parent
5288df0b9e
commit
8b5fcc87c3
4 changed files with 499 additions and 0 deletions
202
token_test.go
Normal file
202
token_test.go
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
package ficha
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func makeToken(perms ...string) *Token {
|
||||
return &Token{
|
||||
id: "tok_test",
|
||||
issuedAt: time.Unix(1_700_000_000, 0),
|
||||
expiresAt: time.Unix(1_700_003_600, 0),
|
||||
permissions: perms,
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenAccessors(t *testing.T) {
|
||||
tok := makeToken("read", "write")
|
||||
|
||||
if tok.ID() != "tok_test" {
|
||||
t.Errorf("ID: got %q", tok.ID())
|
||||
}
|
||||
if got := tok.IssuedAt().Unix(); got != 1_700_000_000 {
|
||||
t.Errorf("IssuedAt: got %d", got)
|
||||
}
|
||||
if got := tok.ExpiresAt().Unix(); got != 1_700_003_600 {
|
||||
t.Errorf("ExpiresAt: got %d", got)
|
||||
}
|
||||
|
||||
perms := tok.Permissions()
|
||||
if len(perms) != 2 || perms[0] != "read" || perms[1] != "write" {
|
||||
t.Errorf("Permissions: got %v", perms)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenPermissionsIsCopy(t *testing.T) {
|
||||
tok := makeToken("read", "write")
|
||||
perms := tok.Permissions()
|
||||
perms[0] = "tampered"
|
||||
|
||||
if tok.Has("tampered") {
|
||||
t.Error("mutating returned slice affected token state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenHas(t *testing.T) {
|
||||
tok := makeToken("read", "write", "admin")
|
||||
|
||||
cases := map[string]bool{
|
||||
"read": true,
|
||||
"write": true,
|
||||
"admin": true,
|
||||
"delete": false,
|
||||
"": false,
|
||||
}
|
||||
for perm, want := range cases {
|
||||
if got := tok.Has(perm); got != want {
|
||||
t.Errorf("Has(%q): got %v, want %v", perm, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenHasAll(t *testing.T) {
|
||||
tok := makeToken("a", "b", "c")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
perms []string
|
||||
want bool
|
||||
}{
|
||||
{"all present", []string{"a", "b"}, true},
|
||||
{"all three", []string{"a", "b", "c"}, true},
|
||||
{"one missing", []string{"a", "z"}, false},
|
||||
{"none present", []string{"x", "y"}, false},
|
||||
{"empty input is vacuously true", []string{}, true},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tok.HasAll(tc.perms...); got != tc.want {
|
||||
t.Errorf("got %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenHasAny(t *testing.T) {
|
||||
tok := makeToken("a", "b")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
perms []string
|
||||
want bool
|
||||
}{
|
||||
{"one matches", []string{"a", "z"}, true},
|
||||
{"all match", []string{"a", "b"}, true},
|
||||
{"none match", []string{"x", "y"}, false},
|
||||
{"empty input is false", []string{}, false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tok.HasAny(tc.perms...); got != tc.want {
|
||||
t.Errorf("got %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenHasNone(t *testing.T) {
|
||||
tok := makeToken("a", "b")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
perms []string
|
||||
want bool
|
||||
}{
|
||||
{"none of them present", []string{"x", "y"}, true},
|
||||
{"one is present", []string{"a", "y"}, false},
|
||||
{"empty input is vacuously true", []string{}, true},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tok.HasNone(tc.perms...); got != tc.want {
|
||||
t.Errorf("got %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenUnmarshalData(t *testing.T) {
|
||||
type meta struct {
|
||||
UserID string `json:"user_id"`
|
||||
Tier int `json:"tier"`
|
||||
}
|
||||
raw, _ := json.Marshal(meta{UserID: "u_42", Tier: 3})
|
||||
|
||||
tok := &Token{data: raw}
|
||||
|
||||
var got meta
|
||||
if err := tok.UnmarshalData(&got); err != nil {
|
||||
t.Fatalf("UnmarshalData: %v", err)
|
||||
}
|
||||
if got.UserID != "u_42" || got.Tier != 3 {
|
||||
t.Errorf("got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenUnmarshalDataEmpty(t *testing.T) {
|
||||
tok := &Token{}
|
||||
var got map[string]any
|
||||
if err := tok.UnmarshalData(&got); err != nil {
|
||||
t.Errorf("expected nil error on empty data, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewToken(t *testing.T) {
|
||||
p := payload{
|
||||
ID: "tok_x",
|
||||
Iat: 1_700_000_000,
|
||||
Exp: 1_700_003_600,
|
||||
Permissions: []string{"read"},
|
||||
Data: json.RawMessage(`{"x":1}`),
|
||||
}
|
||||
tok := newToken(p)
|
||||
|
||||
if tok.ID() != "tok_x" {
|
||||
t.Errorf("ID: got %q", tok.ID())
|
||||
}
|
||||
if !tok.Has("read") {
|
||||
t.Error("expected Has(read)")
|
||||
}
|
||||
if tok.IssuedAt().Unix() != 1_700_000_000 {
|
||||
t.Errorf("IssuedAt: %v", tok.IssuedAt())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenRequiresAll(t *testing.T) {
|
||||
tok := makeToken("a", "b", "c")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
perms []string
|
||||
want bool
|
||||
}{
|
||||
{"all present", []string{"a", "b"}, true},
|
||||
{"all three", []string{"a", "b", "c"}, true},
|
||||
{"one missing", []string{"a", "z"}, false},
|
||||
{"none present", []string{"x", "y"}, false},
|
||||
{"empty input fails closed", []string{}, false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tok.RequiresAll(tc.perms...); got != tc.want {
|
||||
t.Errorf("got %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue