fluid-telegram/internal/secrets/secrets_test.go
tegwick 5ddfee8250 Implement the MTProto client, session bootstrap and apply
Completes the provisioner's write path. internal/tg drives BotFather as a
conversation rather than pretending it is an endpoint, creates channels,
claims usernames with fallbacks, and grants post_messages. internal/apply
sequences it: bot before administrator, test channel before public, and
state saved after every step that changed the world -- a channel that
exists but is unrecorded is worse than one that does not exist, because
the next run creates a second.

The operator session lives in OpenBao, not on disk. gotd's FileStorage
would leave a full-account credential in the working directory, where it
outlives the run and can be committed by accident.

The bot token goes straight from BotFather's reply to OpenBao and is
cleared from memory; if that write fails the error says how to recover by
hand and warns against re-running, since a retry creates a second bot.

Closes T05: the redaction salt is create-if-absent with no overwrite path,
and the test asserts it, because rotating it invalidates every longitudinal
comparison with no visible failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172sgCZEEDJcnQmr4SGDvKa

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1361245@bnt-lap001
Assistant-Session: b3b428ef-f3e6-4688-b091-01f71461d66a
2026-09-04 21:39:30 +02:00

116 lines
3.3 KiB
Go

package secrets
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func testStore(t *testing.T, h http.Handler) *Store {
t.Helper()
srv := httptest.NewServer(h)
t.Cleanup(srv.Close)
t.Setenv("BAO_ADDR", srv.URL)
t.Setenv("BAO_TOKEN", "test-token")
s, err := NewFromEnv("hall-of-helix")
if err != nil {
t.Fatal(err)
}
return s
}
// The salt rule. Rotating it silently invalidates every longitudinal comparison
// the interface has made, with no visible failure -- so the tool must have no
// path that replaces an existing one.
func TestCreateIfAbsentNeverOverwrites(t *testing.T) {
var writes int
existing := map[string]string{"salt": "the-original"}
s := testStore(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
writes++
w.WriteHeader(http.StatusOK)
return
}
json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"data": existing}})
}))
created, err := s.CreateIfAbsent(context.Background(), KeyRedactionSalt,
map[string]string{"salt": "a-replacement"})
if err != nil {
t.Fatal(err)
}
if created {
t.Error("reported creating a salt that already existed")
}
if writes != 0 {
t.Errorf("wrote over an existing salt (%d writes)", writes)
}
}
func TestCreateIfAbsentWritesWhenMissing(t *testing.T) {
var got map[string]any
s := testStore(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
json.NewDecoder(r.Body).Decode(&got)
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusNotFound)
}))
created, err := s.CreateIfAbsent(context.Background(), KeyRedactionSalt,
map[string]string{"salt": "fresh"})
if err != nil {
t.Fatal(err)
}
if !created {
t.Fatal("did not create a salt when none existed")
}
if got["data"].(map[string]any)["salt"] != "fresh" {
t.Errorf("wrote %v", got)
}
}
// A missing path is an ordinary first-run state, not an error.
func TestGetMissingIsNotAnError(t *testing.T) {
s := testStore(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
_, found, err := s.Get(context.Background(), KeyBotToken)
if err != nil || found {
t.Fatalf("found=%v err=%v", found, err)
}
}
// Ref is printed in plans and errors, so it must name a location and never
// carry a value.
func TestRefIsSafeToPrint(t *testing.T) {
s := testStore(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{"data": map[string]string{"token": "123:SECRET"}}})
}))
ref := s.Ref(KeyBotToken)
if strings.Contains(ref, "SECRET") || strings.Contains(ref, "test-token") {
t.Fatalf("Ref leaked a secret: %q", ref)
}
if !strings.Contains(ref, "hall-of-helix") || !strings.Contains(ref, KeyBotToken) {
t.Errorf("Ref should locate the secret: %q", ref)
}
}
// Errors name the path that failed, never what was at it.
func TestErrorsDoNotCarryValues(t *testing.T) {
s := testStore(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
_, _, err := s.Get(context.Background(), KeyOperatorSession)
if err == nil {
t.Fatal("expected an error")
}
if strings.Contains(err.Error(), "test-token") {
t.Fatalf("error leaked the bao token: %v", err)
}
}