| 1 | package edge |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "net/http/httptest" |
| 6 | "sync" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/registry" |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | ) |
| 12 | |
| 13 | func TestUsageMeterRecordAndDrain(t *testing.T) { |
| 14 | m := NewUsageMeter() |
| 15 | rt := registry.Route{Host: "a.example.com", App: "hello", Account: "bab"} |
| 16 | m.Record(rt, 1, 100, false) |
| 17 | m.Record(rt, 1, 50, false) |
| 18 | m.Record(registry.Route{Host: "b.example.com", App: "site"}, 1, 10, false) // Account "" -> release.OpsAccount |
| 19 | |
| 20 | got := m.Drain() |
| 21 | a := got["a.example.com"] |
| 22 | if a.Requests != 2 || a.BytesOut != 150 || a.Account != "bab" || a.App != "hello" { |
| 23 | t.Fatalf("a row = %+v", a) |
| 24 | } |
| 25 | if got["b.example.com"].Account != release.OpsAccount { |
| 26 | t.Fatalf("b row = %+v", got["b.example.com"]) |
| 27 | } |
| 28 | if again := m.Drain(); len(again) != 0 { |
| 29 | t.Fatalf("drain did not reset: %+v", again) |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | // A hopped exchange is recorded ONCE — into the totals, as any exchange is — |
| 34 | // and additionally annotated into the hop counters, which are therefore a |
| 35 | // subset of the totals and never an addition to them. A consumer that sums |
| 36 | // BytesOut has already counted every backhauled byte; one that added |
| 37 | // HopBytesOut on top would bill the tenant twice for the same wire bytes. |
| 38 | func TestRecordHopAnnotatesSubset(t *testing.T) { |
| 39 | m := NewUsageMeter() |
| 40 | rt := registry.Route{Host: "a.example.com", App: "hello", Account: "bab"} |
| 41 | m.Record(rt, 1, 100, true) |
| 42 | m.Record(rt, 1, 50, false) |
| 43 | |
| 44 | row := m.Drain()["a.example.com"] |
| 45 | if row.Requests != 2 || row.BytesOut != 150 { |
| 46 | t.Fatalf("row = %+v, want both exchanges counted once in the totals", row) |
| 47 | } |
| 48 | if row.HopRequests != 1 || row.HopBytesOut != 100 { |
| 49 | t.Fatalf("row = %+v, want only the hopped exchange annotated", row) |
| 50 | } |
| 51 | if row.HopRequests > row.Requests || row.HopBytesOut > row.BytesOut { |
| 52 | t.Fatalf("row = %+v — the hop counters must stay inside the totals", row) |
| 53 | } |
| 54 | if row.Account != "bab" || row.App != "hello" { |
| 55 | t.Fatalf("row = %+v, want the hop attributed to the route's own account and app", row) |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | func TestUsageMeterConcurrent(t *testing.T) { |
| 60 | m := NewUsageMeter() |
| 61 | rt := registry.Route{Host: "a.example.com", App: "hello", Account: "bab"} |
| 62 | var wg sync.WaitGroup |
| 63 | for i := 0; i < 50; i++ { |
| 64 | wg.Add(1) |
| 65 | go func() { |
| 66 | defer wg.Done() |
| 67 | for j := 0; j < 100; j++ { |
| 68 | m.Record(rt, 1, 10, false) |
| 69 | } |
| 70 | }() |
| 71 | } |
| 72 | wg.Wait() |
| 73 | row := m.Drain()["a.example.com"] |
| 74 | if row.Requests != 5000 || row.BytesOut != 50000 { |
| 75 | t.Fatalf("row = %+v", row) |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | func TestCountingWriterCountsBytes(t *testing.T) { |
| 80 | var n int64 |
| 81 | rec := httptest.NewRecorder() |
| 82 | w := &countingWriter{ResponseWriter: rec, n: &n} |
| 83 | w.WriteHeader(http.StatusTeapot) |
| 84 | w.Write([]byte("hello ")) |
| 85 | w.Write([]byte("world")) |
| 86 | if n != 11 { |
| 87 | t.Fatalf("counted %d bytes", n) |
| 88 | } |
| 89 | if rec.Code != http.StatusTeapot || rec.Body.String() != "hello world" { |
| 90 | t.Fatalf("forwarding broken: %d %q", rec.Code, rec.Body.String()) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | func TestCountingWriterObservesStatusAndType(t *testing.T) { |
| 95 | var n int64 |
| 96 | rec := httptest.NewRecorder() |
| 97 | w := &countingWriter{ResponseWriter: rec, n: &n} |
| 98 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| 99 | w.WriteHeader(103) // informational: forwarded, not latched |
| 100 | w.WriteHeader(http.StatusBadGateway) |
| 101 | w.WriteHeader(http.StatusOK) // superfluous second final status: ignored |
| 102 | if w.status != http.StatusBadGateway || w.ctype != "text/html; charset=utf-8" { |
| 103 | t.Fatalf("latched %d %q", w.status, w.ctype) |
| 104 | } |
| 105 | // Implicit 200 at first Write. |
| 106 | rec2 := httptest.NewRecorder() |
| 107 | w2 := &countingWriter{ResponseWriter: rec2, n: &n} |
| 108 | w2.Write([]byte("x")) |
| 109 | if w2.status != http.StatusOK { |
| 110 | t.Fatalf("implicit status = %d", w2.status) |
| 111 | } |
| 112 | // Nothing written at all: status stays 0 (the caller treats it as a |
| 113 | // bodiless 200 that is never a pageview and never an error). |
| 114 | w3 := &countingWriter{ResponseWriter: httptest.NewRecorder(), n: &n} |
| 115 | if w3.status != 0 { |
| 116 | t.Fatal("untouched writer must report 0") |
| 117 | } |
| 118 | } |
| 119 | |