167 lines
4.2 KiB
Go
167 lines
4.2 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||
|
|
|
||
|
|
package tokens
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestCreateAndAuthenticate(t *testing.T) {
|
||
|
|
path := filepath.Join(t.TempDir(), "tokens.toml")
|
||
|
|
s := New(path)
|
||
|
|
record, raw, err := s.Create("ci", nil)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Create: %v", err)
|
||
|
|
}
|
||
|
|
if record == nil || raw == "" {
|
||
|
|
t.Fatal("Create failed")
|
||
|
|
}
|
||
|
|
if !strings.HasPrefix(raw, TokenPrefix) {
|
||
|
|
t.Fatalf("raw = %q", raw)
|
||
|
|
}
|
||
|
|
if record.TokenHash == raw {
|
||
|
|
t.Fatal("raw token persisted")
|
||
|
|
}
|
||
|
|
found := s.Authenticate(raw)
|
||
|
|
if found == nil || found.Name != "ci" {
|
||
|
|
t.Fatalf("Authenticate = %v", found)
|
||
|
|
}
|
||
|
|
if !found.HasScope("write") {
|
||
|
|
t.Fatal("unrestricted token should grant every scope")
|
||
|
|
}
|
||
|
|
if s.Authenticate("vol_wrong") != nil {
|
||
|
|
t.Fatal("wrong token authenticated")
|
||
|
|
}
|
||
|
|
if s.Authenticate("not-our-prefix") != nil {
|
||
|
|
t.Fatal("foreign prefix authenticated")
|
||
|
|
}
|
||
|
|
|
||
|
|
info, err := os.Stat(path)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("stat: %v", err)
|
||
|
|
}
|
||
|
|
if info.Mode().Perm() != 0o600 {
|
||
|
|
t.Fatalf("mode = %v", info.Mode().Perm())
|
||
|
|
}
|
||
|
|
|
||
|
|
rawFile, err := os.ReadFile(path)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("read: %v", err)
|
||
|
|
}
|
||
|
|
if strings.Contains(string(rawFile), raw) {
|
||
|
|
t.Fatal("raw token leaked into the file")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCreateScopes(t *testing.T) {
|
||
|
|
s := New(filepath.Join(t.TempDir(), "tokens.toml"))
|
||
|
|
record, _, err := s.Create("scoped", []string{"write", "bogus", "delete"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Create: %v", err)
|
||
|
|
}
|
||
|
|
if record == nil {
|
||
|
|
t.Fatal("Create failed")
|
||
|
|
}
|
||
|
|
if len(record.Scopes) != 2 || record.Scopes[0] != "write" || record.Scopes[1] != "delete" {
|
||
|
|
t.Fatalf("scopes = %v", record.Scopes)
|
||
|
|
}
|
||
|
|
if record.HasScope("read") {
|
||
|
|
t.Fatal("read scope granted")
|
||
|
|
}
|
||
|
|
if !record.HasScope("write") {
|
||
|
|
t.Fatal("write scope missing")
|
||
|
|
}
|
||
|
|
|
||
|
|
// An explicit list that names no valid scope must be refused, not
|
||
|
|
// turned into an unrestricted token.
|
||
|
|
if _, _, err := s.Create("invalid-only", []string{"bogus"}); !errors.Is(err, ErrNoValidScope) {
|
||
|
|
t.Fatalf("err = %v, want ErrNoValidScope", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCreateRejectsDuplicatesAndEmpty(t *testing.T) {
|
||
|
|
s := New(filepath.Join(t.TempDir(), "tokens.toml"))
|
||
|
|
if record, _, err := s.Create("dup", nil); err != nil || record == nil {
|
||
|
|
t.Fatalf("first Create: %v, %v", record, err)
|
||
|
|
}
|
||
|
|
if record, _, err := s.Create("dup", nil); err == nil || record != nil {
|
||
|
|
t.Fatal("duplicate name accepted")
|
||
|
|
}
|
||
|
|
if record, _, err := s.Create(" ", nil); err == nil || record != nil {
|
||
|
|
t.Fatal("empty name accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPersistedAcrossReopen(t *testing.T) {
|
||
|
|
path := filepath.Join(t.TempDir(), "tokens.toml")
|
||
|
|
s := New(path)
|
||
|
|
_, raw, err := s.Create("ci", []string{"write"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Create: %v", err)
|
||
|
|
}
|
||
|
|
reopened := New(path)
|
||
|
|
if len(reopened.All()) != 1 {
|
||
|
|
t.Fatalf("tokens = %v", reopened.All())
|
||
|
|
}
|
||
|
|
if reopened.Authenticate(raw) == nil {
|
||
|
|
t.Fatal("token lost across reopen")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTouchRefreshesOncePerDay(t *testing.T) {
|
||
|
|
s := New(filepath.Join(t.TempDir(), "tokens.toml"))
|
||
|
|
record, _, err := s.Create("ci", nil)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Create: %v", err)
|
||
|
|
}
|
||
|
|
s.Touch("ci")
|
||
|
|
after := s.All()[0]
|
||
|
|
if after.LastUsed == "" {
|
||
|
|
t.Fatal("last_used not set")
|
||
|
|
}
|
||
|
|
s.Touch("ci")
|
||
|
|
again := s.All()[0]
|
||
|
|
if again.LastUsed != after.LastUsed {
|
||
|
|
t.Fatal("last_used updated twice in one day")
|
||
|
|
}
|
||
|
|
s.Touch("missing")
|
||
|
|
if record.Created == "" {
|
||
|
|
t.Fatal("created missing")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRevoke(t *testing.T) {
|
||
|
|
s := New(filepath.Join(t.TempDir(), "tokens.toml"))
|
||
|
|
s.Create("one", nil)
|
||
|
|
if !s.Revoke("one") {
|
||
|
|
t.Fatal("Revoke failed")
|
||
|
|
}
|
||
|
|
if s.Revoke("one") {
|
||
|
|
t.Fatal("Revoke succeeded twice")
|
||
|
|
}
|
||
|
|
if len(s.All()) != 0 {
|
||
|
|
t.Fatalf("tokens = %v", s.All())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUnreadableFileYieldsNoTokens(t *testing.T) {
|
||
|
|
path := filepath.Join(t.TempDir(), "tokens.toml")
|
||
|
|
if err := os.WriteFile(path, []byte("broken = = ="), 0o600); err != nil {
|
||
|
|
t.Fatalf("write: %v", err)
|
||
|
|
}
|
||
|
|
if got := New(path).All(); got != nil {
|
||
|
|
t.Fatalf("tokens = %v, want none", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMissingFileYieldsNoTokens(t *testing.T) {
|
||
|
|
if got := New(filepath.Join(t.TempDir(), "nope.toml")).All(); got != nil {
|
||
|
|
t.Fatalf("tokens = %v, want none", got)
|
||
|
|
}
|
||
|
|
}
|