diff --git a/api/protos/NetRemoteService.proto b/api/protos/NetRemoteService.proto index 849ecfbb..bba9402c 100644 --- a/api/protos/NetRemoteService.proto +++ b/api/protos/NetRemoteService.proto @@ -15,10 +15,13 @@ service NetRemote rpc WifiAccessPointsEnumerate (Microsoft.Net.Remote.Wifi.WifiAccessPointsEnumerateRequest) returns (Microsoft.Net.Remote.Wifi.WifiAccessPointsEnumerateResult); rpc WifiAccessPointEnable (Microsoft.Net.Remote.Wifi.WifiAccessPointEnableRequest) returns (Microsoft.Net.Remote.Wifi.WifiAccessPointEnableResult); rpc WifiAccessPointDisable (Microsoft.Net.Remote.Wifi.WifiAccessPointDisableRequest) returns (Microsoft.Net.Remote.Wifi.WifiAccessPointDisableResult); + rpc WifiAccessPointTimedEnable (Microsoft.Net.Remote.Wifi.WifiAccessPointTimedEnableRequest) returns (Microsoft.Net.Remote.Wifi.WifiAccessPointTimedEnableResult); + rpc WifiAccessPointTimedDisable (Microsoft.Net.Remote.Wifi.WifiAccessPointTimedDisableRequest) returns (Microsoft.Net.Remote.Wifi.WifiAccessPointTimedDisableResult); rpc WifiAccessPointSetPhyType (Microsoft.Net.Remote.Wifi.WifiAccessPointSetPhyTypeRequest) returns (Microsoft.Net.Remote.Wifi.WifiAccessPointSetPhyTypeResult); rpc WifiAccessPointSetFrequencyBands (Microsoft.Net.Remote.Wifi.WifiAccessPointSetFrequencyBandsRequest) returns (Microsoft.Net.Remote.Wifi.WifiAccessPointSetFrequencyBandsResult); rpc WifiAccessPointSetSsid (Microsoft.Net.Remote.Wifi.WifiAccessPointSetSsidRequest) returns (Microsoft.Net.Remote.Wifi.WifiAccessPointSetSsidResult); rpc WifiAccessPointSetNetworkBridge (Microsoft.Net.Remote.Wifi.WifiAccessPointSetNetworkBridgeRequest) returns (Microsoft.Net.Remote.Wifi.WifiAccessPointSetNetworkBridgeResult); rpc WifiAccessPointSetAuthenticationDot1x (Microsoft.Net.Remote.Wifi.WifiAccessPointSetAuthenticationDot1xRequest) returns (Microsoft.Net.Remote.Wifi.WifiAccessPointSetAuthenticationDot1xResult); rpc WifiAccessPointGetAttributes (Microsoft.Net.Remote.Wifi.WifiAccessPointGetAttributesRequest) returns (Microsoft.Net.Remote.Wifi.WifiAccessPointGetAttributesResult); + } diff --git a/api/protos/NetRemoteWifi.proto b/api/protos/NetRemoteWifi.proto index 4c3f3aaf..91f01679 100644 --- a/api/protos/NetRemoteWifi.proto +++ b/api/protos/NetRemoteWifi.proto @@ -137,3 +137,28 @@ message WifiAccessPointGetAttributesResult WifiAccessPointOperationStatus Status = 2; Microsoft.Net.Wifi.Dot11AccessPointAttributes Attributes = 3; } + +message WifiAccessPointTimedEnableRequest +{ + string AccessPointId = 1; + Microsoft.Net.Wifi.Dot11AccessPointConfiguration Configuration = 2; + uint32 DurationSeconds = 3; // Duration in seconds to keep the access point enabled +} + +message WifiAccessPointTimedEnableResult +{ + string AccessPointId = 1; + WifiAccessPointOperationStatus Status = 2; +} + +message WifiAccessPointTimedDisableRequest +{ + string AccessPointId = 1; + uint32 DurationSeconds = 2; // Duration in seconds to keep the access point disabled +} + +message WifiAccessPointTimedDisableResult +{ + string AccessPointId = 1; + WifiAccessPointOperationStatus Status = 2; +} diff --git a/src/common/service/NetRemoteService.cxx b/src/common/service/NetRemoteService.cxx index 4f158417..64b19ca7 100644 --- a/src/common/service/NetRemoteService.cxx +++ b/src/common/service/NetRemoteService.cxx @@ -1,12 +1,16 @@ #include +#include +#include #include #include +#include #include #include #include #include #include +#include #include #include @@ -279,6 +283,20 @@ NetRemoteService::NetRemoteService(std::shared_ptr networkManage { } +NetRemoteService::~NetRemoteService() +{ + // Signal shutdown to all timer threads + m_shutdown = true; + + // Wait for threads to complete outside of the lock + if (m_timedEnableThread && m_timedEnableThread->joinable()) { + m_timedEnableThread->join(); + } + if (m_timedDisableThread && m_timedDisableThread->joinable()) { + m_timedDisableThread->join(); + } +} + std::shared_ptr NetRemoteService::GetAccessPointManager() noexcept { @@ -435,6 +453,31 @@ NetRemoteService::WifiAccessPointGetAttributes([[maybe_unused]] grpc::ServerCont return grpc::Status::OK; } +grpc::Status +NetRemoteService::WifiAccessPointTimedEnable([[maybe_unused]] grpc::ServerContext* context, const WifiAccessPointTimedEnableRequest* request, WifiAccessPointTimedEnableResult* result) +{ + const NetRemoteWifiApiTrace traceMe{ request->accesspointid(), result->mutable_status() }; + + const auto* dot11AccessPointConfiguration{ request->has_configuration() ? &request->configuration() : nullptr }; + auto wifiOperationStatus = WifiAccessPointTimedEnableImpl(request->accesspointid(), dot11AccessPointConfiguration, request->durationseconds()); + result->set_accesspointid(request->accesspointid()); + *result->mutable_status() = std::move(wifiOperationStatus); + + return grpc::Status::OK; +} + +grpc::Status +NetRemoteService::WifiAccessPointTimedDisable([[maybe_unused]] grpc::ServerContext* context, const WifiAccessPointTimedDisableRequest* request, WifiAccessPointTimedDisableResult* result) +{ + const NetRemoteWifiApiTrace traceMe{ request->accesspointid(), result->mutable_status() }; + + auto wifiOperationStatus = WifiAccessPointTimedDisableImpl(request->accesspointid(), request->durationseconds()); + result->set_accesspointid(request->accesspointid()); + *result->mutable_status() = std::move(wifiOperationStatus); + + return grpc::Status::OK; +} + AccessPointOperationStatus NetRemoteService::TryGetAccessPoint(std::string_view accessPointId, std::shared_ptr& accessPoint) { @@ -642,6 +685,137 @@ NetRemoteService::WifiAccessPointDisableImpl(std::string_view accessPointId, std return wifiOperationStatus; } +WifiAccessPointOperationStatus +NetRemoteService::WifiAccessPointTimedEnableImpl(std::string_view accessPointId, const Dot11AccessPointConfiguration* dot11AccessPointConfiguration, uint32_t durationSeconds, std::shared_ptr accessPointController) +{ + WifiAccessPointOperationStatus wifiOperationStatus{}; + + // Validate duration - must be greater than 0 and cannot exceed 10 minutes (600 seconds) + constexpr uint32_t MaxDurationSeconds = 600; + if (durationSeconds == 0) { + wifiOperationStatus.set_code(WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeInvalidParameter); + wifiOperationStatus.set_message("Duration must be greater than 0 seconds"); + return wifiOperationStatus; + } + if (durationSeconds > MaxDurationSeconds) { + wifiOperationStatus.set_code(WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeInvalidParameter); + wifiOperationStatus.set_message(std::format("Duration {} seconds exceeds maximum allowed duration of {} seconds", durationSeconds, MaxDurationSeconds)); + return wifiOperationStatus; + } + + // Check if a timed enable operation is already running, create and store the timer thread. + // Right now, this service only supports managing singale access point. + { + std::lock_guard lock(m_threadsMutex); + if (m_timedEnableThread && m_timedEnableThread->joinable()) { + wifiOperationStatus.set_code(WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeOperationNotSupported); + wifiOperationStatus.set_message("A timed enable operation is already in progress"); + return wifiOperationStatus; + } + + // Create and store the timer thread + auto timerThread = std::make_shared([this, accessPointId = std::string(accessPointId), hasConfiguration = (dot11AccessPointConfiguration != nullptr), configurationCopy = dot11AccessPointConfiguration ? *dot11AccessPointConfiguration : Dot11AccessPointConfiguration{}, accessPointController, durationSeconds]() { + // Sleep for the specified duration, checking every second for shutdown signal + uint32_t secondsElapsed = 0; + while (secondsElapsed < durationSeconds && !m_shutdown.load()) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + secondsElapsed++; + } + + // If we were shutdown, exit early + if (m_shutdown.load()) { + LOGI << std::format("Timed enable operation for access point {} was cancelled due to service shutdown", accessPointId); + return; + } + + // Enable the access point after the duration expires + const auto* configPtr = hasConfiguration ? &configurationCopy : nullptr; + auto result = WifiAccessPointEnableImpl(accessPointId, configPtr, accessPointController); + if (result.code() != WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeSucceeded) { + LOGW << std::format("Failed to automatically enable access point {} after {} seconds: {}", + accessPointId, + durationSeconds, + result.message()); + } else { + LOGI << std::format("Successfully automatically enabled access point {} after {} seconds", + accessPointId, + durationSeconds); + } + }); + + m_timedEnableThread = timerThread; + } + + LOGI << std::format("Access point {} will be automatically enabled after {} seconds", accessPointId, durationSeconds); + wifiOperationStatus.set_code(WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeSucceeded); + return wifiOperationStatus; +} + +WifiAccessPointOperationStatus +NetRemoteService::WifiAccessPointTimedDisableImpl(std::string_view accessPointId, uint32_t durationSeconds, std::shared_ptr accessPointController) +{ + WifiAccessPointOperationStatus wifiOperationStatus{}; + + // Validate duration - must be greater than 0 and cannot exceed 10 minutes (600 seconds) + constexpr uint32_t MaxDurationSeconds = 600; + if (durationSeconds == 0) { + wifiOperationStatus.set_code(WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeInvalidParameter); + wifiOperationStatus.set_message("Duration must be greater than 0 seconds"); + return wifiOperationStatus; + } + if (durationSeconds > MaxDurationSeconds) { + wifiOperationStatus.set_code(WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeInvalidParameter); + wifiOperationStatus.set_message(std::format("Duration {} seconds exceeds maximum allowed duration of {} seconds", durationSeconds, MaxDurationSeconds)); + return wifiOperationStatus; + } + + // Check if a timed disable operation is already running, if not, create and store the timer thread. + // Right now, this service only supports managing singale access point. + { + std::lock_guard lock(m_threadsMutex); + if (m_timedDisableThread && m_timedDisableThread->joinable()) { + wifiOperationStatus.set_code(WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeOperationNotSupported); + wifiOperationStatus.set_message("A timed disable operation is already in progress"); + return wifiOperationStatus; + } + + // Create and store the timer thread + auto timerThread = std::make_shared([this, accessPointId = std::string(accessPointId), accessPointController, durationSeconds]() { + // Sleep for the specified duration, checking every second for shutdown signal + uint32_t secondsElapsed = 0; + while (secondsElapsed < durationSeconds && !m_shutdown.load()) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + secondsElapsed++; + } + + // If we were shutdown, exit early + if (m_shutdown.load()) { + LOGI << std::format("Timed disable operation for access point {} was cancelled due to service shutdown", accessPointId); + return; + } + + // Disable the access point using the existing implementation + auto result = WifiAccessPointDisableImpl(accessPointId, accessPointController); + if (result.code() != WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeSucceeded) { + LOGW << std::format("Failed to automatically disable access point {} after {} seconds: {}", + accessPointId, + durationSeconds, + result.message()); + } else { + LOGI << std::format("Successfully automatically disabled access point {} after {} seconds", + accessPointId, + durationSeconds); + } + }); + + m_timedDisableThread = timerThread; + } + + LOGI << std::format("Access point {} will be automatically disabled after {} seconds", accessPointId, durationSeconds); + wifiOperationStatus.set_code(WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeSucceeded); + return wifiOperationStatus; +} + WifiAccessPointOperationStatus NetRemoteService::WifiAccessPointSetPhyTypeImpl(std::string_view accessPointId, Dot11PhyType dot11PhyType, std::shared_ptr accessPointController) { diff --git a/src/common/service/include/microsoft/net/remote/service/NetRemoteService.hxx b/src/common/service/include/microsoft/net/remote/service/NetRemoteService.hxx index 806db09b..93aaa8b8 100644 --- a/src/common/service/include/microsoft/net/remote/service/NetRemoteService.hxx +++ b/src/common/service/include/microsoft/net/remote/service/NetRemoteService.hxx @@ -2,9 +2,13 @@ #ifndef NET_REMOTE_SERVICE_HXX #define NET_REMOTE_SERVICE_HXX +#include #include +#include #include #include +#include +#include #include #include @@ -37,6 +41,11 @@ public: */ explicit NetRemoteService(std::shared_ptr networkManager) noexcept; + /** + * @brief Destructor that waits for all timer threads to complete. + */ + ~NetRemoteService(); + /** * @brief Get the AccessPointManager object for this service. * @@ -159,6 +168,28 @@ private: ::grpc::Status WifiAccessPointGetAttributes(grpc::ServerContext* context, const Microsoft::Net::Remote::Wifi::WifiAccessPointGetAttributesRequest* request, Microsoft::Net::Remote::Wifi::WifiAccessPointGetAttributesResult* result) override; + /** + * @brief Enable an access point after a specified duration. + * + * @param context + * @param request + * @param result + * @return ::grpc::Status + */ + ::grpc::Status + WifiAccessPointTimedEnable(grpc::ServerContext* context, const Microsoft::Net::Remote::Wifi::WifiAccessPointTimedEnableRequest* request, Microsoft::Net::Remote::Wifi::WifiAccessPointTimedEnableResult* result) override; + + /** + * @brief Disable an access point after a specified duration. + * + * @param context + * @param request + * @param result + * @return ::grpc::Status + */ + ::grpc::Status + WifiAccessPointTimedDisable(grpc::ServerContext* context, const Microsoft::Net::Remote::Wifi::WifiAccessPointTimedDisableRequest* request, Microsoft::Net::Remote::Wifi::WifiAccessPointTimedDisableResult* result) override; + protected: /** * @brief Attempt to obtain an IAccessPoint instance for the specified access point identifier. @@ -211,6 +242,29 @@ protected: Microsoft::Net::Remote::Wifi::WifiAccessPointOperationStatus WifiAccessPointDisableImpl(std::string_view accessPointId, std::shared_ptr accessPointController = nullptr); + /** + * @brief Enable an access point after a specified duration. + * + * @param accessPointId The access point identifier. + * @param dot11AccessPointConfiguration The access point configuration to apply (optional). + * @param durationSeconds The duration in seconds to enable the access point. + * @param accessPointController The access point controller for the specified access point (optional). + * @return Microsoft::Net::Remote::Wifi::WifiAccessPointOperationStatus + */ + Microsoft::Net::Remote::Wifi::WifiAccessPointOperationStatus + WifiAccessPointTimedEnableImpl(std::string_view accessPointId, const Microsoft::Net::Wifi::Dot11AccessPointConfiguration* dot11AccessPointConfiguration, uint32_t durationSeconds, std::shared_ptr accessPointController = nullptr); + + /** + * @brief Disable an access point after a specified duration. + * + * @param accessPointId The access point identifier. + * @param durationSeconds The duration in seconds to disable the access point. + * @param accessPointController The access point controller for the specified access point (optional). + * @return Microsoft::Net::Remote::Wifi::WifiAccessPointOperationStatus + */ + Microsoft::Net::Remote::Wifi::WifiAccessPointOperationStatus + WifiAccessPointTimedDisableImpl(std::string_view accessPointId, uint32_t durationSeconds, std::shared_ptr accessPointController = nullptr); + /** * @brief Set the active PHY type of the access point. The access point must be enabled. This will cause * the access point to temporarily go offline while the change is being applied. @@ -329,6 +383,12 @@ protected: private: std::shared_ptr m_networkManager; std::shared_ptr m_accessPointManager; + + // Thread management for timed operations + std::mutex m_threadsMutex; + std::atomic m_shutdown{ false }; + std::shared_ptr m_timedEnableThread; + std::shared_ptr m_timedDisableThread; }; } // namespace Microsoft::Net::Remote::Service diff --git a/tests/unit/TestNetRemoteServiceClient.cxx b/tests/unit/TestNetRemoteServiceClient.cxx index 843be489..ab2546a5 100644 --- a/tests/unit/TestNetRemoteServiceClient.cxx +++ b/tests/unit/TestNetRemoteServiceClient.cxx @@ -1191,3 +1191,295 @@ TEST_CASE("WifiAccessPointGetAttributes API", "[basic][rpc][client][remote]") REQUIRE(properties.at(InterfaceAttributesPropertyKey) == InterfaceAttributesPropertyValue); } } + +TEST_CASE("WifiAccessPointTimedEnable API", "[basic][rpc][client][remote][timed]") +{ + using namespace Microsoft::Net::Remote; + using namespace Microsoft::Net::Remote::Service; + using namespace Microsoft::Net::Remote::Test; + using namespace Microsoft::Net::Remote::Wifi; + using namespace Microsoft::Net::Wifi; + using namespace Microsoft::Net::Wifi::Test; + + constexpr auto SsidName{ "TestWifiAccessPointTimedEnable" }; + constexpr auto InterfaceName1{ "TestWifiAccessPointTimedEnable1" }; + constexpr auto InterfaceName2{ "TestWifiAccessPointTimedEnable2" }; + + auto apManagerTest = std::make_shared(); + const Ieee80211AccessPointCapabilities apCapabilities{ + .PhyTypes{ std::cbegin(AllPhyTypes), std::cend(AllPhyTypes) }, + .FrequencyBands{ std::cbegin(AllBands), std::cend(AllBands) } + }; + + auto apTest1 = std::make_shared(InterfaceName1, apCapabilities); + auto apTest2 = std::make_shared(InterfaceName2, apCapabilities); + apManagerTest->AddAccessPoint(apTest1); + apManagerTest->AddAccessPoint(apTest2); + + const auto serverConfiguration = CreateServerConfiguration(apManagerTest); + NetRemoteServer server{ serverConfiguration }; + server.Run(); + + auto channel = grpc::CreateChannel(RemoteServiceAddressHttp, grpc::InsecureChannelCredentials()); + auto client = NetRemote::NewStub(channel); + + SECTION("Can be called with minimal configuration") + { + WifiAccessPointTimedEnableRequest request{}; + request.set_accesspointid(InterfaceName1); + request.set_durationseconds(1); // 1 second duration for quick test + + WifiAccessPointTimedEnableResult result{}; + grpc::ClientContext clientContext{}; + + auto status = client->WifiAccessPointTimedEnable(&clientContext, request, &result); + REQUIRE(status.ok()); + REQUIRE(result.accesspointid() == request.accesspointid()); + REQUIRE(result.has_status()); + REQUIRE(result.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeSucceeded); + REQUIRE(result.status().message().empty()); + REQUIRE(result.status().has_details() == false); + } + + SECTION("Can be called with full configuration") + { + Dot11CipherSuiteConfiguration dot11CipherSuiteConfigurationWpa1{}; + dot11CipherSuiteConfigurationWpa1.set_securityprotocol(Dot11SecurityProtocol::Dot11SecurityProtocolWpa); + dot11CipherSuiteConfigurationWpa1.mutable_ciphersuites()->Add(Dot11CipherSuite::Dot11CipherSuiteCcmp256); + + Dot11AccessPointConfiguration apConfiguration{}; + apConfiguration.set_phytype(Dot11PhyType::Dot11PhyTypeA); + apConfiguration.mutable_ssid()->set_name(SsidName); + apConfiguration.mutable_pairwiseciphersuites()->Add(std::move(dot11CipherSuiteConfigurationWpa1)); + apConfiguration.mutable_authenticationalgorithms()->Add(Dot11AuthenticationAlgorithm::Dot11AuthenticationAlgorithmSharedKey); + apConfiguration.mutable_frequencybands()->Add(Dot11FrequencyBand::Dot11FrequencyBand2_4GHz); + *apConfiguration.mutable_authenticationdata()->mutable_psk()->mutable_psk()->mutable_passphrase() = AsciiPassword; + + WifiAccessPointTimedEnableRequest request{}; + request.set_accesspointid(InterfaceName1); + request.set_durationseconds(2); // 2 seconds duration + *request.mutable_configuration() = std::move(apConfiguration); + + WifiAccessPointTimedEnableResult result{}; + grpc::ClientContext clientContext{}; + + auto status = client->WifiAccessPointTimedEnable(&clientContext, request, &result); + REQUIRE(status.ok()); + REQUIRE(result.accesspointid() == request.accesspointid()); + REQUIRE(result.has_status()); + REQUIRE(result.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeSucceeded); + } + + SECTION("Fails with zero duration") + { + WifiAccessPointTimedEnableRequest request{}; + request.set_accesspointid(InterfaceName1); + request.set_durationseconds(0); // Zero duration should fail + + WifiAccessPointTimedEnableResult result{}; + grpc::ClientContext clientContext{}; + + auto status = client->WifiAccessPointTimedEnable(&clientContext, request, &result); + REQUIRE(status.ok()); + REQUIRE(result.accesspointid() == request.accesspointid()); + REQUIRE(result.has_status()); + REQUIRE(result.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeInvalidParameter); + REQUIRE(result.status().message() == "Duration must be greater than 0 seconds"); + } + + SECTION("Fails with duration exceeding maximum") + { + WifiAccessPointTimedEnableRequest request{}; + request.set_accesspointid(InterfaceName1); + request.set_durationseconds(601); // Exceeds 10 minutes (600 seconds) + + WifiAccessPointTimedEnableResult result{}; + grpc::ClientContext clientContext{}; + + auto status = client->WifiAccessPointTimedEnable(&clientContext, request, &result); + REQUIRE(status.ok()); + REQUIRE(result.accesspointid() == request.accesspointid()); + REQUIRE(result.has_status()); + REQUIRE(result.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeInvalidParameter); + REQUIRE(result.status().message() == "Duration 601 seconds exceeds maximum allowed duration of 600 seconds"); + } + + SECTION("Succeeds with allowed duration") + { + WifiAccessPointTimedEnableRequest request{}; + request.set_accesspointid(InterfaceName1); + request.set_durationseconds(60); // 1 minute + + WifiAccessPointTimedEnableResult result{}; + grpc::ClientContext clientContext{}; + + auto status = client->WifiAccessPointTimedEnable(&clientContext, request, &result); + REQUIRE(status.ok()); + REQUIRE(result.accesspointid() == request.accesspointid()); + REQUIRE(result.has_status()); + REQUIRE(result.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeSucceeded); + } + + SECTION("Fails when a timed enable operation is already in progress") + { + // Start first timed enable operation + WifiAccessPointTimedEnableRequest request1{}; + request1.set_accesspointid(InterfaceName1); + request1.set_durationseconds(5); // 5 seconds duration + + WifiAccessPointTimedEnableResult result1{}; + grpc::ClientContext clientContext1{}; + + auto status1 = client->WifiAccessPointTimedEnable(&clientContext1, request1, &result1); + REQUIRE(status1.ok()); + REQUIRE(result1.has_status()); + REQUIRE(result1.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeSucceeded); + + // Attempt second timed enable operation immediately + WifiAccessPointTimedEnableRequest request2{}; + request2.set_accesspointid(InterfaceName2); + request2.set_durationseconds(2); // 2 seconds duration + + WifiAccessPointTimedEnableResult result2{}; + grpc::ClientContext clientContext2{}; + + auto status2 = client->WifiAccessPointTimedEnable(&clientContext2, request2, &result2); + REQUIRE(status2.ok()); + REQUIRE(result2.has_status()); + REQUIRE(result2.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeOperationNotSupported); + REQUIRE(result2.status().message() == "A timed enable operation is already in progress"); + } +} + +TEST_CASE("WifiAccessPointTimedDisable API", "[basic][rpc][client][remote][timed]") +{ + using namespace Microsoft::Net::Remote; + using namespace Microsoft::Net::Remote::Service; + using namespace Microsoft::Net::Remote::Test; + using namespace Microsoft::Net::Remote::Wifi; + using namespace Microsoft::Net::Wifi; + using namespace Microsoft::Net::Wifi::Test; + + constexpr auto InterfaceName1{ "TestWifiAccessPointTimedDisable1" }; + constexpr auto InterfaceName2{ "TestWifiAccessPointTimedDisable2" }; + + auto apManagerTest = std::make_shared(); + const Ieee80211AccessPointCapabilities apCapabilities{ + .PhyTypes{ std::cbegin(AllPhyTypes), std::cend(AllPhyTypes) }, + .FrequencyBands{ std::cbegin(AllBands), std::cend(AllBands) } + }; + + auto apTest1 = std::make_shared(InterfaceName1, apCapabilities); + auto apTest2 = std::make_shared(InterfaceName2, apCapabilities); + apManagerTest->AddAccessPoint(apTest1); + apManagerTest->AddAccessPoint(apTest2); + + const auto serverConfiguration = CreateServerConfiguration(apManagerTest); + NetRemoteServer server{ serverConfiguration }; + server.Run(); + + auto channel = grpc::CreateChannel(RemoteServiceAddressHttp, grpc::InsecureChannelCredentials()); + auto client = NetRemote::NewStub(channel); + + SECTION("Can be called") + { + WifiAccessPointTimedDisableRequest request{}; + request.set_accesspointid(InterfaceName1); + request.set_durationseconds(1); // 1 second duration for quick test + + WifiAccessPointTimedDisableResult result{}; + grpc::ClientContext clientContext{}; + + grpc::Status status; + REQUIRE_NOTHROW(status = client->WifiAccessPointTimedDisable(&clientContext, request, &result)); + REQUIRE(status.ok()); + REQUIRE(result.accesspointid() == request.accesspointid()); + REQUIRE(result.has_status()); + REQUIRE(result.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeSucceeded); + } + + SECTION("Fails with zero duration") + { + WifiAccessPointTimedDisableRequest request{}; + request.set_accesspointid(InterfaceName1); + request.set_durationseconds(0); // Zero duration should fail + + WifiAccessPointTimedDisableResult result{}; + grpc::ClientContext clientContext{}; + + grpc::Status status; + REQUIRE_NOTHROW(status = client->WifiAccessPointTimedDisable(&clientContext, request, &result)); + REQUIRE(status.ok()); + REQUIRE(result.accesspointid() == request.accesspointid()); + REQUIRE(result.has_status()); + REQUIRE(result.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeInvalidParameter); + REQUIRE(result.status().message() == "Duration must be greater than 0 seconds"); + } + + SECTION("Fails with duration exceeding maximum") + { + WifiAccessPointTimedDisableRequest request{}; + request.set_accesspointid(InterfaceName1); + request.set_durationseconds(601); // Exceeds 10 minutes (600 seconds) + + WifiAccessPointTimedDisableResult result{}; + grpc::ClientContext clientContext{}; + + grpc::Status status; + REQUIRE_NOTHROW(status = client->WifiAccessPointTimedDisable(&clientContext, request, &result)); + REQUIRE(status.ok()); + REQUIRE(result.accesspointid() == request.accesspointid()); + REQUIRE(result.has_status()); + REQUIRE(result.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeInvalidParameter); + REQUIRE(result.status().message() == "Duration 601 seconds exceeds maximum allowed duration of 600 seconds"); + } + + SECTION("Succeeds with allowed duration") + { + WifiAccessPointTimedDisableRequest request{}; + request.set_accesspointid(InterfaceName1); + request.set_durationseconds(60); // 1 minute + + WifiAccessPointTimedDisableResult result{}; + grpc::ClientContext clientContext{}; + + grpc::Status status; + REQUIRE_NOTHROW(status = client->WifiAccessPointTimedDisable(&clientContext, request, &result)); + REQUIRE(status.ok()); + REQUIRE(result.accesspointid() == request.accesspointid()); + REQUIRE(result.has_status()); + REQUIRE(result.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeSucceeded); + } + + SECTION("Fails when a timed disable operation is already in progress") + { + // Start first timed disable operation + WifiAccessPointTimedDisableRequest request1{}; + request1.set_accesspointid(InterfaceName1); + request1.set_durationseconds(5); // 5 seconds duration + + WifiAccessPointTimedDisableResult result1{}; + grpc::ClientContext clientContext1{}; + + grpc::Status status1; + REQUIRE_NOTHROW(status1 = client->WifiAccessPointTimedDisable(&clientContext1, request1, &result1)); + REQUIRE(status1.ok()); + REQUIRE(result1.has_status()); + REQUIRE(result1.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeSucceeded); + + // Attempt second timed disable operation immediately + WifiAccessPointTimedDisableRequest request2{}; + request2.set_accesspointid(InterfaceName2); + request2.set_durationseconds(2); // 2 seconds duration + + WifiAccessPointTimedDisableResult result2{}; + grpc::ClientContext clientContext2{}; + + grpc::Status status2; + REQUIRE_NOTHROW(status2 = client->WifiAccessPointTimedDisable(&clientContext2, request2, &result2)); + REQUIRE(status2.ok()); + REQUIRE(result2.has_status()); + REQUIRE(result2.status().code() == WifiAccessPointOperationStatusCode::WifiAccessPointOperationStatusCodeOperationNotSupported); + REQUIRE(result2.status().message() == "A timed disable operation is already in progress"); + } +}