pw_rpc: Fix destructor ordering problem in test helpers

During destruction, the FakeChannelOutput& in the InvocationContext base
class would be used to send a final RPC message. The ChannelOutput
instance it pointed to was in a derived class, so would be destructed
before the RPC packet was sent, resulting in "pure virtual function
call" crashes in tests.

- Move the FakeChannelOutput instance to the base InvocationContext so
  it is destructed last, after any responses are sent.
- Explicitly delete some copies/moves.
- Fix typo in transfer_test.cc.
- Handle some ignored Status returns.

Change-Id: I1439dc9af59070507c5dbedab58211f2d165c35d
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/59761
Reviewed-by: Carlos Chinchilla <cachinchilla@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
Pigweed-Auto-Submit: Wyatt Hepler <hepler@google.com>
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
index facdb0f..d04110e 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
@@ -13,8 +13,8 @@
 // the License.
 #pragma once
 
-#include <array>
 #include <cstddef>
+#include <cstdint>
 
 #include "pw_assert/assert.h"
 #include "pw_bytes/span.h"
@@ -23,16 +23,11 @@
 #include "pw_rpc/nanopb/internal/method.h"
 
 namespace pw::rpc {
-namespace internal::test::nanopb {
-
-template <typename, auto, uint32_t, size_t, size_t>
-class NanopbInvocationContext;
-
-}  // namespace internal::test::nanopb
 
 // A ChannelOutput implementation that stores the outgoing payloads and status.
 template <typename Response, size_t kMaxResponses, size_t kOutputSize>
-class NanopbFakeChannelOutput final : public internal::test::FakeChannelOutput {
+class NanopbFakeChannelOutput final
+    : public internal::test::FakeChannelOutputBuffer<kOutputSize> {
  public:
   template <auto kMethod, uint32_t kMethodId, typename ServiceType>
   static NanopbFakeChannelOutput Create() {
@@ -41,6 +36,13 @@
         internal::MethodTraits<decltype(kMethod)>::kType);
   }
 
+  // Private constructor, do not use. This constructor is exposed so this class
+  // can be constructed using std::make_from_tuple in InvocationContext.
+  NanopbFakeChannelOutput(MethodType method_type,
+                          const internal::NanopbMethod& kMethod)
+      : internal::test::FakeChannelOutputBuffer<kOutputSize>(method_type),
+        method_(kMethod) {}
+
   const Vector<Response>& responses() const { return responses_; }
 
   const Response& last_response() const {
@@ -56,13 +58,6 @@
   }
 
  private:
-  template <typename, auto, uint32_t, size_t, size_t>
-  friend class NanopbInvocationContext;
-
-  NanopbFakeChannelOutput(const internal::NanopbMethod& kMethod,
-                          MethodType method_type)
-      : FakeChannelOutput(packet_buffer_, method_type), method_(kMethod) {}
-
   void AppendResponse(ConstByteSpan response) override {
     Response& response_struct = AllocateResponse();
     PW_ASSERT(method_.serde().DecodeResponse(response, &response_struct));
@@ -72,7 +67,6 @@
 
   const internal::NanopbMethod& method_;
   Vector<Response, kMaxResponses> responses_;
-  std::array<std::byte, kOutputSize> packet_buffer_;
 };
 
 }  // namespace pw::rpc
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
index 85065b1..c36f76a 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
@@ -30,7 +30,7 @@
 
 namespace test {
 
-template <typename, uint32_t>
+template <typename, typename, uint32_t>
 class InvocationContext;
 
 }  // namespace test
@@ -129,7 +129,7 @@
  private:
   friend class internal::NanopbMethod;
 
-  template <typename, uint32_t>
+  template <typename, typename, uint32_t>
   friend class internal::test::InvocationContext;
 
   NanopbServerReaderWriter(const internal::CallContext& call)
@@ -166,7 +166,7 @@
  private:
   friend class internal::NanopbMethod;
 
-  template <typename, uint32_t>
+  template <typename, typename, uint32_t>
   friend class internal::test::InvocationContext;
 
   NanopbServerReader(const internal::CallContext& call)
@@ -210,7 +210,7 @@
  private:
   friend class internal::NanopbMethod;
 
-  template <typename, uint32_t>
+  template <typename, typename, uint32_t>
   friend class internal::test::InvocationContext;
 
   NanopbServerWriter(const internal::CallContext& call)
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/test_method_context.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/test_method_context.h
index c09f59f..c530f17 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/test_method_context.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/test_method_context.h
@@ -92,55 +92,51 @@
           uint32_t kMethodId,
           size_t kMaxResponses,
           size_t kOutputSize>
-class NanopbInvocationContext : public InvocationContext<Service, kMethodId> {
+class NanopbInvocationContext
+    : public InvocationContext<
+          NanopbFakeChannelOutput<internal::Response<kMethod>,
+                                  kMaxResponses,
+                                  kOutputSize>,
+          Service,
+          kMethodId> {
  public:
   using Request = internal::Request<kMethod>;
   using Response = internal::Response<kMethod>;
 
-  // Returns the responses that have been recorded. The maximum number of
-  // responses is responses().max_size(). responses().back() is always the most
-  // recent response, even if total_responses() > responses().max_size().
-  const Vector<Response>& responses() const { return output_.responses(); }
-
-  // Gives access to the RPC's response.
-  const Response& response() const {
-    PW_ASSERT(!responses().empty());
-    return responses().back();
-  }
+  // Gives access to the RPC's most recent response.
+  const Response& response() const { return Base::output().last_response(); }
 
  protected:
   template <typename... Args>
   NanopbInvocationContext(Args&&... args)
-      : InvocationContext<Service, kMethodId>(
-            kMethodInfo, output_, std::forward<Args>(args)...),
-        output_(
-            decltype(output_)::template Create<kMethod, kMethodId, Service>()) {
-  }
-
-  NanopbFakeChannelOutput<Response, kMaxResponses, kOutputSize>& output() {
-    return output_;
-  }
+      : Base(kMethodInfo,
+             std::forward_as_tuple(MethodTraits<decltype(kMethod)>::kType,
+                                   kMethodInfo),
+             std::forward<Args>(args)...) {}
 
   void SendClientStream(const Request& request) {
     // Borrow a buffer from the ChannelOutput for sending the request.
-    ChannelOutput& channel_output = static_cast<rpc::ChannelOutput&>(output());
+    ChannelOutput& channel_output = Base::output();
     std::span buffer = channel_output.AcquireBuffer();
 
-    InvocationContext<Service, kMethodId>::SendClientStream(buffer.first(
+    Base::SendClientStream(buffer.first(
         kMethodInfo.serde().EncodeRequest(&request, buffer).size()));
 
     channel_output.DiscardBuffer(buffer);
   }
 
  private:
+  using Base = InvocationContext<
+      NanopbFakeChannelOutput<Response, kMaxResponses, kOutputSize>,
+      Service,
+      kMethodId>;
+
   static constexpr NanopbMethod kMethodInfo =
       MethodLookup::GetNanopbMethod<Service, kMethodId>();
-
-  NanopbFakeChannelOutput<Response, kMaxResponses, kOutputSize> output_;
 };
 
 // Method invocation context for a unary RPC. Returns the status in
-// server_call() and provides the response through the response() method.
+// call_context() and provides the response through the response() method.
 template <typename Service,
           auto kMethod,
           uint32_t kMethodId,
@@ -150,6 +146,7 @@
                                                     kMethodId,
                                                     1,
                                                     kOutputSize> {
+ private:
   using Base =
       NanopbInvocationContext<Service, kMethod, kMethodId, 1, kOutputSize>;
 
@@ -165,7 +162,7 @@
     Base::output().clear();
     Response& response = Base::output().AllocateResponse();
     return CallMethodImplFunction<kMethod>(
-        Base::server_call(), request, response);
+        Base::call_context(), request, response);
   }
 };
 
diff --git a/pw_rpc/public/pw_rpc/internal/fake_channel_output.h b/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
index 2a2329c..355f2ce 100644
--- a/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
+++ b/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
@@ -13,6 +13,8 @@
 // the License.
 #pragma once
 
+#include <cstddef>
+
 #include "pw_bytes/span.h"
 #include "pw_rpc/channel.h"
 #include "pw_rpc/method_type.h"
@@ -22,6 +24,12 @@
 // A ChannelOutput implementation that stores the outgoing payloads and status.
 class FakeChannelOutput : public ChannelOutput {
  public:
+  FakeChannelOutput(const FakeChannelOutput&) = delete;
+  FakeChannelOutput(FakeChannelOutput&&) = delete;
+
+  FakeChannelOutput& operator=(const FakeChannelOutput&) = delete;
+  FakeChannelOutput& operator=(FakeChannelOutput&&) = delete;
+
   Status last_status() const {
     PW_ASSERT(done());
     return last_status_;
@@ -35,9 +43,9 @@
   void clear();
 
  protected:
-  constexpr FakeChannelOutput(ByteSpan buffer, MethodType method_type)
+  constexpr FakeChannelOutput(MethodType method_type, ByteSpan packet_buffer)
       : ChannelOutput("pw::rpc::internal::test::FakeChannelOutput"),
-        packet_buffer_(buffer),
+        packet_buffer_(packet_buffer),
         method_type_(method_type) {}
 
  private:
@@ -60,4 +68,14 @@
   const MethodType method_type_;
 };
 
+// Adds the packet output buffer to a FakeChannelOutput.
+template <size_t kOutputSizeBytes>
+class FakeChannelOutputBuffer : public FakeChannelOutput {
+ protected:
+  constexpr FakeChannelOutputBuffer(MethodType method_type)
+      : FakeChannelOutput(method_type, packet_bytes), packet_bytes{} {}
+
+  std::byte packet_bytes[kOutputSizeBytes];
+};
+
 }  // namespace pw::rpc::internal::test
diff --git a/pw_rpc/public/pw_rpc/internal/test_method_context.h b/pw_rpc/public/pw_rpc/internal/test_method_context.h
index 9f162f2..a7b7261 100644
--- a/pw_rpc/public/pw_rpc/internal/test_method_context.h
+++ b/pw_rpc/public/pw_rpc/internal/test_method_context.h
@@ -13,7 +13,6 @@
 // the License.
 #pragma once
 
-#include <array>
 #include <cstddef>
 
 #include "pw_assert/assert.h"
@@ -26,9 +25,15 @@
 namespace pw::rpc::internal::test {
 
 // Collects everything needed to invoke a particular RPC.
-template <typename Service, uint32_t kMethodId>
+template <typename Output, typename Service, uint32_t kMethodId>
 class InvocationContext {
  public:
+  InvocationContext(const InvocationContext&) = delete;
+  InvocationContext(InvocationContext&&) = delete;
+
+  InvocationContext& operator=(const InvocationContext&) = delete;
+  InvocationContext& operator=(InvocationContext&&) = delete;
+
   Service& service() { return service_; }
 
   // Sets the channel ID, which defaults to an arbitrary value.
@@ -38,6 +43,11 @@
   // responses.max_size().
   size_t total_responses() const { return output_.total_responses(); }
 
+  // Returns the responses that have been recorded. The maximum number of
+  // responses is responses().max_size(). responses().back() is always the most
+  // recent response, even if total_responses() > responses().max_size().
+  const auto& responses() const { return output().responses(); }
+
   // True if the RPC has completed.
   bool done() const { return output_.done(); }
 
@@ -76,21 +86,28 @@
   }
 
  protected:
-  template <typename... Args>
+  // Constructs the invocation context. The args for the ChannelOutput type are
+  // passed in a std::tuple. The args for the Service are forwarded directly
+  // from the callsite.
+  template <typename OutputArgTuple, typename... ServiceArgs>
   InvocationContext(const Method& method,
-                    FakeChannelOutput& output,
-                    Args&&... service_args)
-      : output_(output),
+                    OutputArgTuple&& output_args,
+                    ServiceArgs&&... service_args)
+      : output_(std::make_from_tuple<Output>(
+            std::forward<OutputArgTuple>(output_args))),
         channel_(Channel::Create<123>(&output_)),
         server_(std::span(&channel_, 1)),
-        service_(std::forward<Args>(service_args)...),
-        server_call_(static_cast<internal::Server&>(server_),
-                     static_cast<internal::Channel&>(channel_),
-                     service_,
-                     method) {
+        service_(std::forward<ServiceArgs>(service_args)...),
+        context_(static_cast<internal::Server&>(server_),
+                 static_cast<internal::Channel&>(channel_),
+                 service_,
+                 method) {
     server_.RegisterService(service_);
   }
 
+  const Output& output() const { return output_; }
+  Output& output() { return output_; }
+
   template <size_t kMaxPayloadSize = 32>
   void SendClientStream(ConstByteSpan payload) {
     std::byte packet[kNoPayloadPacketSizeBytes + 3 + kMaxPayloadSize];
@@ -126,28 +143,26 @@
     output_.clear();
     T responder = GetResponder<T>();
     return CallMethodImplFunction<kMethod>(
-        InvocationContext<Service, kMethodId>::server_call(),
-        std::forward<RequestArg>(request)...,
-        responder);
+        call_context(), std::forward<RequestArg>(request)..., responder);
   }
 
   template <typename T>
   T GetResponder() {
-    return T(InvocationContext<Service, kMethodId>::server_call());
+    return T(call_context());
   }
 
-  internal::CallContext& server_call() { return server_call_; }
+  internal::CallContext& call_context() { return context_; }
 
  private:
   static constexpr size_t kNoPayloadPacketSizeBytes =
       2 /* type */ + 2 /* channel */ + 5 /* service */ + 5 /* method */ +
       2 /* status */;
 
-  FakeChannelOutput& output_;
+  Output output_;
   rpc::Channel channel_;
   rpc::Server server_;
   Service service_;
-  internal::CallContext server_call_;
+  internal::CallContext context_;
 };
 
 }  // namespace pw::rpc::internal::test
