@@ -8,6 +8,115 @@ import TableProPluginKit
88import Testing
99@testable import TablePro
1010
11+ // MARK: - Mock DriverPlugin for Testing
12+
13+ private final class MockDriverPlugin : NSObject , TableProPlugin , DriverPlugin {
14+ static var pluginName = " MockDriver "
15+ static var pluginVersion = " 1.0.0 "
16+ static var pluginDescription = " Test plugin "
17+ static var capabilities : [ PluginCapability ] = [ . databaseDriver]
18+ static var dependencies : [ String ] = [ ]
19+
20+ static var databaseTypeId = " mock-db "
21+ static var databaseDisplayName = " Mock Database "
22+ static var iconName = " cylinder.fill "
23+ static var defaultPort = 9999
24+
25+ func createDriver( config: DriverConnectionConfig ) -> any PluginDatabaseDriver {
26+ fatalError ( " Not used in tests " )
27+ }
28+
29+ required override init ( ) {
30+ super. init ( )
31+ }
32+
33+ static func reset(
34+ typeId: String = " mock-db " ,
35+ displayName: String = " Mock Database " ,
36+ additionalIds: [ String ] = [ ]
37+ ) {
38+ databaseTypeId = typeId
39+ databaseDisplayName = displayName
40+ additionalDatabaseTypeIds = additionalIds
41+ }
42+
43+ static var additionalDatabaseTypeIds : [ String ] = [ ]
44+ }
45+
46+ // MARK: - validateDriverDescriptor Tests
47+
48+ @Suite ( " PluginManager.validateDriverDescriptor " )
49+ struct ValidateDriverDescriptorTests {
50+
51+ @Test ( " rejects empty databaseTypeId " )
52+ @MainActor func rejectsEmptyTypeId( ) {
53+ MockDriverPlugin . reset ( typeId: " " )
54+ let pm = PluginManager . shared
55+ #expect( throws: PluginError . self) {
56+ try pm. validateDriverDescriptor ( MockDriverPlugin . self, pluginId: " test " )
57+ }
58+ }
59+
60+ @Test ( " rejects whitespace-only databaseTypeId " )
61+ @MainActor func rejectsWhitespaceTypeId( ) {
62+ MockDriverPlugin . reset ( typeId: " " )
63+ let pm = PluginManager . shared
64+ #expect( throws: PluginError . self) {
65+ try pm. validateDriverDescriptor ( MockDriverPlugin . self, pluginId: " test " )
66+ }
67+ }
68+
69+ @Test ( " rejects empty databaseDisplayName " )
70+ @MainActor func rejectsEmptyDisplayName( ) {
71+ MockDriverPlugin . reset ( typeId: " valid-id " , displayName: " " )
72+ let pm = PluginManager . shared
73+ #expect( throws: PluginError . self) {
74+ try pm. validateDriverDescriptor ( MockDriverPlugin . self, pluginId: " test " )
75+ }
76+ }
77+
78+ @Test ( " rejects whitespace-only databaseDisplayName " )
79+ @MainActor func rejectsWhitespaceDisplayName( ) {
80+ MockDriverPlugin . reset ( typeId: " valid-id " , displayName: " " )
81+ let pm = PluginManager . shared
82+ #expect( throws: PluginError . self) {
83+ try pm. validateDriverDescriptor ( MockDriverPlugin . self, pluginId: " test " )
84+ }
85+ }
86+
87+ @Test ( " accepts valid descriptor with no conflicts " )
88+ @MainActor func acceptsValidDescriptor( ) throws {
89+ MockDriverPlugin . reset ( typeId: " unique-test-db-type " , displayName: " Unique Test DB " )
90+ let pm = PluginManager . shared
91+ try pm. validateDriverDescriptor ( MockDriverPlugin . self, pluginId: " test " )
92+ }
93+
94+ @Test ( " rejects duplicate primary type ID already registered " )
95+ @MainActor func rejectsDuplicatePrimaryTypeId( ) {
96+ // "MySQL" is registered by the built-in MySQL plugin
97+ MockDriverPlugin . reset ( typeId: " MySQL " , displayName: " Fake MySQL " )
98+ let pm = PluginManager . shared
99+ #expect( throws: PluginError . self) {
100+ try pm. validateDriverDescriptor ( MockDriverPlugin . self, pluginId: " test " )
101+ }
102+ }
103+
104+ @Test ( " rejects duplicate additional type ID already registered " )
105+ @MainActor func rejectsDuplicateAdditionalTypeId( ) {
106+ MockDriverPlugin . reset (
107+ typeId: " unique-test-db-type-2 " ,
108+ displayName: " Test DB " ,
109+ additionalIds: [ " MySQL " ]
110+ )
111+ let pm = PluginManager . shared
112+ #expect( throws: PluginError . self) {
113+ try pm. validateDriverDescriptor ( MockDriverPlugin . self, pluginId: " test " )
114+ }
115+ }
116+ }
117+
118+ // MARK: - PluginError.invalidDescriptor Formatting
119+
11120@Suite ( " PluginError.invalidDescriptor " )
12121struct PluginErrorInvalidDescriptorTests {
13122
@@ -33,90 +142,61 @@ struct PluginErrorInvalidDescriptorTests {
33142 #expect( description. contains ( " mysql " ) )
34143 #expect( description. contains ( " MySQL " ) )
35144 }
36-
37- @Test ( " error description for empty display name " )
38- func emptyDisplayNameDescription( ) {
39- let error = PluginError . invalidDescriptor (
40- pluginId: " com.example.test " ,
41- reason: " databaseDisplayName is empty "
42- )
43- let description = error. localizedDescription
44- #expect( description. contains ( " databaseDisplayName is empty " ) )
45- }
46-
47- @Test ( " error description for additional type ID conflict " )
48- func additionalTypeIdConflict( ) {
49- let error = PluginError . invalidDescriptor (
50- pluginId: " com.example.multi " ,
51- reason: " additionalDatabaseTypeId 'redshift' is already registered by 'PostgreSQL' "
52- )
53- let description = error. localizedDescription
54- #expect( description. contains ( " additionalDatabaseTypeId " ) )
55- #expect( description. contains ( " redshift " ) )
56- #expect( description. contains ( " PostgreSQL " ) )
57- }
58145}
59146
60- @Suite ( " ConnectionField Validation Logic " )
61- struct ConnectionFieldValidationLogicTests {
147+ // MARK: - validateConnectionFields Tests
148+
149+ @Suite ( " PluginManager.validateConnectionFields " )
150+ struct ValidateConnectionFieldsTests {
62151
63- @Test ( " duplicate field IDs are detectable " )
64- func duplicateFieldIds( ) {
152+ @Test ( " duplicate field IDs are detected " )
153+ @ MainActor func duplicateFieldIds( ) {
65154 let fields = [
66155 ConnectionField ( id: " encoding " , label: " Encoding " ) ,
67156 ConnectionField ( id: " timeout " , label: " Timeout " ) ,
68157 ConnectionField ( id: " encoding " , label: " Character Encoding " )
69158 ]
70- var seenIds = Set < String > ( )
71- var duplicates : [ String ] = [ ]
72- for field in fields {
73- if !seenIds. insert ( field. id) . inserted {
74- duplicates. append ( field. id)
75- }
76- }
77- #expect( duplicates == [ " encoding " ] )
159+ // Should not crash — warns via logger
160+ PluginManager . shared. validateConnectionFields ( fields, pluginId: " test " )
78161 }
79162
80- @Test ( " empty field ID is detectable " )
81- func emptyFieldId( ) {
82- let field = ConnectionField ( id: " " , label: " Something " )
83- #expect ( field . id . trimmingCharacters ( in : . whitespaces ) . isEmpty )
163+ @Test ( " empty field ID is detected without crash " )
164+ @ MainActor func emptyFieldId( ) {
165+ let fields = [ ConnectionField ( id: " " , label: " Something " ) ]
166+ PluginManager . shared . validateConnectionFields ( fields , pluginId : " test " )
84167 }
85168
86- @Test ( " empty field label is detectable " )
87- func emptyFieldLabel( ) {
88- let field = ConnectionField ( id: " test " , label: " " )
89- #expect ( field . label . trimmingCharacters ( in : . whitespaces ) . isEmpty )
169+ @Test ( " empty field label is detected without crash " )
170+ @ MainActor func emptyFieldLabel( ) {
171+ let fields = [ ConnectionField ( id: " test " , label: " " ) ]
172+ PluginManager . shared . validateConnectionFields ( fields , pluginId : " test " )
90173 }
91174
92- @Test ( " dropdown with empty options is detectable " )
93- func emptyDropdownOptions( ) {
94- let field = ConnectionField (
95- id: " encoding " ,
96- label: " Encoding " ,
97- fieldType: . dropdown( options: [ ] )
98- )
99- if case . dropdown( let options) = field. fieldType {
100- #expect( options. isEmpty)
101- } else {
102- Issue . record ( " Expected dropdown field type " )
103- }
175+ @Test ( " dropdown with empty options is detected without crash " )
176+ @MainActor func emptyDropdownOptions( ) {
177+ let fields = [
178+ ConnectionField (
179+ id: " encoding " ,
180+ label: " Encoding " ,
181+ fieldType: . dropdown( options: [ ] )
182+ )
183+ ]
184+ PluginManager . shared. validateConnectionFields ( fields, pluginId: " test " )
104185 }
105186
106- @Test ( " dropdown with options is valid " )
107- func validDropdown( ) {
108- let field = ConnectionField (
109- id: " encoding " ,
110- label: " Encoding " ,
111- fieldType: . dropdown( options: [
112- ConnectionField . DropdownOption ( value: " utf8 " , label: " UTF-8 " ) ,
113- ConnectionField . DropdownOption ( value: " latin1 " , label: " Latin-1 " )
114- ] )
115- )
116- if case . dropdown( let options) = field. fieldType {
117- #expect( options. count == 2 )
118- } else {
119- Issue . record ( " Expected dropdown field type " )
120- }
187+ @Test ( " valid fields pass without issue " )
188+ @MainActor func validFields( ) {
189+ let fields = [
190+ ConnectionField ( id: " encoding " , label: " Encoding " ) ,
191+ ConnectionField (
192+ id: " mode " ,
193+ label: " Mode " ,
194+ fieldType: . dropdown( options: [
195+ ConnectionField . DropdownOption ( value: " fast " , label: " Fast " ) ,
196+ ConnectionField . DropdownOption ( value: " safe " , label: " Safe " )
197+ ] )
198+ )
199+ ]
200+ PluginManager . shared. validateConnectionFields ( fields, pluginId: " test " )
121201 }
122202}
0 commit comments