Repository: alphagov/govuk-mobile-ios-app
Pull request #337: [GOVUKAPP-1347] integrate stubbed local authority
Author: login: RaphealOlumofe
Created: 2025-04-14T08:49:35Z | Merged: 2025-04-14T15:23:10Z

This pr integrates the local stubbed authority api, currently self reviewing 


Diff:
diff --git a/GOVKit/Sources/GOVKit/Extensions/Constants.swift b/GOVKit/Sources/GOVKit/Extensions/Constants.swift
index 39760fc6d..0e23d3a3b 100644
--- a/GOVKit/Sources/GOVKit/Extensions/Constants.swift
+++ b/GOVKit/Sources/GOVKit/Extensions/Constants.swift
@@ -47,6 +47,12 @@ public struct Constants {
         public static var searchSuggestionsPath: String = "/api/search/autocomplete.json"
 
         public static var defaultSearchPath: String = "/v0_1/search.json"
+
+        public static var defaultLocalAuthorityURL: URL = URL(
+            string: "https://development-local-api-05fe62f9622c.herokuapp.com"
+        )!
+
+        public static var localAuthorityPath: String = "/find-local-council/query.json"
     }
 
     public struct SigningKey {
diff --git a/GovUK.xcodeproj/project.pbxproj b/GovUK.xcodeproj/project.pbxproj
index 77a6fc904..8a2aa508e 100644
--- a/GovUK.xcodeproj/project.pbxproj
+++ b/GovUK.xcodeproj/project.pbxproj
@@ -69,6 +69,9 @@
 		4E9AFCE32C99D43B00DBF66F /* AnalyticsConsentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9AFCE22C99D43B00DBF66F /* AnalyticsConsentContainerView.swift */; };
 		4EA1E2812D8C61630062453D /* NotificationPermissionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA1E2802D8C61630062453D /* NotificationPermissionState.swift */; };
 		4EA3C3502C91B8C4000D731C /* RecentActivityCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA3C34F2C91B8C4000D731C /* RecentActivityCoordinator.swift */; };
+		4EBCA3272DAC541B0063A723 /* LocalAuthorityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBCA3262DAC541B0063A723 /* LocalAuthorityService.swift */; };
+		4EBCA3292DAC546A0063A723 /* LocalAuthorityServiceClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBCA3282DAC546A0063A723 /* LocalAuthorityServiceClient.swift */; };
+		4EBCA3362DAC5E2E0063A723 /* GOVRequest+LocalAuthority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBCA3352DAC5E2E0063A723 /* GOVRequest+LocalAuthority.swift */; };
 		4EBE64692D15AABA007A6E97 /* MaximumLengthValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE64682D15AABA007A6E97 /* MaximumLengthValidator.swift */; };
 		4EBE646B2D15AAD6007A6E97 /* MinimumLengthValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE646A2D15AAD6007A6E97 /* MinimumLengthValidator.swift */; };
 		4EBE646D2D15AB05007A6E97 /* CharValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE646C2D15AB05007A6E97 /* CharValidator.swift */; };
@@ -355,6 +358,9 @@
 		4E9AFCE22C99D43B00DBF66F /* AnalyticsConsentContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsConsentContainerView.swift; sourceTree = "<group>"; };
 		4EA1E2802D8C61630062453D /* NotificationPermissionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionState.swift; sourceTree = "<group>"; };
 		4EA3C34F2C91B8C4000D731C /* RecentActivityCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentActivityCoordinator.swift; sourceTree = "<group>"; };
+		4EBCA3262DAC541B0063A723 /* LocalAuthorityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthorityService.swift; sourceTree = "<group>"; };
+		4EBCA3282DAC546A0063A723 /* LocalAuthorityServiceClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthorityServiceClient.swift; sourceTree = "<group>"; };
+		4EBCA3352DAC5E2E0063A723 /* GOVRequest+LocalAuthority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GOVRequest+LocalAuthority.swift"; sourceTree = "<group>"; };
 		4EBE64682D15AABA007A6E97 /* MaximumLengthValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaximumLengthValidator.swift; sourceTree = "<group>"; };
 		4EBE646A2D15AAD6007A6E97 /* MinimumLengthValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinimumLengthValidator.swift; sourceTree = "<group>"; };
 		4EBE646C2D15AB05007A6E97 /* CharValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharValidator.swift; sourceTree = "<group>"; };
@@ -619,6 +625,7 @@
 /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
 
 /* Begin PBXFileSystemSynchronizedRootGroup section */
+		4EBCA32A2DAC54E20063A723 /* LocalAuthority */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LocalAuthority; sourceTree = "<group>"; };
 		5D9CDE3A2CD928FB0045DFC3 /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (5D9CDF202CD928FB0045DFC3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 5D9CDF212CD928FB0045DFC3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 5D9CDF222CD928FB0045DFC3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (govuk_ios/govuk_ios_snapshot_tests/ReferenceImages_64, ); path = Tests; sourceTree = "<group>"; };
 /* End PBXFileSystemSynchronizedRootGroup section */
 
@@ -704,6 +711,7 @@
 				3D09A6DA2C9DC873005858D0 /* AppConfigServiceClient.swift */,
 				5D0BD4582C9B1BBA00220625 /* SearchServiceClient.swift */,
 				D04191852CA6A5910034722A /* TopicsServiceClient.swift */,
+				4EBCA3282DAC546A0063A723 /* LocalAuthorityServiceClient.swift */,
 			);
 			path = ServiceClients;
 			sourceTree = "<group>";
