shithub: hugo

ref: 2838d58b1daa0f6a337125c5a64d06215901c5d6
dir: /deploy/deploy_test.go/

View raw version
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package deploy

import (
	"bytes"
	"compress/gzip"
	"crypto/md5"
	"io/ioutil"
	"os"
	"sort"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/spf13/afero"
	"gocloud.dev/blob"
)

func TestDeploy_FindDiffs(t *testing.T) {
	hash1 := []byte("hash 1")
	hash2 := []byte("hash 2")
	makeLocal := func(path string, size int64, hash []byte) *localFile {
		return &localFile{Path: path, UploadSize: size, md5: hash}
	}
	makeRemote := func(path string, size int64, hash []byte) *blob.ListObject {
		return &blob.ListObject{Key: path, Size: size, MD5: hash}
	}

	tests := []struct {
		Description string
		Local       []*localFile
		Remote      []*blob.ListObject
		Force       bool
		WantUpdates []*fileToUpload
		WantDeletes []string
	}{
		{
			Description: "empty -> no diffs",
		},
		{
			Description: "local == remote -> no diffs",
			Local: []*localFile{
				makeLocal("aaa", 1, hash1),
				makeLocal("bbb", 2, hash1),
				makeLocal("ccc", 3, hash2),
			},
			Remote: []*blob.ListObject{
				makeRemote("aaa", 1, hash1),
				makeRemote("bbb", 2, hash1),
				makeRemote("ccc", 3, hash2),
			},
		},
		{
			Description: "local == remote with force flag true -> diffs",
			Local: []*localFile{
				makeLocal("aaa", 1, hash1),
				makeLocal("bbb", 2, hash1),
				makeLocal("ccc", 3, hash2),
			},
			Remote: []*blob.ListObject{
				makeRemote("aaa", 1, hash1),
				makeRemote("bbb", 2, hash1),
				makeRemote("ccc", 3, hash2),
			},
			Force: true,
			WantUpdates: []*fileToUpload{
				{makeLocal("aaa", 1, nil), reasonForce},
				{makeLocal("bbb", 2, nil), reasonForce},
				{makeLocal("ccc", 3, nil), reasonForce},
			},
		},
		{
			Description: "local == remote with route.Force true -> diffs",
			Local: []*localFile{
				{Path: "aaa", UploadSize: 1, matcher: &matcher{Force: true}, md5: hash1},
				makeLocal("bbb", 2, hash1),
			},
			Remote: []*blob.ListObject{
				makeRemote("aaa", 1, hash1),
				makeRemote("bbb", 2, hash1),
			},
			WantUpdates: []*fileToUpload{
				{makeLocal("aaa", 1, nil), reasonForce},
			},
		},
		{
			Description: "extra local file -> upload",
			Local: []*localFile{
				makeLocal("aaa", 1, hash1),
				makeLocal("bbb", 2, hash2),
			},
			Remote: []*blob.ListObject{
				makeRemote("aaa", 1, hash1),
			},
			WantUpdates: []*fileToUpload{
				{makeLocal("bbb", 2, nil), reasonNotFound},
			},
		},
		{
			Description: "extra remote file -> delete",
			Local: []*localFile{
				makeLocal("aaa", 1, hash1),
			},
			Remote: []*blob.ListObject{
				makeRemote("aaa", 1, hash1),
				makeRemote("bbb", 2, hash2),
			},
			WantDeletes: []string{"bbb"},
		},
		{
			Description: "diffs in size or md5 -> upload",
			Local: []*localFile{
				makeLocal("aaa", 1, hash1),
				makeLocal("bbb", 2, hash1),
				makeLocal("ccc", 1, hash2),
			},
			Remote: []*blob.ListObject{
				makeRemote("aaa", 1, nil),
				makeRemote("bbb", 1, hash1),
				makeRemote("ccc", 1, hash1),
			},
			WantUpdates: []*fileToUpload{
				{makeLocal("aaa", 1, nil), reasonMD5Missing},
				{makeLocal("bbb", 2, nil), reasonSize},
				{makeLocal("ccc", 1, nil), reasonMD5Differs},
			},
		},
		{
			Description: "mix of updates and deletes",
			Local: []*localFile{
				makeLocal("same", 1, hash1),
				makeLocal("updated", 2, hash1),
				makeLocal("updated2", 1, hash2),
				makeLocal("new", 1, hash1),
				makeLocal("new2", 2, hash2),
			},
			Remote: []*blob.ListObject{
				makeRemote("same", 1, hash1),
				makeRemote("updated", 1, hash1),
				makeRemote("updated2", 1, hash1),
				makeRemote("stale", 1, hash1),
				makeRemote("stale2", 1, hash1),
			},
			WantUpdates: []*fileToUpload{
				{makeLocal("new", 1, nil), reasonNotFound},
				{makeLocal("new2", 2, nil), reasonNotFound},
				{makeLocal("updated", 2, nil), reasonSize},
				{makeLocal("updated2", 1, nil), reasonMD5Differs},
			},
			WantDeletes: []string{"stale", "stale2"},
		},
	}

	for _, tc := range tests {
		t.Run(tc.Description, func(t *testing.T) {
			local := map[string]*localFile{}
			for _, l := range tc.Local {
				local[l.Path] = l
			}
			remote := map[string]*blob.ListObject{}
			for _, r := range tc.Remote {
				remote[r.Key] = r
			}
			gotUpdates, gotDeletes := findDiffs(local, remote, tc.Force)
			sort.Slice(gotUpdates, func(i, j int) bool { return gotUpdates[i].Local.Path < gotUpdates[j].Local.Path })
			sort.Slice(gotDeletes, func(i, j int) bool { return gotDeletes[i] < gotDeletes[j] })
			if diff := cmp.Diff(gotUpdates, tc.WantUpdates, cmpopts.IgnoreUnexported(localFile{})); diff != "" {
				t.Errorf("updates differ:\n%s", diff)

			}
			if diff := cmp.Diff(gotDeletes, tc.WantDeletes); diff != "" {
				t.Errorf("deletes differ:\n%s", diff)
			}
		})
	}
}

