204 lines
6.6 KiB
Go
204 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"felis.lolicon.best/internal/fileedit"
|
|
)
|
|
|
|
// filesResult is the Result a `felis files` run printed on its marked line.
|
|
func filesResult(t *testing.T, stdout string) fileedit.Result {
|
|
t.Helper()
|
|
line, ok := strings.CutPrefix(strings.TrimSpace(stdout), fileedit.ResultPrefix)
|
|
if !ok {
|
|
t.Fatalf("stdout has no result line: %q", stdout)
|
|
}
|
|
var res fileedit.Result
|
|
if err := json.Unmarshal([]byte(line), &res); err != nil {
|
|
t.Fatalf("result line %q: %v", line, err)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// stagedUpload serves body to a request carrying Bearer token, and 404 to any
|
|
// other, the way felis-api's internal face does.
|
|
func stagedUpload(t *testing.T, token string, body []byte) *httptest.Server {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") != "Bearer "+token {
|
|
http.Error(w, "no such upload", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Write(body)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
func uploadArgs(root, sourceURL string, body []byte) []string {
|
|
sum := sha256.Sum256(body)
|
|
return []string{
|
|
"--op", "upload", "--path", "plugins/a.jar", "--worlds-root", root,
|
|
"--source-url", sourceURL, "--size", "4", "--sha256", hex.EncodeToString(sum[:]),
|
|
}
|
|
}
|
|
|
|
// uploadRoot is a world with the plugins folder an upload lands in.
|
|
func uploadRoot(t *testing.T) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(root, "plugins"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return root
|
|
}
|
|
|
|
func TestCmdFilesUpload(t *testing.T) {
|
|
body := []byte("PK\x03\x04")
|
|
|
|
t.Run("fetches the staged bytes with its token and lands them", func(t *testing.T) {
|
|
root := uploadRoot(t)
|
|
srv := stagedUpload(t, "tok", body)
|
|
t.Setenv(fileedit.UploadTokenEnv, "tok")
|
|
var stdout, stderr bytes.Buffer
|
|
if code := cmdFiles(uploadArgs(root, srv.URL+"/u", body), &stdout, &stderr); code != 0 {
|
|
t.Fatalf("exit %d, stderr %q", code, stderr.String())
|
|
}
|
|
if res := filesResult(t, stdout.String()); res.Code != "" {
|
|
t.Fatalf("result = %+v", res)
|
|
}
|
|
got, err := os.ReadFile(filepath.Join(root, "plugins", "a.jar"))
|
|
if err != nil || !bytes.Equal(got, body) {
|
|
t.Fatalf("landed %q, %v", got, err)
|
|
}
|
|
})
|
|
|
|
// A refused fetch is the Job failing, never a Result: the API answers it with a
|
|
// 500 the caller retries whole.
|
|
t.Run("a refused fetch exits 1 and lands nothing", func(t *testing.T) {
|
|
root := uploadRoot(t)
|
|
srv := stagedUpload(t, "tok", body)
|
|
t.Setenv(fileedit.UploadTokenEnv, "wrong")
|
|
var stdout, stderr bytes.Buffer
|
|
if code := cmdFiles(uploadArgs(root, srv.URL+"/u", body), &stdout, &stderr); code != 1 {
|
|
t.Fatalf("exit %d, want 1; stdout %q", code, stdout.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "404") {
|
|
t.Fatalf("stderr %q does not name the status", stderr.String())
|
|
}
|
|
if _, err := os.Lstat(filepath.Join(root, "plugins", "a.jar")); !os.IsNotExist(err) {
|
|
t.Fatalf("a refused fetch left a file: %v", err)
|
|
}
|
|
})
|
|
|
|
// The request carries the token, and the internal face never redirects, so a
|
|
// redirect is refused rather than followed with the token attached.
|
|
t.Run("a redirect is not followed", func(t *testing.T) {
|
|
root := uploadRoot(t)
|
|
var hits atomic.Int32
|
|
elsewhere := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
hits.Add(1)
|
|
w.Write(body)
|
|
}))
|
|
defer elsewhere.Close()
|
|
redirecting := httptest.NewServer(http.RedirectHandler(elsewhere.URL+"/u", http.StatusFound))
|
|
defer redirecting.Close()
|
|
t.Setenv(fileedit.UploadTokenEnv, "tok")
|
|
var stdout, stderr bytes.Buffer
|
|
if code := cmdFiles(uploadArgs(root, redirecting.URL+"/u", body), &stdout, &stderr); code != 1 {
|
|
t.Fatalf("exit %d, want 1", code)
|
|
}
|
|
if n := hits.Load(); n != 0 {
|
|
t.Fatalf("the redirect target was fetched %d times", n)
|
|
}
|
|
})
|
|
|
|
for name, tc := range map[string]struct {
|
|
token string
|
|
drop string
|
|
}{
|
|
"no token": {"", ""},
|
|
"no source URL": {"tok", "--source-url"},
|
|
} {
|
|
t.Run(name+" exits 2", func(t *testing.T) {
|
|
srv := stagedUpload(t, "tok", body)
|
|
t.Setenv(fileedit.UploadTokenEnv, tc.token)
|
|
args := uploadArgs(uploadRoot(t), srv.URL+"/u", body)
|
|
if tc.drop != "" {
|
|
for i, a := range args {
|
|
if a == tc.drop {
|
|
args = append(args[:i:i], args[i+2:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
var stdout, stderr bytes.Buffer
|
|
if code := cmdFiles(args, &stdout, &stderr); code != 2 {
|
|
t.Fatalf("exit %d, want 2", code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCmdFilesWrite(t *testing.T) {
|
|
root := t.TempDir()
|
|
args := []string{"--op", "write", "--path", "ops.json", "--worlds-root", root}
|
|
|
|
t.Run("reassembles the content parts", func(t *testing.T) {
|
|
content := []byte("[]\r\n")
|
|
t.Setenv(fileedit.ContentPartsEnv, "1")
|
|
t.Setenv(fileedit.ContentEnv+"_0", base64.StdEncoding.EncodeToString(content))
|
|
var stdout, stderr bytes.Buffer
|
|
if code := cmdFiles(args, &stdout, &stderr); code != 0 {
|
|
t.Fatalf("exit %d, stderr %q", code, stderr.String())
|
|
}
|
|
if res := filesResult(t, stdout.String()); res.Code != "" {
|
|
t.Fatalf("result = %+v", res)
|
|
}
|
|
if got, err := os.ReadFile(filepath.Join(root, "ops.json")); err != nil || !bytes.Equal(got, content) {
|
|
t.Fatalf("wrote %q, %v", got, err)
|
|
}
|
|
})
|
|
|
|
// Writing what did arrive of an incomplete spec would truncate the file.
|
|
t.Run("an incomplete content spec exits 2 and writes nothing", func(t *testing.T) {
|
|
t.Setenv(fileedit.ContentPartsEnv, "2")
|
|
t.Setenv(fileedit.ContentEnv+"_0", base64.StdEncoding.EncodeToString([]byte("x")))
|
|
var stdout, stderr bytes.Buffer
|
|
if code := cmdFiles([]string{"--op", "write", "--path", "new.txt", "--worlds-root", root}, &stdout, &stderr); code != 2 {
|
|
t.Fatalf("exit %d, want 2", code)
|
|
}
|
|
if _, err := os.Lstat(filepath.Join(root, "new.txt")); !os.IsNotExist(err) {
|
|
t.Fatalf("an incomplete spec wrote a file: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// A caller-fault outcome is a successful run carrying a code, so felis-api can
|
|
// answer the precise 4xx instead of a 500.
|
|
func TestCmdFilesCallerFaultIsAResult(t *testing.T) {
|
|
root := t.TempDir()
|
|
var stdout, stderr bytes.Buffer
|
|
if code := cmdFiles([]string{"--op", "mkdir", "--path", "../out", "--worlds-root", root}, &stdout, &stderr); code != 0 {
|
|
t.Fatalf("exit %d, stderr %q", code, stderr.String())
|
|
}
|
|
if res := filesResult(t, stdout.String()); res.Code != fileedit.CodeBadPath {
|
|
t.Fatalf("result = %+v, want code %s", res, fileedit.CodeBadPath)
|
|
}
|
|
stdout.Reset()
|
|
if code := cmdFiles([]string{"--worlds-root", root}, &stdout, &stderr); code != 2 {
|
|
t.Fatalf("no --op: exit %d, want 2", code)
|
|
}
|
|
}
|