@@ -862,6 +870,7 @@
 				5DBE080D2C49233200B58DBE /* OnboardingService.swift */,
 				5DD09EC72C9ADB8500494550 /* SearchService.swift */,
 				D04191772CA57A6F0034722A /* TopicsService.swift */,
+				4EBCA3262DAC541B0063A723 /* LocalAuthorityService.swift */,
 			);
 			path = Services;
 			sourceTree = "<group>";
@@ -1188,6 +1197,7 @@
 				5DB6370C2D7736AD00A44E36 /* GOVRequest+App.swift */,
 				5DC79FB12CA18A9A00014446 /* GOVRequest+Search.swift */,
 				5DB637072D7732A800A44E36 /* GOVRequest+Topics.swift */,
+				4EBCA3352DAC5E2E0063A723 /* GOVRequest+LocalAuthority.swift */,
 			);
 			path = GOVRequest;
 			sourceTree = "<group>";
@@ -1219,6 +1229,7 @@
 		5DE970512C297637009D66C4 /* Model */ = {
 			isa = PBXGroup;
 			children = (
+				4EBCA32A2DAC54E20063A723 /* LocalAuthority */,
 				D04C542A2D7081C00070EE79 /* Analytics */,
 				D0D9CB922D3024C00051EDDD /* Search */,
 				4E003E7D2C75F497008FCBF2 /* AppConfig */,
@@ -1408,6 +1419,9 @@
 			dependencies = (
 				5DCAFA672D3FE0E800C3F284 /* PBXTargetDependency */,
 			);
+			fileSystemSynchronizedGroups = (
+				4EBCA32A2DAC54E20063A723 /* LocalAuthority */,
+			);
 			name = govuk_ios;
 			packageProductDependencies = (
 				5D7F267D2C0F0EF000842090 /* Factory */,
@@ -1690,6 +1704,7 @@
 				5D0CFAA72C33E72C00B3DEFD /* DeeplinkDataStore.swift in Sources */,
 				5DFA017B2D89651100EAC839 /* ActivityItemCreateParams+Extensions.swift in Sources */,
 				5DFA017C2D89651100EAC839 /* ActivityService+Extensions.swift in Sources */,
+				4EBCA3292DAC546A0063A723 /* LocalAuthorityServiceClient.swift in Sources */,
 				16C34B432C492FF90031AD32 /* HomeDeepLinkRoute.swift in Sources */,
 				D0E7923B2D6CD66600EAB81F /* TopicDetailViewModelInterface.swift in Sources */,
 				5D962D612C29BD0B0038BC0A /* AppCoordinator.swift in Sources */,
@@ -1728,6 +1743,7 @@
 				5DD4CB092D95483100459FB5 /* MonthGroupKey.swift in Sources */,
 				5DD4CB0A2D95483100459FB5 /* RecentActivitySection.swift in Sources */,
 				5DD4CB0B2D95483100459FB5 /* RecentActivitiesViewStructure.swift in Sources */,
+				4EBCA3362DAC5E2E0063A723 /* GOVRequest+LocalAuthority.swift in Sources */,
 				5DD4CB0C2D95483100459FB5 /* ActivityItemCreateParams.swift in Sources */,
 				16FD684A2D39AEB5002F1597 /* SuggestionHighlighterProcessor.swift in Sources */,
 				5D79AEC02C32B9180061F344 /* AnimationView.swift in Sources */,
@@ -1752,6 +1768,7 @@
 				5D0BD4592C9B1BBA00220625 /* SearchServiceClient.swift in Sources */,
 				5D66D19A2CBFF51100006723 /* AppEvent+Topics.swift in Sources */,
 				5DFE5F1E2D6DE5900041E3B4 /* UNUserNotificationCenter+Interface.swift in Sources */,
+				4EBCA3272DAC541B0063A723 /* LocalAuthorityService.swift in Sources */,
 				16A98EA52D30333F00C72DEB /* SuggestionCell.swift in Sources */,
 				4E6663C82C7370750063E81C /* AppConfig.swift in Sources */,
 				D0D9B87B2D2D547D0051EDDD /* StringValidator.swift in Sources */,
diff --git a/Production/govuk_ios/Model/LocalAuthority/Authority.swift b/Production/govuk_ios/Model/LocalAuthority/Authority.swift
new file mode 100644
index 000000000..7b931a864
--- /dev/null
+++ b/Production/govuk_ios/Model/LocalAuthority/Authority.swift
@@ -0,0 +1,31 @@
+import Foundation
+
+class Authority: Codable,
+                 LocalAuthorityType {
+    let name: String
+    let homepageUrl: String
+    let tier: String
+    let slug: String
+    var parent: Authority?
+
+    init(name: String,
+         homepageUrl: String,
+         tier: String,
+         slug: String,
+         parent: Authority? = nil) {
+        self.name = name
+        self.homepageUrl = homepageUrl
+        self.tier = tier
+        self.slug = slug
+        self.parent = parent
+    }
+
+    enum CodingKeys: String,
+                     CodingKey {
+        case homepageUrl = "homepage_url"
+        case name
+        case tier
+        case slug
+        case parent
+    }
+}
diff --git a/Production/govuk_ios/Model/LocalAuthority/LocalAuthoritiesList.swift b/Production/govuk_ios/Model/LocalAuthority/LocalAuthoritiesList.swift
new file mode 100644
index 000000000..31283d98f
--- /dev/null
+++ b/Production/govuk_ios/Model/LocalAuthority/LocalAuthoritiesList.swift
@@ -0,0 +1,6 @@
+import Foundation
+
+struct LocalAuthoritiesList: Codable,
+                             LocalAuthorityType {
+    let addresses: [LocalAuthorityAddress]
+}
diff --git a/Production/govuk_ios/Model/LocalAuthority/LocalAuthority.swift b/Production/govuk_ios/Model/LocalAuthority/LocalAuthority.swift
new file mode 100644
index 000000000..ef52fdc4f
--- /dev/null
+++ b/Production/govuk_ios/Model/LocalAuthority/LocalAuthority.swift
@@ -0,0 +1,10 @@
+import Foundation
+
+struct LocalAuthority: Codable,
+                       LocalAuthorityType {
+    let localAuthority: Authority
+
+    enum CodingKeys: String, CodingKey {
+        case localAuthority = "local_authority"
+    }
+}
diff --git a/Production/govuk_ios/Model/LocalAuthority/LocalAuthorityAddress.swift b/Production/govuk_ios/Model/LocalAuthority/LocalAuthorityAddress.swift
new file mode 100644
index 000000000..4e3c6719e
--- /dev/null
+++ b/Production/govuk_ios/Model/LocalAuthority/LocalAuthorityAddress.swift
@@ -0,0 +1,8 @@
+import Foundation
+
+struct LocalAuthorityAddress: Codable,
+                              LocalAuthorityType {
+    let address: String
+    let slug: String
+    let name: String
+}
diff --git a/Production/govuk_ios/Model/LocalAuthority/LocalAuthorityErrorMessage.swift b/Production/govuk_ios/Model/LocalAuthority/LocalAuthorityErrorMessage.swift
new file mode 100644
index 000000000..7f9a1a2d7
--- /dev/null
+++ b/Production/govuk_ios/Model/LocalAuthority/LocalAuthorityErrorMessage.swift
@@ -0,0 +1,6 @@
+import Foundation
+
+struct LocalErrorMessage: LocalAuthorityType,
+                          Codable {
+    let message: String
+}
diff --git a/Production/govuk_ios/Networking/GOVRequest/GOVRequest+LocalAuthority.swift b/Production/govuk_ios/Networking/GOVRequest/GOVRequest+LocalAuthority.swift
new file mode 100644
index 000000000..2792ea6b6
--- /dev/null
+++ b/Production/govuk_ios/Networking/GOVRequest/GOVRequest+LocalAuthority.swift
@@ -0,0 +1,14 @@
+import Foundation
+import GOVKit
+
+extension GOVRequest {
+    static func localAuthority(postcode: String) -> GOVRequest {
+        GOVRequest(
+            urlPath: Constants.API.localAuthorityPath,
+            method: .get,
+            bodyParameters: nil,
+            queryParameters: ["postcode": postcode],
+            additionalHeaders: nil
+        )
+    }
+}
diff --git a/Production/govuk_ios/ServiceClients/LocalAuthorityServiceClient.swift b/Production/govuk_ios/ServiceClients/LocalAuthorityServiceClient.swift
new file mode 100644
index 000000000..4c3adbed1
--- /dev/null
+++ b/Production/govuk_ios/ServiceClients/LocalAuthorityServiceClient.swift
@@ -0,0 +1,63 @@
+import Foundation
+
+typealias FetchLocalAuthorityCompletion = (sending FetchLocalAuthorityResult) -> Void
+typealias FetchLocalAuthorityResult = Result<LocalAuthorityType, LocalAuthorityError>
+
+protocol LocalAuthorityServiceClientInterface {
+    func fetchLocalAuthority(postcode: String, completion: @escaping FetchLocalAuthorityCompletion)
+}
+
+enum LocalAuthorityError: LocalizedError {
+    case networkUnavailable
+    case apiUnavailable
+    case decodingError
+}
+
+struct LocalAuthorityServiceClient: LocalAuthorityServiceClientInterface {
+    private let serviceClient: APIServiceClientInterface
+
+    init(serviceClient: APIServiceClientInterface) {
+        self.serviceClient = serviceClient
+    }
+
+    func fetchLocalAuthority(postcode: String,
+                             completion: @escaping FetchLocalAuthorityCompletion) {
+        serviceClient.send(request: .localAuthority(postcode: postcode)) { result in
+            completion(mapResult(result))
+        }
+    }
+
+    private func mapResult(
+        _ result: NetworkResult<Data>) -> Result<LocalAuthorityType, LocalAuthorityError> {
+            return result.mapError { error in
+                let nsError = (error as NSError)
+                if nsError.code == NSURLErrorNotConnectedToInternet {
+                    return LocalAuthorityError.networkUnavailable
+                } else {
+                    return LocalAuthorityError.apiUnavailable
+                }
+            }.flatMap {
+                do {
+                    guard let response = decode(data: $0) else {
+                        return .failure(LocalAuthorityError.decodingError)
+                    }
+                    return .success(response)
+                }
+            }
+        }
+
+    private func decode(data: Data) -> LocalAuthorityType? {
+        if let localAuthority = try? JSONDecoder().decode(LocalAuthority.self, from: data) {
+            return localAuthority
+        }
+        if let addressList = try? JSONDecoder().decode(LocalAuthoritiesList.self, from: data) {
+            return addressList
+        }
+        if let errorMessage = try? JSONDecoder().decode(LocalErrorMessage.self, from: data) {
+            return errorMessage
+        }
+        return nil
+    }
+}
+
+protocol LocalAuthorityType { }
diff --git a/Production/govuk_ios/Services/LocalAuthorityService.swift b/Production/govuk_ios/Services/LocalAuthorityService.swift
new file mode 100644
index 000000000..70b41b9b7
--- /dev/null
+++ b/Production/govuk_ios/Services/LocalAuthorityService.swift
@@ -0,0 +1,20 @@
+import Foundation
+
+protocol LocalAuthorityServiceInterface {
+    func fetchLocalAuthority(postcode: String, completion: @escaping FetchLocalAuthorityCompletion)
+}
+
+class LocalAuthorityService: LocalAuthorityServiceInterface {
+    private let serviceClient: LocalAuthorityServiceClientInterface
+
+    init(serviceClient: LocalAuthorityServiceClientInterface) {
+        self.serviceClient = serviceClient
+    }
+
+    func fetchLocalAuthority(postcode: String,
+                             completion: @escaping FetchLocalAuthorityCompletion) {
+        serviceClient.fetchLocalAuthority(postcode: postcode) { result in
+            completion(result)
+        }
+    }
+}
diff --git a/Tests/govuk_ios/govuk_ios_unit_tests/Mocks/ServiceClients/MockLocalAuthorityServiceClient.swift b/Tests/govuk_ios/govuk_ios_unit_tests/Mocks/ServiceClients/MockLocalAuthorityServiceClient.swift
new file mode 100644
index 000000000..67b51470c
--- /dev/null
+++ b/Tests/govuk_ios/govuk_ios_unit_tests/Mocks/ServiceClients/MockLocalAuthorityServiceClient.swift
@@ -0,0 +1,15 @@
+import Foundation
+
+@testable import govuk_ios
+
+class MockLocalServiceClient: LocalAuthorityServiceClientInterface {
+    var _stubbedPostcode: String?
+    var _stubbedLocalResult: Result<LocalAuthorityType, LocalAuthorityError>?
+    var _receivedLocalCompletion: FetchLocalAuthorityCompletion?
+    func fetchLocalAuthority(postcode: String, completion: @escaping FetchLocalAuthorityCompletion) {
+        _stubbedPostcode = postcode
+        if let result = _stubbedLocalResult {
+            completion(result)
+        }
+    }
+}
diff --git a/Tests/govuk_ios/govuk_ios_unit_tests/Specs/Networking/GOVRequest/GOVRequest+LocalAuthorityTests.swift b/Tests/govuk_ios/govuk_ios_unit_tests/Specs/Networking/GOVRequest/GOVRequest+LocalAuthorityTests.swift
new file mode 100644
index 000000000..6d1baed7b
--- /dev/null
+++ b/Tests/govuk_ios/govuk_ios_unit_tests/Specs/Networking/GOVRequest/GOVRequest+LocalAuthorityTests.swift
@@ -0,0 +1,24 @@
+import Testing
+import GOVKit
+import Foundation
+
+@testable import govuk_ios
+
+@Suite
+struct GOVRequest_LocalAuthorityTests {
+
+    @Test
+    func localAuthority_returnsExpectedValues() async throws {
+        let expectedPostcode = "SW1A 0AA"
+        let request = GOVRequest.localAuthority(postcode: expectedPostcode)
+
+        #expect(request.urlPath == "/find-local-council/query.json")
+        #expect(request.method == .get)
+        #expect(request.signingKey == nil)
+        #expect(request.bodyParameters == nil)
+        #expect(request.queryParameters?.count == 1)
+        #expect(request.queryParameters?["postcode"] == expectedPostcode)
+        #expect(request.additionalHeaders == nil)
+
+    }
+}
diff --git a/Tests/govuk_ios/govuk_ios_unit_tests/Specs/ServiceClients/LocalAuthorityServiceClientTests.swift b/Tests/govuk_ios/govuk_ios_unit_tests/Specs/ServiceClients/LocalAuthorityServiceClientTests.swift
new file mode 100644
index 000000000..760e266b2
--- /dev/null
+++ b/Tests/govuk_ios/govuk_ios_unit_tests/Specs/ServiceClients/LocalAuthorityServiceClientTests.swift
@@ -0,0 +1,222 @@
+import Foundation
+import Testing
+
+@testable import govuk_ios
+
+@Suite
+struct LocalAuthorityServiceClientTests {
+
+    @Test
+    func fetchLocalAuthority_sendsExpectedResults() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(serviceClient: mockAPI)
+        let expectedPostcode = "SE129PT"
+
+        sut.fetchLocalAuthority(
+            postcode: expectedPostcode) { _ in }
+
+        #expect(mockAPI._receivedSendRequest?.urlPath == "/find-local-council/query.json")
+        #expect(mockAPI._receivedSendRequest?.method == .get)
+        #expect(mockAPI._receivedSendRequest?.queryParameters?["postcode"] as? String == expectedPostcode)
+    }
+
+    @Test
+    func fetchLocalAuthority_addressLists_returnsExpectedResult() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(
+            serviceClient: mockAPI
+        )
+
+        mockAPI._stubbedSendResponse = .success(Self.localAuthorityListData)
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        let addressList = localResult as? LocalAuthoritiesList
+        #expect(addressList?.addresses.count == 2)
+        #expect(addressList?.addresses.first?.name == "Dorset County Council")
+        #expect(addressList?.addresses.last?.name == "Bournemouth, Christchurch, and Poole")
+        #expect(addressList?.addresses.first?.slug == "dorset")
+    }
+
+    @Test
+    func fetchLocalAuthority_tierOnelocalAuthority_returnsExpectedResult() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(
+            serviceClient: mockAPI
+        )
+        mockAPI._stubbedSendResponse = .success(Self.localAuthorityTierOneData)
+
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(postcode: "test") { result in
+                continuation.resume(returning: result)
+            }
+        }
+        let localResult = try? result.get()
+        let localAuthority = localResult as? LocalAuthority
+        #expect(localAuthority?.localAuthority.homepageUrl == "https://www.towerhamlets.gov.uk")
+        #expect(localAuthority?.localAuthority.name == "London Borough of Tower Hamlets")
+        #expect(localAuthority?.localAuthority.tier == "unitary")
+        #expect(localAuthority?.localAuthority.slug == "tower-hamlets")
+    }
+
+    @Test
+    func fetchLocalAuthority_tierTwolocalAuthority_returnsExpectedResult() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(
+            serviceClient: mockAPI
+        )
+        mockAPI._stubbedSendResponse = .success(Self.localAuthorityTierTwoData)
+
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(postcode: "test") { result in
+                continuation.resume(returning: result)
+            }
+        }
+        let localResult = try? result.get()
+        let localAuthority = localResult as? LocalAuthority
+        #expect(localAuthority?.localAuthority.homepageUrl == "https://www.derbyshiredales.gov.uk/")
+        #expect(localAuthority?.localAuthority.name == "Derbyshire Dales District Council")
+        #expect(localAuthority?.localAuthority.tier == "district")
+        #expect(localAuthority?.localAuthority.slug == "derbyshire-dales")
+        #expect(localAuthority?.localAuthority.parent?.name == "Derbyshire County Council")
+    }
+
+    @Test
+    func fetchLocalAuthority_apiUnavailable_returnsExpectedResult() async {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(serviceClient: mockAPI)
+        mockAPI._stubbedSendResponse = .failure(TestError.fakeNetwork)
+
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(postcode: "test") { result in
+                continuation.resume(returning: result)
+            }
+        }
+        let localResult = try? result.get()
+        #expect(localResult == nil)
+        #expect(result.getError() == .apiUnavailable)
+    }
+
+    @Test
+    func fetchLocalAuthority_networkUnavailable_returnsExpectedResult() async {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(
+            serviceClient: mockAPI
+        )
+        mockAPI._stubbedSendResponse = .failure(
+            NSError(domain: "TestError", code: NSURLErrorNotConnectedToInternet)
+        )
+
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(postcode: "test") { result in
+                continuation.resume(returning: result)
+            }
+        }
+        let localResult = try? result.get()
+        #expect(localResult == nil)
+        #expect(result.getError() == .networkUnavailable)
+    }
+
+    @Test
+    func fetchLocalAuthority_decodingError_returnsExpectedResult() async {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(serviceClient: mockAPI)
+        mockAPI._stubbedSendResponse = .success(Self.invalidObjectData)
+
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(postcode: "test") { result in
+                continuation.resume(returning: result)
+            }
+        }
+        let localResult = try? result.get()
+        #expect(localResult == nil)
+        #expect(result.getError() == .decodingError)
+    }
+
+    @Test
+    func fetchLocalAuthority_localErrorMessage_returnsExpectedresult() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(serviceClient: mockAPI)
+        mockAPI._stubbedSendResponse = .success(Self.localAuthorityErrorMessage)
+
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        let errormessage = localResult as? LocalErrorMessage
+        #expect(errormessage?.message == "Postcode not found")
+    }
+}
+private extension LocalAuthorityServiceClientTests {
+
+    static let localAuthorityTierOneData =
+    """
+    {
+        "local_authority": {
+            "name": "London Borough of Tower Hamlets",
+            "homepage_url": "https://www.towerhamlets.gov.uk",
+            "tier": "unitary",
+            "slug": "tower-hamlets",
+            "parent": null
+        }
+    }
+    """.data(using: .utf8)!
+
+    static let localAuthorityListData =
+    """
+    {
+        "addresses": [
+            {
+                "address": "APPLETREE COTTAGE, BARRACK ROAD, WEST PARLEY, FERNDOWN, BH22 8UB",
+                "slug": "dorset",
+                "name": "Dorset County Council"
+            },
+            {
+                "address": "LONGCROFT BRICK, BARRACK ROAD, FERNDOWN, DORSET, BH22 8UB",
+                "slug": "bournemouth-christchurch-poole",
+                "name": "Bournemouth, Christchurch, and Poole"
+            }
+        ]
+    }
+    """.data(using: .utf8)!
+
+    static let localAuthorityTierTwoData =
+    """
+    {
+        "local_authority": {
+            "name": "Derbyshire Dales District Council",
+            "homepage_url": "https://www.derbyshiredales.gov.uk/",
+            "tier": "district",
+            "slug": "derbyshire-dales",
+            "parent": {
+                "name": "Derbyshire County Council",
+                "homepage_url": "https://www.derbyshiredales.gov.uk/",
+                "tier": "county",
+                "slug": "derbyshire",
+                "parent": null
+            }
+        }
+    }
+    """.data(using: .utf8)!
+
+    static let localAuthorityErrorMessage =
+    """
+    {
+    "message": "Postcode not found"
+    }
+    """.data(using: .utf8)!
+
+    static let invalidObjectData =
+    """
+    {
+    "Test"
+    }
+    """.data(using: .utf8)!
+}
diff --git a/Tests/govuk_ios/govuk_ios_unit_tests/Specs/Services/LocalAuthortiyServiceTests.swift b/Tests/govuk_ios/govuk_ios_unit_tests/Specs/Services/LocalAuthortiyServiceTests.swift
new file mode 100644
index 000000000..b9201b413
--- /dev/null
+++ b/Tests/govuk_ios/govuk_ios_unit_tests/Specs/Services/LocalAuthortiyServiceTests.swift
@@ -0,0 +1,183 @@
+import Foundation
+import Testing
+
+@testable import govuk_ios
+
+@Suite
+struct LocalAuthorityServiceTests {
+
+    @Test
+    func fetchLocalAuthority_callsServiceClient() async throws {
+        let mockServiceClient = MockLocalServiceClient()
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let expectedPostcode = "SW1"
+
+        sut.fetchLocalAuthority(
+            postcode: expectedPostcode) { _ in }
+        #expect(mockServiceClient._stubbedPostcode == expectedPostcode)
+    }
+
+    @Test
+    func fetchLocalAuthority_addressLists_returnsExpectedResult() async throws {
+        let addresses:[LocalAuthorityAddress] = [
+            LocalAuthorityAddress(
+                address: "address1",
+                slug: "slug1",
+                name: "name1"
+            ),
+            LocalAuthorityAddress(
+                address: "address2",
+                slug: "slug2",
+                name: "name2"
+            )
+        ]
+        let expectedResult = LocalAuthoritiesList(addresses: addresses)
+        let mockServiceClient = MockLocalServiceClient()
+        mockServiceClient._stubbedLocalResult = .success(expectedResult)
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        let addressList = localResult as? LocalAuthoritiesList
+        #expect(addressList?.addresses.count == 2)
+        #expect(addressList?.addresses.first?.name == "name1")
+        #expect(addressList?.addresses.last?.name == "name2")
+        #expect(addressList?.addresses.first?.slug == "slug1")
+        #expect(addressList?.addresses.last?.name == "name2")
+    }
+
+    @Test
+    func fetchLocalAuthority_tierOnelocalAuthority_returnsExpectedResult() async throws {
+        let authority = Authority(
+            name: "name1",
+            homepageUrl: "homepageUrl",
+            tier: "tier1",
+            slug: "slug1",
+            parent: nil
+        )
+        let expectedResult = LocalAuthority(localAuthority: authority)
+        let mockServiceClient = MockLocalServiceClient()
+        mockServiceClient._stubbedLocalResult = .success(expectedResult)
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        let localAuthority = localResult as? LocalAuthority
+        #expect(localAuthority?.localAuthority.homepageUrl == "homepageUrl")
+        #expect(localAuthority?.localAuthority.name == "name1")
+        #expect(localAuthority?.localAuthority.tier == "tier1")
+        #expect(localAuthority?.localAuthority.slug == "slug1")
+    }
+
+    @Test
+    func fetchLocalAuthority_tierTwolocalAuthority_returnsExpectedResult() async throws {
+        let parentAuthority = Authority(
+            name: "parentAuthority",
+            homepageUrl: "homepageUrl",
+            tier: "parentTier",
+            slug: "slug",
+            parent: nil
+        )
+        let authority = Authority(
+            name: "name2",
+            homepageUrl: "homepageUrl",
+            tier: "tier2",
+            slug: "slug2",
+            parent: parentAuthority
+        )
+        let expectedResult = LocalAuthority(localAuthority: authority)
+        let mockServiceClient = MockLocalServiceClient()
+        mockServiceClient._stubbedLocalResult = .success(expectedResult)
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        let localAuthority = localResult as? LocalAuthority
+        #expect(localAuthority?.localAuthority.homepageUrl == "homepageUrl")
+        #expect(localAuthority?.localAuthority.name == "name2")
+        #expect(localAuthority?.localAuthority.tier == "tier2")
+        #expect(localAuthority?.localAuthority.slug == "slug2")
+        #expect(localAuthority?.localAuthority.parent?.name == "parentAuthority")
+    }
+
+    @Test
+    func fetchLocalAuthority_apiUnavailable_returnsExpectedResult() async throws {
+        let mockServiceClient = MockLocalServiceClient()
+        mockServiceClient._stubbedLocalResult = .failure(.apiUnavailable)
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        #expect(localResult == nil)
+        #expect(result.getError() == .apiUnavailable)
+    }
+
+
+    @Test
+    func fetchLocalAuthority_decodingError_returnsExpectedResult() async throws {
+        let mockServiceClient = MockLocalServiceClient()
+        mockServiceClient._stubbedLocalResult = .failure(.decodingError)
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        #expect(localResult == nil)
+        #expect(result.getError() == .decodingError)
+    }
+
+    @Test
+    func fetchLocalAuthority_networkUnavailable_returnsExpectedResult() async throws {
+        let mockServiceClient = MockLocalServiceClient()
+        mockServiceClient._stubbedLocalResult = .failure(.networkUnavailable)
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        #expect(localResult == nil)
+        #expect(result.getError() == .networkUnavailable)
+    }
+
+    @Test
+    func fetchLocalAuthority_localErrorMessage_returnsExpectedResult() async throws {
+        let mockServiceClient = MockLocalServiceClient()
+        mockServiceClient._stubbedLocalResult = .success(LocalErrorMessage(message: "errorMessage"))
+
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        let errorMessage = localResult as? LocalErrorMessage
+        #expect(errorMessage?.message == "errorMessage")
+    }
+}