diff --git a/pw_rpc/raw/codegen_test.cc b/pw_rpc/raw/codegen_test.cc
index 6d7de57..dcf6ba4 100644
--- a/pw_rpc/raw/codegen_test.cc
+++ b/pw_rpc/raw/codegen_test.cc
@@ -26,10 +26,8 @@
   Vector<std::byte, 64> buffer(64);
   test::TestRequest::MemoryEncoder test_request(buffer);
 
-  test_request.WriteInteger(integer)
-      .IgnoreError();  // TODO(pwbug/387): Handle Status properly
-  test_request.WriteStatusCode(status.code())
-      .IgnoreError();  // TODO(pwbug/387): Handle Status properly
+  EXPECT_EQ(OkStatus(), test_request.WriteInteger(integer));
+  EXPECT_EQ(OkStatus(), test_request.WriteStatusCode(status.code()));
 
   EXPECT_EQ(OkStatus(), test_request.status());
   buffer.resize(test_request.size());
@@ -40,8 +38,7 @@
   Vector<std::byte, 64> buffer(64);
   test::TestStreamResponse::MemoryEncoder test_response(buffer);
 
-  test_response.WriteNumber(number)
-      .IgnoreError();  // TODO(pwbug/387): Handle Status properly
+  EXPECT_EQ(OkStatus(), test_response.WriteNumber(number));
 
   EXPECT_EQ(OkStatus(), test_response.status());
   buffer.resize(test_response.size());
