package catalogue import ( "reflect" "strings" "testing" ) // A machine's backup lines are placement (design 57 L3, issue 794): the holder of node-backup names them as // `${backup:lines}` and `${backup:data}`, which a handed holder has its client fill from the plan and an unhanded // one has the controller fill as it fills `${contribution:node-backup:backup}` and `${...:data}`: the same text. const ( oldBackupText = "# lines\n${contribution:node-backup:backup}" oldDataText = "# data\n${contribution:node-backup:data}" placedBackupTxt = "# lines\n${backup:lines}" placedDataTxt = "# data\n${backup:data}" backupHolder = `{"module":"restic","version":"1","moved":true,"claims":[{"name":"node-backup","scope":"node"}], "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, "resources":[{"id":"declared","type":"file","path":"/etc/backups.conf","template":"files/backups"}, {"id":"data-declared","type":"file","path":"/etc/data.conf","template":"files/data"}]}` ) func dataModule(name string) string { return `{"module":"` + name + `","version":"1","resources":[{"id":"d","type":"directory","mode":"0700"}], "data":{"own":[{"id":"d","path":"${dir:d}","class":"irreplaceable","why":"the ` + name + ` data"}]}}` } // backupsOn is a machine of a holder whose files say these texts and the modules that declare data. func backupsOn(t *testing.T, lines, data string, modules ...string) Resolution { t.Helper() all := []Manifest{held(t, backupHolder, builtWith("restic", map[string]string{"backups": lines, "data": data}))} names := []string{"restic"} for _, name := range modules { all = append(all, mustParse(t, dataModule(name))) names = append(names, name) } r, err := Resolve(shelf(all...), names, workstation(), World{}) if err != nil { t.Fatal(err) } r.Account = "op" return r } var backupModules = []string{"alpha", "beta", "gamma"} // gammaLeftOut is a machine whose gamma does not place: its lines are kept, best effort. func gammaLeftOut(with Rendering) Rendering { with.NotPlaced = map[string]string{"gamma": "its placement setting does not read"} return with } func TestTheBackupPlacementIsTheTextTheOldPlaceholdersRender(t *testing.T) { // The old form is an unmoved holder: its placeholders are the controller's alone. oldHolder := strings.Replace(backupHolder, `"moved":true,`, "", 1) all := []Manifest{held(t, oldHolder, builtWith("restic", map[string]string{"backups": oldBackupText, "data": oldDataText}))} for _, name := range backupModules { all = append(all, mustParse(t, dataModule(name))) } old, err := Resolve(shelf(all...), append([]string{"restic"}, backupModules...), workstation(), World{}) if err != nil { t.Fatal(err) } old.Account = "op" wantLines, wantData := fileOf(t, old, gammaLeftOut(Rendering{}), "restic.declared"), fileOf(t, old, gammaLeftOut(Rendering{}), "restic.data-declared") if !strings.Contains(wantLines, "# alpha\npath ") || !strings.Contains(wantLines, "# gamma\n") || !strings.Contains(wantData, "item d") { t.Fatalf("the old form renders\n%s\n%s", wantLines, wantData) } moved := backupsOn(t, placedBackupTxt, placedDataTxt, backupModules...) // Not handed: the controller fills the new placeholders with the old text. if got := fileOf(t, moved, gammaLeftOut(Rendering{}), "restic.declared"); got != wantLines { t.Errorf("not handed, the lines are\n%s\nwant\n%s", got, wantLines) } if got := fileOf(t, moved, gammaLeftOut(Rendering{}), "restic.data-declared"); got != wantData { t.Errorf("not handed, the data is\n%s\nwant\n%s", got, wantData) } // Handed: the plan carries the same text as placement, and the files are the client's. c, _ := composeWith(t, moved, gammaLeftOut(handingTo("store:5000"))) p := c.Plans["restic"].Placement if got := "# lines\n" + p["backup:lines"]; got != wantLines { t.Errorf("handed, the plan's lines are\n%s\nwant\n%s", got, wantLines) } if got := "# data\n" + p["backup:data"]; got != wantData { t.Errorf("handed, the plan's data is\n%s\nwant\n%s", got, wantData) } if len(p) != 2 { t.Errorf("the plan's placement is %v", p) } } func TestAModuleAddedOrTakenAwayChangesTheBackupPlan(t *testing.T) { plan := func(modules ...string) map[string]string { c, _ := composeWith(t, backupsOn(t, placedBackupTxt, placedDataTxt, modules...), handingTo("store:5000")) return c.Plans["restic"].Placement } two, three := plan("alpha", "beta"), plan("alpha", "beta", "gamma") if reflect.DeepEqual(two, three) || strings.Contains(two["backup:lines"], "gamma") || !strings.Contains(three["backup:lines"], "# gamma\n") { t.Errorf("two modules give %v, three give %v", two, three) } } func TestAnUnknownBackupKeyIsRefused(t *testing.T) { r := backupsOn(t, "${backup:rows}", placedDataTxt, backupModules...) if _, err := r.Compose(handingTo("store:5000")); err == nil || !strings.Contains(err.Error(), "backup:rows") { t.Errorf("handed, an unknown key is %v", err) } if _, err := r.Declaration(Rendering{}); err == nil || !strings.Contains(err.Error(), "backup:rows") { t.Errorf("not handed, an unknown key is %v", err) } } func fileOf(t *testing.T, r Resolution, with Rendering, id string) string { t.Helper() out, err := r.Declaration(with) if err != nil { t.Fatal(err) } for _, res := range out { if res["id"] == id { return res["content"].(string) } } t.Fatalf("%s was not composed", id) return "" }