package main import ( "context" "io" "log" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" ) func newTestAS(t *testing.T, dispatched *[][]Event) (*AppService, *Store) { t.Helper() st, err := OpenStore(filepath.Join(t.TempDir(), "t.db")) if err != nil { t.Fatalf("open store: %v", err) } as := NewAppService( &Config{HSToken: "secret", BotMXID: "@ai:vojo.chat"}, log.New(io.Discard, "", 0), st, func(_ context.Context, ev []Event) { *dispatched = append(*dispatched, ev) }, ) as.baseCtx = context.Background() return as, st } func txnReq(txnID, auth, body string) *http.Request { r := httptest.NewRequest(http.MethodPut, "/_matrix/app/v1/transactions/"+txnID, strings.NewReader(body)) r.SetPathValue("txnId", txnID) if auth != "" { r.Header.Set("Authorization", "Bearer "+auth) } return r } func TestTransactionAuthAndIdempotency(t *testing.T) { var dispatched [][]Event as, st := newTestAS(t, &dispatched) defer st.Close() body := `{"events":[{"type":"m.room.message","room_id":"!r:vojo.chat","event_id":"$1","sender":"@u:vojo.chat"}]}` // Bad hs_token → 403, nothing dispatched. w := httptest.NewRecorder() as.handleTransaction(w, txnReq("txn1", "wrong", body)) if w.Code != http.StatusForbidden { t.Fatalf("bad token: got %d, want 403", w.Code) } if len(dispatched) != 0 { t.Fatalf("bad token must not dispatch, got %d", len(dispatched)) } // Good hs_token → 200, one batch dispatched. w = httptest.NewRecorder() as.handleTransaction(w, txnReq("txn1", "secret", body)) if w.Code != http.StatusOK { t.Fatalf("good token: got %d, want 200", w.Code) } if len(dispatched) != 1 || len(dispatched[0]) != 1 { t.Fatalf("expected one dispatched batch of one event, got %v", dispatched) } // Same txnId again → idempotent no-op (still 200, no re-dispatch). w = httptest.NewRecorder() as.handleTransaction(w, txnReq("txn1", "secret", body)) if w.Code != http.StatusOK { t.Fatalf("retry: got %d, want 200", w.Code) } if len(dispatched) != 1 { t.Fatalf("retried transaction must not re-dispatch, got %d batches", len(dispatched)) } } func TestTransactionLegacyQueryTokenAccepted(t *testing.T) { var dispatched [][]Event as, st := newTestAS(t, &dispatched) defer st.Close() r := httptest.NewRequest(http.MethodPut, "/transactions/txnX?access_token=secret", strings.NewReader(`{"events":[]}`)) r.SetPathValue("txnId", "txnX") w := httptest.NewRecorder() as.handleTransaction(w, r) if w.Code != http.StatusOK { t.Fatalf("legacy access_token query: got %d, want 200", w.Code) } } func TestUserQuery(t *testing.T) { var dispatched [][]Event as, st := newTestAS(t, &dispatched) defer st.Close() mk := func(uid string) *http.Request { r := httptest.NewRequest(http.MethodGet, "/_matrix/app/v1/users/"+uid, nil) r.SetPathValue("userId", uid) r.Header.Set("Authorization", "Bearer secret") return r } w := httptest.NewRecorder() as.handleUserQuery(w, mk("@ai:vojo.chat")) if w.Code != http.StatusOK { t.Fatalf("own user: got %d, want 200", w.Code) } w = httptest.NewRecorder() as.handleUserQuery(w, mk("@someone:vojo.chat")) if w.Code != http.StatusNotFound { t.Fatalf("foreign user: got %d, want 404", w.Code) } }