@@ -65,8 +62,7 @@
     }
 
     TestResponse::MemoryEncoder test_response(response);
-    test_response.WriteValue(integer + 1)
-        .IgnoreError();  // TODO(pwbug/387): Handle Status properly
+    EXPECT_EQ(OkStatus(), test_response.WriteValue(integer + 1));
 
     return StatusWithSize(status, test_response.size());
   }
@@ -82,24 +78,20 @@
       ByteSpan buffer = writer.PayloadBuffer();
 
       TestStreamResponse::MemoryEncoder test_stream_response(buffer);
-      test_stream_response.WriteNumber(i)
-          .IgnoreError();  // TODO(pwbug/387): Handle Status properly
-      writer.Write(test_stream_response)
-          .IgnoreError();  // TODO(pwbug/387): Handle Status properly
+      EXPECT_EQ(OkStatus(), test_stream_response.WriteNumber(i));
+      EXPECT_EQ(OkStatus(), writer.Write(test_stream_response));
     }
 
-    writer.Finish(status)
-        .IgnoreError();  // TODO(pwbug/387): Handle Status properly
+    EXPECT_EQ(OkStatus(), writer.Finish(status));
   }
 
   void TestClientStreamRpc(ServerContext&, RawServerReader& reader) {
     last_reader_ = std::move(reader);
 
     last_reader_.set_on_next([this](ConstByteSpan payload) {
-      last_reader_
-          .Finish(EncodeResponse(ReadInteger(payload)),
-                  Status::Unauthenticated())
-          .IgnoreError();  // TODO(pwbug/387): Handle Status properly
+      EXPECT_EQ(OkStatus(),
+                last_reader_.Finish(EncodeResponse(ReadInteger(payload)),
+                                    Status::Unauthenticated()));
     });
   }
 
