Repository: alphagov/govuk-mobile-ios-app
Pull request #340: GOVUKAPP-1355: Store Local Authority
Author: login: joshdubey
Created: 2025-04-16T08:29:57Z | Merged: 2025-04-17T12:32:14Z

Add a LocalAuthorityRepository and corresponding core data model updates and classes.
As users will only ever have one local authority, or a parent-child pair, delete old authority if saving a new one.


Diff:
diff --git a/GovUK.xcodeproj/project.pbxproj b/GovUK.xcodeproj/project.pbxproj
index 8a2aa508e..d2fc7e6d0 100644
--- a/GovUK.xcodeproj/project.pbxproj
+++ b/GovUK.xcodeproj/project.pbxproj
@@ -219,6 +219,7 @@
 		D05AF4DC2CAD3C4E00F86DD0 /* TopicsWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05AF4DB2CAD3C4E00F86DD0 /* TopicsWidgetViewModel.swift */; };
 		D06EB96C2CA6F71E007EA081 /* TopicCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06EB96B2CA6F71E007EA081 /* TopicCard.swift */; };
 		D076F9192D3544F0000278D5 /* SearchHistoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D076F9182D3544F0000278D5 /* SearchHistoryCell.swift */; };
+		D07E7A582DAE662A00B92D40 /* LocalAuthorityRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07E7A572DAE662A00B92D40 /* LocalAuthorityRepository.swift */; };
 		D093BA632CB0499A004F489B /* EditTopicsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D093BA622CB0499A004F489B /* EditTopicsView.swift */; };
 		D093BA652CB04C68004F489B /* EditTopicsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D093BA642CB04C68004F489B /* EditTopicsViewModel.swift */; };
 		D093BA672CB3CBEC004F489B /* EditTopicsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D093BA662CB3CBEC004F489B /* EditTopicsCoordinator.swift */; };
@@ -506,6 +507,7 @@
 		D05AF4DB2CAD3C4E00F86DD0 /* TopicsWidgetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicsWidgetViewModel.swift; sourceTree = "<group>"; };
 		D06EB96B2CA6F71E007EA081 /* TopicCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicCard.swift; sourceTree = "<group>"; };
 		D076F9182D3544F0000278D5 /* SearchHistoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryCell.swift; sourceTree = "<group>"; };
+		D07E7A572DAE662A00B92D40 /* LocalAuthorityRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthorityRepository.swift; sourceTree = "<group>"; };
 		D093BA622CB0499A004F489B /* EditTopicsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTopicsView.swift; sourceTree = "<group>"; };
 		D093BA642CB04C68004F489B /* EditTopicsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTopicsViewModel.swift; sourceTree = "<group>"; };
 		D093BA662CB3CBEC004F489B /* EditTopicsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTopicsCoordinator.swift; sourceTree = "<group>"; };
@@ -599,6 +601,7 @@
 				govuk_ios/govuk_ios_snapshot_tests/Specs/ViewControllers/TopicDetailViewSnapshotTests.swift,
 				govuk_ios/govuk_ios_snapshot_tests/Specs/ViewControllers/TopicOnboardingViewControllerSnapshotTests.swift,
 				"govuk_ios/govuk_ios_unit_tests/Arrangers/Model/TopicResponseItem+Arrangers.swift",
+				govuk_ios/govuk_ios_unit_tests/Mocks/Repositories/MockLocalAuthorityRepository.swift,
 				govuk_ios/govuk_ios_unit_tests/Mocks/Repositories/MockTopicsRepository.swift,
 				"govuk_ios/shared/Arrangers/Foundation/URL+Arrangers.swift",
 				"govuk_ios/shared/Arrangers/Model/RecentActivity/ActivityItem+Arrangers.swift",
@@ -908,6 +911,7 @@
 				D0EB04812D37F34200B310D8 /* GOV.xcdatamodeld */,
 				D0D9CFD42D302F2A0051EDDD /* SearchHistoryRepository.swift */,
 				D028CCF32CB4247100742620 /* TopicsRepository.swift */,
