94f44931dd
gates / gates (push) Successful in 45s
R-1111 reserveAttempt is the one limiter body (share password, escrow re-auth, dashboard sign-in). R-1112 family.json holds fingerprints; old files are converted at load. R-1113 the household's pass reuses its family session. R-1109 the samba compose carries the smb.conf hash. R-1110 TestNoBareDrivePathToPathHelpers. R-1114 shared_scripts.py resolves the felhom.eu sibling from a worktree. Each red-proved. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
139 lines
5.2 KiB
Go
139 lines
5.2 KiB
Go
package infra
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func sampleSambaData() SambaData {
|
|
return SambaData{
|
|
ServerName: "FELHOM",
|
|
UID: 1000,
|
|
Shares: []SambaShareRender{
|
|
{Name: "dokumentumok", Path: "/mnt/felhom-drives/scratch1/shares/dokumentumok", ReadOnly: false},
|
|
{Name: "filmek", Path: "/mnt/felhom-drives/media/filmek", ReadOnly: true},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Exact golden for smb.conf (§10): the hardened global block + one section per share, in registry
|
|
// order, with the force-user block. A drift in any managed directive fails here.
|
|
func TestRenderSambaConfig_Golden(t *testing.T) {
|
|
const want = `# Samba (LAN network-sharing) — managed by felhom-controller (R-7).
|
|
# WARNING: auto-generated. Manual edits are overwritten on the next share change.
|
|
[global]
|
|
workgroup = WORKGROUP
|
|
server string = Felhom hálózati megosztás
|
|
netbios name = FELHOM
|
|
security = user
|
|
map to guest = never
|
|
server min protocol = SMB2
|
|
disable netbios = no
|
|
bind interfaces only = yes
|
|
interfaces = lo eth0
|
|
smb ports = 445
|
|
load printers = no
|
|
printing = bsd
|
|
printcap name = /dev/null
|
|
disable spoolss = yes
|
|
|
|
[dokumentumok]
|
|
path = /mnt/felhom-drives/scratch1/shares/dokumentumok
|
|
read only = no
|
|
valid users = felhom
|
|
force user = felhom
|
|
force group = felhom
|
|
create mask = 0644
|
|
directory mask = 0755
|
|
|
|
[filmek]
|
|
path = /mnt/felhom-drives/media/filmek
|
|
read only = yes
|
|
valid users = felhom
|
|
force user = felhom
|
|
force group = felhom
|
|
create mask = 0644
|
|
directory mask = 0755
|
|
`
|
|
got := RenderSambaConfig(sampleSambaData())
|
|
if got != want {
|
|
t.Errorf("smb.conf golden mismatch.\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRenderSambaConfig_NoShares(t *testing.T) {
|
|
got := RenderSambaConfig(SambaData{ServerName: "OTTHON", UID: 1000})
|
|
if !strings.Contains(got, "netbios name = OTTHON") {
|
|
t.Error("server name not rendered")
|
|
}
|
|
if strings.Count(got, "[") != 1 { // only [global]
|
|
t.Errorf("no shares should mean only the [global] section:\n%s", got)
|
|
}
|
|
}
|
|
|
|
// Compose: host network + pinned image + config :ro + passdb volume, and — the Scenario B core — a
|
|
// read-only share gets a :ro bind while a writable one does not. Share binds are sorted (filmek<scratch1).
|
|
func TestRenderSambaCompose(t *testing.T) {
|
|
got := RenderSambaCompose(sampleSambaData())
|
|
for _, want := range []string{
|
|
"network_mode: host",
|
|
"image: " + SambaImage,
|
|
"- ./smb.conf:/etc/samba/smb.conf:ro",
|
|
"- samba-passdb:/var/lib/samba",
|
|
"FELHOM_SERVER_NAME=FELHOM",
|
|
"FELHOM_UID=1000",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("compose missing %q\n%s", want, got)
|
|
}
|
|
}
|
|
// The tag must be EXPLICIT — a floating :latest breaks reproducible pins and is the thing the
|
|
// constant exists to prevent. Asserted on the constant's shape, so an image bump is a one-line
|
|
// change in infra.go and never a test edit.
|
|
if strings.HasSuffix(SambaImage, ":latest") || !strings.Contains(SambaImage, ":") {
|
|
t.Errorf("SambaImage %q must carry an explicit non-latest tag", SambaImage)
|
|
}
|
|
|
|
// Read-only share → :ro bind (red-proof target: drop the :ro in the renderer and this fails).
|
|
roBind := "- /mnt/felhom-drives/media/filmek:/mnt/felhom-drives/media/filmek:ro"
|
|
if !strings.Contains(got, roBind) {
|
|
t.Errorf("read-only share must get a :ro bind, missing %q\n%s", roBind, got)
|
|
}
|
|
// Writable share → plain bind, NOT :ro.
|
|
rwBind := "- /mnt/felhom-drives/scratch1/shares/dokumentumok:/mnt/felhom-drives/scratch1/shares/dokumentumok\n"
|
|
if !strings.Contains(got, rwBind) {
|
|
t.Errorf("writable share must get a plain bind, missing %q\n%s", rwBind, got)
|
|
}
|
|
if strings.Contains(got, "/mnt/felhom-drives/scratch1/shares/dokumentumok:/mnt/felhom-drives/scratch1/shares/dokumentumok:ro") {
|
|
t.Error("writable share must NOT be :ro")
|
|
}
|
|
}
|
|
|
|
// R-1109: a share renamed at the SAME path changes smb.conf only. The compose must change with it (the config-hash
|
|
// label), or `compose up -d` keeps the running container on the old bind-mounted inode and the new name is never
|
|
// served. And an unchanged config must render a byte-identical compose (no recreate on every pass).
|
|
func TestRenderSambaCompose_ConfigHashLabel(t *testing.T) {
|
|
path := "/mnt/felhom-drives/hdd_1/userdata/shares/iratok"
|
|
before := SambaData{ServerName: "FELHOM", UID: 1000, Shares: []SambaShareRender{{Name: "dokumentumok", Path: path}}}
|
|
after := SambaData{ServerName: "FELHOM", UID: 1000, Shares: []SambaShareRender{{Name: "iratok", Path: path}}}
|
|
|
|
if RenderSambaConfig(before) == RenderSambaConfig(after) {
|
|
t.Fatal("setup: the rename must change smb.conf")
|
|
}
|
|
cb, ca := RenderSambaCompose(before), RenderSambaCompose(after)
|
|
if cb == ca {
|
|
t.Fatalf("a same-path rename left the compose byte-identical — compose up -d will not recreate (R-1109):\n%s", ca)
|
|
}
|
|
if want := "- felhom.smb-conf-sha256=" + SambaConfigHash(after); !strings.Contains(ca, want) {
|
|
t.Errorf("compose missing the label %q\n%s", want, ca)
|
|
}
|
|
if RenderSambaCompose(after) != ca {
|
|
t.Error("the same data must render the same compose (a recreate on every pass otherwise)")
|
|
}
|
|
// The premise of the bug, pinned: the binds are the same on both sides, so before the label nothing else differed.
|
|
bind := " - " + path + ":" + path + "\n"
|
|
if !strings.Contains(cb, bind) || !strings.Contains(ca, bind) {
|
|
t.Errorf("both composes must bind %q", bind)
|
|
}
|
|
}
|