Files
miti99bot/internal/migration/cloudflare_kv_client_test.go
T
tiennm99 5117222e5e feat(migration): cf→aws migration toolchain + DynamoDB value as String
Operator-run tooling that moves durable Cloudflare KV data into the live
AWS DynamoDB table, plus a runtime swap of the `value` attribute from
Binary to String so payloads are human-readable in the AWS console.

- cmd/migrate_cf_data: subcommands inventory, kv-import (idempotent via
  attribute_not_exists), trading-audit-dump, convert-value-to-string
- internal/migration: policy allowlist, CF KV+D1 REST clients, DynamoDB
  writer, report formatter with per-prefix counts + tests
- internal/storage/dynamodb_kv.go: Put writes MemberS, Get reads MemberS;
  dropped empty-bytes sentinel (DynamoDB allows empty strings)
2026-05-16 10:33:35 +07:00

108 lines
2.8 KiB
Go

package migration
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestListKeysSinglePage(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("Authorization"); got != "Bearer test-token" {
t.Errorf("auth header = %q", got)
}
if !strings.HasSuffix(r.URL.Path, "/storage/kv/namespaces/ns123/keys") {
t.Errorf("unexpected path %q", r.URL.Path)
}
_, _ = w.Write([]byte(`{
"result": [{"name":"wordle:stats:1"},{"name":"trading:sym:FPT"}],
"success": true,
"result_info": {"count": 2, "cursor": ""}
}`))
}))
defer srv.Close()
c := NewCloudflareKVClient("acct", "ns123", "test-token")
c.SetBaseURL(srv.URL)
keys, err := c.ListKeys(context.Background())
if err != nil {
t.Fatalf("list: %v", err)
}
if len(keys) != 2 || keys[0] != "wordle:stats:1" || keys[1] != "trading:sym:FPT" {
t.Fatalf("got %v", keys)
}
}
func TestListKeysPaginates(t *testing.T) {
page := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
page++
switch page {
case 1:
_, _ = w.Write([]byte(`{
"result": [{"name":"a"}],
"success": true,
"result_info": {"cursor": "p2"}
}`))
case 2:
if got := r.URL.Query().Get("cursor"); got != "p2" {
t.Errorf("cursor=%q want p2", got)
}
_, _ = w.Write([]byte(`{
"result": [{"name":"b"}],
"success": true,
"result_info": {"cursor": ""}
}`))
default:
t.Fatalf("too many pages")
}
}))
defer srv.Close()
c := NewCloudflareKVClient("acct", "ns", "tok")
c.SetBaseURL(srv.URL)
keys, err := c.ListKeys(context.Background())
if err != nil {
t.Fatalf("list: %v", err)
}
if len(keys) != 2 || keys[0] != "a" || keys[1] != "b" {
t.Fatalf("got %v", keys)
}
}
func TestGetValueReturns404AsErrKeyNotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
c := NewCloudflareKVClient("acct", "ns", "tok")
c.SetBaseURL(srv.URL)
_, err := c.GetValue(context.Background(), "misc:last_ping")
if !errors.Is(err, ErrKeyNotFound) {
t.Fatalf("got %v, want ErrKeyNotFound", err)
}
}
func TestGetValueRawBytes(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"currency":{"VND":100},"meta":{"invested":0}}`))
}))
defer srv.Close()
c := NewCloudflareKVClient("acct", "ns", "tok")
c.SetBaseURL(srv.URL)
val, err := c.GetValue(context.Background(), "trading:user:42")
if err != nil {
t.Fatalf("get: %v", err)
}
want := `{"currency":{"VND":100},"meta":{"invested":0}}`
if string(val) != want {
t.Errorf("got %q want %q", string(val), want)
}
}