+				D07E7A572DAE662A00B92D40 /* LocalAuthorityRepository.swift */,
 			);
 			path = Repositories;
 			sourceTree = "<group>";
@@ -1791,6 +1795,7 @@
 				5D4F97EB2BFF7725002CBDDE /* CoordinatorBuilder.swift in Sources */,
 				5DDD58892D5A556A001C0B3E /* NotificationOnboardingCoordinator.swift in Sources */,
 				5DD4CB0F2D95485200459FB5 /* Date+Extensions.swift in Sources */,
+				D07E7A582DAE662A00B92D40 /* LocalAuthorityRepository.swift in Sources */,
 				5DD4CB102D95485200459FB5 /* DateFormatter+Convenience.swift in Sources */,
 				5D18E83E2CCFB68900F20419 /* Container+APIClients.swift in Sources */,
 				D0D9CB932D3024C00051EDDD /* SearchResult.swift in Sources */,
diff --git a/Production/govuk_ios/Model/LocalAuthority/LocalAuthorityItem.swift b/Production/govuk_ios/Model/LocalAuthority/LocalAuthorityItem.swift
new file mode 100644
index 000000000..4b297af2c
--- /dev/null
+++ b/Production/govuk_ios/Model/LocalAuthority/LocalAuthorityItem.swift
@@ -0,0 +1,25 @@
+import Foundation
+import CoreData
+
+@objc(LocalAuthorityItem)
+class LocalAuthorityItem: NSManagedObject,
+                          Identifiable {
+    @nonobjc public class func fetchRequest() -> NSFetchRequest<LocalAuthorityItem> {
+        return NSFetchRequest<LocalAuthorityItem>(entityName: "LocalAuthorityItem")
+    }
+
+    @NSManaged public var name: String
+    @NSManaged public var homepageUrl: String
+    @NSManaged public var tier: String
+    @NSManaged public var slug: String
+    @NSManaged public var parent: LocalAuthorityItem?
+}
+
+extension LocalAuthorityItem {
+    func update(with authority: Authority) {
+        name = authority.name
+        homepageUrl = authority.homepageUrl
+        slug = authority.slug
+        tier = authority.tier
+    }
+}
diff --git a/Production/govuk_ios/Repositories/GOV.xcdatamodeld/GOV_v2.xcdatamodel/contents b/Production/govuk_ios/Repositories/GOV.xcdatamodeld/GOV_v2.xcdatamodel/contents
index 7f211800e..dbd9bda23 100644
--- a/Production/govuk_ios/Repositories/GOV.xcdatamodeld/GOV_v2.xcdatamodel/contents
+++ b/Production/govuk_ios/Repositories/GOV.xcdatamodeld/GOV_v2.xcdatamodel/contents
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24C101" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788" systemVersion="24E248" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
     <entity name="ActivityItem" representedClassName="ActivityItem" syncable="YES">
         <attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="id" attributeType="String"/>
@@ -11,6 +11,13 @@
             </uniquenessConstraint>
         </uniquenessConstraints>
     </entity>
+    <entity name="LocalAuthorityItem" representedClassName="LocalAuthorityItem" syncable="YES">
+        <attribute name="homepageUrl" attributeType="String"/>
+        <attribute name="name" attributeType="String"/>
+        <attribute name="slug" attributeType="String"/>
+        <attribute name="tier" attributeType="String"/>
+        <relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LocalAuthorityItem"/>
+    </entity>
     <entity name="SearchHistoryItem" representedClassName="SearchHistoryItem" syncable="YES">
         <attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="searchText" attributeType="String"/>
