carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// End-to-end acceptance for `carlos logs` (#131/#78): a REAL console.Server
2// over a DirStore holding shipped log chunks, a device-flow login, then the
3// real runLogs driven in-process — the member's whole path from "my app is
4// 503ing" to reading the wake failure, with no box access anywhere.
5// TestE2EInstancesThroughConsole's shape.
6package main
7
8import (
9 "context"
10 "net/http"
11 "net/http/httptest"
12 "path/filepath"
13 "strings"
14 "testing"
15 "time"
16
17 "github.com/carlosframework/platform/internal/applog"
18 "github.com/carlosframework/platform/internal/console"
19 "github.com/carlosframework/platform/internal/release"
20)
21
22func TestE2ELogsThroughConsole(t *testing.T) {
23 useTempCredentials(t)
24 clearDeploymentEnv(t)
25 checkNoRegistry := requireNoRegistryTouch(t)
26
27 tmp := t.TempDir()
28 relStore := release.NewDirStore(filepath.Join(tmp, "store"))
29 seed, pubB64, err := release.GenerateKey()
30 if err != nil {
31 t.Fatal(err)
32 }
33 priv, err := release.ParseSigningKey(seed, "e2e")
34 if err != nil {
35 t.Fatal(err)
36 }
37 pub, err := release.ParsePublicKey(pubB64, "e2e")
38 if err != nil {
39 t.Fatal(err)
40 }
41 cst, err := console.Open(filepath.Join(tmp, "console.db"))
42 if err != nil {
43 t.Fatal(err)
44 }
45 t.Cleanup(func() { cst.Close() })
46
47 var srv *console.Server
48 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49 srv.ServeHTTP(w, r)
50 }))
51 t.Cleanup(ts.Close)
52 srv, err = console.NewServer(console.Config{
53 Origin: ts.URL,
54 Keymail: "http://keymail.invalid", // never dialled — see e2eDeviceLogin
55 Registry: filepath.Join(tmp, "registry.db"),
56 Releases: relStore,
57 ReleasePub: pub,
58 Key: priv,
59 AppScheme: "http",
60 AppsDomain: "oncarlos.test",
61 }, cst)
62 if err != nil {
63 t.Fatal(err)
64 }
65
66 const addr = "paul@keymail.dev"
67 if err := cst.AddMember(console.OpsAccountID, addr, console.RoleOwner, time.Now()); err != nil {
68 t.Fatal(err)
69 }
70 if _, err := cst.CreateAppIn(console.OpsAccountID, "titogo", addr, time.Now()); err != nil {
71 t.Fatal(err)
72 }
73 token, expiresAt := e2eDeviceLogin(t, ts.URL, cst, addr, "e2e@laptop")
74 credFile, err := loadCredentials()
75 if err != nil {
76 t.Fatal(err)
77 }
78 credFile.put(credential{Console: ts.URL, Address: addr, Token: token, ExpiresAt: expiresAt})
79 if err := credFile.save(); err != nil {
80 t.Fatal(err)
81 }
82
83 // A box shipped two chunks: routine output, then a wake failure — the
84 // #131 case, as the shipping loop would have written it.
85 now := time.Now().UTC()
86 writeChunk := func(seq uint64, recs []applog.Record) {
87 t.Helper()
88 body, err := applog.EncodeChunk(recs)
89 if err != nil {
90 t.Fatal(err)
91 }
92 key, err := applog.ChunkKey(release.OpsAccount, "titogo", "box1", applog.HourOf(recs[0].TS), seq, len(body))
93 if err != nil {
94 t.Fatal(err)
95 }
96 if _, err := relStore.Put(context.Background(), key, body, ""); err != nil {
97 t.Fatal(err)
98 }
99 }
100 writeChunk(100, []applog.Record{
101 {TS: now.Add(-20 * time.Minute), Box: "box1", Host: "titogo.bab.oncarlos.test",
102 Stream: applog.StreamApp, Msg: "listening on socket", Seq: 100,
103 Fields: map[string]string{"request_id": "r1"}},
104 })
105 writeChunk(101, []applog.Record{
106 {TS: now.Add(-2 * time.Minute), Box: "box1", Host: "titogo.bab.oncarlos.test",
107 Stream: applog.StreamPlatform, Level: applog.LevelError, Seq: 101,
108 Msg: "wake failed: nothing is promoted for this instance's channel"},
109 })
110
111 // The plain read: both streams merged, oldest first, wake reason visible.
112 buf := captureStdout(t)
113 runLogs([]string{"-app", "titogo", "-account", "ops"})
114 out := buf.String()
115 if !strings.Contains(out, "listening on socket") || !strings.Contains(out, "wake failed") {
116 t.Fatalf("logs stdout missing the timeline:\n%s", out)
117 }
118 if strings.Index(out, "listening on socket") > strings.Index(out, "wake failed") {
119 t.Fatalf("timeline out of order:\n%s", out)
120 }
121 if !strings.Contains(out, "[platform]") || !strings.Contains(out, "[app]") {
122 t.Fatalf("stream tags missing:\n%s", out)
123 }
124 if !strings.Contains(out, "request_id=r1") {
125 t.Fatalf("structured fields missing:\n%s", out)
126 }
127
128 // Filters reach the wire: -stream platform drops the app line.
129 buf.Reset()
130 runLogs([]string{"-app", "titogo", "-account", "ops", "-stream", "platform"})
131 if out := buf.String(); strings.Contains(out, "listening on socket") || !strings.Contains(out, "wake failed") {
132 t.Fatalf("--stream platform: %q", out)
133 }
134
135 // -f follows: a line shipped between polls arrives; the poll cap is the
136 // test's exit.
137 logsMaxPolls = 2
138 t.Cleanup(func() { logsMaxPolls = 0 })
139 done := make(chan string, 1)
140 go func() {
141 fbuf := captureStdout(t)
142 runLogs([]string{"-app", "titogo", "-account", "ops", "-f"})
143 done <- fbuf.String()
144 }()
145 time.Sleep(500 * time.Millisecond) // after the initial read, before poll 1 lands
146 writeChunk(102, []applog.Record{
147 {TS: now.Add(-time.Second), Box: "box1", Host: "titogo.bab.oncarlos.test",
148 Stream: applog.StreamApp, Msg: "fresh follow line", Seq: 102},
149 })
150 select {
151 case out := <-done:
152 if !strings.Contains(out, "fresh follow line") {
153 t.Fatalf("follow missed the new line:\n%s", out)
154 }
155 case <-time.After(15 * time.Second):
156 t.Fatal("follow did not finish its bounded polls")
157 }
158
159 // The whole flow ran through the console: nothing here may have touched
160 // a local registry.
161 checkNoRegistry()
162}
163