@@ -108,10 +100,10 @@
     last_reader_writer_ = std::move(reader_writer);
 
     last_reader_writer_.set_on_next([this](ConstByteSpan payload) {
-      last_reader_writer_.Write(EncodeResponse(ReadInteger(payload)))
-          .IgnoreError();  // TODO(pwbug/387): Handle Status properly
-      last_reader_writer_.Finish(Status::NotFound())
-          .IgnoreError();  // TODO(pwbug/387): Handle Status properly
+      EXPECT_EQ(
+          OkStatus(),
+          last_reader_writer_.Write(EncodeResponse(ReadInteger(payload))));
+      EXPECT_EQ(OkStatus(), last_reader_writer_.Finish(Status::NotFound()));
     });
   }
 
@@ -192,8 +184,7 @@
     switch (static_cast<test::TestResponse::Fields>(decoder.FieldNumber())) {
       case test::TestResponse::Fields::VALUE: {
         int32_t value;
-        decoder.ReadInt32(&value)
-            .IgnoreError();  // TODO(pwbug/387): Handle Status properly
+        EXPECT_EQ(OkStatus(), decoder.ReadInt32(&value));
         EXPECT_EQ(value, 124);
         break;
       }
@@ -215,8 +206,7 @@
         static_cast<test::TestStreamResponse::Fields>(decoder.FieldNumber())) {
       case test::TestStreamResponse::Fields::NUMBER: {
         int32_t value;
-        decoder.ReadInt32(&value)
-            .IgnoreError();  // TODO(pwbug/387): Handle Status properly
+        EXPECT_EQ(OkStatus(), decoder.ReadInt32(&value));
         EXPECT_EQ(value, 4);
         break;
       }
@@ -234,8 +224,7 @@
     switch (
         static_cast<test::TestStreamResponse::Fields>(decoder.FieldNumber())) {
       case test::TestStreamResponse::Fields::NUMBER: {
-        decoder.ReadInt32(&value)
-            .IgnoreError();  // TODO(pwbug/387): Handle Status properly
+        EXPECT_EQ(OkStatus(), decoder.ReadInt32(&value));
         break;
       }
       default:
diff --git a/pw_rpc/raw/public/pw_rpc/raw/fake_channel_output.h b/pw_rpc/raw/public/pw_rpc/raw/fake_channel_output.h
index 55ab6c2..c7b62fd 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/fake_channel_output.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/fake_channel_output.h
@@ -26,10 +26,11 @@
 
 // A ChannelOutput implementation that stores the outgoing payloads and status.
 template <size_t kOutputSize, size_t kMaxResponses>
-class RawFakeChannelOutput final : public internal::test::FakeChannelOutput {
+class RawFakeChannelOutput final
+    : public internal::test::FakeChannelOutputBuffer<kOutputSize> {
  public:
   RawFakeChannelOutput(MethodType method_type)
-      : FakeChannelOutput(packet_buffer_, method_type) {}
+      : internal::test::FakeChannelOutputBuffer<kOutputSize>(method_type) {}
 
   const Vector<ByteSpan>& responses() const { return responses_; }
 
@@ -65,7 +66,6 @@
     response_buffers_.clear();
   }
 
-  std::array<std::byte, kOutputSize> packet_buffer_;
   Vector<ByteSpan, kMaxResponses> responses_;
   Vector<std::array<std::byte, kOutputSize>, kMaxResponses> response_buffers_;
 };
diff --git a/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h b/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
index 39852f8..7c3523a 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
@@ -31,7 +31,7 @@
 
 namespace test {
 
-template <typename, uint32_t>
+template <typename, typename, uint32_t>
 class InvocationContext;
 
 }  // namespace test
@@ -88,7 +88,7 @@
  private:
   friend class internal::RawMethod;
 
-  template <typename, uint32_t>
+  template <typename, typename, uint32_t>
   friend class internal::test::InvocationContext;
 };
 
@@ -119,7 +119,7 @@
  private:
   friend class internal::RawMethod;  // Needed for conversions from ReaderWriter
 
-  template <typename, uint32_t>
+  template <typename, typename, uint32_t>
   friend class internal::test::InvocationContext;
 
   RawServerReader(const internal::CallContext& call)
@@ -149,7 +149,7 @@
  private:
   friend class RawServerReaderWriter;  // Needed for conversions.
 
-  template <typename, uint32_t>
+  template <typename, typename, uint32_t>
   friend class internal::test::InvocationContext;
 
   friend class internal::RawMethod;
diff --git a/pw_rpc/raw/public/pw_rpc/raw/test_method_context.h b/pw_rpc/raw/public/pw_rpc/raw/test_method_context.h
index d0d257f..33b2c84 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/test_method_context.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/test_method_context.h
@@ -96,32 +96,26 @@
           uint32_t kMethodId,
           size_t kMaxResponses,
           size_t kOutputSize>
-class RawInvocationContext : public InvocationContext<Service, kMethodId> {
+class RawInvocationContext
+    : public InvocationContext<RawFakeChannelOutput<kOutputSize, kMaxResponses>,
+                               Service,
+                               kMethodId> {
  public:
-  // Returns the responses that have been recorded. The maximum number of
-  // responses is responses().max_size(). responses().back() is always the most
-  // recent response, even if total_responses() > responses().max_size().
-  const Vector<ByteSpan>& responses() const { return output_.responses(); }
-
   // Gives access to the RPC's most recent response.
-  ConstByteSpan response() const {
-    PW_ASSERT(!responses().empty());
-    return responses().back();
-  }
+  ConstByteSpan response() const { return Base::output().last_response(); }
 
  protected:
   template <typename... Args>
   RawInvocationContext(Args&&... args)
-      : InvocationContext<Service, kMethodId>(
-            MethodLookup::GetRawMethod<Service, kMethodId>(),
-            output_,
-            std::forward<Args>(args)...),
-        output_(MethodTraits<decltype(kMethod)>::kType) {}
-
-  RawFakeChannelOutput<kOutputSize, kMaxResponses>& output() { return output_; }
+      : Base(MethodLookup::GetRawMethod<Service, kMethodId>(),
+             std::forward_as_tuple(MethodTraits<decltype(kMethod)>::kType),
+             std::forward<Args>(args)...) {}
 
  private:
-  RawFakeChannelOutput<kOutputSize, kMaxResponses> output_;
+  using Base =
+      InvocationContext<RawFakeChannelOutput<kOutputSize, kMaxResponses>,
+                        Service,
+                        kMethodId>;
 };
 
 // Method invocation context for a unary RPC. Returns the status in call() and
@@ -143,8 +137,8 @@
   StatusWithSize call(ConstByteSpan request) {
     Base::output().clear();
     ByteSpan& response = Base::output().AllocateResponse();
-    auto sws =
-        CallMethodImplFunction<kMethod>(Base::server_call(), request, response);
+    auto sws = CallMethodImplFunction<kMethod>(
+        Base::call_context(), request, response);
     response = response.first(sws.size());
     return sws;
   }
diff --git a/pw_transfer/public/pw_transfer/transfer.h b/pw_transfer/public/pw_transfer/transfer.h
index febe3d1..9a512e0 100644
--- a/pw_transfer/public/pw_transfer/transfer.h
+++ b/pw_transfer/public/pw_transfer/transfer.h
@@ -45,6 +45,12 @@
         max_chunk_size_bytes_(max_chunk_size_bytes),
         default_max_bytes_to_receive_(default_max_bytes_to_receive) {}
 
+  TransferService(const TransferService&) = delete;
+  TransferService(TransferService&&) = delete;
+
+  TransferService& operator=(const TransferService&) = delete;
+  TransferService& operator=(TransferService&&) = delete;
+
   void Read(ServerContext&, RawServerReaderWriter& reader_writer);
 
   void Write(ServerContext&, RawServerReaderWriter& reader_writer);
diff --git a/pw_transfer/transfer_test.cc b/pw_transfer/transfer_test.cc
index 0032caf..e29fe3c 100644
--- a/pw_transfer/transfer_test.cc
+++ b/pw_transfer/transfer_test.cc
@@ -71,7 +71,7 @@
   stream::MemoryReader reader_;
 };
 
-TEST(Tranfser, Read_SingleChunk) {
+TEST(Transfer, Read_SingleChunk) {
   constexpr auto data = bytes::Initialized<32>([](size_t i) { return i; });
   SimpleReadTransfer handler(3, data);
 
@@ -108,7 +108,7 @@
   EXPECT_EQ(handler.finalize_read_status, OkStatus());
 }
 
-TEST(Tranfser, Read_MultiChunk) {
+TEST(Transfer, Read_MultiChunk) {
   constexpr auto data = bytes::Initialized<32>([](size_t i) { return i; });
   SimpleReadTransfer handler(3, data);
 
@@ -157,7 +157,7 @@
   EXPECT_EQ(handler.finalize_read_status, OkStatus());
 }
 
-TEST(Tranfser, Read_MaxChunkSize_Client) {
+TEST(Transfer, Read_MaxChunkSize_Client) {
   constexpr auto data = bytes::Initialized<32>([](size_t i) { return i; });
   SimpleReadTransfer handler(3, data);
 
@@ -212,7 +212,7 @@
   EXPECT_EQ(handler.finalize_read_status, OkStatus());
 }
 
-TEST(Tranfser, Read_MaxChunkSize_Server) {
+TEST(Transfer, Read_MaxChunkSize_Server) {
   constexpr auto data = bytes::Initialized<32>([](size_t i) { return i; });
   SimpleReadTransfer handler(3, data);
 
@@ -269,7 +269,7 @@
   EXPECT_EQ(handler.finalize_read_status, OkStatus());
 }
 
-TEST(Tranfser, Read_ClientError) {
+TEST(Transfer, Read_ClientError) {
   constexpr auto data = bytes::Initialized<32>([](size_t i) { return i; });
   SimpleReadTransfer handler(3, data);
 
@@ -294,7 +294,7 @@
   EXPECT_EQ(handler.finalize_read_status, Status::OutOfRange());
 }
 
-TEST(Tranfser, Read_MalformedParametersChunk) {
+TEST(Transfer, Read_MalformedParametersChunk) {
   constexpr auto data = bytes::Initialized<32>([](size_t i) { return i; });
   SimpleReadTransfer handler(3, data);
 
@@ -318,7 +318,7 @@
   EXPECT_EQ(chunk.status.value(), Status::InvalidArgument());
 }
 
-TEST(Tranfser, Read_UnregisteredHandler) {
+TEST(Transfer, Read_UnregisteredHandler) {
   PW_RAW_TEST_METHOD_CONTEXT(TransferService, Read) ctx(64, 64);
 
   ctx.call();
@@ -647,7 +647,7 @@
   int call_count_;
 };
 
-TEST(Tranfser, PrepareError) {
+TEST(Transfer, PrepareError) {
   constexpr auto data = bytes::Initialized<32>([](size_t i) { return i; });
   SometimesUnavailableReadHandler handler(3, data);