🏡 index : quine-global-labs/quple.git

package main

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"
)

// fakeVerifier treats the bearer token as the caller's subject.
type fakeVerifier struct{}

func (fakeVerifier) Verify(_ context.Context, token string) (string, error) {
	if token == "" || token == "bad" {
		return "", errors.New("invalid")
	}
	return token, nil
}

func newTestApp(t *testing.T) *httptest.Server {
	t.Helper()
	contacts, err := LoadContacts(filepath.Join(t.TempDir(), "contacts.json"))
	if err != nil {
		t.Fatal(err)
	}
	app := &App{
		verifier:        fakeVerifier{},
		store:           NewStore(),
		contacts:        contacts,
		defaultJitsiURL: "https://meet.jit.si",
	}
	mux := http.NewServeMux()
	app.Routes(mux)
	srv := httptest.NewServer(mux)
	t.Cleanup(srv.Close)
	return srv
}

// do sends a request as `as` (the fake token) and decodes the JSON reply.
func do(t *testing.T, srv *httptest.Server, as, method, path, body string) (int, map[string]any) {
	t.Helper()
	var r io.Reader
	if body != "" {
		r = strings.NewReader(body)
	}
	req, _ := http.NewRequest(method, srv.URL+path, r)
	if as != "" {
		req.Header.Set("Authorization", "Bearer "+as)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var out map[string]any
	_ = json.NewDecoder(resp.Body).Decode(&out)
	return resp.StatusCode, out
}

func TestAuth(t *testing.T) {
	srv := newTestApp(t)
	if status, _ := do(t, srv, "", "GET", "/me", ""); status != 401 {
		t.Errorf("no token: %d", status)
	}
	if status, _ := do(t, srv, "bad", "GET", "/me", ""); status != 401 {
		t.Errorf("bad token: %d", status)
	}
	status, me := do(t, srv, "alice", "GET", "/me", "")
	if status != 200 || me["username"] != "alice" || me["defaultJitsiUrl"] != "https://meet.jit.si" {
		t.Errorf("/me: %d %v", status, me)
	}
}

func TestCallOnDefaultServer(t *testing.T) {
	srv := newTestApp(t)
	status, call := do(t, srv, "alice", "POST", "/call", `{"to":"Bob"}`)
	if status != 200 || call["jitsiUrl"] != "https://meet.jit.si" || call["meetingId"] == "" {
		t.Fatalf("call: %d %v", status, call)
	}
	_, poll := do(t, srv, "bob", "GET", "/poll", "")
	inv := poll["invites"].([]any)[0].(map[string]any)
	if inv["from"] != "alice" || inv["jitsiUrl"] != "https://meet.jit.si" || inv["meetingId"] != call["meetingId"] {
		t.Fatalf("invite: %v", inv)
	}
}

func TestCallOnAnotherServer(t *testing.T) {
	srv := newTestApp(t)
	// The caller picks any Jitsi; the callee is sent to the same one.
	status, call := do(t, srv, "alice", "POST", "/call", `{"to":"bob","jitsiUrl":"Jitsi.Example.org/"}`)
	if status != 200 || call["jitsiUrl"] != "https://jitsi.example.org" {
		t.Fatalf("call: %d %v", status, call)
	}
	_, poll := do(t, srv, "bob", "GET", "/poll", "")
	inv := poll["invites"].([]any)[0].(map[string]any)
	if inv["jitsiUrl"] != "https://jitsi.example.org" || inv["meetingId"] != call["meetingId"] {
		t.Fatalf("invite: %v", inv)
	}
}

func TestCallValidation(t *testing.T) {
	srv := newTestApp(t)
	for _, tc := range []struct{ body, why string }{
		{`{"to":"alice"}`, "self"},
		{`{"to":"not a name"}`, "malformed name"},
		{`{"to":"bob@example.com"}`, "not a username"},
		{`{"to":"bob","jitsiUrl":"javascript:alert(1)"}`, "non-http server"},
		{`{"to":"bob","jitsiUrl":"file:///etc/passwd"}`, "file server"},
		{`{"to":"bob","jitsiUrl":"https://x.example/?a=b"}`, "query in server"},
		{`{"to":"bob","jitsiUrl":"https://user@x.example"}`, "credentials in server"},
	} {
		if status, _ := do(t, srv, "alice", "POST", "/call", tc.body); status != 400 {
			t.Errorf("%s: status %d", tc.why, status)
		}
	}
}

func TestContacts(t *testing.T) {
	srv := newTestApp(t)
	if status, _ := do(t, srv, "alice", "POST", "/contacts", `{"username":"Joe"}`); status != 200 {
		t.Fatalf("add: %d", status)
	}
	do(t, srv, "alice", "POST", "/contacts", `{"username":"bob"}`)
	if status, _ := do(t, srv, "alice", "POST", "/contacts", `{"username":"alice"}`); status != 400 {
		t.Errorf("adding yourself: %d", status)
	}
	do(t, srv, "joe", "GET", "/poll", "") // joe comes online

	_, list := do(t, srv, "alice", "GET", "/contacts", "")
	got, _ := json.Marshal(list["contacts"])
	want := `[{"online":false,"username":"bob"},{"online":true,"username":"joe"}]`
	if string(got) != want {
		t.Fatalf("contacts = %s, want %s", got, want)
	}

	do(t, srv, "alice", "DELETE", "/contacts/bob", "")
	_, list = do(t, srv, "alice", "GET", "/contacts", "")
	if n := len(list["contacts"].([]any)); n != 1 {
		t.Fatalf("after delete: %d contacts", n)
	}
	// Contact lists are per person.
	_, other := do(t, srv, "bob", "GET", "/contacts", "")
	if n := len(other["contacts"].([]any)); n != 0 {
		t.Fatalf("bob sees %d of alice's contacts", n)
	}
}