eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "io"
5 "net"
6 "net/http"
7 "os"
8 "path/filepath"
9 "testing"
10)
11
12// withTempOpt points optDir at a fresh temp dir for a test and restores it after,
13// so the version-store helpers never touch the real /opt/lchat.
14func withTempOpt(t *testing.T) string {
15 t.Helper()
16 dir := t.TempDir()
17 prev := optDir
18 optDir = dir
19 t.Cleanup(func() { optDir = prev })
20 return dir
21}
22
23// writeFakeBinary drops an executable file to stand in for a built binary.
24func writeFakeBinary(t *testing.T, path, content string) {
25 t.Helper()
26 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
27 t.Fatal(err)
28 }
29 if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
30 t.Fatal(err)
31 }
32}
33
34func TestValidVersion(t *testing.T) {
35 ok := []string{"abc123", "v1.2.3", "test-scroll", "dev", "a_b.c-d"}
36 bad := []string{"", ".", "..", "a/b", "with space", "semi;colon", "../etc"}
37 for _, v := range ok {
38 if !validVersion(v) {
39 t.Errorf("validVersion(%q) = false, want true", v)
40 }
41 }
42 for _, v := range bad {
43 if validVersion(v) {
44 t.Errorf("validVersion(%q) = true, want false", v)
45 }
46 }
47}
48
49func TestStageAndPointLive(t *testing.T) {
50 withTempOpt(t)
51 src := filepath.Join(t.TempDir(), "lchat-build")
52 writeFakeBinary(t, src, "#!/bin/sh\necho hi\n")
53
54 if err := stageVersion(src, "abc123"); err != nil {
55 t.Fatalf("stageVersion: %v", err)
56 }
57 // The staged binary exists and is executable.
58 info, err := os.Stat(versionBinPath("abc123"))
59 if err != nil {
60 t.Fatalf("staged binary missing: %v", err)
61 }
62 if info.Mode().Perm()&0o100 == 0 {
63 t.Errorf("staged binary not executable: %v", info.Mode())
64 }
65
66 // Pointing an instance's live link resolves to the staged binary and reports
67 // the right version.
68 if err := pointLive("test.example.com", "abc123"); err != nil {
69 t.Fatalf("pointLive: %v", err)
70 }
71 got, err := liveVersion("test.example.com")
72 if err != nil {
73 t.Fatalf("liveVersion: %v", err)
74 }
75 if got != "abc123" {
76 t.Errorf("liveVersion = %q, want abc123", got)
77 }
78 // The symlink actually points at the version binary.
79 resolved, err := filepath.EvalSymlinks(liveLink("test.example.com"))
80 if err != nil {
81 t.Fatalf("EvalSymlinks: %v", err)
82 }
83 if want, _ := filepath.EvalSymlinks(versionBinPath("abc123")); resolved != want {
84 t.Errorf("live link resolves to %q, want %q", resolved, want)
85 }
86}
87
88func TestPointLiveUnstagedFails(t *testing.T) {
89 withTempOpt(t)
90 if err := pointLive("test.example.com", "nope"); err == nil {
91 t.Error("pointLive to an unstaged version should fail, but succeeded")
92 }
93}
94
95func TestPointLiveAtomicSwap(t *testing.T) {
96 withTempOpt(t)
97 src := filepath.Join(t.TempDir(), "lchat-build")
98 writeFakeBinary(t, src, "bin")
99 for _, v := range []string{"v1", "v2"} {
100 if err := stageVersion(src, v); err != nil {
101 t.Fatalf("stageVersion %s: %v", v, err)
102 }
103 }
104 if err := pointLive("h", "v1"); err != nil {
105 t.Fatal(err)
106 }
107 // Repointing to a new version replaces the link in place (no leftover .tmp).
108 if err := pointLive("h", "v2"); err != nil {
109 t.Fatal(err)
110 }
111 if got, _ := liveVersion("h"); got != "v2" {
112 t.Errorf("after repoint liveVersion = %q, want v2", got)
113 }
114 if _, err := os.Lstat(liveLink("h") + ".tmp"); !os.IsNotExist(err) {
115 t.Errorf("leftover temp symlink after swap: %v", err)
116 }
117}
118
119// liveVersion on an instance that was never versioned returns ("", nil), not an
120// error — init-versions/deploy rely on that to detect an unmanaged instance.
121func TestLiveVersionMissing(t *testing.T) {
122 withTempOpt(t)
123 got, err := liveVersion("never.seen")
124 if err != nil {
125 t.Errorf("liveVersion(missing) err = %v, want nil", err)
126 }
127 if got != "" {
128 t.Errorf("liveVersion(missing) = %q, want empty", got)
129 }
130}
131
132// probeVersion reads the build a socket actually serves — the honest half of
133// `update status --probe` (docs/awake-instance-convergence.md). A tiny HTTP
134// server over a real unix socket stands in for a running instance.
135func TestProbeVersion(t *testing.T) {
136 dir := t.TempDir()
137 sock := filepath.Join(dir, "inst.sock")
138 ln, err := net.Listen("unix", sock)
139 if err != nil {
140 t.Fatal(err)
141 }
142 srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
143 if r.URL.Path == "/api/version" {
144 io.WriteString(w, `{"build":"abc1234","version":"deadbeef"}`)
145 return
146 }
147 http.NotFound(w, r)
148 })}
149 go srv.Serve(ln)
150 defer srv.Close()
151
152 build, ok := probeVersion(sock)
153 if !ok || build != "abc1234" {
154 t.Fatalf("probeVersion served socket: build=%q ok=%v, want abc1234/true", build, ok)
155 }
156
157 // A socket that doesn't answer → ok=false (asleep / no instance), never a panic.
158 if _, ok := probeVersion(filepath.Join(dir, "nope.sock")); ok {
159 t.Fatal("probeVersion of a dead socket returned ok=true")
160 }
161}
162