Reviews:
user: login: joshdubey
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:17:56Z
user: login: RaphealOlumofe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:54:32Z
user: login: RaphealOlumofe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:54:40Z
user: login: RaphealOlumofe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:54:48Z
user: login: joshdubey
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T15:00:22Z

Review comments:
diff_hunk: @@ -0,0 +1,199 @@
+import Foundation
+import Testing
+
+@testable import govuk_ios
+
+@Suite
+struct LocalAuthorityServiceClientTests {
+
+    @Test
+    func fetchLocalAuthority_sendsExpectedResults() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(serviceClient: mockAPI)
+        let expectedPostcode = "SE129PT"
+
+        sut.fetchLocalAuthority(
+            postcode: expectedPostcode) { _ in }
+
+        #expect(mockAPI._receivedSendRequest?.urlPath == "/find-local-council/query.json")
+        #expect(mockAPI._receivedSendRequest?.method == .get)
+        #expect(mockAPI._receivedSendRequest?.queryParameters?["postcode"] as? String == expectedPostcode)
+    }
+
+    @Test
+    func fetchLocalAuthority_addressLists_returnsExpectedResult() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(
+            serviceClient: mockAPI
+        )
+        let addresses:[LocalAuthorityAddress] = [
+            LocalAuthorityAddress(
+                address: "address1",
+                slug: "slug1",
+                name: "name1"
+            ),
+            LocalAuthorityAddress(
+                address: "address2",
+                slug: "slug2",
+                name: "name2"
+            )
+        ]
+
+        let expectedResult = LocalAuthoritiesList(addresses: addresses)
+        let stubbedData = try! JSONEncoder().encode(expectedResult)
+        mockAPI._stubbedSendResponse = .success(stubbedData)
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        let addressList = localResult as? LocalAuthoritiesList
+        #expect(addressList?.addresses.count == 2)
+        #expect(addressList?.addresses.first?.name == "name1")
+        #expect(addressList?.addresses[1].name == "name2")
path: Tests/govuk_ios/govuk_ios_unit_tests/Specs/ServiceClients/LocalAuthorityServiceClientTests.swift
user: login: joshdubey
body: Not that it's going to happen, but to avoid out-of-bounds exceptions, rather than use the hard index `addresses[1]`, use `addresses.last?` instead

