github.com/nektos/act@v0.2.63/pkg/artifactcache/handler_test.go (about)

     1  package artifactcache
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"path/filepath"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  	"github.com/timshannon/bolthold"
    18  	"go.etcd.io/bbolt"
    19  )
    20  
    21  func TestHandler(t *testing.T) {
    22  	dir := filepath.Join(t.TempDir(), "artifactcache")
    23  	handler, err := StartHandler(dir, "", 0, nil)
    24  	require.NoError(t, err)
    25  
    26  	base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase)
    27  
    28  	defer func() {
    29  		t.Run("inpect db", func(t *testing.T) {
    30  			db, err := handler.openDB()
    31  			require.NoError(t, err)
    32  			defer db.Close()
    33  			require.NoError(t, db.Bolt().View(func(tx *bbolt.Tx) error {
    34  				return tx.Bucket([]byte("Cache")).ForEach(func(k, v []byte) error {
    35  					t.Logf("%s: %s", k, v)
    36  					return nil
    37  				})
    38  			}))
    39  		})
    40  		t.Run("close", func(t *testing.T) {
    41  			require.NoError(t, handler.Close())
    42  			assert.Nil(t, handler.server)
    43  			assert.Nil(t, handler.listener)
    44  			_, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil)
    45  			assert.Error(t, err)
    46  		})
    47  	}()
    48  
    49  	t.Run("get not exist", func(t *testing.T) {
    50  		key := strings.ToLower(t.Name())
    51  		version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
    52  		resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
    53  		require.NoError(t, err)
    54  		require.Equal(t, 204, resp.StatusCode)
    55  	})
    56  
    57  	t.Run("reserve and upload", func(t *testing.T) {
    58  		key := strings.ToLower(t.Name())
    59  		version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
    60  		content := make([]byte, 100)
    61  		_, err := rand.Read(content)
    62  		require.NoError(t, err)
    63  		uploadCacheNormally(t, base, key, version, content)
    64  	})
    65  
    66  	t.Run("clean", func(t *testing.T) {
    67  		resp, err := http.Post(fmt.Sprintf("%s/clean", base), "", nil)
    68  		require.NoError(t, err)
    69  		assert.Equal(t, 200, resp.StatusCode)
    70  	})
    71  
    72  	t.Run("reserve with bad request", func(t *testing.T) {
    73  		body := []byte(`invalid json`)
    74  		require.NoError(t, err)
    75  		resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
    76  		require.NoError(t, err)
    77  		assert.Equal(t, 400, resp.StatusCode)
    78  	})
    79  
    80  	t.Run("duplicate reserve", func(t *testing.T) {
    81  		key := strings.ToLower(t.Name())
    82  		version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
    83  		var first, second struct {
    84  			CacheID uint64 `json:"cacheId"`
    85  		}
    86  		{
    87  			body, err := json.Marshal(&Request{
    88  				Key:     key,
    89  				Version: version,
    90  				Size:    100,
    91  			})
    92  			require.NoError(t, err)
    93  			resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
    94  			require.NoError(t, err)
    95  			assert.Equal(t, 200, resp.StatusCode)
    96  
    97  			require.NoError(t, json.NewDecoder(resp.Body).Decode(&first))
    98  			assert.NotZero(t, first.CacheID)
    99  		}
   100  		{
   101  			body, err := json.Marshal(&Request{
   102  				Key:     key,
   103  				Version: version,
   104  				Size:    100,
   105  			})
   106  			require.NoError(t, err)
   107  			resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
   108  			require.NoError(t, err)
   109  			assert.Equal(t, 200, resp.StatusCode)
   110  
   111  			require.NoError(t, json.NewDecoder(resp.Body).Decode(&second))
   112  			assert.NotZero(t, second.CacheID)
   113  		}
   114  
   115  		assert.NotEqual(t, first.CacheID, second.CacheID)
   116  	})
   117  
   118  	t.Run("upload with bad id", func(t *testing.T) {
   119  		req, err := http.NewRequest(http.MethodPatch,
   120  			fmt.Sprintf("%s/caches/invalid_id", base), bytes.NewReader(nil))
   121  		require.NoError(t, err)
   122  		req.Header.Set("Content-Type", "application/octet-stream")
   123  		req.Header.Set("Content-Range", "bytes 0-99/*")
   124  		resp, err := http.DefaultClient.Do(req)
   125  		require.NoError(t, err)
   126  		assert.Equal(t, 400, resp.StatusCode)
   127  	})
   128  
   129  	t.Run("upload without reserve", func(t *testing.T) {
   130  		req, err := http.NewRequest(http.MethodPatch,
   131  			fmt.Sprintf("%s/caches/%d", base, 1000), bytes.NewReader(nil))
   132  		require.NoError(t, err)
   133  		req.Header.Set("Content-Type", "application/octet-stream")
   134  		req.Header.Set("Content-Range", "bytes 0-99/*")
   135  		resp, err := http.DefaultClient.Do(req)
   136  		require.NoError(t, err)
   137  		assert.Equal(t, 400, resp.StatusCode)
   138  	})
   139  
   140  	t.Run("upload with complete", func(t *testing.T) {
   141  		key := strings.ToLower(t.Name())
   142  		version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
   143  		var id uint64
   144  		content := make([]byte, 100)
   145  		_, err := rand.Read(content)
   146  		require.NoError(t, err)
   147  		{
   148  			body, err := json.Marshal(&Request{
   149  				Key:     key,
   150  				Version: version,
   151  				Size:    100,
   152  			})
   153  			require.NoError(t, err)
   154  			resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
   155  			require.NoError(t, err)
   156  			assert.Equal(t, 200, resp.StatusCode)
   157  
   158  			got := struct {
   159  				CacheID uint64 `json:"cacheId"`
   160  			}{}
   161  			require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
   162  			id = got.CacheID
   163  		}
   164  		{
   165  			req, err := http.NewRequest(http.MethodPatch,
   166  				fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
   167  			require.NoError(t, err)
   168  			req.Header.Set("Content-Type", "application/octet-stream")
   169  			req.Header.Set("Content-Range", "bytes 0-99/*")
   170  			resp, err := http.DefaultClient.Do(req)
   171  			require.NoError(t, err)
   172  			assert.Equal(t, 200, resp.StatusCode)
   173  		}
   174  		{
   175  			resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
   176  			require.NoError(t, err)
   177  			assert.Equal(t, 200, resp.StatusCode)
   178  		}
   179  		{
   180  			req, err := http.NewRequest(http.MethodPatch,
   181  				fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
   182  			require.NoError(t, err)
   183  			req.Header.Set("Content-Type", "application/octet-stream")
   184  			req.Header.Set("Content-Range", "bytes 0-99/*")
   185  			resp, err := http.DefaultClient.Do(req)
   186  			require.NoError(t, err)
   187  			assert.Equal(t, 400, resp.StatusCode)
   188  		}
   189  	})
   190  
   191  	t.Run("upload with invalid range", func(t *testing.T) {
   192  		key := strings.ToLower(t.Name())
   193  		version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
   194  		var id uint64
   195  		content := make([]byte, 100)
   196  		_, err := rand.Read(content)
   197  		require.NoError(t, err)
   198  		{
   199  			body, err := json.Marshal(&Request{
   200  				Key:     key,
   201  				Version: version,
   202  				Size:    100,
   203  			})
   204  			require.NoError(t, err)
   205  			resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
   206  			require.NoError(t, err)
   207  			assert.Equal(t, 200, resp.StatusCode)
   208  
   209  			got := struct {
   210  				CacheID uint64 `json:"cacheId"`
   211  			}{}
   212  			require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
   213  			id = got.CacheID
   214  		}
   215  		{
   216  			req, err := http.NewRequest(http.MethodPatch,
   217  				fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
   218  			require.NoError(t, err)
   219  			req.Header.Set("Content-Type", "application/octet-stream")
   220  			req.Header.Set("Content-Range", "bytes xx-99/*")
   221  			resp, err := http.DefaultClient.Do(req)
   222  			require.NoError(t, err)
   223  			assert.Equal(t, 400, resp.StatusCode)
   224  		}
   225  	})
   226  
   227  	t.Run("commit with bad id", func(t *testing.T) {
   228  		{
   229  			resp, err := http.Post(fmt.Sprintf("%s/caches/invalid_id", base), "", nil)
   230  			require.NoError(t, err)
   231  			assert.Equal(t, 400, resp.StatusCode)
   232  		}
   233  	})
   234  
   235  	t.Run("commit with not exist id", func(t *testing.T) {
   236  		{
   237  			resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil)
   238  			require.NoError(t, err)
   239  			assert.Equal(t, 400, resp.StatusCode)
   240  		}
   241  	})
   242  
   243  	t.Run("duplicate commit", func(t *testing.T) {
   244  		key := strings.ToLower(t.Name())
   245  		version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
   246  		var id uint64
   247  		content := make([]byte, 100)
   248  		_, err := rand.Read(content)
   249  		require.NoError(t, err)
   250  		{
   251  			body, err := json.Marshal(&Request{
   252  				Key:     key,
   253  				Version: version,
   254  				Size:    100,
   255  			})
   256  			require.NoError(t, err)
   257  			resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
   258  			require.NoError(t, err)
   259  			assert.Equal(t, 200, resp.StatusCode)
   260  
   261  			got := struct {
   262  				CacheID uint64 `json:"cacheId"`
   263  			}{}
   264  			require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
   265  			id = got.CacheID
   266  		}
   267  		{
   268  			req, err := http.NewRequest(http.MethodPatch,
   269  				fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
   270  			require.NoError(t, err)
   271  			req.Header.Set("Content-Type", "application/octet-stream")
   272  			req.Header.Set("Content-Range", "bytes 0-99/*")
   273  			resp, err := http.DefaultClient.Do(req)
   274  			require.NoError(t, err)
   275  			assert.Equal(t, 200, resp.StatusCode)
   276  		}
   277  		{
   278  			resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
   279  			require.NoError(t, err)
   280  			assert.Equal(t, 200, resp.StatusCode)
   281  		}
   282  		{
   283  			resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
   284  			require.NoError(t, err)
   285  			assert.Equal(t, 400, resp.StatusCode)
   286  		}
   287  	})
   288  
   289  	t.Run("commit early", func(t *testing.T) {
   290  		key := strings.ToLower(t.Name())
   291  		version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
   292  		var id uint64
   293  		content := make([]byte, 100)
   294  		_, err := rand.Read(content)
   295  		require.NoError(t, err)
   296  		{
   297  			body, err := json.Marshal(&Request{
   298  				Key:     key,
   299  				Version: version,
   300  				Size:    100,
   301  			})
   302  			require.NoError(t, err)
   303  			resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
   304  			require.NoError(t, err)
   305  			assert.Equal(t, 200, resp.StatusCode)
   306  
   307  			got := struct {
   308  				CacheID uint64 `json:"cacheId"`
   309  			}{}
   310  			require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
   311  			id = got.CacheID
   312  		}
   313  		{
   314  			req, err := http.NewRequest(http.MethodPatch,
   315  				fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content[:50]))
   316  			require.NoError(t, err)
   317  			req.Header.Set("Content-Type", "application/octet-stream")
   318  			req.Header.Set("Content-Range", "bytes 0-59/*")
   319  			resp, err := http.DefaultClient.Do(req)
   320  			require.NoError(t, err)
   321  			assert.Equal(t, 200, resp.StatusCode)
   322  		}
   323  		{
   324  			resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
   325  			require.NoError(t, err)
   326  			assert.Equal(t, 500, resp.StatusCode)
   327  		}
   328  	})
   329  
   330  	t.Run("get with bad id", func(t *testing.T) {
   331  		resp, err := http.Get(fmt.Sprintf("%s/artifacts/invalid_id", base))
   332  		require.NoError(t, err)
   333  		require.Equal(t, 400, resp.StatusCode)
   334  	})
   335  
   336  	t.Run("get with not exist id", func(t *testing.T) {
   337  		resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100))
   338  		require.NoError(t, err)
   339  		require.Equal(t, 404, resp.StatusCode)
   340  	})
   341  
   342  	t.Run("get with not exist id", func(t *testing.T) {
   343  		resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100))
   344  		require.NoError(t, err)
   345  		require.Equal(t, 404, resp.StatusCode)
   346  	})
   347  
   348  	t.Run("get with multiple keys", func(t *testing.T) {
   349  		version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
   350  		key := strings.ToLower(t.Name())
   351  		keys := [3]string{
   352  			key + "_a_b_c",
   353  			key + "_a_b",
   354  			key + "_a",
   355  		}
   356  		contents := [3][]byte{
   357  			make([]byte, 100),
   358  			make([]byte, 200),
   359  			make([]byte, 300),
   360  		}
   361  		for i := range contents {
   362  			_, err := rand.Read(contents[i])
   363  			require.NoError(t, err)
   364  			uploadCacheNormally(t, base, keys[i], version, contents[i])
   365  			time.Sleep(time.Second) // ensure CreatedAt of caches are different
   366  		}
   367  
   368  		reqKeys := strings.Join([]string{
   369  			key + "_a_b_x",
   370  			key + "_a_b",
   371  			key + "_a",
   372  		}, ",")
   373  
   374  		resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
   375  		require.NoError(t, err)
   376  		require.Equal(t, 200, resp.StatusCode)
   377  
   378  		/*
   379  			Expect `key_a_b` because:
   380  			- `key_a_b_x" doesn't match any caches.
   381  			- `key_a_b" matches `key_a_b` and `key_a_b_c`, but `key_a_b` is newer.
   382  		*/
   383  		except := 1
   384  
   385  		got := struct {
   386  			Result          string `json:"result"`
   387  			ArchiveLocation string `json:"archiveLocation"`
   388  			CacheKey        string `json:"cacheKey"`
   389  		}{}
   390  		require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
   391  		assert.Equal(t, "hit", got.Result)
   392  		assert.Equal(t, keys[except], got.CacheKey)
   393  
   394  		contentResp, err := http.Get(got.ArchiveLocation)
   395  		require.NoError(t, err)
   396  		require.Equal(t, 200, contentResp.StatusCode)
   397  		content, err := io.ReadAll(contentResp.Body)
   398  		require.NoError(t, err)
   399  		assert.Equal(t, contents[except], content)
   400  	})
   401  
   402  	t.Run("case insensitive", func(t *testing.T) {
   403  		version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
   404  		key := strings.ToLower(t.Name())
   405  		content := make([]byte, 100)
   406  		_, err := rand.Read(content)
   407  		require.NoError(t, err)
   408  		uploadCacheNormally(t, base, key+"_ABC", version, content)
   409  
   410  		{
   411  			reqKey := key + "_aBc"
   412  			resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version))
   413  			require.NoError(t, err)
   414  			require.Equal(t, 200, resp.StatusCode)
   415  			got := struct {
   416  				Result          string `json:"result"`
   417  				ArchiveLocation string `json:"archiveLocation"`
   418  				CacheKey        string `json:"cacheKey"`
   419  			}{}
   420  			require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
   421  			assert.Equal(t, "hit", got.Result)
   422  			assert.Equal(t, key+"_abc", got.CacheKey)
   423  		}
   424  	})
   425  
   426  	t.Run("exact keys are preferred (key 0)", func(t *testing.T) {
   427  		version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
   428  		key := strings.ToLower(t.Name())
   429  		keys := [3]string{
   430  			key + "_a",
   431  			key + "_a_b_c",
   432  			key + "_a_b",
   433  		}
   434  		contents := [3][]byte{
   435  			make([]byte, 100),
   436  			make([]byte, 200),
   437  			make([]byte, 300),
   438  		}
   439  		for i := range contents {
   440  			_, err := rand.Read(contents[i])
   441  			require.NoError(t, err)
   442  			uploadCacheNormally(t, base, keys[i], version, contents[i])
   443  			time.Sleep(time.Second) // ensure CreatedAt of caches are different
   444  		}
   445  
   446  		reqKeys := strings.Join([]string{
   447  			key + "_a",
   448  			key + "_a_b",
   449  		}, ",")
   450  
   451  		resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
   452  		require.NoError(t, err)
   453  		require.Equal(t, 200, resp.StatusCode)
   454  
   455  		/*
   456  			Expect `key_a` because:
   457  			- `key_a` matches `key_a`, `key_a_b` and `key_a_b_c`, but `key_a` is an exact match.
   458  			- `key_a_b` matches `key_a_b` and `key_a_b_c`, but previous key had a match
   459  		*/
   460  		expect := 0
   461  
   462  		got := struct {
   463  			ArchiveLocation string `json:"archiveLocation"`
   464  			CacheKey        string `json:"cacheKey"`
   465  		}{}
   466  		require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
   467  		assert.Equal(t, keys[expect], got.CacheKey)
   468  
   469  		contentResp, err := http.Get(got.ArchiveLocation)
   470  		require.NoError(t, err)
   471  		require.Equal(t, 200, contentResp.StatusCode)
   472  		content, err := io.ReadAll(contentResp.Body)
   473  		require.NoError(t, err)
   474  		assert.Equal(t, contents[expect], content)
   475  	})
   476  
   477  	t.Run("exact keys are preferred (key 1)", func(t *testing.T) {
   478  		version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
   479  		key := strings.ToLower(t.Name())
   480  		keys := [3]string{
   481  			key + "_a",
   482  			key + "_a_b_c",
   483  			key + "_a_b",
   484  		}
   485  		contents := [3][]byte{
   486  			make([]byte, 100),
   487  			make([]byte, 200),
   488  			make([]byte, 300),
   489  		}
   490  		for i := range contents {
   491  			_, err := rand.Read(contents[i])
   492  			require.NoError(t, err)
   493  			uploadCacheNormally(t, base, keys[i], version, contents[i])
   494  			time.Sleep(time.Second) // ensure CreatedAt of caches are different
   495  		}
   496  
   497  		reqKeys := strings.Join([]string{
   498  			"------------------------------------------------------",
   499  			key + "_a",
   500  			key + "_a_b",
   501  		}, ",")
   502  
   503  		resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
   504  		require.NoError(t, err)
   505  		require.Equal(t, 200, resp.StatusCode)
   506  
   507  		/*
   508  			Expect `key_a` because:
   509  			- `------------------------------------------------------` doesn't match any caches.
   510  			- `key_a` matches `key_a`, `key_a_b` and `key_a_b_c`, but `key_a` is an exact match.
   511  			- `key_a_b` matches `key_a_b` and `key_a_b_c`, but previous key had a match
   512  		*/
   513  		expect := 0
   514  
   515  		got := struct {
   516  			ArchiveLocation string `json:"archiveLocation"`
   517  			CacheKey        string `json:"cacheKey"`
   518  		}{}
   519  		require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
   520  		assert.Equal(t, keys[expect], got.CacheKey)
   521  
   522  		contentResp, err := http.Get(got.ArchiveLocation)
   523  		require.NoError(t, err)
   524  		require.Equal(t, 200, contentResp.StatusCode)
   525  		content, err := io.ReadAll(contentResp.Body)
   526  		require.NoError(t, err)
   527  		assert.Equal(t, contents[expect], content)
   528  	})
   529  }
   530  
   531  func uploadCacheNormally(t *testing.T, base, key, version string, content []byte) {
   532  	var id uint64
   533  	{
   534  		body, err := json.Marshal(&Request{
   535  			Key:     key,
   536  			Version: version,
   537  			Size:    int64(len(content)),
   538  		})
   539  		require.NoError(t, err)
   540  		resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body))
   541  		require.NoError(t, err)
   542  		assert.Equal(t, 200, resp.StatusCode)
   543  
   544  		got := struct {
   545  			CacheID uint64 `json:"cacheId"`
   546  		}{}
   547  		require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
   548  		id = got.CacheID
   549  	}
   550  	{
   551  		req, err := http.NewRequest(http.MethodPatch,
   552  			fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(content))
   553  		require.NoError(t, err)
   554  		req.Header.Set("Content-Type", "application/octet-stream")
   555  		req.Header.Set("Content-Range", "bytes 0-99/*")
   556  		resp, err := http.DefaultClient.Do(req)
   557  		require.NoError(t, err)
   558  		assert.Equal(t, 200, resp.StatusCode)
   559  	}
   560  	{
   561  		resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
   562  		require.NoError(t, err)
   563  		assert.Equal(t, 200, resp.StatusCode)
   564  	}
   565  	var archiveLocation string
   566  	{
   567  		resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
   568  		require.NoError(t, err)
   569  		require.Equal(t, 200, resp.StatusCode)
   570  		got := struct {
   571  			Result          string `json:"result"`
   572  			ArchiveLocation string `json:"archiveLocation"`
   573  			CacheKey        string `json:"cacheKey"`
   574  		}{}
   575  		require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
   576  		assert.Equal(t, "hit", got.Result)
   577  		assert.Equal(t, strings.ToLower(key), got.CacheKey)
   578  		archiveLocation = got.ArchiveLocation
   579  	}
   580  	{
   581  		resp, err := http.Get(archiveLocation) //nolint:gosec
   582  		require.NoError(t, err)
   583  		require.Equal(t, 200, resp.StatusCode)
   584  		got, err := io.ReadAll(resp.Body)
   585  		require.NoError(t, err)
   586  		assert.Equal(t, content, got)
   587  	}
   588  }
   589  
   590  func TestHandler_gcCache(t *testing.T) {
   591  	dir := filepath.Join(t.TempDir(), "artifactcache")
   592  	handler, err := StartHandler(dir, "", 0, nil)
   593  	require.NoError(t, err)
   594  
   595  	defer func() {
   596  		require.NoError(t, handler.Close())
   597  	}()
   598  
   599  	now := time.Now()
   600  
   601  	cases := []struct {
   602  		Cache *Cache
   603  		Kept  bool
   604  	}{
   605  		{
   606  			// should be kept, since it's used recently and not too old.
   607  			Cache: &Cache{
   608  				Key:       "test_key_1",
   609  				Version:   "test_version",
   610  				Complete:  true,
   611  				UsedAt:    now.Unix(),
   612  				CreatedAt: now.Add(-time.Hour).Unix(),
   613  			},
   614  			Kept: true,
   615  		},
   616  		{
   617  			// should be removed, since it's not complete and not used for a while.
   618  			Cache: &Cache{
   619  				Key:       "test_key_2",
   620  				Version:   "test_version",
   621  				Complete:  false,
   622  				UsedAt:    now.Add(-(keepTemp + time.Second)).Unix(),
   623  				CreatedAt: now.Add(-(keepTemp + time.Hour)).Unix(),
   624  			},
   625  			Kept: false,
   626  		},
   627  		{
   628  			// should be removed, since it's not used for a while.
   629  			Cache: &Cache{
   630  				Key:       "test_key_3",
   631  				Version:   "test_version",
   632  				Complete:  true,
   633  				UsedAt:    now.Add(-(keepUnused + time.Second)).Unix(),
   634  				CreatedAt: now.Add(-(keepUnused + time.Hour)).Unix(),
   635  			},
   636  			Kept: false,
   637  		},
   638  		{
   639  			// should be removed, since it's used but too old.
   640  			Cache: &Cache{
   641  				Key:       "test_key_3",
   642  				Version:   "test_version",
   643  				Complete:  true,
   644  				UsedAt:    now.Unix(),
   645  				CreatedAt: now.Add(-(keepUsed + time.Second)).Unix(),
   646  			},
   647  			Kept: false,
   648  		},
   649  		{
   650  			// should be kept, since it has a newer edition but be used recently.
   651  			Cache: &Cache{
   652  				Key:       "test_key_1",
   653  				Version:   "test_version",
   654  				Complete:  true,
   655  				UsedAt:    now.Add(-(keepOld - time.Minute)).Unix(),
   656  				CreatedAt: now.Add(-(time.Hour + time.Second)).Unix(),
   657  			},
   658  			Kept: true,
   659  		},
   660  		{
   661  			// should be removed, since it has a newer edition and not be used recently.
   662  			Cache: &Cache{
   663  				Key:       "test_key_1",
   664  				Version:   "test_version",
   665  				Complete:  true,
   666  				UsedAt:    now.Add(-(keepOld + time.Second)).Unix(),
   667  				CreatedAt: now.Add(-(time.Hour + time.Second)).Unix(),
   668  			},
   669  			Kept: false,
   670  		},
   671  	}
   672  
   673  	db, err := handler.openDB()
   674  	require.NoError(t, err)
   675  	for _, c := range cases {
   676  		require.NoError(t, insertCache(db, c.Cache))
   677  	}
   678  	require.NoError(t, db.Close())
   679  
   680  	handler.gcAt = time.Time{} // ensure gcCache will not skip
   681  	handler.gcCache()
   682  
   683  	db, err = handler.openDB()
   684  	require.NoError(t, err)
   685  	for i, v := range cases {
   686  		t.Run(fmt.Sprintf("%d_%s", i, v.Cache.Key), func(t *testing.T) {
   687  			cache := &Cache{}
   688  			err = db.Get(v.Cache.ID, cache)
   689  			if v.Kept {
   690  				assert.NoError(t, err)
   691  			} else {
   692  				assert.ErrorIs(t, err, bolthold.ErrNotFound)
   693  			}
   694  		})
   695  	}
   696  	require.NoError(t, db.Close())
   697  }