87 lines
2.6 KiB
Go
87 lines
2.6 KiB
Go
|
|
package vault
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestSeed(t *testing.T) {
|
||
|
|
var mu sync.Mutex
|
||
|
|
records := map[string]Record{
|
||
|
|
"short/api": {Data: map[string]any{"private_key": "PLACEHOLDER"}, Version: 1},
|
||
|
|
"short/public": {Data: map[string]any{"public_key": "PLACEHOLDER"}, Version: 1},
|
||
|
|
}
|
||
|
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
mu.Lock()
|
||
|
|
defer mu.Unlock()
|
||
|
|
path := strings.TrimPrefix(r.URL.Path, "/v1/secret/data/")
|
||
|
|
record, ok := records[path]
|
||
|
|
if !ok {
|
||
|
|
http.NotFound(w, r)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if r.Method == http.MethodGet {
|
||
|
|
json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"data": record.Data, "metadata": map[string]int{"version": record.Version}}})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var request struct {
|
||
|
|
Data map[string]any `json:"data"`
|
||
|
|
Options struct {
|
||
|
|
CAS int `json:"cas"`
|
||
|
|
} `json:"options"`
|
||
|
|
}
|
||
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||
|
|
t.Error(err)
|
||
|
|
}
|
||
|
|
if request.Options.CAS != record.Version {
|
||
|
|
w.WriteHeader(400)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
records[path] = Record{Data: request.Data, Version: record.Version + 1}
|
||
|
|
fmt.Fprint(w, `{}`)
|
||
|
|
}))
|
||
|
|
defer s.Close()
|
||
|
|
c := Client{URL: s.URL, HTTP: s.Client()}
|
||
|
|
ctx := context.Background()
|
||
|
|
if err := c.Seed(ctx, "short/api"); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
key := records["short/api"].Data["private_key"]
|
||
|
|
for range 2 {
|
||
|
|
if err := c.Seed(ctx, "short/api"); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if records["short/api"].Version != 2 || records["short/api"].Data["private_key"] != key {
|
||
|
|
t.Fatal("repeat seed rotated API identity")
|
||
|
|
}
|
||
|
|
if records["short/public"].Version != 2 || len(records["short/public"].Data) != 1 {
|
||
|
|
t.Fatal("public publication contains private material or redundant writes")
|
||
|
|
}
|
||
|
|
records["short/public"] = Record{Data: map[string]any{"public_key": "PLACEHOLDER"}, Version: 3}
|
||
|
|
if err := c.Seed(ctx, "short/api"); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if records["short/api"].Data["private_key"] != key || records["short/public"].Version != 4 {
|
||
|
|
t.Fatal("interrupted seed did not resume without rotation")
|
||
|
|
}
|
||
|
|
records["short/api"] = Record{Data: map[string]any{"private_key": "PLACEHOLDER"}, Version: 5}
|
||
|
|
records["short/public"] = Record{Data: map[string]any{"public_key": "PLACEHOLDER"}, Version: 5}
|
||
|
|
var tasks sync.WaitGroup
|
||
|
|
for range 8 {
|
||
|
|
tasks.Go(func() { _ = c.Seed(ctx, "short/api") })
|
||
|
|
}
|
||
|
|
tasks.Wait()
|
||
|
|
if err := c.Seed(ctx, "short/api"); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if records["short/api"].Version != 6 || records["short/public"].Version != 6 {
|
||
|
|
t.Fatal("concurrent bootstrap rotated or republished the API identity")
|
||
|
|
}
|
||
|
|
}
|