diff --git a/Production/govuk_ios/Repositories/LocalAuthorityRepository.swift b/Production/govuk_ios/Repositories/LocalAuthorityRepository.swift
new file mode 100644
index 000000000..edf4c56e6
--- /dev/null
+++ b/Production/govuk_ios/Repositories/LocalAuthorityRepository.swift
@@ -0,0 +1,49 @@
+import Foundation
+import CoreData
+import GOVKit
+
+protocol LocalAuthorityRepositoryInterface {
+    func save(_ localAuthority: LocalAuthority)
+    func fetchLocalAuthority() -> [LocalAuthorityItem]
+}
+
+struct LocalAuthorityRepository: LocalAuthorityRepositoryInterface {
+    private let coreData: CoreDataRepositoryInterface
+
+    init(coreData: CoreDataRepositoryInterface) {
+        self.coreData = coreData
+    }
+
+    func save(_ localAuthority: LocalAuthority) {
+        let context = coreData.backgroundContext
+        var oldLocalAuthorityItems = [LocalAuthorityItem]()
+
+        context.performAndWait {
+            oldLocalAuthorityItems = fetch(context: context)
+        }
+        oldLocalAuthorityItems.forEach {
+            context.delete($0)
+        }
+
+        let localAuthorityItem = LocalAuthorityItem(context: context)
+        let authority = localAuthority.localAuthority
+        localAuthorityItem.update(with: authority)
+
+        if let parentAuthority = authority.parent {
+            let localParent = LocalAuthorityItem(context: context)
+            localParent.update(with: parentAuthority)
+            localAuthorityItem.parent = localParent
+        }
+
+        try? context.save()
+    }
+
+    func fetchLocalAuthority() -> [LocalAuthorityItem] {
+        fetch(context: coreData.viewContext)
+    }
+
+    private func fetch(context: NSManagedObjectContext) -> [LocalAuthorityItem] {
+        let fetchRequest = LocalAuthorityItem.fetchRequest()
+        return (try? context.fetch(fetchRequest)) ?? []
+    }
+}
diff --git a/Production/govuk_ios/Services/LocalAuthorityService.swift b/Production/govuk_ios/Services/LocalAuthorityService.swift
index 70b41b9b7..753ea3349 100644
--- a/Production/govuk_ios/Services/LocalAuthorityService.swift
+++ b/Production/govuk_ios/Services/LocalAuthorityService.swift
@@ -6,15 +6,30 @@ protocol LocalAuthorityServiceInterface {
 
 class LocalAuthorityService: LocalAuthorityServiceInterface {
     private let serviceClient: LocalAuthorityServiceClientInterface
+    private let repository: LocalAuthorityRepositoryInterface
 
-    init(serviceClient: LocalAuthorityServiceClientInterface) {
+    init(serviceClient: LocalAuthorityServiceClientInterface,
+         repository: LocalAuthorityRepositoryInterface) {
         self.serviceClient = serviceClient
+        self.repository = repository
     }
 
     func fetchLocalAuthority(postcode: String,
                              completion: @escaping FetchLocalAuthorityCompletion) {
-        serviceClient.fetchLocalAuthority(postcode: postcode) { result in
-            completion(result)
+        serviceClient.fetchLocalAuthority(postcode: postcode) { [weak self] result in
+            switch result {
+            case .success(let authorityType):
+                self?.updateLocalAuthority(authorityType)
+                completion(.success(authorityType))
+            case .failure(let error):
+                completion(.failure(error))
+            }
+        }
+    }
+
+    private func updateLocalAuthority(_ authorityType: LocalAuthorityType) {
+        if let localAuthority = authorityType as? LocalAuthority {
+            repository.save(localAuthority)
         }
     }
 }
diff --git a/Tests/govuk_ios/govuk_ios_unit_tests/Mocks/Repositories/MockLocalAuthorityRepository.swift b/Tests/govuk_ios/govuk_ios_unit_tests/Mocks/Repositories/MockLocalAuthorityRepository.swift
new file mode 100644
index 000000000..846c7589e
--- /dev/null
+++ b/Tests/govuk_ios/govuk_ios_unit_tests/Mocks/Repositories/MockLocalAuthorityRepository.swift
@@ -0,0 +1,16 @@
+import Foundation
+
+@testable import govuk_ios
+
+final class MockLocalAuthorityRepository: LocalAuthorityRepositoryInterface {
+    var _didSaveLocalAuthority = false
+    func save(_ localAuthority: LocalAuthority) {
+        _didSaveLocalAuthority = true
+    }
+
+    var _didFetchLocalAuthority = false
+    func fetchLocalAuthority() -> [LocalAuthorityItem] {
+        _didFetchLocalAuthority = true
+        return []
+    }
+}
diff --git a/Tests/govuk_ios/govuk_ios_unit_tests/Specs/Repositories/LocalAuthorityRepositoryTests.swift b/Tests/govuk_ios/govuk_ios_unit_tests/Specs/Repositories/LocalAuthorityRepositoryTests.swift
new file mode 100644
index 000000000..34c4cee12
--- /dev/null
+++ b/Tests/govuk_ios/govuk_ios_unit_tests/Specs/Repositories/LocalAuthorityRepositoryTests.swift
@@ -0,0 +1,91 @@
+import Foundation
+import Testing
+import CoreData
+
+@testable import govuk_ios
+
+@Suite
+@MainActor
+struct LocalAuthorityRepositoryTests {
+
+    @Test func save_localAuthorityItem_savesObject() {
+        let coreData = CoreDataRepository.arrangeAndLoad
+        let sut = LocalAuthorityRepository(coreData: coreData)
+
+        let parentAuthority = Authority(
+            name: "Derbyshire County Council",
+            homepageUrl: "https://www.derbyshiredales.gov.uk/",
+            tier: "county",
+            slug: "derbyshire"
+        )
+        let localAuthority = LocalAuthority(
+            localAuthority: Authority(
+                name: "Derbyshire Dales District Council",
+                homepageUrl: "https://www.derbyshiredales.gov.uk/",
+                tier: "district",
+                slug: "derbyshire-dales",
+                parent: parentAuthority
+            )
+        )
+
+        sut.save(localAuthority)
+
+        let localAuthorityItems = sut.fetchLocalAuthority()
+        #expect(localAuthorityItems.count == 2)
+        let childAuthority = localAuthorityItems.first(where: { $0.parent != nil })
+        #expect(childAuthority?.name == "Derbyshire Dales District Council")
+        #expect(childAuthority?.homepageUrl == "https://www.derbyshiredales.gov.uk/")
+        #expect(childAuthority?.tier == "district")
+        #expect(childAuthority?.slug == "derbyshire-dales")
+        let parent = childAuthority?.parent
+        #expect(parent?.name == "Derbyshire County Council")
+        #expect(parent?.homepageUrl == "https://www.derbyshiredales.gov.uk/")
+        #expect(parent?.tier == "county")
+        #expect(parent?.slug == "derbyshire")
+        #expect(parent?.parent == nil)
+
+    }
+
+    @Test func save_localAuthorityItem_replacesOldItems() {
+        let coreData = CoreDataRepository.arrangeAndLoad
+        let sut = LocalAuthorityRepository(coreData: coreData)
+
+        let parentAuthority = Authority(
+            name: "Derbyshire County Council",
+            homepageUrl: "https://www.derbyshiredales.gov.uk/",
+            tier: "county",
+            slug: "derbyshire-dales"
+        )
+        let localAuthority = LocalAuthority(
+            localAuthority: Authority(
+                name: "Derbyshire Dales District Council",
+                homepageUrl: "https://www.derbyshiredales.gov.uk/",
+                tier: "district",
+                slug: "derbyshire-dales",
+                parent: parentAuthority
+            )
+        )
+
+        sut.save(localAuthority)
+
+        let newLocalAuthority = LocalAuthority(
+            localAuthority: Authority(
+                name: "London Borough of Tower Hamlets",
+                homepageUrl: "https://www.towerhamlets.gov.uk",
+                tier: "unitary",
+                slug: "tower-hamlets"
+            )
+        )
+
+        sut.save(newLocalAuthority)
+
+        let localAuthorityItems = sut.fetchLocalAuthority()
+        #expect(localAuthorityItems.count == 1)
+        #expect(localAuthorityItems.first?.name == "London Borough of Tower Hamlets")
+        #expect(localAuthorityItems.first?.homepageUrl == "https://www.towerhamlets.gov.uk")
+        #expect(localAuthorityItems.first?.tier == "unitary")
+        #expect(localAuthorityItems.first?.slug == "tower-hamlets")
+        #expect(localAuthorityItems.first?.parent == nil)
+    }
+
+}
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
index b9201b413..bfec292d4 100644
--- a/Tests/govuk_ios/govuk_ios_unit_tests/Specs/Services/LocalAuthortiyServiceTests.swift
+++ b/Tests/govuk_ios/govuk_ios_unit_tests/Specs/Services/LocalAuthortiyServiceTests.swift
@@ -9,7 +9,11 @@ struct LocalAuthorityServiceTests {
     @Test
     func fetchLocalAuthority_callsServiceClient() async throws {
         let mockServiceClient = MockLocalServiceClient()
-        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let mockRepository = MockLocalAuthorityRepository()
+        let sut = LocalAuthorityService(
+            serviceClient: mockServiceClient,
+            repository: mockRepository
+        )
         let expectedPostcode = "SW1"
 
         sut.fetchLocalAuthority(
@@ -34,7 +38,11 @@ struct LocalAuthorityServiceTests {
         let expectedResult = LocalAuthoritiesList(addresses: addresses)
         let mockServiceClient = MockLocalServiceClient()
         mockServiceClient._stubbedLocalResult = .success(expectedResult)
-        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let mockRepository = MockLocalAuthorityRepository()
+        let sut = LocalAuthorityService(
+            serviceClient: mockServiceClient,
+            repository: mockRepository
+        )
 
         let result = await withCheckedContinuation { continuation in
             sut.fetchLocalAuthority(
@@ -49,6 +57,7 @@ struct LocalAuthorityServiceTests {
         #expect(addressList?.addresses.last?.name == "name2")
         #expect(addressList?.addresses.first?.slug == "slug1")
         #expect(addressList?.addresses.last?.name == "name2")
+        #expect(!mockRepository._didSaveLocalAuthority)
     }
 
     @Test
@@ -63,7 +72,11 @@ struct LocalAuthorityServiceTests {
         let expectedResult = LocalAuthority(localAuthority: authority)
         let mockServiceClient = MockLocalServiceClient()
         mockServiceClient._stubbedLocalResult = .success(expectedResult)
-        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let mockRepository = MockLocalAuthorityRepository()
+        let sut = LocalAuthorityService(
+            serviceClient: mockServiceClient,
+            repository: mockRepository
+        )
 
         let result = await withCheckedContinuation { continuation in
             sut.fetchLocalAuthority(
@@ -77,6 +90,7 @@ struct LocalAuthorityServiceTests {
         #expect(localAuthority?.localAuthority.name == "name1")
         #expect(localAuthority?.localAuthority.tier == "tier1")
         #expect(localAuthority?.localAuthority.slug == "slug1")
+        #expect(mockRepository._didSaveLocalAuthority)
     }
 
     @Test
@@ -98,7 +112,11 @@ struct LocalAuthorityServiceTests {
         let expectedResult = LocalAuthority(localAuthority: authority)
         let mockServiceClient = MockLocalServiceClient()
         mockServiceClient._stubbedLocalResult = .success(expectedResult)
-        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let mockRepository = MockLocalAuthorityRepository()
+        let sut = LocalAuthorityService(
+            serviceClient: mockServiceClient,
+            repository: mockRepository
+        )
 
         let result = await withCheckedContinuation { continuation in
             sut.fetchLocalAuthority(
@@ -113,13 +131,18 @@ struct LocalAuthorityServiceTests {
         #expect(localAuthority?.localAuthority.tier == "tier2")
         #expect(localAuthority?.localAuthority.slug == "slug2")
         #expect(localAuthority?.localAuthority.parent?.name == "parentAuthority")
+        #expect(mockRepository._didSaveLocalAuthority)
     }
 
     @Test
     func fetchLocalAuthority_apiUnavailable_returnsExpectedResult() async throws {
         let mockServiceClient = MockLocalServiceClient()
         mockServiceClient._stubbedLocalResult = .failure(.apiUnavailable)
-        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let mockRepository = MockLocalAuthorityRepository()
+        let sut = LocalAuthorityService(
+            serviceClient: mockServiceClient,
+            repository: mockRepository
+        )
         let result = await withCheckedContinuation { continuation in
             sut.fetchLocalAuthority(
                 postcode: "test") { result in
@@ -129,6 +152,7 @@ struct LocalAuthorityServiceTests {
         let localResult = try? result.get()
         #expect(localResult == nil)
         #expect(result.getError() == .apiUnavailable)
+        #expect(!mockRepository._didSaveLocalAuthority)
     }
 
 
@@ -136,7 +160,11 @@ struct LocalAuthorityServiceTests {
     func fetchLocalAuthority_decodingError_returnsExpectedResult() async throws {
         let mockServiceClient = MockLocalServiceClient()
         mockServiceClient._stubbedLocalResult = .failure(.decodingError)
-        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let mockRepository = MockLocalAuthorityRepository()
+        let sut = LocalAuthorityService(
+            serviceClient: mockServiceClient,
+            repository: mockRepository
+        )
         let result = await withCheckedContinuation { continuation in
             sut.fetchLocalAuthority(
                 postcode: "test") { result in
@@ -146,13 +174,18 @@ struct LocalAuthorityServiceTests {
         let localResult = try? result.get()
         #expect(localResult == nil)
         #expect(result.getError() == .decodingError)
+        #expect(!mockRepository._didSaveLocalAuthority)
     }
 
     @Test
     func fetchLocalAuthority_networkUnavailable_returnsExpectedResult() async throws {
         let mockServiceClient = MockLocalServiceClient()
         mockServiceClient._stubbedLocalResult = .failure(.networkUnavailable)
-        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let mockRepository = MockLocalAuthorityRepository()
+        let sut = LocalAuthorityService(
+            serviceClient: mockServiceClient,
+            repository: mockRepository
+        )
         let result = await withCheckedContinuation { continuation in
             sut.fetchLocalAuthority(
                 postcode: "test") { result in
@@ -162,6 +195,7 @@ struct LocalAuthorityServiceTests {
         let localResult = try? result.get()
         #expect(localResult == nil)
         #expect(result.getError() == .networkUnavailable)
+        #expect(!mockRepository._didSaveLocalAuthority)
     }
 
     @Test
@@ -169,7 +203,11 @@ struct LocalAuthorityServiceTests {
         let mockServiceClient = MockLocalServiceClient()
         mockServiceClient._stubbedLocalResult = .success(LocalErrorMessage(message: "errorMessage"))
 
-        let sut = LocalAuthorityService(serviceClient: mockServiceClient)
+        let mockRepository = MockLocalAuthorityRepository()
+        let sut = LocalAuthorityService(
+            serviceClient: mockServiceClient,
+            repository: mockRepository
+        )
         let result = await withCheckedContinuation { continuation in
             sut.fetchLocalAuthority(
                 postcode: "test") { result in
@@ -179,5 +217,6 @@ struct LocalAuthorityServiceTests {
         let localResult = try? result.get()
         let errorMessage = localResult as? LocalErrorMessage
         #expect(errorMessage?.message == "errorMessage")
+        #expect(!mockRepository._didSaveLocalAuthority)
     }
 }


Reviews:
user: login: batesyuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T07:16:14Z
user: login: batesyuk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T09:59:52Z
user: login: joshdubey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T10:19:07Z
user: login: joshdubey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T10:19:16Z
user: login: RaphealOlumofe
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T11:44:36Z
user: login: batesyuk
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T11:47:07Z

Review comments:
diff_hunk: @@ -0,0 +1,91 @@
+import Foundation
+import Testing
+import CoreData
+
+@testable import govuk_ios
+
+@Suite
+@MainActor
+struct LocalAuthorityRepositoryTests {
+
+    @Test func save_localAuthorityItem_savesObject() async throws {
path: Tests/govuk_ios/govuk_ios_unit_tests/Specs/Repositories/LocalAuthorityRepositoryTests.swift
user: login: batesyuk
body: Do these need to be `async throws`?
created_at: 2025-04-17T07:12:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,15 +6,30 @@ protocol LocalAuthorityServiceInterface {
 
 class LocalAuthorityService: LocalAuthorityServiceInterface {
     private let serviceClient: LocalAuthorityServiceClientInterface
+    private let repository: LocalAuthorityRepositoryInterface
 
-    init(serviceClient: LocalAuthorityServiceClientInterface) {
+    init(serviceClient: LocalAuthorityServiceClientInterface,
+         repository: LocalAuthorityRepositoryInterface) {
         self.serviceClient = serviceClient
+        self.repository = repository
     }
 
     func fetchLocalAuthority(postcode: String,
                              completion: @escaping FetchLocalAuthorityCompletion) {
         serviceClient.fetchLocalAuthority(postcode: postcode) { result in
-            completion(result)
+            switch result {
+            case .success(let authorityType):
+                self.updateLocalAuthority(authorityType)
path: Production/govuk_ios/Services/LocalAuthorityService.swift
user: login: batesyuk
body: Should this be weak?
created_at: 2025-04-17T09:59:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,91 @@
+import Foundation
+import Testing
+import CoreData
+
+@testable import govuk_ios
+
+@Suite
+@MainActor
+struct LocalAuthorityRepositoryTests {
+
+    @Test func save_localAuthorityItem_savesObject() async throws {
path: Tests/govuk_ios/govuk_ios_unit_tests/Specs/Repositories/LocalAuthorityRepositoryTests.swift
user: login: joshdubey
body: Done
created_at: 2025-04-17T10:19:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,15 +6,30 @@ protocol LocalAuthorityServiceInterface {
 
 class LocalAuthorityService: LocalAuthorityServiceInterface {
     private let serviceClient: LocalAuthorityServiceClientInterface
+    private let repository: LocalAuthorityRepositoryInterface
 
-    init(serviceClient: LocalAuthorityServiceClientInterface) {
+    init(serviceClient: LocalAuthorityServiceClientInterface,
+         repository: LocalAuthorityRepositoryInterface) {
         self.serviceClient = serviceClient
+        self.repository = repository
     }
 
     func fetchLocalAuthority(postcode: String,
                              completion: @escaping FetchLocalAuthorityCompletion) {
         serviceClient.fetchLocalAuthority(postcode: postcode) { result in
-            completion(result)
+            switch result {
+            case .success(let authorityType):
+                self.updateLocalAuthority(authorityType)
path: Production/govuk_ios/Services/LocalAuthorityService.swift
user: login: joshdubey
body: Done
created_at: 2025-04-17T10:19:16Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-17T11:02:41Z
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=340) **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=340&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=alphagov_govuk-mobile-ios-app&pullRequest=340&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=340&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=340&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=340&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=alphagov_govuk-mobile-ios-app&pullRequest=340)


author_association: NONE