//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/. 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) }