Files
verdande/internal/httpapi/tokens_test.go
T

167 lines
5.0 KiB
Go
Raw Normal View History

package httpapi
import (
"net/http"
"strings"
"testing"
)
func TestAPITokenLifecycle(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
resp, body := ts.do(t, "GET", "/api/v1/tokens", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("list: status %d, body %v", resp.StatusCode, body)
}
if tokens, _ := body["tokens"].([]any); len(tokens) != 0 {
t.Errorf("a fresh account already has tokens: %v", tokens)
}
resp, created := ts.do(t, "POST", "/api/v1/tokens", map[string]any{
"name": "min server", "expires_in_days": 30,
})
if resp.StatusCode != http.StatusCreated {
t.Fatalf("create: status %d, body %v", resp.StatusCode, created)
}
plaintext, _ := created["token"].(string)
if !strings.HasPrefix(plaintext, "vrd_") {
t.Fatalf("token %q does not carry the scanner-visible prefix", plaintext)
}
if created["expires_at"] == nil {
t.Error("an expiry was asked for and none came back")
}
prefix, _ := created["prefix"].(string)
if !strings.HasPrefix(plaintext, prefix) {
t.Errorf("prefix %q is not the start of the token", prefix)
}
// The token authenticates as its owner.
req, err := http.NewRequest("GET", ts.URL+"/api/v1/auth/me", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+plaintext)
me, err := (&http.Client{}).Do(req)
if err != nil {
t.Fatalf("bearer request: %v", err)
}
defer me.Body.Close()
if me.StatusCode != http.StatusOK {
t.Errorf("the new token does not authenticate: status %d", me.StatusCode)
}
// Listing shows it, without the secret.
_, body = ts.do(t, "GET", "/api/v1/tokens", nil)
tokens, _ := body["tokens"].([]any)
if len(tokens) != 1 {
t.Fatalf("want one token listed, got %v", tokens)
}
listed := tokens[0].(map[string]any)
if _, leaked := listed["token"]; leaked {
t.Error("the listing carries the token itself")
}
if listed["name"] != "min server" {
t.Errorf("name = %v", listed["name"])
}
id, _ := created["id"].(string)
resp, _ = ts.do(t, "DELETE", "/api/v1/tokens/"+id, nil)
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("delete: status %d", resp.StatusCode)
}
// And is dead the moment it is deleted.
req, _ = http.NewRequest("GET", ts.URL+"/api/v1/auth/me", nil)
req.Header.Set("Authorization", "Bearer "+plaintext)
me, err = (&http.Client{}).Do(req)
if err != nil {
t.Fatalf("bearer request: %v", err)
}
defer me.Body.Close()
if me.StatusCode != http.StatusUnauthorized {
t.Errorf("a deleted token still works: status %d", me.StatusCode)
}
}
// A token must not be able to mint another one. Otherwise a leaked token is
// permanent: revoking it leaves behind the one it issued.
func TestAPITokenCannotMintAnother(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
_, created := ts.do(t, "POST", "/api/v1/tokens", map[string]any{"name": "første"})
plaintext, _ := created["token"].(string)
for _, tc := range []struct{ method, path string }{
{"GET", "/api/v1/tokens"},
{"POST", "/api/v1/tokens"},
{"DELETE", "/api/v1/tokens/" + created["id"].(string)},
} {
req, err := http.NewRequest(tc.method, ts.URL+tc.path, strings.NewReader(`{"name":"anden"}`))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+plaintext)
req.Header.Set("Content-Type", "application/json")
resp, err := (&http.Client{}).Do(req)
if err != nil {
t.Fatalf("%s %s: %v", tc.method, tc.path, err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Errorf("%s %s with a bearer token: status %d, want 403",
tc.method, tc.path, resp.StatusCode)
}
}
}
func TestAPITokenValidation(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
for _, tc := range []struct {
name string
body map[string]any
field string
}{
{"no name", map[string]any{"name": " "}, "name"},
{"negative lifetime", map[string]any{"name": "x", "expires_in_days": -1}, "expires_in_days"},
{"absurd lifetime", map[string]any{"name": "x", "expires_in_days": 4000}, "expires_in_days"},
} {
t.Run(tc.name, func(t *testing.T) {
resp, body := ts.do(t, "POST", "/api/v1/tokens", tc.body)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("status %d, want 422", resp.StatusCode)
}
fields, _ := body["fields"].(map[string]any)
if fields[tc.field] == nil {
t.Errorf("no error on %q: %v", tc.field, body)
}
})
}
}
// Somebody else's token id must delete nothing, and must not confirm it exists.
func TestAPITokenDeleteIsScopedToOwner(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
_, created := ts.do(t, "POST", "/api/v1/tokens", map[string]any{"name": "min"})
id := created["id"].(string)
other := ts.newUser(t, "anden@example.dk", "Anden")
resp, _ := other.do(t, "DELETE", "/api/v1/tokens/"+id, nil)
if resp.StatusCode != http.StatusNotFound {
t.Errorf("status %d, want 404 for a token belonging to somebody else", resp.StatusCode)
}
// Still there for its owner.
_, body := ts.do(t, "GET", "/api/v1/tokens", nil)
if tokens, _ := body["tokens"].([]any); len(tokens) != 1 {
t.Errorf("the owner's token was affected: %v", tokens)
}
}