func TestDeploy_LocalFile(t *testing.T) {
	const (
		content = "hello world!"
	)
	contentBytes := []byte(content)
	contentLen := int64(len(contentBytes))
	contentMD5 := md5.Sum(contentBytes)
	var buf bytes.Buffer
	gz := gzip.NewWriter(&buf)
	if _, err := gz.Write(contentBytes); err != nil {
		t.Fatal(err)
	}
	gz.Close()
	gzBytes := buf.Bytes()
	gzLen := int64(len(gzBytes))
	gzMD5 := md5.Sum(gzBytes)

	tests := []struct {
		Description         string
		Path                string
		Matcher             *matcher
		WantContent         []byte
		WantSize            int64
		WantMD5             []byte
		WantContentType     string // empty string is always OK, since content type detection is OS-specific
		WantCacheControl    string
		WantContentEncoding string
	}{
		{
			Description: "file with no suffix",
			Path:        "foo",
			WantContent: contentBytes,
			WantSize:    contentLen,
			WantMD5:     contentMD5[:],
		},
		{
			Description: "file with .txt suffix",
			Path:        "foo.txt",
			WantContent: contentBytes,
			WantSize:    contentLen,
			WantMD5:     contentMD5[:],
		},
		{
			Description:      "CacheControl from matcher",
			Path:             "foo.txt",
			Matcher:          &matcher{CacheControl: "max-age=630720000"},
			WantContent:      contentBytes,
			WantSize:         contentLen,
			WantMD5:          contentMD5[:],
			WantCacheControl: "max-age=630720000",
		},
		{
			Description:         "ContentEncoding from matcher",
			Path:                "foo.txt",
			Matcher:             &matcher{ContentEncoding: "foobar"},
			WantContent:         contentBytes,
			WantSize:            contentLen,
			WantMD5:             contentMD5[:],
			WantContentEncoding: "foobar",
		},
		{
			Description:     "ContentType from matcher",
			Path:            "foo.txt",
			Matcher:         &matcher{ContentType: "foo/bar"},
			WantContent:     contentBytes,
			WantSize:        contentLen,
			WantMD5:         contentMD5[:],
			WantContentType: "foo/bar",
		},
		{
			Description:         "gzipped content",
			Path:                "foo.txt",
			Matcher:             &matcher{Gzip: true},
			WantContent:         gzBytes,
			WantSize:            gzLen,
			WantMD5:             gzMD5[:],
			WantContentEncoding: "gzip",
		},
	}

	for _, tc := range tests {
		t.Run(tc.Description, func(t *testing.T) {
			fs := new(afero.MemMapFs)
			if err := afero.WriteFile(fs, tc.Path, []byte(content), os.ModePerm); err != nil {
				t.Fatal(err)
			}
			lf, err := newLocalFile(fs, tc.Path, tc.Matcher)
			if err != nil {
				t.Fatal(err)
			}
			if got := lf.UploadSize; got != tc.WantSize {
				t.Errorf("got size %d want %d", got, tc.WantSize)
			}
			if got := lf.MD5(); !bytes.Equal(got, tc.WantMD5) {
				t.Errorf("got MD5 %x want %x", got, tc.WantMD5)
			}
			if got := lf.CacheControl(); got != tc.WantCacheControl {
				t.Errorf("got CacheControl %q want %q", got, tc.WantCacheControl)
			}
			if got := lf.ContentEncoding(); got != tc.WantContentEncoding {
				t.Errorf("got ContentEncoding %q want %q", got, tc.WantContentEncoding)
			}
			if tc.WantContentType != "" {
				if got := lf.ContentType(); got != tc.WantContentType {
					t.Errorf("got ContentType %q want %q", got, tc.WantContentType)
				}
			}
			// Verify the content reader last to ensure the
			// previous operations don't interfere with it.
			gotContent, err := ioutil.ReadAll(lf.UploadContentReader)
			if err != nil {
				t.Fatal(err)
			}
			if !bytes.Equal(gotContent, tc.WantContent) {
				t.Errorf("got content %q want %q", string(gotContent), string(tc.WantContent))
			}
		})
	}
}