package apply import ( "context" "errors" "os" "path/filepath" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/declaration" "github.com/novox/mesh-host/internal/store" ) func referenceTo(t *testing.T, path string, body []byte) *declaration.File { t.Helper() return &declaration.File{ID: "m.big", Type: declaration.TypeFile, Path: path, Digest: declaration.DigestOf(body), Size: int64(len(body))} } func objects(m map[string][]byte, asked *[]string) FetchObject { return func(_ context.Context, name string, limit int64) ([]byte, error) { *asked = append(*asked, name) b, ok := m[name] if !ok { return nil, errors.New("object not found") } return b, nil } } func objectOf(body []byte) string { name, _ := declaration.ObjectName(declaration.DigestOf(body)) return name } // 1695/AC1: both forms apply: a file by reference becomes the inline file, an inline one is left as it is. func TestAFileByReferenceBecomesTheInlineFile(t *testing.T) { body := []byte(strings.Repeat("a line\n", 20000)) inline := &declaration.File{ID: "m.small", Type: declaration.TypeFile, Path: "/etc/small", Content: "x"} d := &declaration.Declaration{Resources: []declaration.Resource{inline, referenceTo(t, "/nowhere/big", body)}} var asked []string if err := ResolveFileReferences(context.Background(), d, objects(map[string][]byte{objectOf(body): body}, &asked)); err != nil { t.Fatal(err) } got := d.Resources[1].(*declaration.File) if got.IsReference() || got.Content != string(body) || got.Path != "/nowhere/big" || d.Resources[0] != declaration.Resource(inline) { t.Errorf("1695/AC1: resolved to %+v", got) } if len(asked) != 1 || asked[0] != objectOf(body) { t.Errorf("1695/AC1: asked %v", asked) } } // 1695/AC2: an object that differs by one byte, or in size, fails the whole resolve and gives no content. func TestAnObjectThatDiffersFailsTheApply(t *testing.T) { body := []byte(strings.Repeat("x", 70000)) bad := append([]byte(nil), body...) bad[3] ^= 1 for name, served := range map[string][]byte{"one byte": bad, "shorter": body[1:]} { d := &declaration.Declaration{Resources: []declaration.Resource{referenceTo(t, "/nowhere/big", body)}} var asked []string err := ResolveFileReferences(context.Background(), d, objects(map[string][]byte{objectOf(body): served}, &asked)) if err == nil || !strings.Contains(err.Error(), "nothing was applied") { t.Errorf("1695/AC2: %s: %v", name, err) } if !d.Resources[0].(*declaration.File).IsReference() { t.Errorf("1695/AC2: %s: the reference was replaced by unverified content", name) } } } // 1695/AC3: a missing object, or a node with no bus, fails the apply naming the file. func TestAMissingObjectFailsTheApply(t *testing.T) { body := []byte(strings.Repeat("y", 70000)) d := &declaration.Declaration{Resources: []declaration.Resource{referenceTo(t, "/nowhere/big", body)}} var asked []string if err := ResolveFileReferences(context.Background(), d, objects(nil, &asked)); err == nil || !strings.Contains(err.Error(), "m.big") || !strings.Contains(err.Error(), "declaration-files") { t.Errorf("1695/AC3: missing object: %v", err) } if err := ResolveFileReferences(context.Background(), d, nil); err == nil || !strings.Contains(err.Error(), "no link to the bus") { t.Errorf("1695/AC3: no bus: %v", err) } } // 1695/AC4: a file already on the machine that verifies is used and nothing is fetched; one that does not is fetched. func TestAVerifiedFileOnTheMachineFetchesNothing(t *testing.T) { body := []byte(strings.Repeat("z", 70000)) path := filepath.Join(t.TempDir(), "big") if err := os.WriteFile(path, body, 0o644); err != nil { t.Fatal(err) } var asked []string d := &declaration.Declaration{Resources: []declaration.Resource{referenceTo(t, path, body)}} if err := ResolveFileReferences(context.Background(), d, objects(nil, &asked)); err != nil || len(asked) != 0 { t.Errorf("1695/AC4: err %v, asked %v", err, asked) } if err := os.WriteFile(path, body[:100], 0o644); err != nil { t.Fatal(err) } d = &declaration.Declaration{Resources: []declaration.Resource{referenceTo(t, path, body)}} if err := ResolveFileReferences(context.Background(), d, objects(map[string][]byte{objectOf(body): body}, &asked)); err != nil || len(asked) != 1 { t.Errorf("1695/AC4: a differing file: err %v, asked %v", err, asked) } } // 1695/AC5: binary content comes back as bytes, and two files naming one digest fetch it once. func TestBinaryContentAndOneFetchPerDigest(t *testing.T) { body := append([]byte{0xff, 0xfe, 0x00}, []byte(strings.Repeat("b", 70000))...) d := &declaration.Declaration{Resources: []declaration.Resource{referenceTo(t, "/n/a", body), referenceTo(t, "/n/b", body)}} var asked []string if err := ResolveFileReferences(context.Background(), d, objects(map[string][]byte{objectOf(body): body}, &asked)); err != nil { t.Fatal(err) } a, _ := d.Resources[0].(*declaration.File).Body() if d.Resources[0].(*declaration.File).Bytes == "" || string(a) != string(body) || len(asked) != 1 { t.Errorf("1695/AC5: bytes %q asked %v", d.Resources[0].(*declaration.File).Bytes[:4], asked) } } // 1695/AC9: an unresolved reference reaching the apply is refused, never written as an empty file. func TestAnUnresolvedReferenceIsNeverWrittenEmpty(t *testing.T) { path := filepath.Join(t.TempDir(), "big") f := referenceTo(t, path, []byte(strings.Repeat("q", 70000))) if _, err := applyFile(f, store.Applied{}, nil, nil); err == nil || !strings.Contains(err.Error(), "not written empty") { t.Errorf("1695/AC9: %v", err) } if _, err := os.Stat(path); err == nil { t.Error("1695/AC9: a file was written") } }