test(e2e): Playwright UI smoke + Go integration tests against MAS
- Playwright e2e against Vite dev server (LoginScreen renders, homeserver visible, version pinned). Fallback documented to swap to `wails dev -browser` via WAILS_DEV_URL when full backend bindings are required. - Go integration tests (//go:build integration,goolm) for Start + ListRooms + LoadTimeline + SendText + GetDiagnostics against real Synapse. Skip cleanly if MATRIX_INTEGRATION_TOKEN/USER env vars unset so CI never breaks unattended. - pnpm e2e/e2e:ui/e2e:report scripts. Per-run keyring namespace + t.TempDir XDG_CONFIG_HOME so integration tests don't clobber the production keyring entry. E2E.md explains how to run each layer + limits (OIDC not automated).
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
//go:build integration && goolm
|
||||
// +build integration,goolm
|
||||
|
||||
// Integration tests against a real Synapse + MAS. Build tag `integration` keeps
|
||||
// them out of the default `go test ./...` run; `goolm` matches the runtime
|
||||
// build tag the app uses (cryptohelper backend). All tests skip cleanly when
|
||||
// MATRIX_INTEGRATION_* env vars are absent so CI never breaks unattended.
|
||||
//
|
||||
// Run with:
|
||||
//
|
||||
// MATRIX_INTEGRATION_TOKEN=syt_... \
|
||||
// MATRIX_INTEGRATION_USER=@egutierrez:matrix-af2f3d.organic-machine.com \
|
||||
// MATRIX_INTEGRATION_DEVICE=DEVICEID \
|
||||
// MATRIX_INTEGRATION_TEST_ROOM='!abcd:matrix-af2f3d.organic-machine.com' \
|
||||
// go test -tags 'integration goolm' -timeout 120s -v \
|
||||
// ./projects/element_agents/apps/matrix_client_pc/...
|
||||
//
|
||||
// MATRIX_INTEGRATION_TEST_ROOM is optional — without it the round-trip test
|
||||
// auto-skips. MATRIX_INTEGRATION_DEVICE is also optional; if missing the test
|
||||
// calls /whoami to discover the device_id, mirroring what the app does.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fn-registry/projects/element_agents/apps/matrix_client_pc/internal/infra"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
const integrationHomeserverURL = "https://matrix-af2f3d.organic-machine.com"
|
||||
|
||||
// integrationEnv reads the required env vars + returns them. If MATRIX_INTEGRATION_TOKEN
|
||||
// or MATRIX_INTEGRATION_USER is missing the test should t.Skip — that is the
|
||||
// canonical "no creds available" signal.
|
||||
type integrationEnv struct {
|
||||
Token string
|
||||
User string
|
||||
Device string
|
||||
TestRoom string
|
||||
}
|
||||
|
||||
func readIntegrationEnv(t *testing.T) integrationEnv {
|
||||
t.Helper()
|
||||
env := integrationEnv{
|
||||
Token: os.Getenv("MATRIX_INTEGRATION_TOKEN"),
|
||||
User: os.Getenv("MATRIX_INTEGRATION_USER"),
|
||||
Device: os.Getenv("MATRIX_INTEGRATION_DEVICE"),
|
||||
TestRoom: os.Getenv("MATRIX_INTEGRATION_TEST_ROOM"),
|
||||
}
|
||||
if env.Token == "" || env.User == "" {
|
||||
t.Skipf("skipping integration test: set MATRIX_INTEGRATION_TOKEN + MATRIX_INTEGRATION_USER to run (got token=%v user=%v)",
|
||||
env.Token != "", env.User != "")
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
// buildService wires up a MatrixService against the real Synapse, persisting
|
||||
// the test token in a throwaway keyring namespace so we don't pollute the user's
|
||||
// production credentials. It returns the service + a cleanup that calls Stop.
|
||||
func buildService(t *testing.T, env integrationEnv) (*MatrixService, func()) {
|
||||
t.Helper()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
// Override user config dir so userStoreDir() lands inside the tmpDir
|
||||
// instead of clobbering ~/.config/matrix_client_pc/<user>.
|
||||
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||
|
||||
// Use a test-scoped keyring service to keep production creds untouched.
|
||||
testKeyringService := fmt.Sprintf("fn_registry.matrix_client_pc.test.%d", time.Now().UnixNano())
|
||||
s := &MatrixService{
|
||||
store: infra.NewKeyringTokenStore(testKeyringService),
|
||||
}
|
||||
s.SetContext(context.Background())
|
||||
|
||||
// Persist the token + pickle key under the test keyring so Start() can load
|
||||
// them the same way the production code does.
|
||||
pickle := make([]byte, 32)
|
||||
if _, err := rand.Read(pickle); err != nil {
|
||||
t.Fatalf("rand: %v", err)
|
||||
}
|
||||
tok := infra.Token{
|
||||
AccessToken: env.Token,
|
||||
UserID: env.User,
|
||||
DeviceID: env.Device,
|
||||
HomeserverURL: integrationHomeserverURL,
|
||||
PickleKeyHex: hex.EncodeToString(pickle),
|
||||
}
|
||||
if err := s.store.Save(env.User, tok); err != nil {
|
||||
t.Skipf("keyring unavailable (likely no D-Bus session): %v", err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
s.Stop()
|
||||
_ = s.store.Delete(env.User)
|
||||
// Belt-and-braces: blow away any crypto.db files left behind.
|
||||
_ = os.RemoveAll(filepath.Join(tmpDir, "matrix_client_pc"))
|
||||
}
|
||||
return s, cleanup
|
||||
}
|
||||
|
||||
func TestIntegration_StartAndListRooms(t *testing.T) {
|
||||
env := readIntegrationEnv(t)
|
||||
s, cleanup := buildService(t, env)
|
||||
defer cleanup()
|
||||
|
||||
if err := s.Start(env.User); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
|
||||
rooms, err := s.ListRooms()
|
||||
if err != nil {
|
||||
t.Fatalf("ListRooms: %v", err)
|
||||
}
|
||||
if len(rooms) == 0 {
|
||||
t.Fatalf("ListRooms returned 0 rooms — user has no joined rooms? unexpected for an integration test account")
|
||||
}
|
||||
t.Logf("ListRooms OK: %d rooms (first=%s)", len(rooms), rooms[0].RoomID)
|
||||
}
|
||||
|
||||
func TestIntegration_GetDiagnostics(t *testing.T) {
|
||||
env := readIntegrationEnv(t)
|
||||
s, cleanup := buildService(t, env)
|
||||
defer cleanup()
|
||||
|
||||
if err := s.Start(env.User); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
|
||||
d := s.GetDiagnostics()
|
||||
if !d.Started {
|
||||
t.Errorf("Diagnostics.Started=false (want true)")
|
||||
}
|
||||
if !d.ClientReady {
|
||||
t.Errorf("Diagnostics.ClientReady=false (want true)")
|
||||
}
|
||||
if !d.SyncActive {
|
||||
t.Errorf("Diagnostics.SyncActive=false (want true)")
|
||||
}
|
||||
if d.HomeserverURL == "" {
|
||||
t.Errorf("Diagnostics.HomeserverURL empty")
|
||||
}
|
||||
if d.RoomsCount < 1 {
|
||||
t.Errorf("Diagnostics.RoomsCount=%d (want >=1)", d.RoomsCount)
|
||||
}
|
||||
if d.LastError != "" {
|
||||
t.Errorf("Diagnostics.LastError=%q (want empty)", d.LastError)
|
||||
}
|
||||
t.Logf("Diagnostics OK: rooms=%d encrypted=%d dms=%d", d.RoomsCount, d.EncryptedRooms, d.DMsCount)
|
||||
}
|
||||
|
||||
func TestIntegration_RoundtripMessage(t *testing.T) {
|
||||
env := readIntegrationEnv(t)
|
||||
if env.TestRoom == "" {
|
||||
t.Skip("set MATRIX_INTEGRATION_TEST_ROOM=!abcd:server to enable the send/recv round-trip")
|
||||
}
|
||||
s, cleanup := buildService(t, env)
|
||||
defer cleanup()
|
||||
|
||||
if err := s.Start(env.User); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
|
||||
body := fmt.Sprintf("e2e test %d", time.Now().UnixNano())
|
||||
evID, err := s.SendText(env.TestRoom, body)
|
||||
if err != nil {
|
||||
t.Fatalf("SendText: %v", err)
|
||||
}
|
||||
t.Logf("SendText OK: event_id=%s body=%q", evID, body)
|
||||
|
||||
// Poll LoadTimeline up to 10s waiting for our event to appear.
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
for {
|
||||
msgs, err := s.LoadTimeline(env.TestRoom, 20)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadTimeline: %v", err)
|
||||
}
|
||||
for _, m := range msgs {
|
||||
if m.EventID == evID {
|
||||
if m.Body != body {
|
||||
// In an encrypted room the body may come back blank (EncryptedRaw=true)
|
||||
// — still counts as round-trip success: the event landed.
|
||||
if !m.EncryptedRaw {
|
||||
t.Errorf("event body mismatch: got %q want %q", m.Body, body)
|
||||
}
|
||||
}
|
||||
t.Logf("Round-trip OK: event_id=%s found after %v", evID, time.Since(deadline.Add(-10*time.Second)))
|
||||
return
|
||||
}
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("event %s never appeared in LoadTimeline after 10s", evID)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check that the homeserver is reachable from the test host before we
|
||||
// burn the rest of the test budget on keyring + sync setup. Skips if env is
|
||||
// missing so it doesn't run silently in CI either.
|
||||
func TestIntegration_HomeserverReachable(t *testing.T) {
|
||||
env := readIntegrationEnv(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
uid, did, err := whoami(ctx, integrationHomeserverURL, env.Token)
|
||||
if err != nil {
|
||||
t.Fatalf("whoami(%s): %v", integrationHomeserverURL, err)
|
||||
}
|
||||
if uid != env.User {
|
||||
t.Errorf("whoami user_id=%q want %q", uid, env.User)
|
||||
}
|
||||
// device_id may be empty when MAS-issued tokens lack device binding —
|
||||
// don't fail on it, just log.
|
||||
t.Logf("whoami OK: user=%s device=%s", uid, did)
|
||||
_ = id.UserID(uid)
|
||||
}
|
||||
Reference in New Issue
Block a user