eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "encoding/json"
5 "net/http"
6 "testing"
7)
8
9func TestParseReplicationClasses(t *testing.T) {
10 got, err := parseReplicationClasses("free=daily,family=daily,community=batched,business=stream")
11 if err != nil {
12 t.Fatal(err)
13 }
14 want := map[string]string{"free": "daily", "family": "daily", "community": "batched", "business": "stream"}
15 if len(got) != len(want) {
16 t.Fatalf("parsed %v, want %v", got, want)
17 }
18 for k, v := range want {
19 if got[k] != v {
20 t.Errorf("classes[%q] = %q, want %q", k, got[k], v)
21 }
22 }
23
24 // Unset = feature off, not an error.
25 if m, err := parseReplicationClasses(""); err != nil || m != nil {
26 t.Errorf("empty env = (%v, %v), want (nil, nil)", m, err)
27 }
28 // A class the router doesn't know must fail at startup, not silently
29 // stream forever on the box.
30 if _, err := parseReplicationClasses("free=hourly"); err == nil {
31 t.Error("unknown class accepted")
32 }
33 if _, err := parseReplicationClasses("free"); err == nil {
34 t.Error("entry without = accepted")
35 }
36 // An EMPTY class value is rejected too — "" is valid to validReplication
37 // (it's the unset registry default) but meaningless as config.
38 if _, err := parseReplicationClasses("free="); err == nil {
39 t.Error("empty class value accepted")
40 }
41}
42
43// replicationFor is the policy: configured keys act, free without a "free"
44// entry pushes nothing, and an unknown PAID tier fails toward durability
45// (stream) — a config typo must never quietly put a paying space on the
46// nightly rung.
47func TestReplicationFor(t *testing.T) {
48 s := &cloudServer{replicationClasses: map[string]string{
49 "free": "daily", "family": "daily", "community": "batched", "business": "stream",
50 }}
51 for _, tc := range []struct {
52 ent, tier, want string
53 }{
54 {entFree, "", "daily"},
55 {entFree, "family", "daily"}, // stale tier on a lapsed row: entitlement wins
56 {entPaid, "family", "daily"},
57 {entPaid, "community", "batched"},
58 {entPaid, "business", "stream"},
59 {entPaid, "buisness", "stream"}, // typo'd/unknown paid tier → stream, never a downgrade
60 } {
61 if got := s.replicationFor(tc.ent, tc.tier); got != tc.want {
62 t.Errorf("replicationFor(%q, %q) = %q, want %q", tc.ent, tc.tier, got, tc.want)
63 }
64 }
65
66 // Unconfigured = push nothing anywhere.
67 off := &cloudServer{}
68 if got := off.replicationFor(entPaid, "business"); got != "" {
69 t.Errorf("unconfigured replicationFor = %q, want \"\"", got)
70 }
71 // Configured but no "free" key: free spaces keep whatever they have.
72 partial := &cloudServer{replicationClasses: map[string]string{"business": "stream"}}
73 if got := partial.replicationFor(entFree, ""); got != "" {
74 t.Errorf("free with no free entry = %q, want \"\"", got)
75 }
76}
77
78// replicationControl fakes controld's stats + replication endpoints, recording
79// class pushes (the stats half keeps meterOnce's sweep loop running).
80func replicationControl(t *testing.T, pushed map[string]string) string {
81 t.Helper()
82 mux := http.NewServeMux()
83 mux.HandleFunc("GET /instances/{host}/stats", func(w http.ResponseWriter, r *http.Request) {
84 writeJSON(w, http.StatusOK, map[string]any{"storage_bytes": int64(1)})
85 })
86 mux.HandleFunc("POST /instances/{host}/replication", func(w http.ResponseWriter, r *http.Request) {
87 var req struct {
88 Class string `json:"class"`
89 }
90 json.NewDecoder(r.Body).Decode(&req)
91 pushed[r.PathValue("host")] = req.Class
92 writeJSON(w, http.StatusOK, map[string]any{"ok": true, "changed": true})
93 })
94 return fakeControl(t, mux.ServeHTTP)
95}
96
97// The class enforcement loop, mirroring TestQuotaPushOnUpgradeAndSweep: a
98// checkout upgrade pushes the tier's class immediately, the meter sweep
99// reconciles everyone, and a lapse drops the space back to the free rung.
100func TestReplicationPushOnUpgradeAndSweep(t *testing.T) {
101 db := openTestCloudDB(t)
102 paidID := insertSpaceRow(t, db, "paid.11ch.at", 1000)
103 insertSpaceRow(t, db, "free.11ch.at", 1000)
104
105 pushed := map[string]string{}
106 sock := replicationControl(t, pushed)
107 s := &cloudServer{
108 db: db, control: controlClient(sock), controlToken: "t",
109 replicationClasses: map[string]string{"free": "daily", "business": "stream"},
110 }
111
112 // The upgrade path (what the checkout webhook calls).
113 if err := db.markSpacePaid(paidID, "cus", "sub", "business", ""); err != nil {
114 t.Fatal(err)
115 }
116 s.pushReplicationByID(paidID)
117 if pushed["paid.11ch.at"] != "stream" {
118 t.Fatalf("upgrade push = %q, want stream", pushed["paid.11ch.at"])
119 }
120
121 // The sweep reconciles both (controld's changed=false makes re-pushes
122 // harmless, so unlike quota there is no pushed-state comparison here).
123 s.meterOnce()
124 if pushed["free.11ch.at"] != "daily" {
125 t.Fatalf("sweep push = %q, want daily", pushed["free.11ch.at"])
126 }
127
128 // A lapse (subscription deleted → free) re-applies the free rung.
129 if err := db.setEntitlement(paidID, entFree, 0); err != nil {
130 t.Fatal(err)
131 }
132 s.pushReplicationByID(paidID)
133 if pushed["paid.11ch.at"] != "daily" {
134 t.Fatalf("lapse push = %q, want back to daily", pushed["paid.11ch.at"])
135 }
136
137 // No classes configured → nothing pushes anywhere.
138 pushed2 := map[string]string{}
139 sock2 := replicationControl(t, pushed2)
140 s2 := &cloudServer{db: db, control: controlClient(sock2), controlToken: "t"}
141 s2.meterOnce()
142 s2.pushReplicationByID(paidID)
143 if len(pushed2) != 0 {
144 t.Errorf("unconfigured classes pushed: %v", pushed2)
145 }
146}
147