// Copyright 2024 Matthew Rich . All rights reserved. package resource import ( "context" _ "encoding/json" "fmt" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" "io" _ "log" _ "net/http" _ "net/http/httptest" _ "net/url" "os" "path/filepath" "strings" "syscall" "testing" "time" "os/user" "io/fs" "decl/internal/codec" "decl/internal/data" "decl/internal/folio" "log/slog" ) func TestNewFileResource(t *testing.T) { f := NewFile() assert.NotEqual(t, nil, f) } func TestNewFileNormalized(t *testing.T) { indirectFile := fmt.Sprintf("%s/%s", string(TempDir), "bar/../fooread.txt") absFilePath,_ := filepath.Abs(indirectFile) f := NewNormalizedFile() assert.NotNil(t, f) f.Path = indirectFile assert.Nil(t, f.Init(nil)) assert.NotEqual(t, indirectFile, f.Path) assert.Equal(t, absFilePath, f.Path) assert.NotEqual(t, "file://" + indirectFile, f.URI()) assert.Equal(t, "file://" + absFilePath, f.URI()) } func TestApplyResourceTransformation(t *testing.T) { f := NewFile() assert.NotEqual(t, nil, f) //e := f.Apply() //assert.Equal(t, nil, e) } func TestReadFile(t *testing.T) { ctx := context.Background() file, _ := filepath.Abs(TempDir.FilePath("fooread.txt")) expectedTime, timeErr := time.Parse(time.RFC3339Nano, "2001-12-15T01:01:01.000000001Z") assert.Nil(t, timeErr) expectedTimestamp := expectedTime.Local().Format(time.RFC3339Nano) declarationAttributes := ` path: "%s" owner: "%s" group: "%s" mode: "0600" atime: %s ctime: %s mtime: %s content: |- test line 1 test line 2 sha256: f2082f984f1bf1a7886e2af32ccc9ca474fbff3553d131204b070c438114dd51 size: 23 filetype: "regular" state: present ` decl := fmt.Sprintf(declarationAttributes, file, ProcessTestUserName, ProcessTestGroupName, expectedTimestamp, expectedTimestamp, expectedTimestamp) testFile := NewFile() e := testFile.LoadDecl(decl) assert.Nil(t, e) assert.Equal(t, "present", testFile.Common.State) assert.Equal(t, file, testFile.Common.Path) applyErr := testFile.Apply() assert.Nil(t, applyErr) assert.FileExists(t, file) f := NewFile() assert.NotNil(t, f) f.Path = file assert.Nil(t, f.Init(nil)) r, e := f.Read(ctx) assert.Nil(t, e) assert.Equal(t, ProcessTestUserName, f.Owner) info, statErr := os.Stat(file) assert.Nil(t, statErr) stat, ok := info.Sys().(*syscall.Stat_t) assert.True(t, ok) cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) expected := fmt.Sprintf(declarationAttributes, file, ProcessTestUserName, ProcessTestGroupName, expectedTimestamp, cTime.Local().Format(time.RFC3339Nano), expectedTimestamp) assert.YAMLEq(t, expected, string(r)) } func TestUseConfig(t *testing.T) { file, _ := filepath.Abs(TempDir.FilePath("missingfile.txt")) f := NewFile() assert.NotNil(t, f) f.UseConfig(MockConfig(func(key string) (any, error) { if key == "prefix" { return "/tmp", nil } return nil, data.ErrUnknownConfigurationKey })) uri := folio.URI(fmt.Sprintf("file://%s", file)) assert.Nil(t, f.Init(uri.Parse())) assert.Equal(t, filepath.Join("/tmp", file), f.FilePath()) } func TestReadFileError(t *testing.T) { ctx := context.Background() file, _ := filepath.Abs(TempDir.FilePath("missingfile.txt")) f := NewFile() assert.NotNil(t, f) f.Path = file _, e := f.Read(ctx) assert.ErrorIs(t, e, fs.ErrNotExist) assert.Equal(t, "absent", f.State) } func TestCreateFile(t *testing.T) { file, _ := filepath.Abs(TempDir.FilePath("foo.txt")) decl := fmt.Sprintf(` path: "%s" owner: "%s" group: "%s" mode: "0600" content: |- test line 1 test line 2 state: present `, file, ProcessTestUserName, ProcessTestGroupName) f := NewFile() e := f.LoadDecl(decl) assert.Nil(t, e) assert.Equal(t, ProcessTestUserName, f.Owner) applyErr := f.Apply() assert.Equal(t, nil, applyErr) assert.FileExists(t, file, nil) s, e := os.Stat(file) assert.Equal(t, nil, e) assert.Greater(t, s.Size(), int64(0)) f.State = "absent" assert.Nil(t, f.Apply()) assert.NoFileExists(t, file, nil) } func TestLoadFile(t *testing.T) { file, _ := filepath.Abs(TempDir.FilePath("foo.txt")) decl := fmt.Sprintf(` path: "%s" owner: "%s" group: "%s" mode: "0600" content: |- test line 1 test line 2 state: present `, file, ProcessTestUserName, ProcessTestGroupName) f := NewFile() assert.Nil(t, f.Load([]byte(decl), codec.FormatYaml)) assert.Equal(t, ProcessTestUserName, f.Owner) assert.Greater(t, f.Size, int64(0)) reader := io.NopCloser(strings.NewReader(decl)) fr := NewFile() assert.Nil(t, fr.LoadReader(reader, codec.FormatYaml)) assert.Equal(t, ProcessTestUserName, f.Owner) assert.Greater(t, f.Size, int64(0)) contentReaderTransport, trErr := fr.ContentReaderStream() assert.ErrorContains(t, trErr, "Cannot provide transport reader for string content") assert.Nil(t, contentReaderTransport) contentWriterTransport, trErr := fr.ContentWriterStream() assert.ErrorContains(t, trErr, "Cannot provide transport writer for string content") assert.Nil(t, contentWriterTransport) } func TestFileType(t *testing.T) { fileType := []byte(` filetype: "directory" `) var testFile File err := yaml.Unmarshal(fileType, &testFile) assert.Nil(t, err) } func TestFileDirectory(t *testing.T) { file, _ := filepath.Abs(TempDir.FilePath("testdir")) decl := fmt.Sprintf(` path: "%s" owner: "%s" group: "%s" mode: "0700" filetype: "directory" state: present `, file, ProcessTestUserName, ProcessTestGroupName) f := NewFile() e := f.LoadDecl(decl) assert.Equal(t, nil, e) assert.Equal(t, ProcessTestUserName, f.Owner) applyErr := f.Apply() assert.Equal(t, nil, applyErr) assert.DirExists(t, file) f.State = "absent" deleteErr := f.Apply() assert.Nil(t, deleteErr) assert.NoDirExists(t, file) } func TestFileTimes(t *testing.T) { file, _ := filepath.Abs(TempDir.FilePath("testtimes.txt")) decl := fmt.Sprintf(` path: "%s" owner: "%s" group: "%s" mtime: 2001-12-15T01:01:01.1Z mode: "0600" filtetype: "regular" state: "present" `, file, ProcessTestUserName, ProcessTestGroupName) expectedTime, timeErr := time.Parse(time.RFC3339, "2001-12-15T01:01:01.1Z") assert.Nil(t, timeErr) f := NewFile() e := f.LoadDecl(decl) assert.Nil(t, e) assert.Equal(t, ProcessTestUserName, f.Owner) assert.True(t, f.Mtime.Equal(expectedTime)) } func TestFileSetURI(t *testing.T) { file, _ := filepath.Abs(TempDir.FilePath("testuri.txt")) f := NewFile() assert.NotNil(t, f) uri := folio.URI("file://" + file).Parse() assert.Nil(t, f.Init(uri)) assert.Equal(t, "file", f.Type()) assert.Equal(t, file, f.Path) } func TestFileNormalizePath(t *testing.T) { absFile, absFilePathErr := filepath.Abs(TempDir.FilePath("./testuri.txt")) assert.Nil(t, absFilePathErr) file := TempDir.FilePath("./testuri.txt") f := NewFile() assert.NotNil(t, f) f.Path = file e := f.NormalizePath() assert.Nil(t, e) assert.Equal(t, absFile, f.Path) } func TestFileUpdateAttributesFromFileInfo(t *testing.T) { f := NewFile() assert.NotNil(t, f) info, e := os.Lstat(string(TempDir)) assert.Nil(t, e) updateAttributesErr := f.UpdateAttributesFromFileInfo(info) assert.Nil(t, updateAttributesErr) assert.Equal(t, DirectoryFile, f.FileType) assert.ErrorIs(t, f.SetFileInfo(nil), ErrInvalidFileInfo) } func TestFileReadStat(t *testing.T) { ctx := context.Background() link := TempDir.FilePath("link.txt") linkTargetFile := TempDir.FilePath("testuri.txt") f := NewFile() assert.NotNil(t, f) f.Path = linkTargetFile f.PathNormalization(true) assert.Nil(t, f.Init(nil)) statErr := f.ReadStat() assert.Error(t, statErr) f.Owner = ProcessTestUserName f.Group = ProcessTestGroupName f.State = "present" assert.Nil(t, f.Apply()) assert.Nil(t, f.ReadStat()) l := NewFile() assert.NotNil(t, l) l.PathNormalization(true) assert.Nil(t, l.Init(nil)) l.FileType = SymbolicLinkFile l.Path = link l.Target = linkTargetFile l.State = "present" slog.Info("TestFileReadStat()", "file", f, "link", l) applyErr := l.Apply() assert.Nil(t, applyErr) readStatErr := l.ReadStat() assert.Nil(t, readStatErr) testRead := NewFile() testRead.Path = link assert.Nil(t, testRead.Init(nil)) _,testReadErr := testRead.Read(ctx) assert.Nil(t, testReadErr) assert.Equal(t, linkTargetFile, testRead.Target) } func TestFileResourceFileInfo(t *testing.T) { testFile := TempDir.FilePath("testuri.txt") f := NewFile() assert.NotNil(t, f) f.Path = testFile f.Mode = "0600" f.Content = "some test data" f.State = "present" assert.Nil(t, f.Init(nil)) assert.Nil(t, f.Apply()) _, readErr := f.Read(context.Background()) assert.Nil(t, readErr) fi := f.FileInfo() assert.Equal(t, os.FileMode(0600), fi.Mode().Perm()) assert.Equal(t, testFile, fi.Name()) assert.False(t, fi.IsDir()) assert.Nil(t, fi.Sys()) assert.Greater(t, time.Now(), fi.ModTime()) assert.Greater(t, fi.Size(), int64(0)) } func TestFileClone(t *testing.T) { ctx := context.Background() testFile := TempDir.FilePath("testorig.txt") testCloneFile := TempDir.FilePath("testclone.txt") f := NewFile() assert.NotNil(t, f) f.Path = testFile assert.Nil(t, f.Init(nil)) f.Mode = "0600" f.State = "present" assert.Nil(t, f.Apply()) origin := time.Now() _,readErr := f.Read(ctx) assert.Nil(t, readErr) time.Sleep(100 * time.Millisecond) assert.Greater(t, origin, f.Mtime) clone := f.Clone().(*File) assert.Equal(t, f.Common.Path, clone.Common.Path) assert.Equal(t, f.Common.absPath, clone.Common.absPath) assert.Equal(t, f.Common.parsedURI, clone.Common.parsedURI) assert.Equal(t, f.Common.exttype, clone.Common.exttype) assert.Equal(t, f.Common.fileext, clone.Common.fileext) assert.Equal(t, f.Common.State, clone.Common.State) assert.Equal(t, f.Size, clone.Size) assert.Equal(t, f.Owner, clone.Owner) assert.Equal(t, f.Group, clone.Group) assert.Equal(t, f.Mode, clone.Mode) assert.Equal(t, f.Atime, clone.Atime) assert.Equal(t, f.Mtime, clone.Mtime) assert.Equal(t, f.Ctime, clone.Ctime) assert.Equal(t, f.Content, clone.Content) assert.Equal(t, f.Sha256, clone.Sha256) clone.Mtime = time.Now() clone.Path = testCloneFile assert.Nil(t, clone.Init(nil)) assert.NotEqual(t, f.absPath, clone.absPath) slog.Info("TestFileClone", "clone", clone) assert.Nil(t, clone.Apply()) slog.Info("TestFileClone - applied mtime change", "clone", clone) _,updateReadErr := f.Read(ctx) assert.Nil(t, updateReadErr) _, cloneReadErr := clone.Read(ctx) assert.Nil(t, cloneReadErr) slog.Info("TestFileClone - read mtime change", "orig", f.Mtime, "clone", clone.Mtime) fmt.Printf("file %#v\n %#v\nclone %#v\n %#v\n", f, f.Common, clone, clone.Common) assert.NotEqual(t, f.Mtime, clone.Mtime) } func TestFileErrors(t *testing.T) { //ctx := context.Background() testFile := TempDir.FilePath("testerr.txt") f := NewFile() assert.NotNil(t, f) stater := f.StateMachine() f.Path = testFile assert.Nil(t, f.Init(nil)) f.Mode = "631" assert.Nil(t, stater.Trigger("create")) assert.FileExists(t, f.Path) read := NewFile() readStater := read.StateMachine() read.Path = testFile assert.Nil(t, read.Init(nil)) assert.Nil(t, readStater.Trigger("read")) assert.Equal(t, FileMode("0631"), read.Mode) f.Mode = "900" assert.ErrorAs(t, stater.Trigger("create"), &ErrInvalidFileMode, "Apply should fail with NumError when converting invalid octal") assert.Nil(t, readStater.Trigger("read")) assert.Equal(t, FileMode("0631"), read.Mode) f.Mode = "0631" f.Owner = "bar" uidErr := f.Apply() var UnknownUser user.UnknownUserError assert.Error(t, uidErr, UnknownUser) } func TestFileDelete(t *testing.T) { file, _ := filepath.Abs(TempDir.FilePath("foo.txt")) decl := fmt.Sprintf(` path: "%s" owner: "%s" group: "%s" mode: "0600" content: |- test line 1 test line 2 state: present `, file, ProcessTestUserName, ProcessTestGroupName) f := NewFile() stater := f.StateMachine() e := f.LoadDecl(decl) assert.Nil(t, e) assert.Equal(t, ProcessTestUserName, f.Owner) assert.Nil(t, stater.Trigger("create")) assert.FileExists(t, file, nil) s, e := os.Stat(file) assert.Nil(t, e) assert.Greater(t, s.Size(), int64(0)) assert.Nil(t, stater.Trigger("delete")) assert.NoFileExists(t, file, nil) } func TestFileContentRef(t *testing.T) { file, _ := filepath.Abs(TempDir.FilePath("src.txt")) copyFile, _ := filepath.Abs(TempDir.FilePath("copy.txt")) decl := fmt.Sprintf(` path: "%s" owner: "%s" group: "%s" mode: "0600" content: |- test line 1 test line 2 state: present `, file, ProcessTestUserName, ProcessTestGroupName) contentRef := fmt.Sprintf(` path: "%s" owner: "%s" group: "%s" mode: "0600" sourceref: "file://%s" state: present `, copyFile, ProcessTestUserName, ProcessTestGroupName, file) f := NewFile() stater := f.StateMachine() e := f.LoadDecl(decl) assert.Nil(t, e) assert.Equal(t, ProcessTestUserName, f.Owner) assert.Nil(t, stater.Trigger("create")) assert.FileExists(t, file, nil) s, e := os.Stat(file) assert.Nil(t, e) assert.Greater(t, s.Size(), int64(0)) fr := NewFile() loadErr := fr.LoadDecl(contentRef) assert.Nil(t, loadErr) assert.Equal(t, ProcessTestUserName, fr.Owner) assert.Nil(t, fr.StateMachine().Trigger("create")) assert.FileExists(t, file, nil) _, statErr := os.Stat(file) assert.Nil(t, statErr) assert.Nil(t, stater.Trigger("delete")) assert.NoFileExists(t, file, nil) } func TestFilePathURI(t *testing.T) { // file, _ := filepath.Abs(filepath.Join(TempDir, "foo.txt")) decl := fmt.Sprintf(` path: "%s" owner: "%s" group: "%s" mode: "0600" content: |- test line 1 test line 2 `, "", ProcessTestUserName, ProcessTestGroupName) f := NewFile() e := f.LoadDecl(decl) assert.Nil(t, e) assert.Equal(t, "", f.FilePath()) // assert.ErrorContains(t, f.Validate(), "path: String length must be greater than or equal to 1") assert.ErrorContains(t, f.Validate(), "path is required") } func TestFileAbsent(t *testing.T) { file, _ := filepath.Abs(TempDir.FilePath("testabsentstate.txt")) decl := fmt.Sprintf(` path: "%s" owner: "%s" group: "%s" mode: "0600" filetype: "regular" `, file, ProcessTestUserName, ProcessTestGroupName) f := NewFile() stater := f.StateMachine() e := f.LoadDecl(decl) assert.Nil(t, e) assert.Equal(t, ProcessTestUserName, f.Owner) err := stater.Trigger("read") assert.Nil(t, err) assert.Equal(t, "absent", f.State) } func TestFileReader(t *testing.T) { expected := "datatoreadusinganio.readers" dataReader := strings.NewReader(expected) file, _ := filepath.Abs(TempDir.FilePath("testabsentstate.txt")) decl := fmt.Sprintf(` path: "%s" owner: "%s" group: "%s" mode: "0600" content: "%s" filetype: "regular" `, file, ProcessTestUserName, ProcessTestGroupName, expected) f := NewFile() assert.Nil(t, f.LoadString(decl, codec.FormatYaml)) assert.Nil(t, f.SetContent(dataReader)) reader, err := f.GetContent(nil) assert.Nil(t, err) value, ioErr := io.ReadAll(reader) assert.Nil(t, ioErr) assert.Equal(t, expected, string(value)) var writer strings.Builder _, writerErr := f.GetContent(&writer) assert.Nil(t, writerErr) assert.Equal(t, expected, writer.String()) } func TestFileSetURIError(t *testing.T) { file := TempDir.FilePath("fooread.txt") f := NewFile() assert.NotNil(t, f) uri := folio.URI("foo://" + file).Parse() e := f.Init(uri) assert.ErrorIs(t, e, ErrInvalidResourceURI) } func TestFileContentType(t *testing.T) { file := TempDir.FilePath("fooread.txt") f := NewFile() assert.NotNil(t, f) uri := folio.URI("file://" + file).Parse() assert.Nil(t, f.Init(uri)) assert.Equal(t, "txt", f.ContentType()) }