284 lines
9.1 KiB
Go
284 lines
9.1 KiB
Go
package launcher
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"aego/project"
|
|
"aego/project/remoteexample"
|
|
)
|
|
|
|
type remoteCall struct {
|
|
kind, source, origin, id string
|
|
clone bool
|
|
}
|
|
|
|
func TestProtocolLinkRequiresConfirmationAndCannotReplacePendingRequest(t *testing.T) {
|
|
copy := remoteTestCopy(t, false)
|
|
fake := &fakeRemote{calls: make(chan remoteCall, 4), copy: copy}
|
|
l := New(Options{Remote: fake})
|
|
defer l.Close()
|
|
for _, raw := range []string{
|
|
"aego://example?manifest=http%3A%2F%2Fexample.com%2Fx",
|
|
"aego://example?manifest=https%3A%2F%2Fexample.com%2Fx&command=rm",
|
|
"aego://example?manifest=https%3A%2F%2Fexample.com%2Fx&path=%2Ftmp%2Fproject",
|
|
"aego://example?manifest=https%3A%2F%2Fexample.com%2Fx&mode=play",
|
|
} {
|
|
if err := l.RequestExampleLink(raw); err == nil || l.remote.pending != nil {
|
|
t.Fatalf("unsafe OS request was accepted: %s", raw)
|
|
}
|
|
}
|
|
raw, err := remoteexample.MakeLink(copy.Source, "scene")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := l.RequestExampleLink(raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if l.Tab() != TabExamples || !l.remote.fromLink || l.remote.pending == nil || l.remote.pending.origin != "https://example.com" || l.remote.pending.source != copy.Source {
|
|
t.Fatal("OS request did not show the frozen source confirmation")
|
|
}
|
|
if _, ok := l.Result(); ok {
|
|
t.Fatal("OS request selected a project without confirmation")
|
|
}
|
|
select {
|
|
case <-fake.calls:
|
|
t.Fatal("OS request accessed the network before confirmation")
|
|
default:
|
|
}
|
|
other, _ := remoteexample.MakeLink("https://other.example/project.json", "project")
|
|
if err := l.RequestExampleLink(other); err == nil || l.remote.pending.source != copy.Source {
|
|
t.Fatal("second OS request replaced an unconfirmed source")
|
|
}
|
|
req := *l.remote.pending
|
|
l.remote.pending = nil
|
|
l.startRemote(req)
|
|
call := <-fake.calls
|
|
if call.source != copy.Source || call.origin != "https://example.com" {
|
|
t.Fatal("confirmation changed its origin")
|
|
}
|
|
waitRemote(t, l)
|
|
if root, ok := l.Result(); !ok || root != copy.Directory {
|
|
t.Fatal("confirmed protocol request did not open local project")
|
|
}
|
|
}
|
|
|
|
func TestDroppedProtocolLinkWaitsForConfirmation(t *testing.T) {
|
|
copy := remoteTestCopy(t, false)
|
|
fake := &fakeRemote{calls: make(chan remoteCall, 1), copy: copy}
|
|
l := New(Options{Remote: fake})
|
|
defer l.Close()
|
|
link, err := remoteexample.MakeLink(copy.Source, "scene")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !l.Drop(link) || l.remote.pending == nil || l.remote.pending.origin != "https://example.com" {
|
|
t.Fatal("dropped URL did not prepare the source confirmation")
|
|
}
|
|
if _, ok := l.Result(); ok {
|
|
t.Fatal("dropped URL chose a project before confirmation")
|
|
}
|
|
select {
|
|
case call := <-fake.calls:
|
|
t.Fatalf("dropped URL started a remote operation before confirmation: %+v", call)
|
|
default:
|
|
}
|
|
other, err := remoteexample.MakeLink("https://other.example/other.json", "project")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if l.Drop(other) || l.remote.pending == nil || l.remote.pending.source != copy.Source {
|
|
t.Fatal("second dropped URL replaced the unconfirmed request")
|
|
}
|
|
}
|
|
|
|
type fakeRemote struct {
|
|
calls chan remoteCall
|
|
copy RemoteCopy
|
|
block bool
|
|
}
|
|
|
|
func (f *fakeRemote) List(context.Context) ([]RemoteCopy, error) {
|
|
f.calls <- remoteCall{kind: "list"}
|
|
return []RemoteCopy{f.copy}, nil
|
|
}
|
|
func (f *fakeRemote) Open(ctx context.Context, source, origin string, clone bool) (RemoteCopy, error) {
|
|
f.calls <- remoteCall{kind: "open", source: source, origin: origin, clone: clone}
|
|
if f.block {
|
|
<-ctx.Done()
|
|
return RemoteCopy{}, ctx.Err()
|
|
}
|
|
return f.copy, nil
|
|
}
|
|
func (f *fakeRemote) Reopen(_ context.Context, id string) (RemoteCopy, error) {
|
|
f.calls <- remoteCall{kind: "reopen", id: id}
|
|
return f.copy, nil
|
|
}
|
|
func (f *fakeRemote) Check(_ context.Context, id, origin string) (RemoteUpdate, error) {
|
|
f.calls <- remoteCall{kind: "check", id: id, origin: origin}
|
|
return RemoteUpdate{Available: true, Current: "1.0.0", Remote: "1.1.0"}, nil
|
|
}
|
|
|
|
func remoteTestCopy(t *testing.T, native bool) RemoteCopy {
|
|
t.Helper()
|
|
root := filepath.Join(t.TempDir(), "project")
|
|
if _, err := project.Create(root, "Example", native); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entry := filepath.Join(root, "scenes", "demo.aescene")
|
|
if err := os.WriteFile(entry, []byte("scene version=1 {}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return RemoteCopy{ID: "demo-000000000000000000000001", Name: "demo", Version: "1.0.0", Source: "https://example.com/demo.json", Directory: root, Entry: entry}
|
|
}
|
|
|
|
func waitRemote(t *testing.T, l *Launcher) {
|
|
t.Helper()
|
|
if l.remote.job == nil {
|
|
t.Fatal("no remote operation started")
|
|
}
|
|
select {
|
|
case reply := <-l.remote.job.done:
|
|
l.remote.job.done <- reply
|
|
l.pollRemote()
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("remote worker did not finish")
|
|
}
|
|
}
|
|
|
|
func TestRemoteConfirmationFreezesOriginAndOpensEntry(t *testing.T) {
|
|
copy := remoteTestCopy(t, false)
|
|
fake := &fakeRemote{calls: make(chan remoteCall, 4), copy: copy}
|
|
l := New(Options{Remote: fake})
|
|
defer l.Close()
|
|
l.confirmRemote("open", "http://example.com/demo.json", "")
|
|
if l.remote.pending != nil || l.err == "" {
|
|
t.Fatal("insecure URL was accepted")
|
|
}
|
|
l.confirmRemote("open", copy.Source, "")
|
|
if l.remote.pending == nil || l.remote.pending.origin != "https://example.com" {
|
|
t.Fatal("confirmation did not show the exact origin")
|
|
}
|
|
select {
|
|
case call := <-fake.calls:
|
|
t.Fatalf("network started before confirmation: %+v", call)
|
|
default:
|
|
}
|
|
request := *l.remote.pending
|
|
l.remote.url = "https://different.example/other.json"
|
|
l.remote.pending = nil
|
|
l.startRemote(request)
|
|
call := <-fake.calls
|
|
if call.source != copy.Source || call.origin != "https://example.com" || call.clone {
|
|
t.Fatalf("the confirmed request changed: %+v", call)
|
|
}
|
|
waitRemote(t, l)
|
|
if root, ok := l.Result(); !ok || root != copy.Directory {
|
|
t.Fatalf("launcher did not open the local copy: %q", root)
|
|
}
|
|
if entry, remote := l.ResultEntry(); !remote || entry != "scenes/demo.aescene" {
|
|
t.Fatalf("entry scene not carried into the editor: %q remote=%v", entry, remote)
|
|
}
|
|
if err := l.Open(copy.Directory); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if entry, remote := l.ResultEntry(); remote || entry != "" {
|
|
t.Fatal("a later local open retained remote launch state")
|
|
}
|
|
}
|
|
|
|
func TestRemoteCancellationAndNativeProjectsFailClosed(t *testing.T) {
|
|
copy := remoteTestCopy(t, false)
|
|
fake := &fakeRemote{calls: make(chan remoteCall, 4), copy: copy, block: true}
|
|
l := New(Options{Remote: fake})
|
|
l.confirmRemote("clone", copy.Source, "")
|
|
request := *l.remote.pending
|
|
l.remote.pending = nil
|
|
l.startRemote(request)
|
|
<-fake.calls
|
|
l.remote.job.canceled = true
|
|
l.remote.job.cancel()
|
|
waitRemote(t, l)
|
|
if _, ok := l.Result(); ok || !strings.Contains(l.remote.status, "Canceled") {
|
|
t.Fatal("a canceled remote request opened a project")
|
|
}
|
|
l.Close()
|
|
l.startRemote(request)
|
|
if l.remote.job != nil {
|
|
t.Fatal("a closed launcher started another worker")
|
|
}
|
|
l = New(Options{})
|
|
if err := l.acceptRemoteCopy(remoteTestCopy(t, true)); err == nil {
|
|
t.Fatal("remote native Go project was accepted")
|
|
}
|
|
copy.Entry = filepath.Join(t.TempDir(), "outside.aescene")
|
|
if err := os.WriteFile(copy.Entry, []byte("scene {}"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := l.acceptRemoteCopy(copy); err == nil {
|
|
t.Fatal("remote entry outside its project was accepted")
|
|
}
|
|
}
|
|
|
|
func TestRemoteListReopenAndCheckStaySeparate(t *testing.T) {
|
|
copy := remoteTestCopy(t, false)
|
|
fake := &fakeRemote{calls: make(chan remoteCall, 4), copy: copy}
|
|
l := New(Options{Remote: fake})
|
|
defer l.Close()
|
|
l.startRemote(remoteRequest{kind: "list"})
|
|
if call := <-fake.calls; call.kind != "list" {
|
|
t.Fatal(call)
|
|
}
|
|
waitRemote(t, l)
|
|
if len(l.remote.copies) != 1 || l.remote.copies[0].ID != copy.ID {
|
|
t.Fatal("saved copy was not listed")
|
|
}
|
|
l.confirmRemote("check", copy.Source, copy.ID)
|
|
select {
|
|
case <-fake.calls:
|
|
t.Fatal("update check started before confirmation")
|
|
default:
|
|
}
|
|
request := *l.remote.pending
|
|
l.remote.pending = nil
|
|
l.startRemote(request)
|
|
if call := <-fake.calls; call.kind != "check" || call.origin != "https://example.com" {
|
|
t.Fatal(call)
|
|
}
|
|
waitRemote(t, l)
|
|
if !strings.Contains(l.remote.status, "1.1.0") || !strings.Contains(l.remote.status, "not changed") {
|
|
t.Fatal("update check did not explicitly preserve the local copy")
|
|
}
|
|
l.startRemote(remoteRequest{kind: "reopen", id: copy.ID})
|
|
if call := <-fake.calls; call.kind != "reopen" || call.id != copy.ID {
|
|
t.Fatal(call)
|
|
}
|
|
waitRemote(t, l)
|
|
if _, ok := l.Result(); !ok {
|
|
t.Fatal("offline reopen did not open the local project")
|
|
}
|
|
}
|
|
|
|
func TestChoosingLocalProjectCancelsPendingRemoteOpen(t *testing.T) {
|
|
copy := remoteTestCopy(t, false)
|
|
fake := &fakeRemote{calls: make(chan remoteCall, 1), copy: copy, block: true}
|
|
l := New(Options{Remote: fake})
|
|
l.startRemote(remoteRequest{kind: "open", source: copy.Source, origin: "https://example.com"})
|
|
<-fake.calls
|
|
local := remoteTestCopy(t, false)
|
|
if err := l.Open(local.Directory); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
waitRemote(t, l)
|
|
if root, ok := l.Result(); !ok || root != local.Directory {
|
|
t.Fatalf("late remote result replaced the chosen local project: %q", root)
|
|
}
|
|
if _, remote := l.ResultEntry(); remote {
|
|
t.Fatal("late remote operation changed the local result type")
|
|
}
|
|
}
|