├── .codebeatignore ├── .codecov.yml ├── .github └── workflows │ ├── documentation.yml │ └── test.yml ├── .gitignore ├── .swiftlint.yml ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── NMeta │ ├── Extensions │ ├── Application+NMeta.swift │ └── Request+NMeta.swift │ ├── NMetaError.swift │ ├── NMetaMiddleware.swift │ └── Version.swift └── Tests ├── LinuxMain.swift └── NMetaTests └── NMetaTests.swift /.codebeatignore: -------------------------------------------------------------------------------- 1 | Public/** 2 | Resources/Assets/** 3 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "0...100" 3 | ignore: 4 | - "Tests" 5 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/documentation.yml 2 | name: Documentation 3 | 4 | on: [release] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Generate Documentation 13 | uses: SwiftDocOrg/swift-doc@master 14 | with: 15 | inputs: "Sources" 16 | module-name: NMeta 17 | output: "Documentation" 18 | - name: Upload Documentation to Wiki 19 | uses: SwiftDocOrg/github-wiki-publish-action@v1 20 | with: 21 | path: "Documentation" 22 | env: 23 | GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.WIKI_ACCESS_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | linux: 9 | runs-on: ubuntu-latest 10 | container: swift:5.2-bionic 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v2 14 | - name: Run tests with Thread Sanitizer 15 | run: swift test --enable-test-discovery --sanitize=thread 16 | macOS: 17 | runs-on: macos-latest 18 | steps: 19 | - name: Select latest available Xcode 20 | uses: maxim-lobanov/setup-xcode@v1 21 | with: 22 | xcode-version: latest 23 | - name: Check out code 24 | uses: actions/checkout@v2 25 | - name: Run tests with Thread Sanitizer 26 | run: swift test --enable-test-discovery --sanitize=thread -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | .idea 4 | .DS_Store 5 | *.xcodeproj 6 | DerivedData/ 7 | Package.resolved 8 | .swiftpm 9 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | function_body_length: 4 | warning: 60 5 | identifier_name: 6 | min_length: 7 | warning: 2 8 | line_length: 100 9 | disabled_rules: 10 | - opening_brace 11 | colon: 12 | flexible_right_spacing: true 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2018 Nodes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "n-meta", 6 | platforms: [ 7 | .macOS(.v10_15) 8 | ], 9 | products: [ 10 | .library(name: "NMeta", targets: ["NMeta"]) 11 | ], 12 | 13 | dependencies: [ 14 | .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0") 15 | ], 16 | targets: [ 17 | 18 | .target( 19 | name: "NMeta", 20 | dependencies: [ 21 | .product(name: "Vapor", package: "vapor") 22 | ]), 23 | .testTarget(name: "NMetaTests", dependencies: [ 24 | .target(name: "NMeta"), 25 | .product(name: "XCTVapor", package: "vapor"), 26 | ]) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # N-Meta Ⓜ️ 2 | [![Swift Version](https://img.shields.io/badge/Swift-5.2-brightgreen.svg)](http://swift.org) 3 | [![Vapor Version](https://img.shields.io/badge/Vapor-4-30B6FC.svg)](http://vapor.codes) 4 | [![Circle CI](https://circleci.com/gh/nodes-vapor/n-meta/tree/master.svg?style=shield)](https://circleci.com/gh/nodes-vapor/n-meta) 5 | [![codebeat badge](https://codebeat.co/badges/5dfa4439-cd97-4210-8595-40b57830196a)](https://codebeat.co/projects/github-com-nodes-vapor-n-meta-master) 6 | [![codecov](https://codecov.io/gh/nodes-vapor/n-meta/branch/master/graph/badge.svg)](https://codecov.io/gh/nodes-vapor/n-meta) 7 | [![Readme Score](http://readme-score-api.herokuapp.com/score.svg?url=https://github.com/nodes-vapor/n-meta)](http://clayallsopp.github.io/readme-score?url=https://github.com/nodes-vapor/n-meta) 8 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/nodes-vapor/n-meta/master/LICENSE) 9 | 10 | 11 | This package enforces clients to send a specific header in all requests: 12 | 13 | ``` 14 | N-Meta: [PLATFORM];[ENVIRONMENT];[APP_VERSION];[DEVICE_OS];[DEVICE] 15 | ``` 16 | 17 | If you're running an older version of Vapor then have a look here: 18 | 19 | - [Vapor 1.x](https://github.com/nodes-vapor/n-meta/tree/vapor-1) 20 | - [Vapor 2.x](https://github.com/nodes-vapor/n-meta/tree/vapor-2) 21 | - [Vapor 3.x](https://github.com/nodes-vapor/n-meta/tree/vapor-3) 22 | 23 | This header can look like this `android;production;1.2.3;4.4;Samsung S7` 24 | - platform 25 | - environment 26 | - app version 27 | - device os 28 | - device 29 | 30 | For web platform only platform and environment is required, since the rest can be found in `User-Agent`. 31 | 32 | Why not just use `User-Agent`? 33 | - `User-Agent` is missing some of these details 34 | - `User-Agent` can be hard to extend/override 35 | - Default `User-Agent` in iOS & Android can be their client (OkHttp, Alamofire etc). 36 | 37 | 38 | ## 📦 Installation 39 | 40 | Update your `Package.swift` file. 41 | ```swift 42 | ... 43 | dependencies: [ 44 | ... 45 | .package(url: "https://github.com/nodes-vapor/n-meta.git", from: "4.0.0") 46 | ], 47 | targets: [ 48 | .target( 49 | name: "App", 50 | dependencies: [ 51 | ... 52 | .product(name: "NMeta", package: "n-meta"), 53 | ] 54 | ) 55 | ... 56 | ``` 57 | 58 | ## Getting started 🚀 59 | 60 | Configure NMeta as per your needs, for example: 61 | 62 | ```swift 63 | app.nMeta = .init(exceptPath: ["/admin"]) 64 | ``` 65 | 66 | Next, add the middleware directly to your routes (e.g. in `routes.swift`): 67 | 68 | ```swift 69 | app.grouped(NMetaMiddleware()).get("hello") { req in 70 | "Hello, world!" 71 | } 72 | ``` 73 | 74 | or add the middleware globally (e.g. in `configure.swift`) which will add it to all routes: 75 | 76 | ```swift 77 | app.middlewares.use(NMetaMiddleware()) 78 | ``` 79 | 80 | ## 🏆 Credits 81 | 82 | This package is developed and maintained by the Vapor team at [Nodes](https://www.nodesagency.com). 83 | 84 | 85 | ## 📄 License 86 | 87 | This package is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) 88 | -------------------------------------------------------------------------------- /Sources/NMeta/Extensions/Application+NMeta.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | public extension Application { 4 | struct NMeta { 5 | 6 | /// Request header where NMeta data will be extracted from 7 | public var headerName: String 8 | 9 | /// Supported platforms 10 | public var platforms: [String] 11 | 12 | /// Supported environments 13 | public var environments: [String] 14 | 15 | /// Ignore requirement on following paths 16 | public var exceptPaths: [String] 17 | 18 | /// Only check header on following environments 19 | public var requiredEnvironments: [String] 20 | 21 | /// Create a new `NMeta` configuration value. 22 | /// - Parameters: 23 | /// - headerName: the request header where NMeta data will be extracted from 24 | /// - platforms: supported platforms 25 | /// - environments: supported environments 26 | /// - exceptPaths: 27 | /// paths to ignore NMeta requirement on. Must start with `/` and may end with `/*` to match all sub-paths. 28 | /// - requiredEnvironments: environments to check NMeta header for 29 | public init( 30 | headerName: String = "N-Meta", 31 | platforms: [String] = ["web", "android", "ios"], 32 | environments: [String] = ["local", "development", "staging", "production"], 33 | exceptPaths: [String] = ["/js/*", "/css/*", "/images/*", "/favicons/*", "/admin/*"], 34 | requiredEnvironments: [String] = ["local", "development", "staging", "production"] 35 | ) { 36 | self.headerName = headerName 37 | self.platforms = platforms 38 | self.environments = environments 39 | self.exceptPaths = exceptPaths 40 | self.requiredEnvironments = requiredEnvironments 41 | } 42 | 43 | func assertValid(request: Request) throws { 44 | if try isMetaRequired(request: request) { 45 | // Extract and add meta to request. 46 | request.nMeta = try metaOrFail(request: request) 47 | } 48 | } 49 | 50 | private func isMetaRequired(request: Request) throws -> Bool { 51 | // Check required environments 52 | if !requiredEnvironments.contains(request.application.environment.name) { 53 | return false 54 | } 55 | 56 | // Bypass CORS requests 57 | if request.method == .OPTIONS { 58 | return false 59 | } 60 | 61 | // Except paths 62 | for check: String in exceptPaths { 63 | // Check complete except paths 64 | if check == request.url.description { 65 | return false 66 | } 67 | 68 | // Check except paths and subfolders 69 | if check.last == "*" && request.url.description.hasPrefix(String(check.dropLast())) { 70 | return false 71 | } 72 | } 73 | 74 | return true 75 | } 76 | 77 | private func metaOrFail(request: Request) throws -> Request.NMeta { 78 | let meta = try Request.NMeta(request: request) 79 | 80 | // Validate platform. 81 | guard platforms.contains(meta.platform) else { 82 | throw NMetaError.platformUnsupported 83 | } 84 | 85 | // Validate environment. 86 | guard environments.contains(meta.environment) else { 87 | throw NMetaError.environmentUnsupported 88 | } 89 | 90 | return meta 91 | } 92 | } 93 | 94 | private struct Key: StorageKey { 95 | typealias Value = NMeta 96 | } 97 | 98 | var nMeta: NMeta { 99 | get { 100 | if let existing = storage[Key.self] { 101 | return existing 102 | } else { 103 | let new = NMeta() 104 | storage[Key.self] = new 105 | return new 106 | } 107 | } 108 | 109 | set { storage[Key.self] = newValue } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/NMeta/Extensions/Request+NMeta.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | private struct NMetaKey: StorageKey { 4 | typealias Value = Request.NMeta 5 | } 6 | 7 | public extension Request { 8 | struct NMeta { 9 | 10 | public let platform: String 11 | public let environment: String 12 | public let version: Version 13 | public let deviceOS: String 14 | public let device: String 15 | 16 | init(request: Request) throws { 17 | guard let metaString = request.headers.first(name: request.application.nMeta.headerName) else { 18 | throw NMetaError.headerMissing 19 | } 20 | 21 | let components = metaString.components(separatedBy: ";") 22 | let componentsCount = components.count 23 | 24 | guard 25 | (components.first == "web" && componentsCount >= 2) || 26 | componentsCount == 5 27 | else { 28 | throw NMetaError.invalidHeaderFormat 29 | } 30 | 31 | self.platform = components[0] 32 | self.environment = components[1] 33 | self.version = try Version(string: componentsCount > 2 ? components[2] : "0.0.0") 34 | self.deviceOS = componentsCount > 3 ? components[3] : "N/A" 35 | self.device = componentsCount > 4 ? components[4] : "N/A" 36 | } 37 | } 38 | 39 | var nMeta: NMeta? { 40 | get { storage[NMetaKey.self] } 41 | set { storage[NMetaKey.self] = newValue } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/NMeta/NMetaError.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | public enum NMetaError: String, Error { 4 | case environmentUnsupported 5 | case headerMissing 6 | case invalidHeaderFormat 7 | case invalidVersionFormat 8 | case platformUnsupported 9 | } 10 | 11 | extension NMetaError: AbortError { 12 | public var identifier: String { rawValue } 13 | 14 | public var reason: String { 15 | switch self { 16 | case .environmentUnsupported: return "NMeta: Environment unsupported" 17 | case .headerMissing: return "NMeta: Header missing" 18 | case .invalidHeaderFormat: return """ 19 | NMeta: Invalid header format. Format is platform;environment;version;deviceOS;device \ 20 | OR platform;environment if you are using web 21 | """ 22 | case .invalidVersionFormat: return """ 23 | NMeta: Invalid version format. Format is 1.2.3 (major.minor.patch). 24 | """ 25 | case .platformUnsupported: return "NMeta: Platform unsupported" 26 | } 27 | } 28 | 29 | public var status: HTTPResponseStatus { 30 | switch self { 31 | case .invalidVersionFormat, 32 | .headerMissing, 33 | .platformUnsupported, 34 | .environmentUnsupported, 35 | .invalidHeaderFormat: 36 | return .badRequest 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/NMeta/NMetaMiddleware.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | public final class NMetaMiddleware: Middleware { 4 | 5 | public init() {} 6 | 7 | /// See `Middleware.respond(to:)` 8 | public func respond( 9 | to request: Request, 10 | chainingTo next: Responder 11 | ) -> EventLoopFuture { 12 | do { 13 | try request.application.nMeta.assertValid(request: request) 14 | } catch(let error) { 15 | return request.eventLoop.makeFailedFuture(error) 16 | } 17 | return next.respond(to: request) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/NMeta/Version.swift: -------------------------------------------------------------------------------- 1 | public struct Version { 2 | public let major: Int 3 | public let minor: Int 4 | public let patch: Int 5 | 6 | public var string: String { 7 | "\(major).\(minor).\(patch)" 8 | } 9 | 10 | public init(string: String) throws { 11 | let components = string.components(separatedBy: ".") 12 | var numbers = components.compactMap(Int.init) 13 | 14 | guard !numbers.isEmpty else { 15 | throw NMetaError.invalidVersionFormat 16 | } 17 | 18 | major = numbers.removeFirst() 19 | minor = numbers.isEmpty ? 0 : numbers.removeFirst() 20 | patch = numbers.isEmpty ? 0 : numbers.removeFirst() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | fatalError("Please use swift test --enable-test-discovery to run the tests instead") -------------------------------------------------------------------------------- /Tests/NMetaTests/NMetaTests.swift: -------------------------------------------------------------------------------- 1 | import XCTVapor 2 | import NMeta 3 | 4 | class NMetaTests: XCTestCase { 5 | var app: Application! 6 | var nMeta: Request.NMeta? 7 | 8 | let headerName = "N-Meta" 9 | let platforms = ["web", "android", "ios"] 10 | let environments = ["testing"] 11 | let exceptPaths = ["/js/*", "/css/*", "/images/*", "/favicons/*", "/admin/*"] 12 | let requiredEnvironments = ["testing"] 13 | 14 | override func setUp() { 15 | app = Application(.testing) 16 | configure(app) 17 | } 18 | 19 | override func tearDown() { 20 | app.shutdown() 21 | nMeta = nil 22 | } 23 | 24 | func testVersionFull() throws { 25 | let version = try Version(string: "1.2.3") 26 | 27 | XCTAssertEqual(version.major, 1) 28 | XCTAssertEqual(version.minor, 2) 29 | XCTAssertEqual(version.patch, 3) 30 | } 31 | 32 | func testVersionMinor() throws { 33 | let version = try Version(string: "1") 34 | 35 | XCTAssertEqual(version.major, 1) 36 | XCTAssertEqual(version.minor, 0) 37 | XCTAssertEqual(version.patch, 0) 38 | } 39 | 40 | func testVersionPatch() throws { 41 | let version = try Version(string: "1.2") 42 | 43 | XCTAssertEqual(version.major, 1) 44 | XCTAssertEqual(version.minor, 2) 45 | XCTAssertEqual(version.patch, 0) 46 | } 47 | 48 | func testNMetaSuccess() throws { 49 | try app.test( 50 | .GET, 51 | "", 52 | headers: [headerName: "android;testing;1.2.3;4.4;Samsung S7"] 53 | ) { res in 54 | XCTAssertEqual(res.status, .ok) 55 | } 56 | 57 | let nMeta = try XCTUnwrap(self.nMeta) 58 | 59 | XCTAssertEqual(nMeta.platform, "android") 60 | XCTAssertEqual(nMeta.environment, "testing") 61 | XCTAssertEqual(nMeta.version.string, "1.2.3") 62 | XCTAssertEqual(nMeta.deviceOS, "4.4") 63 | XCTAssertEqual(nMeta.device, "Samsung S7") 64 | } 65 | 66 | func testNMetaEmpty() throws { 67 | try app.test(.GET, "", headers: [headerName: ""]) { res in 68 | XCTAssertEqual(res.status, .badRequest) 69 | XCTAssertEqual( 70 | try res.content.decode(ErrorReponse.self).reason, 71 | NMetaError.invalidHeaderFormat.reason 72 | ) 73 | } 74 | XCTAssertNil(nMeta) 75 | } 76 | 77 | func testNMetaMissingEnv() throws { 78 | try app.test(.GET, "", headers: [headerName: "a"]) { res in 79 | XCTAssertEqual(res.status, .badRequest) 80 | XCTAssertEqual( 81 | try res.content.decode(ErrorReponse.self).reason, 82 | NMetaError.invalidHeaderFormat.reason 83 | ) 84 | } 85 | XCTAssertNil(nMeta) 86 | } 87 | 88 | func testNMetaMissingVersion() throws { 89 | try app.test(.GET, "", headers: [headerName: "a;b"]) { res in 90 | XCTAssertEqual(res.status, .badRequest) 91 | XCTAssertEqual( 92 | try res.content.decode(ErrorReponse.self).reason, 93 | NMetaError.invalidHeaderFormat.reason 94 | ) 95 | } 96 | XCTAssertNil(nMeta) 97 | } 98 | 99 | func testNMetaMissingDeviceOs() throws { 100 | try app.test(.GET, "", headers: [headerName: "a;b;c"]) { res in 101 | XCTAssertEqual(res.status, .badRequest) 102 | XCTAssertEqual( 103 | try res.content.decode(ErrorReponse.self).reason, 104 | NMetaError.invalidHeaderFormat.reason 105 | ) 106 | } 107 | XCTAssertNil(nMeta) 108 | } 109 | 110 | func testNMetaMissingDevice() throws { 111 | try app.test(.GET, "", headers: [headerName: "a;b;c;d"]) { res in 112 | XCTAssertEqual(res.status, .badRequest) 113 | XCTAssertEqual( 114 | try res.content.decode(ErrorReponse.self).reason, 115 | NMetaError.invalidHeaderFormat.reason 116 | ) 117 | } 118 | XCTAssertNil(nMeta) 119 | } 120 | 121 | func testNMetaIncorrectVersion() throws { 122 | try app.test(.GET, "", headers: [headerName: "a;b;c;d;e"]) { res in 123 | XCTAssertEqual(res.status, .badRequest) 124 | XCTAssertEqual( 125 | try res.content.decode(ErrorReponse.self).reason, 126 | "NMeta: Invalid version format. Format is 1.2.3 (major.minor.patch)." 127 | ) 128 | } 129 | XCTAssertNil(nMeta) 130 | } 131 | 132 | func testNMetaUnsupportedPlatform() throws { 133 | try app.test(.GET, "", headers: [headerName: "a;b;1;d;e"]) { res in 134 | XCTAssertEqual(res.status, .badRequest) 135 | XCTAssertEqual( 136 | try res.content.decode(ErrorReponse.self).reason, 137 | "NMeta: Platform unsupported" 138 | ) 139 | } 140 | XCTAssertNil(nMeta) 141 | } 142 | 143 | func testNMetaUnsupportedEnvironment() throws { 144 | try app.test(.GET, "", headers: [headerName: "web;b;1;d;e"]) { res in 145 | XCTAssertEqual(res.status, .badRequest) 146 | XCTAssertEqual( 147 | try res.content.decode(ErrorReponse.self).reason, 148 | "NMeta: Environment unsupported" 149 | ) 150 | } 151 | XCTAssertNil(nMeta) 152 | } 153 | 154 | func testNMetaWebEnvironment() throws { 155 | try app.test( 156 | .GET, 157 | "", 158 | headers: [headerName: "web;testing"] 159 | ) { res in 160 | XCTAssertEqual(res.status, .ok) 161 | } 162 | 163 | let nMeta = try XCTUnwrap(self.nMeta) 164 | 165 | XCTAssertEqual(nMeta.platform, "web") 166 | XCTAssertEqual(nMeta.environment, "testing") 167 | XCTAssertEqual(nMeta.version.string, "0.0.0") 168 | XCTAssertEqual(nMeta.deviceOS, "N/A") 169 | XCTAssertEqual(nMeta.device, "N/A") 170 | } 171 | 172 | func testNMetaWebEnvironmentWithMoreThanTwoValuesValidVersion() throws { 173 | try app.test( 174 | .GET, 175 | "", 176 | headers: [headerName: "web;testing;1.0.0;else;here"] 177 | ) { res in 178 | XCTAssertEqual(res.status, .ok) 179 | } 180 | 181 | let nMeta = try XCTUnwrap(self.nMeta) 182 | 183 | XCTAssertEqual(nMeta.platform, "web") 184 | XCTAssertEqual(nMeta.environment, "testing") 185 | XCTAssertEqual(nMeta.version.string, "1.0.0") 186 | XCTAssertEqual(nMeta.deviceOS, "else") 187 | XCTAssertEqual(nMeta.device, "here") 188 | } 189 | 190 | func testNMetaWebEnvironmentWithMoreThanTwoValuesNotValidVersion() throws { 191 | try app.test( 192 | .GET, 193 | "", 194 | headers: [headerName: "web;testing;invalid;else;here"] 195 | ) { res in 196 | XCTAssertEqual(res.status, .badRequest) 197 | XCTAssertEqual( 198 | try res.content.decode(ErrorReponse.self).reason, 199 | NMetaError.invalidVersionFormat.reason 200 | ) 201 | } 202 | } 203 | 204 | func testNMetaWebEnvironmentMissingValue() throws { 205 | try app.test( 206 | .GET, 207 | "", 208 | headers: [headerName: "web"] 209 | ) { res in 210 | XCTAssertEqual(res.status, .badRequest) 211 | XCTAssertEqual( 212 | try res.content.decode(ErrorReponse.self).reason, 213 | NMetaError.invalidHeaderFormat.reason 214 | ) 215 | } 216 | } 217 | 218 | func configure(_ app: Application) { 219 | app.nMeta.headerName = headerName 220 | app.nMeta.platforms = platforms 221 | app.nMeta.environments = environments 222 | app.nMeta.exceptPaths = exceptPaths 223 | app.nMeta.requiredEnvironments = requiredEnvironments 224 | 225 | app.grouped(NMetaMiddleware()).get("") { req -> String in 226 | self.nMeta = req.nMeta 227 | return "" 228 | } 229 | } 230 | } 231 | 232 | private struct ErrorReponse: Decodable { 233 | let reason: String 234 | } 235 | --------------------------------------------------------------------------------