primeiro commit
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
package syncer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
)
|
||||
|
||||
func TestSyncDownloadsObjects(t *testing.T) {
|
||||
modified := time.Unix(100, 0)
|
||||
client := newFakeS3(map[string]fakeObject{
|
||||
"config/app.yml": {body: "port: 8080\n", modified: modified},
|
||||
})
|
||||
dir := t.TempDir()
|
||||
|
||||
result, err := New(client, Config{Bucket: "bucket", Prefix: "config", Dir: dir}).Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Sync() error = %v", err)
|
||||
}
|
||||
if result != (Result{Downloaded: 1}) {
|
||||
t.Fatalf("result = %+v, want one download", result)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(dir, "app.yml"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading downloaded file: %v", err)
|
||||
}
|
||||
if string(content) != "port: 8080\n" {
|
||||
t.Fatalf("downloaded content = %q", content)
|
||||
}
|
||||
|
||||
info, err := os.Stat(filepath.Join(dir, "app.yml"))
|
||||
if err != nil {
|
||||
t.Fatalf("stat downloaded file: %v", err)
|
||||
}
|
||||
if info.ModTime().Unix() != modified.Unix() {
|
||||
t.Fatalf("mod time = %s, want %s", info.ModTime(), modified)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncSkipsUnchangedObjects(t *testing.T) {
|
||||
modified := time.Unix(200, 0)
|
||||
client := newFakeS3(map[string]fakeObject{
|
||||
"app.yml": {body: "same", modified: modified},
|
||||
})
|
||||
dir := t.TempDir()
|
||||
service := New(client, Config{Bucket: "bucket", Dir: dir})
|
||||
|
||||
if _, err := service.Sync(context.Background()); err != nil {
|
||||
t.Fatalf("first Sync() error = %v", err)
|
||||
}
|
||||
client.getCalls = 0
|
||||
client.putCalls = 0
|
||||
|
||||
result, err := service.Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("second Sync() error = %v", err)
|
||||
}
|
||||
if result != (Result{Unchanged: 1}) {
|
||||
t.Fatalf("result = %+v, want one unchanged", result)
|
||||
}
|
||||
if client.getCalls != 0 {
|
||||
t.Fatalf("GetObject calls = %d, want 0", client.getCalls)
|
||||
}
|
||||
if client.putCalls != 0 {
|
||||
t.Fatalf("PutObject calls = %d, want 0", client.putCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncDownloadsRemoteNewerObject(t *testing.T) {
|
||||
firstModified := time.Unix(300, 0)
|
||||
secondModified := time.Unix(301, 0)
|
||||
client := newFakeS3(map[string]fakeObject{
|
||||
"app.yml": {body: "old", modified: firstModified},
|
||||
})
|
||||
dir := t.TempDir()
|
||||
service := New(client, Config{Bucket: "bucket", Dir: dir})
|
||||
|
||||
if _, err := service.Sync(context.Background()); err != nil {
|
||||
t.Fatalf("first Sync() error = %v", err)
|
||||
}
|
||||
|
||||
client.objects["app.yml"] = fakeObject{body: "new", modified: secondModified}
|
||||
result, err := service.Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("second Sync() error = %v", err)
|
||||
}
|
||||
if result != (Result{Updated: 1}) {
|
||||
t.Fatalf("result = %+v, want one update", result)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(dir, "app.yml"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading updated file: %v", err)
|
||||
}
|
||||
if string(content) != "new" {
|
||||
t.Fatalf("updated content = %q, want new", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncUploadsLocalOnlyFile(t *testing.T) {
|
||||
client := newFakeS3(map[string]fakeObject{})
|
||||
dir := t.TempDir()
|
||||
localPath := filepath.Join(dir, "local.txt")
|
||||
writeFileAt(t, localPath, "local content", time.Unix(600, 0))
|
||||
|
||||
result, err := New(client, Config{Bucket: "bucket", Prefix: "config", Dir: dir}).Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Sync() error = %v", err)
|
||||
}
|
||||
if result != (Result{Uploaded: 1}) {
|
||||
t.Fatalf("result = %+v, want one upload", result)
|
||||
}
|
||||
if got := client.objects["config/local.txt"].body; got != "local content" {
|
||||
t.Fatalf("uploaded body = %q, want local content", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncUploadsLocalNewerObject(t *testing.T) {
|
||||
client := newFakeS3(map[string]fakeObject{
|
||||
"app.yml": {body: "remote", modified: time.Unix(700, 0)},
|
||||
})
|
||||
dir := t.TempDir()
|
||||
service := New(client, Config{Bucket: "bucket", Dir: dir})
|
||||
|
||||
if _, err := service.Sync(context.Background()); err != nil {
|
||||
t.Fatalf("first Sync() error = %v", err)
|
||||
}
|
||||
localPath := filepath.Join(dir, "app.yml")
|
||||
writeFileAt(t, localPath, "local", time.Unix(800, 0))
|
||||
|
||||
result, err := service.Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("second Sync() error = %v", err)
|
||||
}
|
||||
if result != (Result{Updated: 1}) {
|
||||
t.Fatalf("result = %+v, want one update", result)
|
||||
}
|
||||
if got := client.objects["app.yml"].body; got != "local" {
|
||||
t.Fatalf("uploaded body = %q, want local", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncPruneDeletesUnchangedLocalAfterRemoteDelete(t *testing.T) {
|
||||
client := newFakeS3(map[string]fakeObject{
|
||||
"stale.txt": {body: "remote", modified: time.Unix(400, 0)},
|
||||
})
|
||||
dir := t.TempDir()
|
||||
service := New(client, Config{Bucket: "bucket", Dir: dir, Prune: true})
|
||||
|
||||
if _, err := service.Sync(context.Background()); err != nil {
|
||||
t.Fatalf("first Sync() error = %v", err)
|
||||
}
|
||||
delete(client.objects, "stale.txt")
|
||||
|
||||
result, err := service.Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("second Sync() error = %v", err)
|
||||
}
|
||||
if result != (Result{Deleted: 1}) {
|
||||
t.Fatalf("result = %+v, want one delete", result)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "stale.txt")); !os.IsNotExist(err) {
|
||||
t.Fatalf("stale file still exists or unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncPruneDeletesUnchangedRemoteAfterLocalDelete(t *testing.T) {
|
||||
client := newFakeS3(map[string]fakeObject{
|
||||
"stale.txt": {body: "remote", modified: time.Unix(450, 0)},
|
||||
})
|
||||
dir := t.TempDir()
|
||||
service := New(client, Config{Bucket: "bucket", Dir: dir, Prune: true})
|
||||
|
||||
if _, err := service.Sync(context.Background()); err != nil {
|
||||
t.Fatalf("first Sync() error = %v", err)
|
||||
}
|
||||
if err := os.Remove(filepath.Join(dir, "stale.txt")); err != nil {
|
||||
t.Fatalf("removing local file: %v", err)
|
||||
}
|
||||
|
||||
result, err := service.Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("second Sync() error = %v", err)
|
||||
}
|
||||
if result != (Result{Deleted: 1}) {
|
||||
t.Fatalf("result = %+v, want one delete", result)
|
||||
}
|
||||
if _, ok := client.objects["stale.txt"]; ok {
|
||||
t.Fatal("remote object still exists")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncRejectsUnsafeKeys(t *testing.T) {
|
||||
client := newFakeS3(map[string]fakeObject{
|
||||
"../secret.txt": {body: "secret", modified: time.Unix(500, 0)},
|
||||
})
|
||||
dir := t.TempDir()
|
||||
|
||||
_, err := New(client, Config{Bucket: "bucket", Dir: dir}).Sync(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("Sync() error = nil, want unsafe key error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unsafe") {
|
||||
t.Fatalf("Sync() error = %v, want unsafe key error", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(filepath.Dir(dir), "secret.txt")); !os.IsNotExist(err) {
|
||||
t.Fatalf("unsafe file was written or unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeObject struct {
|
||||
body string
|
||||
modified time.Time
|
||||
}
|
||||
|
||||
type fakeS3 struct {
|
||||
objects map[string]fakeObject
|
||||
getCalls int
|
||||
putCalls int
|
||||
delCalls int
|
||||
now time.Time
|
||||
}
|
||||
|
||||
func newFakeS3(objects map[string]fakeObject) *fakeS3 {
|
||||
return &fakeS3{
|
||||
objects: objects,
|
||||
now: time.Unix(900, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeS3) ListObjectsV2(_ context.Context, input *s3.ListObjectsV2Input, _ ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) {
|
||||
var contents []types.Object
|
||||
prefix := aws.ToString(input.Prefix)
|
||||
for key, object := range f.objects {
|
||||
if !strings.HasPrefix(key, prefix) {
|
||||
continue
|
||||
}
|
||||
size := int64(len(object.body))
|
||||
modified := object.modified
|
||||
contents = append(contents, types.Object{
|
||||
Key: aws.String(key),
|
||||
LastModified: &modified,
|
||||
Size: &size,
|
||||
})
|
||||
}
|
||||
truncated := false
|
||||
return &s3.ListObjectsV2Output{
|
||||
Contents: contents,
|
||||
IsTruncated: &truncated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fakeS3) GetObject(_ context.Context, input *s3.GetObjectInput, _ ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
|
||||
f.getCalls++
|
||||
object := f.objects[aws.ToString(input.Key)]
|
||||
return &s3.GetObjectOutput{
|
||||
Body: io.NopCloser(strings.NewReader(object.body)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fakeS3) PutObject(_ context.Context, input *s3.PutObjectInput, _ ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
|
||||
f.putCalls++
|
||||
body, err := io.ReadAll(input.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.objects[aws.ToString(input.Key)] = fakeObject{
|
||||
body: string(body),
|
||||
modified: f.now,
|
||||
}
|
||||
return &s3.PutObjectOutput{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeS3) HeadObject(_ context.Context, input *s3.HeadObjectInput, _ ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
|
||||
object := f.objects[aws.ToString(input.Key)]
|
||||
size := int64(len(object.body))
|
||||
modified := object.modified
|
||||
return &s3.HeadObjectOutput{
|
||||
ContentLength: &size,
|
||||
LastModified: &modified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fakeS3) DeleteObject(_ context.Context, input *s3.DeleteObjectInput, _ ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
|
||||
f.delCalls++
|
||||
delete(f.objects, aws.ToString(input.Key))
|
||||
return &s3.DeleteObjectOutput{}, nil
|
||||
}
|
||||
|
||||
func writeFileAt(t *testing.T, path string, content string, modified time.Time) {
|
||||
t.Helper()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatalf("creating parent dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("writing file: %v", err)
|
||||
}
|
||||
if err := os.Chtimes(path, modified, modified); err != nil {
|
||||
t.Fatalf("setting mtime: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user