~bigbes/sr-ht-spec

ref: 36976713874550ff48b51f954a59dd597c36fa87 sr-ht-spec/authn/token_test.go -rw-r--r-- 4.8 KiB
36976713 — Eugene Blikh feat(db): webhook + user tables (Phase 5a foundation) 25 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package authn

import (
	"bytes"
	"context"
	"database/sql"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestHashToken_IsSHA256AndStable(t *testing.T) {
	a := HashToken("s3cret")
	b := HashToken("s3cret")
	if len(a) != 32 {
		t.Fatalf("hash length = %d, want 32", len(a))
	}
	if !bytes.Equal(a, b) {
		t.Fatal("hashing the same token twice produced different values")
	}
	if bytes.Equal(a, HashToken("s3cres")) {
		t.Fatal("distinct tokens hashed to the same value")
	}
}

func TestBearerFromRequest(t *testing.T) {
	cases := map[string]struct{ header, want string }{
		"bearer":            {"Bearer abc123", "abc123"},
		"lowercase scheme":  {"bearer abc123", "abc123"},
		"mixed case scheme": {"BeArEr abc123", "abc123"},
		"padded":            {"Bearer   abc123  ", "abc123"},
		"basic is not ours": {"Basic dXNlcjpwYXNz", ""},
		"no scheme":         {"abc123", ""},
		"absent":            {"", ""},
	}
	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			r := httptest.NewRequest(http.MethodGet, "/", nil)
			if c.header != "" {
				r.Header.Set("Authorization", c.header)
			}
			if got := BearerFromRequest(r); got != c.want {
				t.Fatalf("BearerFromRequest = %q, want %q", got, c.want)
			}
		})
	}
}

func TestResolveAgentToken_Accepted(t *testing.T) {
	store := newStubStore()
	want := store.add("live-token", "laptop")

	got, err := ResolveAgentToken(context.Background(), store, "live-token")
	if err != nil {
		t.Fatalf("ResolveAgentToken: %v", err)
	}
	if got.ID != want.ID || got.Name != "laptop" {
		t.Fatalf("resolved %+v, want id %d name %q", got, want.ID, "laptop")
	}
	if got.IsRevoked() {
		t.Fatal("live token reported as revoked")
	}
}

func TestResolveAgentToken_Revoked(t *testing.T) {
	store := newStubStore()
	store.revoke("dead-token", "cron")

	_, err := ResolveAgentToken(context.Background(), store, "dead-token")
	if !errors.Is(err, ErrRevokedToken) {
		t.Fatalf("error = %v, want ErrRevokedToken", err)
	}
	// A revoked token must not be reported as unknown: the operator needs to
	// tell "I killed this" from "this never existed".
	if errors.Is(err, ErrUnknownToken) {
		t.Fatalf("revoked token also reported as unknown: %v", err)
	}
	if !IsAuthFailure(err) {
		t.Fatalf("revocation must be a permanent auth failure: %v", err)
	}
}

func TestResolveAgentToken_Unknown(t *testing.T) {
	store := newStubStore()
	store.add("live-token", "laptop")

	_, err := ResolveAgentToken(context.Background(), store, "never-issued")
	if !errors.Is(err, ErrUnknownToken) {
		t.Fatalf("error = %v, want ErrUnknownToken", err)
	}
	if !IsAuthFailure(err) {
		t.Fatalf("unknown token must be a permanent auth failure: %v", err)
	}
}

// db/ may hand back a bare sql.ErrNoRows from QueryRow().Scan(); it means the
// same thing as ErrUnknownToken and must not be mistaken for a store outage.
func TestResolveAgentToken_SQLNoRowsIsUnknown(t *testing.T) {
	store := newStubStore()
	store.err = sql.ErrNoRows

	_, err := ResolveAgentToken(context.Background(), store, "whatever")
	if !errors.Is(err, ErrUnknownToken) {
		t.Fatalf("error = %v, want ErrUnknownToken", err)
	}
}

func TestResolveAgentToken_EmptyIsNoToken(t *testing.T) {
	store := newStubStore()
	_, err := ResolveAgentToken(context.Background(), store, "")
	if !errors.Is(err, ErrNoToken) {
		t.Fatalf("error = %v, want ErrNoToken", err)
	}
	if store.calls != 0 {
		t.Fatalf("store consulted %d times for an absent token, want 0", store.calls)
	}
}

// A store outage must never read as a bad credential: fail closed, but tell the
// caller it is transient so it answers 503 and the agent retries.
func TestResolveAgentToken_StoreFailureIsTransient(t *testing.T) {
	boom := errors.New("connection refused")
	store := newStubStore()
	store.add("live-token", "laptop")
	store.err = boom

	_, err := ResolveAgentToken(context.Background(), store, "live-token")
	if !errors.Is(err, boom) {
		t.Fatalf("error = %v, want it to wrap the store error", err)
	}
	if IsAuthFailure(err) {
		t.Fatalf("store failure must not be a permanent auth failure: %v", err)
	}
}

// The constant-time re-check exists so that a store which matched loosely — by
// prefix, or on the wrong column — cannot authenticate anybody.
func TestResolveAgentToken_HashMismatchRejected(t *testing.T) {
	store := newStubStore()
	store.put(HashToken("presented"), AgentToken{
		ID:   7,
		Name: "sloppy-store",
		Hash: HashToken("something-else"),
	})

	_, err := ResolveAgentToken(context.Background(), store, "presented")
	if !errors.Is(err, ErrInvalidToken) {
		t.Fatalf("error = %v, want ErrInvalidToken", err)
	}
}

func TestResolveAgentToken_NilStoreIsNotAnAuthFailure(t *testing.T) {
	_, err := ResolveAgentToken(context.Background(), nil, "live-token")
	if err == nil {
		t.Fatal("nil store must be an error")
	}
	if IsAuthFailure(err) {
		t.Fatalf("a wiring bug must not read as a bad credential: %v", err)
	}
}