package calls import ( "net/http" "net/http/httptest" "net/url" "regexp" "strings" "testing" "time" "amadan.net/oficina/calls/callscore" ) type nativeRoundTripper func(*http.Request) (*http.Response, error) func (f nativeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } // The real shared Go client consumes a session produced by the existing Home // assertion and explicit linking ceremony. No test-only auth route is mounted. func TestNativeCoreUsesHomeSessionAndResumesCall(t *testing.T) { f := newHomeRouterFixture(t) f.link(t) started := f.do("POST", "/meetings/start", "") match := regexp.MustCompile(`href="/m/([A-Za-z0-9]+)/join"`).FindStringSubmatch(started.Body.String()) if len(match) != 2 { t.Fatal("missing new meeting") } room := match[1] f.app.ttl.Store(int64(100 * time.Millisecond)) server := httptest.NewServer(f.handler) defer server.Close() target, _ := url.Parse(server.URL) cookies := []*http.Cookie{} for _, c := range f.cookies { cookies = append(cookies, c) } client := &http.Client{Transport: nativeRoundTripper(func(req *http.Request) (*http.Response, error) { copy := req.Clone(req.Context()) u := *req.URL copy.URL = &u copy.Host = req.URL.Host copy.URL.Host = target.Host copy.URL.Scheme = target.Scheme for _, c := range cookies { copy.AddCookie(c) } return http.DefaultTransport.RoundTrip(copy) })} events := make(chan callscore.Event, 128) native, err := callscore.New(homeTestOrigin, room, client, func(e callscore.Event) { events <- e }) if err != nil { t.Fatal(err) } if err := native.Start(); err != nil { t.Fatal(err) } defer native.Close() deadline := time.NewTimer(5 * time.Second) defer deadline.Stop() peer := "" rosters := 0 gotICE := false for rosters < 2 || !gotICE { select { case e := <-events: if e.Type == "ice" { gotICE = true } if e.Message != nil && e.Message.Type == "roster" { rosters++ s := native.Snapshot() if peer != "" && peer != string(s.Peer) { t.Fatal("resume replaced participation") } peer = string(s.Peer) } case <-deadline.C: t.Fatal("Home native client failed to join and resume") } } if w := f.do("POST", "/meetings/"+room+"/end", ""); w.Code != 303 { t.Fatalf("end: %d", w.Code) } for { select { case e := <-events: if e.State != nil && e.State.Phase == "ended" { if e.State.Reason != "ended" || len(e.State.Peers) != 0 { t.Fatalf("bad terminal state: %+v", e.State) } return } case <-deadline.C: t.Fatal("native client did not observe ended meeting") } } } // Paul's "This meeting is open elsewhere" (Calls 0.3.1): an app that quit // or crashed mid-call leaves its participation inside the thirty-second // resume window, and the next run of the app, on the same kept session, // was refused its fresh stream with 409 — which the core read as // "replaced" before the call had begun. The core now takes its own place // back through the web's takeover, on the real Home-mode router with its // CSRF middleware, and joins. func TestNativeCoreTakesBackItsOwnGhostParticipation(t *testing.T) { f := newHomeRouterFixture(t) f.link(t) started := f.do("POST", "/meetings/start", "") match := regexp.MustCompile(`href="/m/([A-Za-z0-9]+)/join"`).FindStringSubmatch(started.Body.String()) if len(match) != 2 { t.Fatal("missing new meeting") } room := match[1] server := httptest.NewServer(f.handler) defer server.Close() target, _ := url.Parse(server.URL) cookies := []*http.Cookie{} for _, c := range f.cookies { cookies = append(cookies, c) } client := &http.Client{Transport: nativeRoundTripper(func(req *http.Request) (*http.Response, error) { copy := req.Clone(req.Context()) u := *req.URL copy.URL = &u copy.Host = req.URL.Host copy.URL.Host = target.Host copy.URL.Scheme = target.Scheme for _, c := range cookies { copy.AddCookie(c) } return http.DefaultTransport.RoundTrip(copy) })} admitted := func(native *callscore.Room, events chan callscore.Event) string { t.Helper() deadline := time.NewTimer(5 * time.Second) defer deadline.Stop() for { select { case e := <-events: if e.State == nil { continue } if e.State.Phase == "ended" { t.Fatalf("call ended before it began: %q", e.State.Reason) } if e.State.Phase == "admitted" && e.State.Peer != "" { return string(e.State.Peer) } case <-deadline.C: t.Fatal("native client was never admitted") } } } firstEvents := make(chan callscore.Event, 128) first, err := callscore.New(homeTestOrigin, room, client, func(e callscore.Event) { select { case firstEvents <- e: default: } }) if err != nil { t.Fatal(err) } if err := first.Start(); err != nil { t.Fatal(err) } ghost := admitted(first, firstEvents) // The app goes away without leaving: no leave signal, the stream // simply drops, as it does when the process ends. first.Close() events := make(chan callscore.Event, 128) second, err := callscore.New(homeTestOrigin, room, client, func(e callscore.Event) { events <- e }) if err != nil { t.Fatal(err) } if err := second.Start(); err != nil { t.Fatal(err) } defer second.Close() peer := admitted(second, events) if peer == ghost { t.Fatal("the new run should have a participation of its own") } for _, p := range second.Snapshot().Peers { if string(p.Peer) == ghost { t.Fatal("the ghost participation is still in the room") } } } func TestNativeCoreCannotUseAgentBearerAsHumanSession(t *testing.T) { f := newHomeRouterFixture(t) f.link(t) started := f.do("POST", "/meetings/start", "") match := regexp.MustCompile(`href="(/m/[A-Za-z0-9]+)/join"`).FindStringSubmatch(started.Body.String()) if len(match) != 2 { t.Fatal("missing meeting") } created := f.do("POST", "/settings/agents", "name=native-test") token := regexp.MustCompile(`value="(mt_[A-Za-z0-9_-]+)"`).FindStringSubmatch(created.Body.String()) if len(token) != 2 { t.Fatal("missing agent") } r := httptest.NewRequest("GET", homeTestOrigin+match[1]+"/stream", nil) r.Header.Set("Authorization", "Bearer "+token[1]) r.Header.Set("Accept", "text/event-stream") w := httptest.NewRecorder() f.handler.ServeHTTP(w, r) if w.Code == 200 || strings.Contains(w.Body.String(), "event: hello") { t.Fatal("agent entered human call") } }