created_at: 2025-04-14T12:45:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,199 @@
+import Foundation
+import Testing
+
+@testable import govuk_ios
+
+@Suite
+struct LocalAuthorityServiceClientTests {
+
+    @Test
+    func fetchLocalAuthority_sendsExpectedResults() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(serviceClient: mockAPI)
+        let expectedPostcode = "SE129PT"
+
+        sut.fetchLocalAuthority(
+            postcode: expectedPostcode) { _ in }
+
+        #expect(mockAPI._receivedSendRequest?.urlPath == "/find-local-council/query.json")
+        #expect(mockAPI._receivedSendRequest?.method == .get)
+        #expect(mockAPI._receivedSendRequest?.queryParameters?["postcode"] as? String == expectedPostcode)
+    }
+
+    @Test
+    func fetchLocalAuthority_addressLists_returnsExpectedResult() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(
+            serviceClient: mockAPI
+        )
+        let addresses:[LocalAuthorityAddress] = [
+            LocalAuthorityAddress(
+                address: "address1",
+                slug: "slug1",
+                name: "name1"
+            ),
+            LocalAuthorityAddress(
+                address: "address2",
+                slug: "slug2",
+                name: "name2"
+            )
+        ]
+
+        let expectedResult = LocalAuthoritiesList(addresses: addresses)
+        let stubbedData = try! JSONEncoder().encode(expectedResult)
path: Tests/govuk_ios/govuk_ios_unit_tests/Specs/ServiceClients/LocalAuthorityServiceClientTests.swift
user: login: joshdubey
body: Can I suggest that for these tests that require the decoding of results, we follow the approach used in the tests for TopicsServiceClient and AppConfigServiceClient?  That is, rather than encoding the object as you've done here, we use the actual json we expect to get from the server.  That way we know our Codable objects are structured correctly for the JSON.
created_at: 2025-04-14T13:15:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+import Foundation
+import Testing
+
+@testable import govuk_ios
+
+@Suite
+struct LocalAuthorityServiceTests {
+
+    @Test
+    func fetchLocalAuthority_callsServiceClient() async throws {
+        let mockServiceClient = MockLocalServiceClient()
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let expectedPostcode = "SW1"
+
+        sut.fetchLocalAuthority(
+            postcode: expectedPostcode) { _ in }
+        #expect(mockServiceClient._stubbedPostcode == expectedPostcode)
+    }
+
+    @Test
+    func fetchLocalAuthority_addressLists_returnsExpectedResult() async throws {
+        let addresses:[LocalAuthorityAddress] = [
+            LocalAuthorityAddress(
+                address: "address1",
+                slug: "slug1",
+                name: "name1"
+            ),
+            LocalAuthorityAddress(
+                address: "address2",
+                slug: "slug2",
+                name: "name2"
+            )
+        ]
+        let expectedResult = LocalAuthoritiesList(addresses: addresses)
+        let mockServiceClient = MockLocalServiceClient()
+        mockServiceClient._stubbedLocalResult = .success(expectedResult)
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        let addressList = localResult as? LocalAuthoritiesList
+        #expect(addressList?.addresses.count == 2)
path: Tests/govuk_ios/govuk_ios_unit_tests/Specs/Services/LocalAuthortiyService.swift
user: login: joshdubey
body: Same as above, use last?, not [1]
created_at: 2025-04-14T13:17:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,199 @@
+import Foundation
+import Testing
+
+@testable import govuk_ios
+
+@Suite
+struct LocalAuthorityServiceClientTests {
+
+    @Test
+    func fetchLocalAuthority_sendsExpectedResults() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(serviceClient: mockAPI)
+        let expectedPostcode = "SE129PT"
+
+        sut.fetchLocalAuthority(
+            postcode: expectedPostcode) { _ in }
+
+        #expect(mockAPI._receivedSendRequest?.urlPath == "/find-local-council/query.json")
+        #expect(mockAPI._receivedSendRequest?.method == .get)
+        #expect(mockAPI._receivedSendRequest?.queryParameters?["postcode"] as? String == expectedPostcode)
+    }
+
+    @Test
+    func fetchLocalAuthority_addressLists_returnsExpectedResult() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(
+            serviceClient: mockAPI
+        )
+        let addresses:[LocalAuthorityAddress] = [
+            LocalAuthorityAddress(
+                address: "address1",
+                slug: "slug1",
+                name: "name1"
+            ),
+            LocalAuthorityAddress(
+                address: "address2",
+                slug: "slug2",
+                name: "name2"
+            )
+        ]
+
+        let expectedResult = LocalAuthoritiesList(addresses: addresses)
+        let stubbedData = try! JSONEncoder().encode(expectedResult)
+        mockAPI._stubbedSendResponse = .success(stubbedData)
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        let addressList = localResult as? LocalAuthoritiesList
+        #expect(addressList?.addresses.count == 2)
+        #expect(addressList?.addresses.first?.name == "name1")
+        #expect(addressList?.addresses[1].name == "name2")
path: Tests/govuk_ios/govuk_ios_unit_tests/Specs/ServiceClients/LocalAuthorityServiceClientTests.swift
user: login: RaphealOlumofe
body: done
created_at: 2025-04-14T14:54:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,199 @@
+import Foundation
+import Testing
+
+@testable import govuk_ios
+
+@Suite
+struct LocalAuthorityServiceClientTests {
+
+    @Test
+    func fetchLocalAuthority_sendsExpectedResults() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(serviceClient: mockAPI)
+        let expectedPostcode = "SE129PT"
+
+        sut.fetchLocalAuthority(
+            postcode: expectedPostcode) { _ in }
+
+        #expect(mockAPI._receivedSendRequest?.urlPath == "/find-local-council/query.json")
+        #expect(mockAPI._receivedSendRequest?.method == .get)
+        #expect(mockAPI._receivedSendRequest?.queryParameters?["postcode"] as? String == expectedPostcode)
+    }
+
+    @Test
+    func fetchLocalAuthority_addressLists_returnsExpectedResult() async throws {
+        let mockAPI = MockAPIServiceClient()
+        let sut = LocalAuthorityServiceClient(
+            serviceClient: mockAPI
+        )
+        let addresses:[LocalAuthorityAddress] = [
+            LocalAuthorityAddress(
+                address: "address1",
+                slug: "slug1",
+                name: "name1"
+            ),
+            LocalAuthorityAddress(
+                address: "address2",
+                slug: "slug2",
+                name: "name2"
+            )
+        ]
+
+        let expectedResult = LocalAuthoritiesList(addresses: addresses)
+        let stubbedData = try! JSONEncoder().encode(expectedResult)
path: Tests/govuk_ios/govuk_ios_unit_tests/Specs/ServiceClients/LocalAuthorityServiceClientTests.swift
user: login: RaphealOlumofe
body: done
created_at: 2025-04-14T14:54:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+import Foundation
+import Testing
+
+@testable import govuk_ios
+
+@Suite
+struct LocalAuthorityServiceTests {
+
+    @Test
+    func fetchLocalAuthority_callsServiceClient() async throws {
+        let mockServiceClient = MockLocalServiceClient()
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let expectedPostcode = "SW1"
+
+        sut.fetchLocalAuthority(
+            postcode: expectedPostcode) { _ in }
+        #expect(mockServiceClient._stubbedPostcode == expectedPostcode)
+    }
+
+    @Test
+    func fetchLocalAuthority_addressLists_returnsExpectedResult() async throws {
+        let addresses:[LocalAuthorityAddress] = [
+            LocalAuthorityAddress(
+                address: "address1",
+                slug: "slug1",
+                name: "name1"
+            ),
+            LocalAuthorityAddress(
+                address: "address2",
+                slug: "slug2",
+                name: "name2"
+            )
+        ]
+        let expectedResult = LocalAuthoritiesList(addresses: addresses)
+        let mockServiceClient = MockLocalServiceClient()
+        mockServiceClient._stubbedLocalResult = .success(expectedResult)
+        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+
+        let result = await withCheckedContinuation { continuation in
+            sut.fetchLocalAuthority(
+                postcode: "test") { result in
+                    continuation.resume(returning: result)
+                }
+        }
+        let localResult = try? result.get()
+        let addressList = localResult as? LocalAuthoritiesList
+        #expect(addressList?.addresses.count == 2)
path: Tests/govuk_ios/govuk_ios_unit_tests/Specs/Services/LocalAuthortiyService.swift
user: login: RaphealOlumofe
body: done
created_at: 2025-04-14T14:54:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-14T15:22:25Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=alphagov_govuk-mobile-ios-app&pullRequest=337) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=alphagov_govuk-mobile-ios-app&pullRequest=337&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [8 Accepted issues](https://sonarcloud.io/project/issues?id=alphagov_govuk-mobile-ios-app&pullRequest=337&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=alphagov_govuk-mobile-ios-app&pullRequest=337&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=alphagov_govuk-mobile-ios-app&pullRequest=337&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=alphagov_govuk-mobile-ios-app&pullRequest=337&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=alphagov_govuk-mobile-ios-app&pullRequest=337)


author_association: NONE