// 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" "log/slog" ) func TestNewFileResource(t *testing.T) { f := NewFile() assert.NotEqual(t, nil, f) } func TestNewFileNormalized(t *testing.T) { file := fmt.Sprintf("%s/%s", TempDir, "bar/../fooread.txt") absFilePath,_ := filepath.Abs(file) f := NewNormalizedFile() assert.NotNil(t, f) assert.Nil(t, f.SetURI("file://" + file)) assert.NotEqual(t, file, f.Path) assert.Equal(t, absFilePath, f.Path) assert.NotEqual(t, "file://" + file, 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(filepath.Join(TempDir, "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) applyErr := testFile.Apply() assert.Nil(t, applyErr) f := NewFile() assert.NotEqual(t, nil, f) f.Path = file 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(filepath.Join(TempDir, "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 })) assert.Nil(t, f.SetURI(fmt.Sprintf("file://%s", file))) assert.Equal(t, filepath.Join("/tmp", file), f.FilePath()) } func TestReadFileError(t *testing.T) { ctx := context.Background() file, _ := filepath.Abs(filepath.Join(TempDir, "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(filepath.Join(TempDir, "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(filepath.Join(TempDir, "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(filepath.Join(TempDir, "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(filepath.Join(TempDir, "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(filepath.Join(TempDir, "testuri.txt")) f := NewFile() assert.NotNil(t, f) e := f.SetURI("file://" + file) assert.Nil(t, e) assert.Equal(t, "file", f.Type()) assert.Equal(t, file, f.Path) } func TestFileNormalizePath(t *testing.T) { absFile, absFilePathErr := filepath.Abs(filepath.Join(TempDir, "./testuri.txt")) assert.Nil(t, absFilePathErr) file := filepath.Join(TempDir, "./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(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 := filepath.Join(TempDir, "link.txt") linkTargetFile := filepath.Join(TempDir, "testuri.txt") f := NewFile() assert.NotNil(t, f) f.Path = linkTargetFile e := f.NormalizePath() assert.Nil(t, e) 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) assert.Nil(t, l.NormalizePath()) 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 _,testReadErr := testRead.Read(ctx) assert.Nil(t, testReadErr) assert.Equal(t, linkTargetFile, testRead.Target) } func TestFileResourceFileInfo(t *testing.T) { testFile := filepath.Join(TempDir, "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.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 := filepath.Join(TempDir, "testorig.txt") testCloneFile := filepath.Join(TempDir, "testclone.txt") f := NewFile() assert.NotNil(t, f) f.Path = testFile f.Mode = "0600" f.State = "present" assert.Nil(t, f.Apply()) _,readErr := f.Read(ctx) assert.Nil(t, readErr) time.Sleep(100 * time.Millisecond) clone := f.Clone().(*File) assert.Equal(t, f, clone) clone.Mtime = time.Time{} clone.Path = testCloneFile assert.Nil(t, clone.Apply()) _,updateReadErr := f.Read(ctx) assert.Nil(t, updateReadErr) _, cloneReadErr := clone.Read(ctx) assert.Nil(t, cloneReadErr) fmt.Printf("file %#v\nclone %#v\n", f, clone) assert.NotEqual(t, f.Mtime, clone.Mtime) } func TestFileErrors(t *testing.T) { //ctx := context.Background() testFile := filepath.Join(TempDir, "testerr.txt") f := NewFile() assert.NotNil(t, f) stater := f.StateMachine() f.Path = testFile f.Mode = "631" assert.Nil(t, stater.Trigger("create")) read := NewFile() readStater := read.StateMachine() read.Path = testFile 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(filepath.Join(TempDir, "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(filepath.Join(TempDir, "src.txt")) copyFile, _ := filepath.Abs(filepath.Join(TempDir, "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 `, file, ProcessTestUserName, ProcessTestGroupName, copyFile) 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") } func TestFileAbsent(t *testing.T) { file, _ := filepath.Abs(filepath.Join(TempDir, "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(filepath.Join(TempDir, "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 := fmt.Sprintf("%s/%s", TempDir, "fooread.txt") f := NewFile() assert.NotNil(t, f) e := f.SetURI("foo://" + file) assert.NotNil(t, e) assert.ErrorIs(t, e, ErrInvalidResourceURI) } func TestFileContentType(t *testing.T) { file := fmt.Sprintf("%s/%s", TempDir, "fooread.txt") f := NewFile() assert.NotNil(t, f) e := f.SetURI("file://" + file) assert.Nil(t, e) assert.Equal(t, "txt", f.ContentType()) }